tech

웹팩보다 100배 빠른 번들러, esbuild

이번 포스팅은 떠오르는 차세대 자바스크립트 번들러 esbuild에 대한 내용입니다.

작년 Github에서 떠오르는 번들링 프로젝트 중 1위를 차지했고, 오늘을 기준으로 20만개의 가까운 Github Star를 받았습니다.

웹팩보다 100배 빠르다는 건 어그로가 아닙니다. 아래 그림을 봐주시죠.

위 벤치마크는 메이저 자바스크립트 번들러들의 빌드 타임을 비교한 표입니다.

아니 어떻게 이렇게 빠를 수가 있냐구요? 이유는 이러합니다.

  • Go 언어로 작성됨
  • 코드 파싱, 출력과 소스맵 생성을 모두 병렬로 처리함
  • 불필요한 데이터 변환과 할당 없음

하지만 아직 1.0 버전 출시 전이기 때문에, 많은 기능을 제공하고 있지는 않습니다. 현재 지원되는 기능은 이렇습니다.

  • CommonJS, ES6
  • JSX
  • Typescript
  • Tree shaking
  • Source Map
  • Minification
  • 등등 더 많음

훌륭합니다. 사실 이 정도만 지원되도 사용하기에 부족함이 없습니다. 오히려 빌드 타임이 너무 빨라서 프로젝트 규모가 커질수록 이득이죠.

esbuild는 es5 이하의 문법을 아직 100% 지원하지 않습니다. 즉 완벽한 인터넷 익스플로러 대응이 어렵습니다. IE 대응을 하려면 다른 대안을 찾는 것이 좋겠습니다.

꼭 알아야 할 내용은, esbuild는 자바스크립트를 위한 번들러입니다. 타입스크립트의 타입 체킹이나 프론트엔드 언어 (Vue, Angular) 지원, 핫 모듈 리로딩을 포함한 개발 서버 오픈 등 번들링과 관계 없는 기능들은 일체 없습니다. 그래서 저는 이 툴이 마음에 듭니다.

esbuild 사용해보기

먼저 프로젝트를 초기화 해줍시다.

$ mkdir esbuild
$ cd esbuild
$ yarn init -y
$ yarn add -D esbuild

esbuild는 번들러이기 때문에 최초 진입할 파일 1개가 필요합니다. 그것도 같이 만들어주도록 하겠습니다.

$ mkdir src
$ touch src/main.js

저는 이번 포스팅에서 동물들을 찍어내고, 동물들의 울음소리를 출력하는 그러한 클래스를 만들어보겠습니다.

src/main.js
class Animal {
  constructor(sound) {
    this.sound = sound
  }

  Bark() {
    console.log(this.sound + '!')
  }
}

먼저 동물이라는 기본 베이스가 될 클래스를 만들고, 짖는 소리를 내는 Bark라는 함수를 만들었습니다.

다음은 이 동물을 상속받는 를 한 번 만들어보도록 하겠습니다.

src/main.js
class Animal {
  constructor(sound) {
    this.sound = sound
  }

  Bark() {
    console.log(this.sound + '!')
  }
}

class Dog extends Animal {
  constructor() {
    super('멍멍')
  }
}

new Dog().Bark()
// 멍멍!

간단하게 개가 짖는 것 까지 만들었습니다. 그럼 바로 번들링을 해보도록 하겠습니다.

package.json
{
  "scripts": {
    "build": "esbuild src/main.js --bundle --outdir=dist"
  }
}

기존 package.json에 scripts 키를 추가하고, build라는 명령어를 실행시키는 스크립트를 작성했습니다. --bundle 옵션은 번들링을 하겠다는 뜻이고, --outdir=dist는 최종 결과물 파일을 dist 폴더 아래에 넣겠다는 뜻입니다.

그럼 빌드를 해봅시다.

$ yarn build # npm run build

# Log
$ esbuild src/main.js --bundle --outdir=dist
✨  Done in 0.05s.

작성한 코드가 거의 없긴 하지만, 0.05초는 정말 빠르네요.

결과물 파일은 이렇습니다.

dist/main.js
;(() => {
  // src/main.js
  var Animal = class {
    constructor(sound) {
      this.sound = sound
    }
    Bark() {
      console.log(this.sound + '!')
    }
  }
  var Dog = class extends Animal {
    constructor() {
      super('\uBA4D\uBA4D')
    }
  }
  new Dog().Bark()
})()

애초에 ES6로 작성해서 그런지, 딱히 바뀐 건 크게 없어보입니다. 한글로 된 부분은 유니코드로 변환되었고, 상단에 코드의 출처를 주석으로 달아주었네요.

번들링을 좀 더 자세히 알아보자

용도에 맞게 번들링을 시작하기 전에 알아두면 좋은 개념이 있습니다. 바로 format 입니다. 이 포맷에 관한 내용은 esbuild 뿐만 아니라, 다른 번들러들에서도 사용되는 개념이니 알아두면 좋습니다.

포맷은 용도에 따라 3가지로 나눌 수 있습니다.

  • iife
  • cjs
  • esm

iife 는 immediately-invoked function expression의 약자이고, 브라우저에서 동작하는 포맷입니다.

cjs는 CommonJS라는 뜻이고, Node에서 default로 동작하는 포맷입니다.

마지막으로 esm은 ECMA Script라는 뜻으로, 브라우저와 노드 양쪽 모두에서 사용 가능한 포맷입니다.

내 코드가 브라우저랑 노드 양쪽 다 지원하면 좋으니까 포맷을 무조건 esm으로 해야지~ 라고 생각할 수 있지만, 당연히 결과물의 코드 양이 많아집니다. 용도에 맞게 포맷을 지정해서, 불필요하게 최종 결과물의 크기를 커지지 않도록 합시다.

esbuild에선 platform 이라는 옵션을 줘서 방금 소개한 format을 자동으로 지정해줍니다.

기본적으로 브라우저에서 사용 가능하도록 번들링하는 browser가 기본이고, 이 경우 iife포맷으로 트랜스파일링합니다.

만약 node에서만 사용 가능하도록 번들링하고 싶다면 platform 옵션을 node로 주면 됩니다. 이 경우 포맷은 cjs입니다.

아니면 브라우저와 노드 양쪽 모두에서 사용하고 싶을 땐 platform 옵션을 neutral로 설정합시다. 이 경우 포맷은 esm입니다.

저는 방금 만든 이 코드를 node에서만 사용할 예정이라, 아까 설정한 빌드 명령어를 바꾸도록 하겠습니다.

package.json
{
  "scripts": {
    "build": "esbuild src/main.js --bundle --outdir=dist --platform=node"
  }
}

빌드 스크립트 작성하기

벌써부터 빌드 명령어가 한 줄로 길어져서 보기가 안좋습니다. 앞으로 어떤 옵션이 더 추가될지 모르는데 이런 방식은 지양하는게 좋습니다.

보통 다른 툴들은 .js.json 형태의 설정 파일을 만들면, 해당 파일을 자동으로 읽어서 빌드를 실행합니다. 하지만 esbuild는 자바스크립트 모듈을 직접 실행시키는 방법을 사용합니다.

바로 스크립트를 작성해보도록 하겠습니다.

$ mkdir scripts
$ touch scripts/build.js
scripts/build.js
require('esbuild')
  .build({
    entryPoints: ['src/main.js'],
    outdir: 'dist',
    bundle: true,
    platform: 'node'
  })
  .catch(() => process.exit(1))

프로젝트에 scripts 폴더를 만들고, 그 아래 build.js 파일을 만들었습니다. 아까 명령어를 이해했으면, 이 설정 값도 직관적으로 바로 이해가 됩니다.

다음은 package.json에서 esbuild CLI가 아닌, 방금 작성한 자바스크립트를 실행시키게끔 코드를 변경해줍니다.

package.json
{
  "scripts": {
    "build": "node scripts/build.js"
  }
}

다시 yarn build 명령어로 빌드를 해보면, 같은 결과가 나옵니다.

코드 모듈화하기

우리가 만든 동물 클래스를 외부로 내보내진 않았기 때문에, 모듈은 아닙니다.

모듈로 만들어야 하는 경우는, 다른 프로젝트에서 이 동물 클래스를 사용하게 만들고 싶을 때입니다. npmjs.com에 올라온 패키지들이 전부 모듈인 것이죠.

모듈로 만드는 방법은 굉장히 간단합니다. 선언 시, export라는 접두어를 붙이면 됩니다.

src/main.js
export class Dog extends Animal {
  constructor() {
    super('멍멍')
  }
}

Dog 클래스에 export 접두어를 붙여주었습니다. 이 상태에서 빌드를 하면 다른 노드 프로젝트에서 이런 식으로 불러서 쓸 수 있게 됩니다.

test.js
import { Dog } from './dist/main'

new Dog().Bark()

루트 디렉토리에 test.js를 만들었고, 잘 작동하는지 테스트하기 위해 실행시켜줍시다.

$ node test.js

import { Dog } from './dist/main'
^^^^^^

SyntaxError: Cannot use import statement outside a module

오류가 발생했습니다. 이 문제는 Node가 자바스크립트를 읽을 때 CommonJS 방식으로 해석하기 때문에 발생하는 문제입니다. 우리가 번들링한 방식은 ECMA Script 포맷이었죠.

타입스크립트

그렇다면 어떻게 테스트 하느냐?

바로 자바스크립트 트랜스파일러인 Babel을 사용하면 됩니다. 하지만 타입스크립트를 도입한다면 바벨을 쓰지 않더라도 어느정도는 해결이 가능합니다.

그래서 여기에서 typescript를 이용하면 바벨 없이 런타임에 Node를 실행시키면서, 다른 유저들을 위한 .d.ts 파일까지 지원 가능해집니다. 심지어 개발 단계에서 타입 체킹까지 가능하니 일석삼조입니다.

고맙게도 esbuild는 확장자가 .ts인 파일에 대해 자동으로 처리해줍니다.

그럼 아까 만든 파일의 확장자를 .ts로 바꿔줍시다.

src/main.ts
export interface IAnimal {
  sound: string
}

class Animal implements IAnimal {
  sound: string

  constructor(sound: string) {
    this.sound = sound
  }

  Bark() {
    console.log(this.sound + '!')
  }
}

export class Dog extends Animal {
  constructor() {
    super('멍멍')
  }
}

이렇게만 해도 번들링은 잘 되지만, esbuild는 타입스크립트의 타입 체킹을 빌드할 때 해주지는 않습니다. 단순히 코드를 읽어서 바꿔주기만 하는 것이죠. 또 .d.ts 파일을 만들어주지도 않습니다.

공식 문서에선 esbuild가 번들링에만 치중하기 때문에, 앞으로도 지원할 가능성은 매우 낮다고 얘기합니다.

제대로 타입스크립트를 활용하려면 몇 가지 패키지를 설치하고, 설정 파일인 tsconfig.json도 필요합니다.

$ yarn add -D typescript ts-node @types/node
$ node_modules/.bin/tsc --init

두 번째 명령어를 실행하면 tsconfig.json 보일러 플레이트를 만들 수 있고, 저는 이렇게 사용하도록 하겠습니다.

tsconfig.js
{
  "compilerOptions": {
    "target": "ES6",
    "module": "commonjs",
    "outDir": "dist",
    "declaration": true,
    "emitDeclarationOnly": true,
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["./src/**/*"]
}

여기선 두 가지가 중요합니다.

declaration 옵션은 d.ts 파일을 만들겠다는 뜻이고, emitDeclarationOnly는 tsc 내장 번들링을 사용하지 않고, 단순 타입 체킹만 하겠다는 뜻입니다.

그럼 다음으로 CLI를 실행할 빌드 스크립트로 수정해줍시다.

package.json
"scripts": {
  "build": "tsc && node scripts/build.js"
},

이제 명령어를 실행하면 tsc로 타입 체킹을 한뒤, 문제가 없다면 d.ts 파일을 만들고, 그 이후 esbuild를 이용해 빠르게 번들링하는 과정이 진행됩니다.

마무리

여기까지 간단하게 esbuildtypescript를 이용해 아주 간단한 모듈을 만드는 것 까지 알아보았습니다.

참고