C.W.K.
Stream
Lesson 05 of 05 · published

Bundler 와 build — esbuild / TypeScript / dist/ pipeline

~13 min · bundler, esbuild, typescript, build

Level 0Extension 입덕
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"한 file 위한 vanilla JS 가 fine. Npm dependency 와 TypeScript 가진 multi-file 가 bundler 손 뻗는 곳. Lesson 5 가 src/ 를 1 초 안에 dist/ 로 바꾸는, sourcemap / watch mode / Web Store-clean 출력 가진 minimum esbuild setup."

왜 bundler

File 당 약 200 줄까지, content.js / background.js / popup.js / panel.js 등 흩뿌려진 vanilla JS 가 fine. Bundler 가 자기 몫 하는 시점:

  • Type 위해 TypeScript 원할 때.
  • Internal ES module import 가진 third-party npm package import (flat single file 아닌 npm 의 어떤 것이든).
  • Codebase 가 여러 entry point 가 쓰는 shared helper 가짐 (popup + panel + content 모두 같은 clip-row renderer 필요).
  • src/ 의 한 source layout, dist/ 의 한 production layout 원함, build 동안 manifest 와 HTML 복사/transform.

Default 로서 esbuild

esbuild 가 JS/TS 를 밀리초 안에 compile. 몇 entry point 가진 주로 TypeScript 의 extension 에, entire build 가:

esbuild \
  src/background.ts \
  src/content.ts \
  src/popup.ts \
  src/panel.ts \
  --bundle \
  --outdir=dist \
  --target=chrome120 \
  --sourcemap=inline \
  --format=iife

네 entry point, 한 command, 다 bundle 된 네 output file (background.js, content.js, popup.js, panel.js). Output 이 Chrome 이 content script 와 classic background page 에 원하는 IIFE format. --target=chrome120 가 esbuild 한테 transpile 안 하고 두는 JS feature 알려 줘.

Layout

clipdeck/
  src/
    background.ts
    content.ts
    popup.ts
    panel.ts
    shared/
      clip-row.ts
      escape-html.ts
      types.ts
  static/
    manifest.json
    popup.html
    panel.html
    icons/
      16.png 32.png 48.png 128.png
    vendor/
      Readability.js
  scripts/
    build.mjs
  dist/         # gitignore, 생성됨
  package.json
  tsconfig.json

Build script 가 static/ 를 dist/ 로 복사하고 src/ 를 dist/ 로 bundle. npm run build 후 chrome-loadable dist/ 가짐.

Build script

esbuild 의 Node API 가 node scripts/build.mjs 로 돌릴 수 있는 single file 줘:

import { build } from 'esbuild';
import { copyFile, mkdir, cp } from 'fs/promises';

await mkdir('dist', { recursive: true });
await cp('static', 'dist', { recursive: true });
await build({
  entryPoints: ['src/background.ts', 'src/content.ts', 'src/popup.ts', 'src/panel.ts'],
  bundle: true,
  outdir: 'dist',
  target: 'chrome120',
  format: 'iife',
  sourcemap: 'inline',
  logLevel: 'info',
});

development 중 rebuild-on-save 위해 --watch flag 와 esbuild 의 context API 추가. Chrome extension reload library (extension-reloader, 또는 dev SW 통한 작은 chrome.runtime.reload trigger) 와 pair 해서 file 저장이 Chrome 의 extension reload trigger.

TypeScript setup

Extension 용 tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "types": ["chrome"],
    "noEmit": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

chrome.* API type 위해 @types/chrome install. noEmit: true 인 이유는 esbuild 가 실제 compilation 하고; tsc 는 type-check 위해서만 돌림. 별도 check 로 tsc --noEmit 돌리는 npm run typecheck script 추가.

package.json script

{
  "scripts": {
    "build": "node scripts/build.mjs",
    "watch": "node scripts/build.mjs --watch",
    "typecheck": "tsc --noEmit",
    "package": "npm run typecheck && npm run build && cd dist && zip -r ../clipdeck.zip ."
  },
  "devDependencies": {
    "esbuild": "^0.25.0",
    "typescript": "^5.5.0",
    "@types/chrome": "^0.0.300"
  }
}

npm run package 가 typecheck, build, zip 을 한 shot 으로. 그 zip 이 Web Store 에 upload 하는 것. npm run watch 가 dev loop; chrome://extensions auto-reload (browser extension 이나 작은 chrome.runtime.reload SW listener) 와 pair.

Vendor 에서 migration

Track 7 의 Readability.js 가 flat file 로 vendor 됨. Bundler 와 함께, npm install @mozilla/readability 하고 normal 하게 import:

// src/content.ts
import { Readability } from '@mozilla/readability';
// ...이전과 정확히 같이 사용

Bundled content.js 가 Readability 의 코드 inline. 더 깔끔, version-tracked, 가능한 곳에서 tree-shaken. Static/vendor/ 디렉토리가 unused 됨 — build output 에서 삭제.

한 file 엔 vanilla JS. 그 너머 자라면 esbuild + TS + npm ecosystem. dist/ output 이 Chrome 이 load 하고 Web Store 에 zip 하는 것. Static manifest + HTML + icon 이 그대로 복사; src/ 가 bundle.
대안으로 Vite. Vite 가 manifest transform 과 dev-server hot reload 잘 처리하는 extension plugin (vite-plugin-web-extension) 가짐. esbuild 가 ClipDeck scope 엔 충분; Vite 가 development 중 popup/panel UI 의 HMR 원할 때 빛남. 둘 다 valid dist/ 생산. 다른 tooling 매칭하는 어느 거든 선택.

Code

scripts/build.mjs — optional watch mode 가진 Node API 통한 esbuild·javascript
// scripts/build.mjs — esbuild 의 Node API 사용 single-file build script
import { build, context } from "esbuild";
import { cp, mkdir, rm } from "fs/promises";

const watch = process.argv.includes("--watch");

await rm("dist", { recursive: true, force: true });
await mkdir("dist", { recursive: true });
await cp("static", "dist", { recursive: true });

const options = {
  entryPoints: [
    "src/background.ts",
    "src/content.ts",
    "src/popup.ts",
    "src/panel.ts",
  ],
  bundle: true,
  outdir: "dist",
  target: "chrome120",
  format: "iife",
  sourcemap: "inline",
  logLevel: "info",
};

if (watch) {
  const ctx = await context(options);
  await ctx.watch();
  console.log("[ClipDeck build] watching src/ — Ctrl+C to stop");
} else {
  await build(options);
  console.log("[ClipDeck build] dist/ ready");
}
package.json — ClipDeck build 위한 script + dev dependency·json
{
  "name": "clipdeck",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "node scripts/build.mjs",
    "watch": "node scripts/build.mjs --watch",
    "typecheck": "tsc --noEmit",
    "package": "npm run typecheck && npm run build && cd dist && zip -r ../clipdeck.zip ."
  },
  "devDependencies": {
    "esbuild": "^0.25.0",
    "typescript": "^5.5.0",
    "@types/chrome": "^0.0.300",
    "@mozilla/readability": "^0.6.0"
  }
}
tsconfig.json — chrome.* type 가진 strict TypeScript·json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "types": ["chrome"],
    "noEmit": true,
    "skipLibCheck": true,
    "lib": ["ES2022", "DOM", "DOM.Iterable"]
  },
  "include": ["src"]
}

External links

Exercise

ClipDeck 을 vanilla JS 에서 bundled TS 프로젝트로 변환. 모든 .js source 를 .ts extension 으로 src/ 에 옮김. manifest.json + popup.html + panel.html + icons/ 를 static/ 으로 옮김. scripts/build.mjs (첫 번째 code block), package.json (두 번째), tsconfig.json (세 번째) 추가. npm install 실행, 다음 npm run build. dist/ folder 가 manifest.json + bundle 된 .js file + html + icon 포함해야. chrome://extensions 에서 'Load unpacked' target 을 clipdeck/ 에서 clipdeck/dist/ 로 swap. Reload — 모든 게 전과 같이 동작해야. 작은 TypeScript 변경 (type annotation 추가, chrome.storage.local.set 에 일부러 잘못된 type 전달) 후 npm run typecheck 실행 — tsc 가 error 잡는지 확인. npm run package 실행해서 upload-ready zip 생산.
Hint
npm run build 가 'Could not resolve @mozilla/readability' 로 error 면, npm install 이 package 포함 안 함 — devDependencies 에 있는지 확인하고 npm install 다시. Bundled content.js 가 page 에서 'Cannot find name chrome' 로 load 실패면, tsc 가 @types/chrome pick up 안 함 — tsconfig.json 에 "types": ["chrome"] 있는지 확인. Bundler 변환 후 popup 실패면, entry rename 했으면 popup.html 의 <script src="popup.js"> 도 update 했는지 확인 — esbuild 가 entryPoints 이름 기반으로 output, src/popup.ts 가 dist/popup.js 됨.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.