avatar
Published on

npm workspace와 esbuild로 monorepo 구축해보기

Author
  • avatar
    Name
    yceffort

매번 느끼는 거지만 자바스크립트 생태계는 진짜 쉴새 없이 변한다. 하루에도 수십 수백가지의 패키지가 만들어지고, 또 잘나가는 프로젝트는 오늘도 버전업과 기능 추가에 여념이 없다.

그런 와중에 내 눈에 들어온 것이 npm workspace와 esbuild다. npm workspace는 과연 lerna의 아성을 넘을 만큼 잘만들어졌을까? esbuild는 또 걔네들이 말하는 것처럼 엄청 빠를까?

예제 레파지토리

https://github.com/yceffort/workspaces-esbuild-example

npm workspace

npm v7 이 정식으로 나오면서 모노레포를 지원하게 되었다. 원래 monorepo는 lerna와 yarn이 꽉잡고 있던 영역이었는데, 이번에 npm이 등장하게 되면서 npm cli로도 workspace를 활용하면 모노레포를 구축할 수 있게 되었다.

https://docs.npmjs.com/cli/v7/using-npm/workspaces

이 기능을 활용하면, 로컬 파일시스템에서 연결된 패키지를 훨씬 더 효율적으로 관리할 수 있게 해준다. 기존에 원래 있던 명령어인 npm install을 활용하면 자동으로 패키지를 link 해주고, 서로 다른 패키지 레벨에서 npm link할 필요 없이 알아서 현재 폴더의 node_modules를 가지고 연결해준다.

npm workspace를 어떻게 구축하는지 먼저 살펴보자.

{
  "name": "@yceffort/monorepo",
  "version": "0.0.1",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build:all": "npm run build --workspaces",
    "deploy:all": "npm run deploy --workspaces",
    "lint": "eslint '**/*.{js,ts,tsx}'",
    "lint:fix": "npm run lint -- --fix",
    "prettier": "prettier '**/*.{json,yaml,md}' --check",
    "prettier:fix": "prettier '**/*.{json,yaml,md}' --write"
  },
  "author": "yceffort",
  "license": "ISC",
  "devDependencies": {
    "esbuild": "^0.12.12",
    "esbuild-node-externals": "^1.3.0",
    "eslint-config-yceffort": "0.0.5",
    "typescript": "^4.3.4"
  },
  "workspaces": ["./packages/*"]
}

먼저 프로젝트 루트 디렉토리에서 workspaces를 정의해줘야 한다. 위 예제에서는, packages 디렉토리 이하에 있는 프로젝트를 각각 모노레포의 모듈로 가져가기 위해 설정해두었다.

그리고 모노레포로 가져갈 레파지토리에 package.json을 설정해 둬야 한다. 이는 일반적인 패키지 설정과 별 차이가 없다.

여기서 npm install을 해보자.

한가지 중요한 것은 (당연한 이야기지만) npm v7 이어야 한다는 것이다. 이 실험을 위해서 무작정 npm v7을 설치하는 것은 권장하지 않는다. v7의 또한가지 변경점은 package-lock.json의 관리방식이 변경되었다는 것이다. (lock-version이 2로 올라갔다) 그래서 npm v7 을 버전업 한 후에 다른 프로젝트에서 npm i 를 날리면 package-lock.json에 무지막지한 diff 가 생성될 것이다. 이를 방지하기 위해 npx npm@7 를 사용하도록 하자. 뭐, 다른 프로젝트도 lockversion v2를 가져가도 괜찮다면 상관없다.

npm-workspace

여러개의 package.json이 있지만, node_modules는 루트에만 생성된 것을 볼 수 있다. 하위 패키지들의 참조는 모두 이 루트의 node_modules로 이어져 있다. (앞서 장황하게 설명한 그것)

이러한 패키지 설정을 매번 수동으로 할 필요는 없다.

npm init -w ./packages/some-package

이렇게 하면 알아서 하위 패키지를 만들어둔다. 단, 루트에 있는 package.jsonworkspaces 값이 새롭게 추가된 패키지도 가르키고 있는지 확인해야 한다. 난 그게 귀찮아서 *로 처리했다.

만약 워크 스페이스에 의존성을 설치하고 싶다면,

npm install react -w some-package

와 같은 방식으로 하면 된다. 물론, package.json에 직접 방문해서 루트에서 npm install을 설치해도 된다.

esbuild

자바스크립트는 느리다. 애초에 이렇게까지 쓰기 위해서 설계된 언어가 아니기 때문이다. 애초에 웹페이지에 있는 폼이나, 간단한 연산 정도만 처리할 용도로만 만들어졌기 때문이다. (싱글 스레드) 따라서 번들러가 느린 것도 어느정도 어쩔 수 없는 문제(?) 로 다들 받아드리고 있었다. 그런데, 아예 번들러를 저수준의 다른언어인 GO로 만들어버려서 이 속도문제를 해결한 것이 바로 esbuild다.

https://esbuild.github.io/

이 esbuild로 모노레포를 한번 만들어보려고 한다.

https://esbuild.github.io/getting-started/#your-first-bundle

// esbuild
const esbuild = require('esbuild')
// 빌드시에 자동으로 node_modules를 제외 해준다.
// https://github.com/pradel/esbuild-node-externals
const { nodeExternalsPlugin } = require('esbuild-node-externals')

esbuild
  .build({
    entryPoints: ['./src/index.ts'],
    outfile: 'dist/index.js',
    bundle: true,
    minify: true,
    platform: 'browser',
    format: 'esm',
    sourcemap: true,
    target: 'es6',
    plugins: [nodeExternalsPlugin()],
  })
  .catch(() => process.exit(1))

처음 설정을 하고 느꼈던 첫인상은, webpack이나 rollup 처럼 json 설정이 불가능하다는 것이다. .babelrc와 같은 cosmicconfig로 설정파일을 만드는 것이 불가능하다.

본격적으로 설정파일을 하나씩 파헤쳐보자.

  • entryPoints: 번들링 알고리즘이 들어가게 되는 애플리케이션의 entry 포인트다. 보시다시피 ts가 자동지원 되기 때문에 (다 지원되는 건 아니다.) 타입스크립트 파일을 넣어도 무방하다.
  • outfile: 번들의 결과물이다. entryPoints와는 다르게, 딱 하나의 파일만 (문자열만) 가능한 것을 볼 수 있다. 단하나의 번들된, 그리고 minified된 파일이 나오게 된다.
  • bundle: 번들링 여부
  • minify: minification (자바스크립트 파일 축소) 여부
  • platform: 번들링된 파일이 어느 환경에서 실행될지를 결정하게 된다.
  • format: 생성된 파일의 형태를 나타낸다. iife, cjs esm이 가능하다.
  • sourcemap: 디버깅을 용이하게 해주는 소스맵 제공 여부
  • target: 어떤 플랫폼의 버전에서 사용할 수 있을지 명시한다. 가능한 옵션은 https://esbuild.github.io/content-types/#javascript 여기에 있다.

위 설정대로 esbuild를 실행해보자.

sum.ts

export default function sum(...args: number[]) {
  return args.reduce((prev, acc) => acc + prev, 0)
}

formatNumber.ts

export default function formatNumberWithComma(value: string | number): string {
  if (typeof value === 'string' && isNaN(+value)) {
    return value
  }

  let formattedNumber = `${value}`

  const reg = /(^[+-]?\d+)(\d{3})/

  while (reg.test(formattedNumber)) {
    formattedNumber = formattedNumber.replace(reg, '$1,$2')
  }

  return formattedNumber
}

index.ts

export { default as sum } from './sum'
export { default as formatNumberWithComma } from './formatNumber'

결과

(minified된 파일을 보기 쉽게 하기 위해 unminifed함.)

function m(...r) {
  return r.reduce((t, e) => e + t, 0)
}

function o(r) {
  if (typeof r == 'string' && isNaN(+r)) return r
  let t = `${r}`,
    e = /(^[+-]?\d+)(\d{3})/
  for (; e.test(t); ) t = t.replace(e, '$1,$2')
  return t
}
export { o as formatNumberWithComma, m as sum }
//# sourceMappingURL=index.js.map

삽질하면서 깨달은 것들, 그리고 감상

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "baseUrl": ".",
    "rootDir": "src",
    "outDir": "./dist",
    "declarationDir": "./dist",
    // 이 아래 두개가 중요
    "emitDeclarationOnly": true,
    "declaration": true,
    "types": ["jest", "node"]
  },
  "include": ["./src/**/*.ts"],
  "exclude": ["node_modules", "dist"]
}
  • css module 번들링이 아직 완벽하지는 않은 것 같다. https://github.com/evanw/esbuild/issues/20 현재 제작자가 최선을 다해서(?) 작업중이라고 한다.
  • 그 밖에도 아직 작업중인 것들이 많다. 0.x 버전인데에는 이유가 있었다.
  • npx npm@7을 매번 해주는 것이 너무 귀찮았다 (...) 가끔 이 사실을 까먹고 workspace를 쓰면 당연히 안된다. default로 7을 쓰고 싶지만, 다른 프로젝트 때문에 쓰지 못해서 아쉬웠다. 물론, nvm을 써서 node@16을 쓰고, 여기의 기본 npm 버전에 의존하는 방법 (7.18.1)도 있었지만, 생각만큼 잘되지는 않았다.
  • 방금 예제는 정말 간단한 패키지라서 속도를 체감할 수는 없었지만, 큰 패키지를 대상으로 실험해본 결과 정말로 크게 속도차이가 나긴 했다.
  • webpack 환경에서도 사용할 수 있도록 esbuild-loader가 존재한다. 앞서 살펴본것처럼, minify, uglify도 esbuild가 해주기 때문에 terser를 대체할 수 있다.
  • 제법 많은 플러그인들이 존재했다. https://github.com/esbuild/community-plugins
  • 개발자가 혼자서 고군 분투 중이었다. 정말 멋있었다. (존경)
  • esbuild로 SSR도 가능한 것처럼 보인다. https://github.com/egoist/maho 깊게 살펴보진 않았지만

웹팩은 이미 메이저버전이 5까지 나와있을 정도로 성숙한 프로젝트고, 또 많은 사람들이 널리 사용하고 있는 프로젝트다. (롤업과 parcel도 잊으면 안된다) 하지만 기존의 당연시 생각되는 것들을 깨는 새로운 것의 등장은 언제나 보는 사람으로 하여금 설레게 하는 것 같다. esbuild도 그런 프로젝트 중 하나로, 정식 버전 업데이트 까지 잘 만들어졌으면 좋겠다.

아, workspace도 npm 생태계에 모노레포를 잘 녹여낸 것 같아서 좋았다. lerna보다는 쓰기 편한 것 같은 느낌?하지만, npm@7에서 workspace말고 그외에는 글쎄... 🤔

https://blog.logrocket.com/whats-new-in-npm-v7/