[DevOps] 하나의 코드로 웹, 앱, 데스크탑을? pnpm과 Turborepo로 삽질하며 배운 것들
꿈: “JavaScript 하나로 세 마리 토끼를 다 잡을 수 있지 않을까?”
[ AI_Todo 프로젝트 개발기 - DevOps #1 ] JavaScript로 세 마리 토끼 잡는 방법
React 로 웹 개발을 시작했을 때, React Native 와 Electron 의 존재는 엄청난 가능성으로 다가왔습니다.
“어차피 다 JavaScript 인데, 공통 로직 (core) 만 잘 분리하고 각 플랫폼의 UI 렌더링 방식만 처리해주면, 코드 하나로 웹, 모바일 앱, 데스크탑 앱을 모두 쉽게 만들 수 있는 거 아닐까? tailwindCSS ? 오잉? 이것도 그냥 모든 플랫폼에서 다 같이 쓸 수 있는거 아니야? 시간 벌었죠? 개이득이죠?”
그 생각은 지극히 합리적이었지만, 동시에 너무나 순진했습니다. 아이디어를 현실로 옮기자마자, 거대한 복잡성의 벽에 부딪혔습니다.
현실: 모노레포의 세 가지 난관
프로젝트를 모노레포로 구성하자 문제는 눈덩이처럼 불어났습니다.
1. 의존성의 지옥
apps/web
, apps/desktop
, packages/core
, packages/ui
… 패키지가 늘어날수록 node_modules
는 비대해졌습니다.
더 큰 문제는 선별적 의존성이었습니다.
예를 들어, tailwindcss
는 web 과 desktop 에선 필요하지만, React Native 기반의 mobile 에선 styled-components
를 써야 했습니다.
이 미묘한 차이가 패키지 관리와 빌드 구성을 지옥으로 만들었습니다.
2. 끝나지 않는 빌드
단순히 npm run build
를 실행하면, 변경되지 않은 core
패키지까지 매번 새로 빌드를 해야만 했습니다.
웹과 core 패키지만 해도 이렇게 귀찮은데 모바일과 PC 개발에 대한 미래를 생각한다면 코드 한 줄 고치고 전체 빌드를 기다리는 시간은 생산성을 갉아먹는 주범이될 거라고 생각했습니다
3. 불투명한 의존성 그래프
web
은 ui
에 의존하고, ui
는 core
에 의존합니다. 이 의존성 순서를 제가 직접 기억하고 순서대로 빌드해야 했습니다.
“분명히 이걸 자동화해주는 똑똑한 방법이 있을 텐데…” 라는 생각이 들었고 좀 자동으로 알잘딱으로 개발할 수 없을까 생각했습니다….
탐험: 수많은 도구들 속에서 최적의 조합을 찾아서
이 문제를 해결하기 위해, 최신 프론트엔드 툴체인을 깊게 파고들기 시작했습니다.
크게 패키지 매니저와 빌드 오케스트레이터(Orchestrator), 두 축으로 나눠 리서치를 진행했습니다.
1: 패키지 매니저 - 누가 디스크 공간과 시간을 아껴주는가?
항목 | npm v10 | yarn berry (v3+) | pnpm v10 |
---|---|---|---|
스토리지 구조 | node_modules에 모두 복사 | 압축(.zip) 파일로 관리 (PnP) | 글로벌 스토어 + 하드링크 |
설치 속도 | Baseline | 비슷하거나 약간 빠름 | 압도적으로 빠름 (최대 2배 이상) |
디스크 사용량 | 1x (매우 큼) | 0.8x (약간 작음) | 0.2x (혁신적으로 작음) |
모노레포 지원 | workspaces 지원 | 강력한 workspaces | 강력 + workspace:* 프로토콜 |
결론은 pnpm
이었습니다.
디스크를 절약해주는 하드링크 구조도 매력적이었지만, 결정적인 이유는 예측 가능한 의존성 관리 때문이었습니다.
pnpm
은 유령 의존성(Phantom Dependency)
문제를 원천 차단하여, 각 패키지가 package.json
에 명시된 패키지에만 접근할 수 있도록 강제합니다.
이는 모노레포의 안정성을 극적으로 높여주었습니다.
2: 빌드 오케스트레이터 - 누가 ‘똑똑하게’ 빌드하는가?
후보 | 특징 | 꺼림직한 이유 |
---|---|---|
Lerna | 모노레포의 원조. 버전 관리 및 배포에 강점. | 빌드 캐싱 기능 부재. 더 이상 적극적으로 쓰이지 않는 분위기. |
Nx | 강력한 캐싱과 풍부한 플러그인 생태계. | 너무 많은 것을 해주려 함. 러닝커브가 높고 Nx 생태계에 대한 Lock-in 우려. |
Turborepo | 단순함. pipeline 문법이 직관적. 강력한 캐싱. | 버전 관리 및 배포 기능은 직접 구현해야 함. (하지만 내겐 아직 불필요) |
선택은 Turborepo
였습니다.
Nx
의 강력함은 분명 매력적이었지만, 1인 프로젝트인 제게는 과하다고 생각했습니다.
Turborepo
는 turbo.json
파일 하나로 “무엇을, 어떤 순서로, 어떻게 캐시할지” 를 선언적으로 관리할 수 있다는 점이 좋았습니다.
복잡한 설정 없이, “변경된 것만 다시 빌드한다” 는 핵심 가치에 가장 충실한 도구였습니다.
발견: pnpm과 Turborepo, 완벽한 한 쌍
이 둘의 조합은 1 + 1 > 2
의 시너지를 냈습니다.
pnpm
이 하드링크로node_modules
의 구조를 예측 가능하고 효율적으로 만듭니다.Turborepo
는 이 구조 위에서 파일 내용과pnpm-lock.yaml
의 해시값을 기반으로 변경 사항을 정확히 감지하고, 단 한 줄의 코드라도 변경되지 않은 패키지의 빌드/테스트 결과는 원격 캐시(Remote Cache)에서 그대로 가져옵니다.
turbo.json
의 pipeline
설정은 이 모든 것을 마법처럼 처리해줍니다.
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
// 이 패키지를 빌드하기 전에, 의존하는(^
// 모든 패키지들의 'build'를 먼저 실행하라.
"dependsOn": ["^build"],
// 빌드 결과물인 dist 폴더를 캐싱 대상으로 지정하라.
"outputs": ["dist/**"]
},
"test": {
// 테스트는 빌드 후에 실행되어야 한다.
"dependsOn": ["build"],
// 테스트 결과는 캐싱하지 않는다.
"outputs": []
},
"lint": {
// 의존성 없음. 독립적으로 실행.
}
}
}
결과: 숫자는 거짓말을 하지 않는다
이론을 현실로 옮겼습니다.
npm
에서 pnpm
으로 마이그레이션하고, 모든 scripts
를 turbo run ...
으로 교체했습니다.
결과는 놀라웠습니다.
지표 | 이전 (npm 단독) | 현재 (pnpm + Turborepo) |
---|---|---|
최초 의존성 설치 | 2분 30초 | 40초 |
전체 빌드 (No Cache) | 4분 10초 | 45초 |
변경된 파일 1개 빌드 (Cache Hit) | 4분 10초 | < 5초 |
CI 파이프라인 시간 | 18분 | 6분 (Cache Hit 시 3분) |
node_modules 디스크 사용량 | 1.2 GB | 260 MB |
교훈: 길고 긴 삽질의 끝에서
모노레포는 단순히 코드를 한곳에 모으는 것이 아니었습니다.
복잡성을 ‘관리’하는 기술이었습니다.
이번 여정을 통해 얻은 가장 큰 교훈은 두 가지입니다.
- 캐시가 왕이다: 빌드는 ‘변경된 것’만 해야 한다.
Turborepo
는 이 원칙을 현실로 만들어주었고, 제 기다림의 시간을 코딩의 시간으로 바꿔주었습니다.
- 도구는 짝이 있다
pnpm
의 예측 가능한 의존성 구조와Turborepo
의 정교한 캐싱 전략은 서로를 위해 태어난 것처럼 완벽하게 맞물렸습니다.
하지만 이 조합만으로 모든 문제가 해결되지는 않았습니다.
코드 품질과 순환 참조 가 남아있었습니다.
백엔드를 개발할 때는 IntelliJ 가 제공하는 강력한 순환 참조 분석 기능에 익숙했지만, VSCode 환경에서는 패키지 간의 의존성이 꼬여도 미리 알기 어려웠습니다.
지금은 혼자 개발하기에 제 머릿속 의존성 그래프에 의존할 수 있지만, 만약 미래에 동료가 합류한다면 어떨까요?
기존에 일하던 사람은 문제가 없지만 문제가 복잡해질수록 새로운 사람이 작업을 하게될 때 실수를 하게 되는 경우가 많습니다. 또한 이런 실수는 그 사람 잘못으로 평가되기 쉽다고 생각합니다.
물론 어느정도 맞는 말이지만 개인적으로 이런 부분을 컴퓨터로 자동화해서 편하게 만드는거야말로 개발자로서의 진정한 특권이라고 생각합니다.
저는 이런 특권을 최대한 힘 안들이고 자동화하면서 어떻게하면 편하게 누릴 수 있을 까를 고민했고,
저는 Husky
와 ESLint
가 있~다 는 거 슬…. 알아버렸습니다
다음 글에서는 이 도구들을 활용해 어떻게 커밋 단계에서 코드 품질을 자동으로 검사하고 잠재적인 오류를 차단하는지, 그 최적화 과정을 공유해 보겠습니다.
댓글남기기