
모노레포 빌드시스템 도구와 그 필요성
monorepo
모노레포 빌드시스템 도구와 그 필요성
최근 디자인 시스템을 구현하면서 모노레포 환경을 구성하게 되었다. 처음에는
pnpm workspaces만으로도 충분할 것 같았다. 공통 컴포넌트 패키지를 분리하고, 여러 애플리케이션에서 이를 참조하도록 연결하는 정도라면 workspace만으로도 구조를 만드는 데 큰 문제는 없기 때문이다.실제로 모노레포를 처음 도입할 때 가장 먼저 보이는 장점도 이런 부분이다. 공통 코드를 한 저장소 안에서 관리할 수 있고, 패키지 버전 정합성을 맞추기 쉽고, 로컬에서 여러 패키지를 동시에 개발하기도 편하다. 디자인 시스템처럼 여러 앱이 동일한 UI 자산을 공유해야 하는 경우, 이런 구조적 이점은 꽤 크다.
하지만 프로젝트가 조금만 커지기 시작하면 다른 문제가 보이기 시작한다. 패키지를 하나의 저장소에 모아두는 것과, 그 패키지들을 효율적으로 빌드하고 테스트하고 배포하는 것은 전혀 다른 문제라는 점이다. 처음에는 모노레포를 “패키지를 한곳에 모으는 구조” 정도로 생각했지만, 실제로 운영해보면 핵심은 구조보다 실행에 가깝다. 특히 디자인 시스템처럼 공통 패키지의 변경이 여러 앱에 영향을 주는 구조에서는 더욱 그렇다.
이 지점에서 자연스럽게 모노레포 빌드시스템 도구를 다시 보게 되었다.
모노레포에서 진짜 중요한 문제
모노레포를 도입하면 패키지 수가 늘어난다. 그리고 패키지가 늘어나면 단순히 디렉터리만 많아지는 것이 아니라, 작업의 관계도 함께 늘어난다.
예를 들어 다음과 같은 구조를 생각해볼 수 있다.
apps/web: 메인 웹 애플리케이션
apps/admin: 관리자 애플리케이션
packages/ui: 디자인 시스템 컴포넌트
packages/eslint-config: 공통 ESLint 설정
packages/typescript-config: 공통 TypeScript 설정
packages/utils: 공통 유틸리티
이 정도만 되어도 이미 빌드와 테스트의 관점에서는 여러 관계가 생긴다.
web은ui와utils에 의존할 수 있다.
admin도ui에 의존할 수 있다.
ui가 변경되면web,admin모두 영향을 받을 수 있다.
- 공통 설정 패키지가 바뀌면 lint, typecheck 결과도 달라질 수 있다.
즉, 모노레포에서는 “무엇이 바뀌었는가”만 보는 것이 아니라 “그 변경이 어디까지 영향을 미치는가”를 함께 판단해야 한다. 작은 레포에서는 이걸 사람이 감으로 처리할 수 있지만, 패키지가 늘어나면 더 이상 수동으로 관리하기 어렵다.
이때부터는 다음과 같은 질문이 생긴다.
- 변경된 패키지만 다시 빌드할 수 없을까?
- 의존성이 있는 작업만 다시 실행할 수 없을까?
- 이전 실행 결과를 재사용할 수 없을까?
- CI에서도 이런 최적화를 적용할 수 없을까?
모노레포 빌드시스템 도구는 바로 이 질문에 대한 답이다.
pnpm workspace만으로는 왜 부족한가
pnpm workspaces는 매우 훌륭한 도구다. 패키지 링크를 효율적으로 관리하고, 디스크 공간도 절약하며, 의존성 구조도 명확하게 만든다. 하지만 본질적으로 pnpm workspace는 패키지 매니저의 기능 확장이지, 빌드 오케스트레이션 도구는 아니다.예를 들어 workspace만 사용할 때는 다음과 같은 식의 스크립트를 작성하게 된다.
처음에는 이 정도도 괜찮아 보인다. 하지만 곧 한계가 보인다.
첫째, 작업 순서를 사람이 직접 관리해야 한다.
둘째, 변경이 없어도 동일한 작업이 매번 다시 실행된다.
셋째, 어떤 작업이 실제로 필요한지 자동으로 판단하지 못한다.
넷째, CI에서도 매번 거의 전체 작업을 돌리게 된다.
즉, workspace는 “패키지를 어떻게 연결할 것인가”는 해결하지만, “작업을 어떻게 효율적으로 실행할 것인가”는 해결하지 않는다.
모노레포를 운영하면서 시간이 많이 드는 부분은 대부분 후자다. 결국 패키지 관리만으로는 부족하고, 그 위에서 동작하는 빌드시스템이 필요해진다.
모노레포 빌드시스템 도구가 하는 일
모노레포 빌드시스템 도구를 단순히 “빌드를 빠르게 해주는 도구”로 생각하면 핵심을 놓치게 된다. 실제로는 다음 세 가지를 함께 해결하는 도구에 가깝다.
1. 작업의 관계를 이해한다
어떤 작업이 어떤 다른 작업에 의존하는지 파악한다. 예를 들어
web의 build는 ui의 build에 의존할 수 있다. 빌드시스템은 이 관계를 바탕으로 실행 순서를 계산한다.2. 변경의 영향 범위를 계산한다
ui만 변경되었는지, 아니면 ui를 사용하는 앱들도 다시 검증해야 하는지 판단한다. 이 과정은 대규모 레포에서 매우 중요하다. 전체를 매번 다시 돌리는 것과, 영향받은 범위만 실행하는 것은 비용 차이가 매우 크다.3. 결과를 재사용한다
이미 같은 입력으로 실행한 작업이라면 다시 계산하지 않고 이전 결과를 재사용한다. 이 재사용의 품질이 곧 생산성으로 연결된다. 로컬 개발에서도, CI에서도, 같은 결과를 다시 만드는 데 시간을 쓰지 않게 되기 때문이다.
Lerna, Nx, Turborepo를 볼 때 무엇을 봐야 하나
모노레포 관련 도구를 찾아보면 흔히 Lerna, Nx, Turborepo가 함께 언급된다. 하지만 단순히 “무엇이 유명한가”보다, 각 도구가 어떤 문제를 얼마나 적극적으로 해결하는지를 보는 것이 더 중요하다.
Lerna
Lerna는 오래전부터 자바스크립트 모노레포에서 널리 쓰이던 도구다. 특히 패키지 버저닝, publish, workspace 기반 패키지 관리 경험을 정리하는 데 큰 역할을 했다. 다만 최근에는 작업 실행 최적화나 캐싱, 그래프 기반 분석 측면에서는 Nx와 함께 언급되는 경우가 많다.
Nx
Nx는 모노레포를 운영하는 데 필요한 기능을 폭넓게 제공한다. 태스크 오케스트레이션, 캐싱, 프로젝트 그래프 분석, 영향을 받는 프로젝트 추적, 원격 캐시, 분산 실행까지 포괄하는 편이다. 기능이 많고 강력한 대신, 도입 시 학습해야 할 개념도 적지 않다.
Turborepo
Turborepo는 상대적으로 간결하다. “작업 파이프라인 정의”와 “캐싱”이라는 핵심에 매우 집중해 있다.
pnpm과 함께 사용할 때도 구조가 단순하고, Next.js 기반 프로젝트나 디자인 시스템 중심 모노레포와도 잘 어울린다. 복잡한 코드 생성이나 프레임워크 통합보다, 실행 최적화에 초점을 맞추고 싶은 경우 접근성이 좋다.이번 글에서는
pnpm + Turborepo를 기준으로 설명해보려 한다. 실제로 내가 다시 구성해보려는 조합도 이쪽에 가깝다.Turborepo가 해결하는 핵심 문제
Turborepo를 이해할 때 가장 중요한 포인트는 “이 도구는 package manager가 아니라 task runner이자 build system이라는 점”이다.
Turborepo는 사용자가 정의한 태스크를 실행하면서, 그 실행의 입력과 출력을 기준으로 결과를 재사용한다. 단순히 여러 스크립트를 한 번에 돌리는 것이 아니라, 어떤 작업을 어떤 순서로 돌려야 하는지, 다시 돌릴 필요가 있는지, 이전 결과를 쓸 수 있는지를 판단한다.
대표적으로 이런 특성이 있다.
- 파일 내용 기준으로 캐시를 판단한다.
- 태스크 간 의존 관계를 정의할 수 있다.
- 병렬 실행을 적극적으로 수행한다.
- 원격 캐시를 통해 팀과 CI가 결과를 공유할 수 있다.
이 네 가지가 합쳐지면 체감이 꽤 크게 달라진다. 특히 디자인 시스템처럼 공통 패키지의 변경이 앱 전체에 전파되는 구조에서는 “어떤 작업은 반드시 다시 해야 하지만, 어떤 작업은 건너뛸 수 있다”는 판단이 매우 중요하다.
왜 파일 내용 기반 캐싱이 중요한가
빌드 최적화에서 흔히 오해하는 부분 중 하나가 “이전보다 빨리 실행되면 된다”는 관점이다. 실제로 중요한 것은 빠르기보다 정확한 생략이다.
Turborepo는 타임스탬프가 아니라 입력 파일의 내용과 환경 정보를 기반으로 캐시 키를 계산한다. 즉, 파일 수정 시간이 바뀌었다는 이유만으로 다시 실행하지 않고, 실제로 입력이 달라졌을 때만 작업을 다시 수행한다.
예를 들어 다음과 같은 상황을 생각할 수 있다.
- 개발자가 브랜치를 바꾸었다.
- 파일의 체크아웃 시점이 달라져 타임스탬프가 달라졌다.
- 하지만 실제 코드 내용은 이전과 같다.
타임스탬프 기반 시스템이라면 불필요한 빌드가 발생할 수 있다. 반면 내용 기반 시스템은 같은 결과를 재사용할 수 있다. CI에서 같은 커밋을 여러 번 검증하는 상황이나, 팀원이 이미 실행한 작업을 원격 캐시로 재활용하는 상황에서 이 차이는 꽤 크다.
예시 프로젝트 구조
실전 이야기를 하기 위해, 다음과 같은 예시 구조를 기준으로 설명해보겠다.
pnpm-workspace.yaml은 대체로 다음처럼 구성할 수 있다.루트
package.json은 이런 형태가 될 수 있다.여기서 중요한 것은 빌드, 린트, 타입체크, 테스트 같은 작업의 진입점을
pnpm이 아니라 turbo run으로 통일하는 것이다. 패키지 설치와 의존성 관리는 pnpm이 맡고, 작업 실행은 turbo가 맡는 식으로 역할을 분리하는 편이 명확하다.실전: turbo.json은 어떻게 설계해야 하나
이제 실제로 많이 궁금해지는 부분이
turbo.json이다. 단순히 공식 문서를 따라 기본 설정만 넣는 것보다, 각 태스크의 성격을 구분해서 설계하는 것이 중요하다.먼저 기본 형태부터 보자.
이 설정 하나하나가 의미를 가진다.
dependsOn: 작업의 선후 관계를 정의한다
가장 핵심적인 필드다.
여기서
^build는 현재 패키지가 의존하는 상위 패키지들의 build를 먼저 실행하라는 뜻이다.예를 들어
apps/web가 packages/ui를 참조하고 있다면, web의 build 전에 ui의 build가 선행된다. 이걸 수동 스크립트로 관리하면 곧 복잡해지지만, Turborepo에서는 의존 관계를 따라 자동으로 계산된다.실무적으로는
build에서 이 설정이 거의 필수에 가깝다. 반면 lint나 typecheck는 상황에 따라 다르다. 어떤 팀은 각 패키지 단위로 독립적으로 lint를 돌리고, 어떤 팀은 공통 설정 패키지의 변경도 영향을 준다고 보고 ^lint, ^typecheck를 추가하기도 한다.중요한 것은 “정답”이 아니라 “작업의 입력이 무엇인지”를 기준으로 판단하는 것이다.
outputs: 캐시 가능한 결과물을 명시한다
캐시를 제대로 활용하려면 어떤 파일이 작업 결과물인지 명확하게 알려줘야 한다.
이 설정이 의미하는 바는 다음과 같다.
ui패키지는dist/에 번들을 내보낼 수 있다.
- Next.js 앱은
.next/를 생성한다.
- Storybook 빌드는
storybook-static/을 생성할 수 있다.
Turborepo는 이 출력물을 기준으로 실행 결과를 저장하고, 캐시 히트 시 복원한다.
여기서 중요한 점은 출력 디렉터리를 실제 프로젝트 구조에 맞게 정확히 적어야 한다는 것이다. 예를 들어 어떤 패키지는
lib/를 쓸 수도 있고, 어떤 앱은 build/를 쓸 수도 있다. 이 값이 틀리면 캐시가 기대대로 동작하지 않거나, 반대로 불필요하게 넓은 범위를 잡아 캐시 효율이 떨어질 수 있다.cache: false: 캐싱하면 안 되는 작업을 구분한다
모든 작업이 캐시에 적합한 것은 아니다.
대표적인 예가 개발 서버 실행이다.
dev는 장시간 실행되는 프로세스이고, 결과물을 복원하는 종류의 작업이 아니다. 따라서 캐시를 끄고 persistent로 설정한다.비슷하게
clean도 보통 캐시 대상이 아니다.clean은 출력물을 삭제하는 작업이기 때문에, 캐시 대상이 되면 오히려 의미가 꼬일 수 있다.즉,
cache는 “속도를 위해 일단 켜는 옵션”이 아니라 “이 작업 결과가 재사용 가능한가”를 기준으로 판단해야 한다.앱과 라이브러리는 다르게 바라봐야 한다
turbo.json을 설계할 때 흔히 놓치는 부분은, 앱과 라이브러리의 태스크 성격이 다르다는 점이다.예를 들어
packages/ui는 보통 번들 결과가 재사용 가능하다. dist/가 명확한 출력물이고, 다른 앱이 이 결과에 의존한다. 반면 apps/web의 dev는 개발자 로컬 상태에 따라 달라지고, 실행 중 프로세스라서 캐싱 대상이 아니다.또
test도 종류에 따라 다르다. 예를 들어 단위 테스트는 캐싱하기 좋지만, 외부 API나 시간, 랜덤 값에 의존하는 통합 테스트는 캐시 적합성이 떨어질 수 있다. 따라서 모든 test를 무조건 캐시하기보다, 테스트 성격에 따라 분리하는 것도 고려할 만하다.예를 들면 이렇게 나눌 수 있다.
이런 식으로 태스크를 분리하면 캐시의 신뢰성과 속도를 동시에 가져가기 쉽다.
패키지별 scripts 설계도 중요하다
루트의
turbo.json만 잘 만든다고 끝나는 것은 아니다. 각 패키지에서 어떤 스크립트를 제공하느냐도 중요하다.예를 들어
packages/ui/package.json은 이런 식이 될 수 있다.apps/web/package.json은 조금 다를 수 있다.이렇게 패키지별로 같은 이름의 스크립트를 정의해두면, 루트에서
turbo run build, turbo run lint처럼 일관된 방식으로 실행할 수 있다. 모노레포에서는 이 일관성이 생각보다 중요하다. 실행 도구가 태스크를 조율해주려면, 패키지 간 인터페이스가 어느 정도 통일되어 있어야 하기 때문이다.캐시는 언제 깨지는가
캐시를 잘 쓰려면 “언제 캐시가 깨지는가”를 이해하는 것이 중요하다. 캐시가 자주 안 맞는다고 느껴질 때 대부분의 원인은 캐싱 원리를 정확히 이해하지 못한 데서 온다.
Turborepo에서 태스크 캐시는 대체로 다음 요소의 영향을 받는다.
- 입력 파일의 내용
- 의존 관계
- 환경 변수
- 실행 명령
- 출력물 설정
- lockfile 등 의존성 변경
즉, 단순히 소스 코드만 보는 것이 아니다.
1. 입력 파일이 바뀌면 캐시가 깨진다
가장 직관적인 경우다.
예를 들어
packages/ui/src/button.tsx를 수정했다면 ui의 build 캐시는 당연히 무효화된다. 그리고 web이 ui에 의존한다면, web의 관련 작업도 다시 실행될 수 있다.이건 캐시가 “깨진다”기보다, 정확하게 invalidation 되는 것이다. 오히려 이렇게 작동해야 신뢰할 수 있다.
2. lockfile이 바뀌면 의존성 관련 캐시가 무효화될 수 있다
예를 들어
pnpm-lock.yaml이 바뀌었다면, 실제 소스 변경이 없어도 빌드 결과가 달라질 수 있다. 패키지 버전이 달라졌기 때문이다.이 경우 캐시가 무효화되는 것은 자연스럽다. 로컬에서는 “코드는 안 바뀌었는데 왜 다시 빌드하지?”라고 느낄 수 있지만, 의존성도 입력의 일부라는 관점에서는 맞는 동작이다.
이런 이유로 CI에서는 lockfile이 바뀌는 PR과 그렇지 않은 PR의 실행 시간이 꽤 다를 수 있다.
3. 환경 변수가 바뀌면 캐시가 깨질 수 있다
이 부분은 실무에서 꽤 중요하다. 특히 Next.js 앱은 환경 변수에 따라 빌드 결과가 달라질 수 있다.
예를 들어
NEXT_PUBLIC_API_URL이 바뀌었다면, 동일한 소스 코드라도 결과물이 달라질 수 있다. 따라서 환경 변수를 태스크 입력의 일부로 다루는 것이 필요하다.프로젝트에 따라서는
turbo.json이나 각 태스크 설정에서 환경 변수를 명시적으로 관리해야 한다. 환경 변수를 입력으로 고려하지 않으면, 겉으로는 캐시가 잘 맞는 것처럼 보여도 실제로는 잘못된 결과를 재사용할 위험이 있다.실무 감각으로 말하면, “빌드 결과에 영향을 주는 모든 것”은 캐시 키에 반영되어야 한다.
4. outputs 설정이 잘못되면 캐시가 기대와 다르게 동작한다
예를 들어
build 결과가 실제로는 dist/에 생성되는데, outputs에 이를 적지 않았다면 캐시 복원이 제대로 되지 않을 수 있다.반대로 출력 범위를 지나치게 넓게 잡아도 문제다. 의도하지 않은 파일까지 결과물로 포함되어 캐시 안정성이 떨어질 수 있다.
그래서
outputs는 가능한 한 정확하게 적는 편이 좋다.좋지 않은 예:
이렇게 광범위하게 잡으면 어떤 파일이 진짜 산출물인지 불분명해진다.
좋은 예:
이렇게 실제 빌드 산출물만 명확히 적는 편이 낫다.
5. 비결정적 작업은 캐시와 잘 맞지 않는다
예를 들어 현재 시간, 랜덤 값, 외부 서비스 응답에 따라 결과가 달라지는 작업은 캐시와 궁합이 좋지 않다.
예를 들어 이런 스크립트가 있다고 해보자.
이 작업은 소스가 같아도 매번 결과가 달라진다. 이런 종류의 작업은 캐시가 의미를 잃거나, 오히려 혼란을 만든다.
빌드시스템을 설계할 때는 가능하면 태스크를 결정적으로 유지하는 것이 좋다. 즉, 같은 입력이면 같은 결과가 나오도록 만드는 편이 캐싱 효과를 극대화한다.
CI에서는 어떻게 최적화할 것인가
로컬에서 Turborepo를 쓰는 것만으로도 체감은 있지만, 진짜 큰 차이는 CI에서 드러난다. 모노레포에서 CI 시간이 길어지는 가장 큰 이유는 변경 범위와 상관없이 전체 태스크를 돌리기 쉽기 때문이다.
CI 최적화의 핵심은 세 가지다.
- 설치 시간을 줄인다
- 불필요한 태스크 실행을 줄인다
- 이전 결과를 재사용한다
1. 패키지 매니저 캐시와 빌드 캐시는 다르다
먼저 구분해야 할 것이 있다. CI에서 흔히 말하는 캐시는 두 종류다.
첫째는
pnpm store 같은 의존성 캐시다.
둘째는 Turborepo 태스크 결과 캐시다.이 둘은 역할이 다르다.
- 의존성 캐시:
node_modules또는 package store를 다시 설치하지 않게 해줌
- 빌드 캐시:
build,lint,test같은 작업 자체를 다시 실행하지 않게 해줌
실제로 CI 속도를 많이 줄이는 것은 두 캐시를 함께 쓰는 것이다.
2. GitHub Actions 예시
예를 들어 GitHub Actions에서는 다음과 같은 흐름으로 구성할 수 있다.
이 설정만으로도 기본 동작은 된다. 하지만 여기서 더 중요한 것은 원격 캐시다.
3. 원격 캐시를 붙이면 CI가 달라진다
로컬에서 이미 어떤 개발자가
build를 수행했고, 그 결과가 원격 캐시에 올라가 있다면, CI는 같은 입력에 대해 그 결과를 재사용할 수 있다. 반대로 CI가 먼저 돌린 결과를 다른 개발자가 로컬에서 가져올 수도 있다.즉, 팀 전체가 같은 작업 결과를 공유하게 된다.
Turborepo에서는 원격 캐시를 붙이면 이 효과를 얻을 수 있다. Vercel Remote Cache를 쓰거나, 프로젝트 환경에 맞는 원격 캐시 방식을 연결할 수 있다. 이 부분은 규모가 커질수록 체감 차이가 매우 커진다.
예를 들어 어떤 PR에서 실제 코드 변경은
web 쪽 문구 수정뿐인데, CI가 ui, admin, storybook, typecheck, test를 모두 다시 수행한다면 상당한 낭비다. 반면 영향받은 범위만 다시 실행하고 나머지는 캐시를 재사용하면 CI 시간이 눈에 띄게 줄어든다.4. 변경 범위 중심으로 실행하는 전략
모든 CI에서 항상 전체 태스크를 도는 것이 정답은 아니다. 브랜치 전략과 신뢰 수준에 따라, PR에서는 영향받은 범위만 검증하고 메인 브랜치에서는 전체 검증을 하는 식의 전략도 가능하다.
예를 들어 PR에서는 다음과 같은 관점을 취할 수 있다.
- 변경된 패키지와 그 의존자만 빌드
- lint, typecheck도 영향 범위 중심으로 수행
- 메인 브랜치 머지 전후에는 전체 통합 검증 수행
이 방식은 빠르고 실용적이다. 다만 그만큼 캐시와 태스크 정의가 정확해야 한다. 잘못 설계하면 “빠르긴 한데 놓치는 검증이 생기는” 문제가 생길 수 있기 때문이다.
결국 CI 최적화는 속도와 신뢰도의 균형 문제다. Turborepo는 속도를 위한 기반을 제공하지만, 실제로 어느 범위까지 생략할지는 팀의 품질 기준에 맞춰 설계해야 한다.
5. fetch-depth 0가 중요한 이유
GitHub Actions 예시에서
fetch-depth: 0를 넣은 이유도 가볍게 지나가면 안 된다.얕은 clone만 하면 변경 이력을 충분히 비교하지 못하는 상황이 생길 수 있다. 모노레포에서 변경 범위를 계산하거나 브랜치 간 차이를 정교하게 다루려면 전체 히스토리 또는 충분한 깊이의 이력이 필요한 경우가 많다.
즉, CI 최적화를 한다고 해서 무조건 checkout을 최소화하는 것이 항상 정답은 아니다. 최적화는 전체 실행 맥락 안에서 판단해야 한다.
실무적으로 추천하는 turbo.json 설계 방향
지금까지의 내용을 바탕으로, 실무에서 비교적 안정적으로 시작할 수 있는 방향을 정리해보면 다음과 같다.
1. 빌드 가능한 패키지와 실행형 앱을 구분하자
라이브러리는
build 산출물이 명확하므로 캐시에 적극적으로 태우기 좋다.
앱의 dev는 캐싱 대상이 아니다.
앱의 build는 산출물이 명확하다면 캐시 가능하다.2. 태스크 이름을 통일하자
모든 패키지에서
build, lint, typecheck, test, clean처럼 공통 이름을 맞춰두면 관리가 훨씬 편하다.3. outputs를 정확하게 적자
캐시 효율과 신뢰도는
outputs 품질에 크게 좌우된다. 실제 산출물 경로를 정확히 적는 것이 중요하다.4. 비결정적 작업은 분리하자
외부 API 의존, 랜덤 값, 현재 시간 기반 작업은 별도 태스크로 분리하거나 캐시를 끄는 편이 안전하다.
5. CI는 단계적으로 최적화하자
처음부터 지나치게 공격적으로 생략하기보다, 우선 원격 캐시를 붙이고, 그 다음 영향 범위 중심 실행을 도입하는 식으로 단계적으로 가는 편이 안정적이다.
예시: 비교적 현실적인 turbo.json
마지막으로, 디자인 시스템 중심의 모노레포에서 시작점으로 쓸 만한 예시를 정리해보면 다음과 같다.
이 설정이 완벽한 정답은 아니다. 프로젝트에 따라
lint는 독립적으로 돌리고 싶을 수 있고, typecheck는 상위 의존 빌드 없이도 가능할 수 있다. 중요한 것은 공식 예제를 그대로 복사하는 것이 아니라, 각 태스크가 어떤 입력과 출력, 어떤 성격을 가지는지 팀이 이해한 상태에서 설계하는 것이다.마무리
모노레포를 도입할 때 흔히 패키지 구조나 폴더 구성에 먼저 집중하게 된다. 물론 그것도 중요하다. 하지만 실제로 프로젝트가 커졌을 때 개발 경험을 결정하는 것은 구조 자체보다 작업 실행 방식이다.
pnpm workspace는 패키지를 잘 연결해준다.
하지만 연결된 패키지들을 얼마나 효율적으로 빌드하고 테스트하고 검증할지는 또 다른 문제다.
그 문제를 해결하는 것이 모노레포 빌드시스템 도구이고, Turborepo나 Nx가 필요한 이유도 여기에 있다.특히 디자인 시스템처럼 공통 패키지가 여러 앱에 영향을 주는 구조에서는 “변경된 것만 정확히 다시 실행하고, 같은 결과는 재사용하는 것”이 곧 개발 경험의 품질이 된다. 로컬 개발 속도도, CI 시간도, 팀 전체 생산성도 결국 이 지점에서 차이가 난다.
이번에 다시 모노레포 구성을 돌아보면서 느낀 것도 비슷했다. 처음에는 workspace만으로 충분하다고 생각했지만, 실제로는 빌드 시스템을 함께 설계해야 비로소 모노레포가 제대로 작동한다. 구조를 만드는 것과 운영 가능한 시스템을 만드는 것은 다르다.