Development··5 min read

pnpm workspace로 모노레포 세팅한 과정

pnpm workspace 기반 모노레포를 처음부터 세팅하면서 겪은 시행착오. 공식 문서만으로는 부족했다.

npm에서 pnpm으로 넘어간 이유

node_modules가 프로젝트마다 1.2GB씩 차지하는 게 미쳐버릴 것 같았다. 프로젝트 3개를 열어두면 디스크에서 3.6GB가 node_modules에 잡힌다. pnpm은 글로벌 스토어에 패키지를 한 번만 저장하고 심볼릭 링크로 연결한다. 이걸 알고 나서 바로 전환했다.

설치 속도도 빨라졌다. npm으로 87초 걸리던 게 pnpm으로 23초. 이유가 있는 게, 이미 글로벌 스토어에 있는 패키지는 다운로드를 안 하니까.

pnpm workspace 세팅

pnpm-workspace.yaml 파일 하나 만들면 끝이다. 처음엔 그렇게 간단한 줄 알았다.

패키지 3개를 만들었다. packages/web(프론트), packages/api(백엔드), packages/shared(공유 유틸). workspace 설정 자체는 5분 만에 끝났다. 근데 그 다음부터 문제가 시작됐다.

TypeScript 경로 설정 지옥

shared 패키지를 web에서 import하려면 TypeScript가 경로를 알아야 한다. tsconfig의 paths 설정을 해야 하는데, 이게 패키지마다 따로 해줘야 한다. 루트 tsconfig에서 해도 되고 각 패키지에서 해도 되는데, 어디서 하든 처음엔 잘 안 됐다.

한 시간 동안 삽질하다가 알아낸 건, shared 패키지의 package.jsonmaintypes 필드를 제대로 설정해야 한다는 거였다. 공식 문서에 이 부분이 불친절하다.

(이거 때문에 Stack Overflow를 4개 탭 열어놓고 비교했다.)

스크립트 관리

루트에서 pnpm -r run build 하면 모든 패키지가 빌드된다. 순서가 중요한데, shared가 먼저 빌드되고 나서 web과 api가 빌드돼야 한다. pnpm이 dependency를 보고 자동으로 순서를 잡아주긴 하는데, 처음에 shared의 빌드 스크립트를 안 만들어놔서 의존성 해석이 안 됐다.

pnpm --filter web dev 같은 필터 기능은 좋다. 특정 패키지만 골라서 스크립트를 실행할 수 있다. 근데 필터 문법을 외우는 데 시간이 좀 걸렸다. --filter web...는 web과 web의 의존성 모두를 포함하고, --filter web는 web만. 점 세 개의 차이가 꽤 크다.

버전 관리의 고민

모노레포에서 패키지 버전을 어떻게 관리할지 고민이 됐다. 방법이 여러 가지다. changeset을 쓰는 방법, 전체를 한 버전으로 관리하는 방법, 각각 독립적으로 관리하는 방법.

우리는 내부용이라 npm에 배포할 일이 없어서 버전 관리를 안 했다. 근데 이게 나중에 문제가 됐다. shared를 수정했는데 web에서 오래된 빌드 결과물을 캐시에서 가져다 쓰는 바람에 변경이 반영이 안 됐다. 결국 빌드할 때마다 shared를 강제로 다시 빌드하는 스크립트를 넣었다. 깔끔하진 않다.

CI에서의 문제

GitHub Actions에서 pnpm을 쓰려면 setup-pnpm 액션을 추가해야 한다. 이것도 5분이면 끝나는 줄 알았는데, pnpm 버전과 Node.js 버전 조합에 따라 동작이 달라져서 한 번 실패했다. CI에서 pnpm install --frozen-lockfile을 써야 lock 파일 변경 없이 설치되는데, 이걸 안 넣으면 PR마다 lock 파일이 변경돼서 diff가 지저분해진다.

돌아보면

pnpm workspace 자체는 단순하다. 근데 TypeScript, 빌드 도구, CI까지 엮이면 초기 세팅에 이틀은 잡아야 한다. 한 번 세팅하면 편하긴 한데, 그 "한 번"이 생각보다 길다.

다음에 세팅할 때는 이 글을 다시 읽으면서 하면 반나절이면 될 것 같다. 아마.

관련 글