Skip to content

매크로는 번들 타임에 JavaScript 함수를 실행하는 메커니즘입니다. 이 함수에서 반환된 값은 번들에 직접 인라인됩니다.

간단한 예제로, 무작위 숫자를 반환하는 이 간단한 함수를 고려하세요.

ts
export function random() {
  return Math.random();
}

이것은 일반 파일의 일반 함수이지만 다음과 같이 매크로로 사용할 수 있습니다:

tsx
import { random } from "./random.ts" with { type: "macro" };

console.log(`무작위 숫자는 ${random()} 입니다`);

NOTE

매크로는 가져오기 속성 구문을 사용하여 표시됩니다. 이 구문을 본 적이 없다면 이는 Stage 3 TC39 제안으로 가져오기 문에 추가 메타데이터를 첨부할 수 있습니다.

이제 bun build 로 이 파일을 번들링합니다. 번들링된 파일은 stdout 에 출력됩니다.

bash
bun build ./cli.tsx
js
console.log(`무작위 숫자는 ${0.6805550949689833} 입니다`);

보시다시피 random 함수의 소스 코드는 번들 어디에도 나타나지 않습니다. 대신 번들링 중에 실행되고 함수 호출 (random()) 이 함수 결과로 대체됩니다. 소스 코드는 번들에 포함되지 않으므로 매크로는 데이터베이스 읽기와 같은 특권 작업을 안전하게 수행할 수 있습니다.

매크로 사용 시기

일회성 빌드 스크립트가 필요한 작은 것들을 위한 여러 빌드 스크립트가 있다면 번들 타임 코드 실행이 유지 관리하기 더 쉽습니다. 이는 나머지 코드와 함께 살며 나머지 빌드와 함께 실행되고 자동으로 병렬화되며 실패하면 빌드도 실패합니다.

번들 타임에 많은 코드를 실행하게 된다면 대신 서버 실행을 고려하세요.

가져오기 속성

Bun 매크로는 다음 중 하나로 주석이 달린 가져오기 문입니다:

  • with { type: 'macro' } — Stage 3 ECMA Script 제안인 가져오기 속성
  • assert { type: 'macro' } — 이제 폐기되었지만 이미 여러 브라우저와 런타임에서 지원되는 가져오기 속성의 초기 버전

보안 고려사항

매크로는 번들 타임에 실행되기 위해 명시적으로 { type: "macro" } 로 가져와야 합니다. 이 가져오기는 호출되지 않으면 효과가 없으며 사이드 이펙트가 있을 수 있는 일반 JavaScript 가져오기와 다릅니다.

Bun 에 --no-macros 플래그를 전달하여 매크로를 완전히 비활성화할 수 있습니다. 다음과 같은 빌드 오류가 발생합니다:

error: 매크로가 비활성화됨

foo();
^
./hello.js:3:1 53

악성 패키지의 잠재적 공격 표면을 줄이기 위해 매크로는 node_modules/**/* 내부에서 호출될 수 없습니다. 패키지가 매크로를 호출하려고 하면 다음과 같은 오류가 표시됩니다:

error: 보안상의 이유로 매크로는 node_modules 에서 실행될 수 없습니다.

beEvil();
^
node_modules/evil/index.js:3:1 50

애플리케이션 코드는 여전히 node_modules 에서 매크로를 가져와서 호출할 수 있습니다.

ts
import { macro } from "some-package" with { type: "macro" };

macro();

내보내기 조건 "macro"

npm 또는 기타 패키지 레지스트리에 매크로가 포함된 라이브러리를 배포할 때 "macro" 내보내기 조건을 사용하여 매크로 환경 전용의 특별한 버전의 패키지를 제공하세요.

json
{
  "name": "my-package",
  "exports": {
    "import": "./index.js",
    "require": "./index.js",
    "default": "./index.js",
    "macro": "./index.macro.js"
  }
}

이 구성을 사용하면 사용자는 런타임 또는 번들 타임에 동일한 가져오기 지정자를 사용하여 패키지를 소비할 수 있습니다:

ts
import pkg from "my-package"; // 런타임 가져오기
import { macro } from "my-package" with { type: "macro" }; // 매크로 가져오기

첫 번째 가져오기는 ./node_modules/my-package/index.js 로 해결되고 두 번째는 Bun 의 번들러에 의해 ./node_modules/my-package/index.macro.js 로 해결됩니다.

실행

Bun 의 트랜스파일러가 매크로 가져오기를 보면 Bun 의 JavaScript 런타임을 사용하여 트랜스파일러 내부에서 함수를 호출하고 JavaScript 의 반환 값을 AST 노드로 변환합니다. 이 JavaScript 함수는 런타임이 아닌 번들 타임에 호출됩니다.

매크로는 방문 단계 동안 트랜스파일러에서 동기적으로 실행되며 플러그인 전과 트랜스파일러가 AST 를 생성하기 전에 실행됩니다. 매크로는 가져오기 순서대로 실행됩니다. 트랜스파일러는 매크로가 실행을 완료할 때까지 기다렸다가 계속합니다. 트랜스파일러는 매크로에서 반환된 Promise 도 await 합니다.

Bun 의 번들러는 멀티스레드입니다. 따라서 매크로는 여러 개의 생성된 JavaScript "워커" 내에서 병렬로 실행됩니다.

데드 코드 제거

번들러는 매크로를 실행하고 인라인한 후 데드 코드 제거를 수행합니다. 따라서 다음 매크로가 주어지면:

ts
export function returnFalse() {
  return false;
}

...다음 파일을 번들링하면 축소 구문 옵션이 활성화된 경우 빈 번들이 생성됩니다.

ts
import { returnFalse } from "./returnFalse.ts" with { type: "macro" };

if (returnFalse()) {
  console.log("이 코드는 제거됩니다");
}

직렬화 가능성

Bun 의 트랜스파일러는 매크로 결과를 직렬화하여 AST 에 인라인할 수 있어야 합니다. 모든 JSON 호환 데이터 구조가 지원됩니다:

ts
export function getObject() {
  return {
    foo: "bar",
    baz: 123,
    array: [1, 2, { nested: "value" }],
  };
}

매크로는 async 일 수 있으며 Promise 인스턴스를 반환할 수 있습니다. Bun 의 트랜스파일러는 Promise 를 자동으로 await 하고 결과를 인라인합니다.

ts
export async function getText() {
  return "async value";
}

트랜스파일러는 Response, Blob, TypedArray 와 같은 일반적인 데이터 형식을 직렬화하기 위한 특별한 로직을 구현합니다.

  • TypedArray: base64 로 인코딩된 문자열로 해결됩니다.
  • Response: Bun 은 Content-Type 을 읽고 그에 따라 직렬화합니다. 예를 들어 application/json 타입의 Response 는 자동으로 객체로 파싱되고 text/plain 은 문자열로 인라인됩니다. 인식할 수 없거나 정의되지 않은 타입의 Responses 는 base-64 로 인코딩됩니다.
  • Blob: Response 와 마찬가지로 type 속성에 따라 다릅니다.

fetch 의 결과는 Promise<Response> 이므로 직접 반환할 수 있습니다.

ts
export function getObject() {
  return fetch("https://bun.com");
}

함수와 대부분의 클래스 인스턴스 (위에서 언급한 것 제외) 는 직렬화할 수 없습니다.

ts
export function getText(url: string) {
  // 이것은 작동하지 않습니다!
  return () => {};
}

인수

매크로는 입력을 받을 수 있지만 제한된 경우에만 가능합니다. 값은 정적으로 알려져 있어야 합니다. 예를 들어 다음은 허용되지 않습니다:

ts
import { getText } from "./getText.ts" with { type: "macro" };

export function howLong() {
  // `foo` 의 값은 정적으로 알 수 없습니다
  const foo = Math.random() ? "foo" : "bar";

  const text = getText(`https://example.com/${foo}`);
  console.log("페이지는 ", text.length, " 문자 길이입니다");
}

하지만 foo 의 값이 번들 타임에 알려져 있다면 (상수이거나 다른 매크로의 결과인 경우) 허용됩니다:

ts
import { getText } from "./getText.ts" with { type: "macro" };
import { getFoo } from "./getFoo.ts" with { type: "macro" };

export function howLong() {
  // getFoo() 는 정적으로 알려져 있으므로 작동합니다
  const foo = getFoo();
  const text = getText(`https://example.com/${foo}`);
  console.log("페이지는", text.length, "문자 길이입니다");
}

이것은 다음을 출력합니다:

js
function howLong() {
  console.log("페이지는", 1322, "문자 길이입니다");
}
export { howLong };

예제

최신 git 커밋 해시 임베드

ts
export function getGitCommitHash() {
  const { stdout } = Bun.spawnSync({
    cmd: ["git", "rev-parse", "HEAD"],
    stdout: "pipe",
  });

  return stdout.toString();
}

빌드하면 getGitCommitHash 는 함수를 호출한 결과로 대체됩니다:

ts
import { getGitCommitHash } from "./getGitCommitHash.ts" with { type: "macro" };

console.log(`현재 Git 커밋 해시는 ${getGitCommitHash()} 입니다`);
ts
console.log(`현재 Git 커밋 해시는 3ee3259104e4507cf62c160f0ff5357ec4c7a7f8 입니다`);

번들 타임에 fetch() 요청 만들기

이 예제에서는 fetch() 를 사용하여 나가는 HTTP 요청을 만들고 HTMLRewriter 를 사용하여 HTML 응답을 파싱하며 번들 타임에 제목과 메타 태그를 포함하는 객체를 반환합니다.

ts
export async function extractMetaTags(url: string) {
  const response = await fetch(url);
  const meta = {
    title: "",
  };
  new HTMLRewriter()
    .on("title", {
      text(element) {
        meta.title += element.text;
      },
    })
    .on("meta", {
      element(element) {
        const name =
          element.getAttribute("name") || element.getAttribute("property") || element.getAttribute("itemprop");

        if (name) meta[name] = element.getAttribute("content");
      },
    })
    .transform(response);

  return meta;
}

extractMetaTags 함수는 번들 타임에 삭제되고 함수 호출의 결과로 대체됩니다. 이는 fetch 요청이 번들 타임에 발생하고 결과가 번들에 임베드됨을 의미합니다. 또한 도달할 수 없는 분기이므로 오류를 throw 하는 분기도 제거됩니다.

jsx
import { extractMetaTags } from "./meta.ts" with { type: "macro" };

export const Head = () => {
  const headTags = extractMetaTags("https://example.com");

  if (headTags.title !== "Example Domain") {
    throw new Error("제목이 'Example Domain'이어야 합니다");
  }

  return (
    <head>
      <title>{headTags.title}</title>
      <meta name="viewport" content={headTags.viewport} />
    </head>
  );
};
jsx
export const Head = () => {
  const headTags = {
    title: "Example Domain",
    viewport: "width=device-width, initial-scale=1",
  };

  return (
    <head>
      <title>{headTags.title}</title>
      <meta name="viewport" content={headTags.viewport} />
    </head>
  );
};

Bun by www.bunjs.com.cn 편집