Turborepo 운영 고도화: remote cache, CI 최적화, shared package 전략

Frontend

이 글은 Turborepo 시리즈 3편입니다.

앞선 글에서 Turborepo가 왜 필요한지, 그리고 pnpm + Turborepo + Next.js 조합으로 어떻게 시작할 수 있는지 정리했다면, 이번에는 그 다음 단계인 운영 고도화를 봐야 합니다.

실무에서 진짜 차이가 나는 지점은 보통 여기입니다.

  • remote cache를 붙였는데 왜 cache hit가 기대만큼 안 나는가
  • CI는 한 줄로 돌릴지, job을 나눌지, filter를 쓸지
  • shared package를 늘릴수록 왜 오히려 build 범위가 넓어지는가
  • 공통 설정 패키지가 왜 레포 전체를 흔드는가

즉, Turborepo 운영의 핵심은 "더 빠르게 돌린다"보다, 무엇이 왜 다시 실행되는지 팀이 설명할 수 있는 상태를 만드는 것에 더 가깝습니다.

한눈에 보면

이번 글의 핵심은 세 가지입니다.

  • remote cache: 같은 task를 팀과 CI가 반복하지 않게 만드는 것
  • CI 최적화: 전체 실행과 부분 실행의 경계를 나누는 것
  • shared package 전략: 공통화와 변경 영향 범위 사이의 균형을 잡는 것

조금 더 자세히 보면 아래처럼 볼 수 있습니다.

운영 포인트 무엇을 봐야 하나
remote cache 같은 입력이면 같은 결과를 안전하게 재사용하는가
CI 어떤 job을 병렬화하고 어떤 job은 통합할 것인가
shared package 공통화가 생산성인지, 영향 범위 확장인지
turbo.json inputs/outputs가 실제 레포 구조를 잘 반영하는가
DX 개발자가 루트 명령만으로도 흐름을 이해할 수 있는가

빠르게 정리하면, Turborepo 운영은 캐시를 붙이는 일보다 task 경계와 package 경계를 명확히 하는 일에 더 가깝습니다.

1. remote cache는 왜 중요한가?

로컬에서만 개발할 때는 Turborepo의 캐시 효과가 제한적으로 느껴질 수 있습니다. 하지만 팀과 CI를 같이 보면 상황이 달라집니다.

예를 들어 같은 PR에서 아래 일이 반복될 수 있습니다.

  • 개발자 로컬에서 web build
  • CI에서 web build
  • preview 배포 단계에서 다시 web build

입력이 같은데 같은 계산을 여러 번 하는 셈입니다.

이때 remote cache를 붙이면 팀 전체가 task 결과를 공유할 수 있습니다.

2. remote cache는 어떻게 붙이게 될까?

실무에서는 보통 아래 두 흐름 중 하나를 보게 됩니다.

1. 로그인 기반 연결

turbo login
turbo link

이 방식은 비교적 빠르게 시작하기 좋습니다.

2. 환경 변수 기반 연결

CI나 팀 환경에서는 보통 환경 변수로 다루게 됩니다.

TURBO_TEAM=repo
TURBO_TOKEN=***

필요하면 custom remote cache 서버를 기준으로 API URL을 지정하는 경우도 있습니다.

TURBO_API=https://turbo-cache.example.com
TURBO_TEAM=repo
TURBO_TOKEN=***

여기서 중요한 것은 설정 방법보다 아래입니다.

  • 어떤 환경에서 같은 캐시를 공유할지
  • preview와 production이 같은 캐시를 써도 되는지
  • 민감한 환경 차이를 inputs에 제대로 반영했는지

즉, remote cache는 단순히 붙이는 기능이 아니라 어떤 결과를 같은 결과로 볼 것인지에 대한 운영 판단이 같이 따라옵니다.

3. cache hit가 기대보다 안 나는 이유는 무엇일까?

실무에서는 "remote cache를 붙였는데 왜 별로 안 빨라졌지?"가 자주 나옵니다.

대부분 원인은 아래 중 하나입니다.

1. 입력이 너무 넓다

예를 들어 공통 설정 파일을 너무 많이 포함하면, 작은 수정에도 캐시가 자주 깨집니다.

{
  "tasks": {
    "build": {
      "inputs": ["$TURBO_DEFAULT$", "../../**/*"]
    }
  }
}

이런 식의 넓은 입력은 운영하기 편해 보이지만, 실제로는 cache miss를 많이 만듭니다.

2. 입력이 너무 좁다

반대로 환경 변수나 설정 파일을 빠뜨리면 캐시는 잘 맞는 것처럼 보여도 결과가 불안정해집니다.

{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**"]
    }
  }
}

이 설정에 .env.production이나 next.config.ts 변경이 반영되지 않으면, 빠르기보다 위험한 캐시가 됩니다.

3. 공통 패키지 변경 범위가 너무 넓다

packages/ui, packages/config-eslint, packages/config-typescript 같은 패키지는 많은 앱이 의존하는 경우가 많습니다.

즉, 공통화가 잘 되어 있을수록 생산성은 좋아지지만, 동시에 cache invalidation 범위는 넓어질 수 있습니다.

4. turbo.json은 운영 단계에서 어떻게 달라질까?

초기 세팅에서는 단순한 설정으로 시작해도 되지만, 운영 단계에서는 입력과 출력을 조금 더 구체적으로 잡게 됩니다.

예를 들어:

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "dev": {
      "cache": false,
      "persistent": true
    },
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["$TURBO_DEFAULT$", ".env.production"],
      "outputs": [".next/**", "dist/**"]
    },
    "lint": {
      "outputs": []
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"]
    }
  }
}

여기서 읽는 포인트는 이렇습니다.

  • build
    • .env.production을 입력에 포함해 환경 차이를 반영
  • lint
    • 보통 산출물이 없으므로 outputs를 비워두는 구성이 흔함
  • test
    • coverage 결과를 output으로 둘지 여부는 팀 정책에 따라 다름

즉, 운영 단계의 turbo.json은 단순 실행 설정이 아니라 캐시의 신뢰도와 실행 범위를 조절하는 파일이 됩니다.

5. CI는 한 번에 돌릴까, job을 나눌까?

처음에는 보통 한 줄로 시작합니다.

- run: pnpm turbo run lint test build

이 방식의 장점은 단순함입니다.

  • 설정이 간단하고
  • 디버깅 포인트가 적고
  • 온보딩이 쉽습니다

하지만 레포가 커지면 job을 나누는 쪽이 더 낫기도 합니다.

예시: 단계 분리

- run: pnpm turbo run lint
- run: pnpm turbo run test
- run: pnpm turbo run build

이 방식은 실패 지점을 더 빨리 찾기 좋습니다.

예시: 앱 기준 분리

- run: pnpm turbo run build --filter=web
- run: pnpm turbo run build --filter=admin

이 방식은 앱별 배포 파이프라인이 분리되어 있을 때 유리합니다.

예시: 공통 패키지 검증 분리

- run: pnpm turbo run lint test --filter=@repo/ui

즉, CI 전략은 정답이 하나가 아니라, 실패를 어디서 끊어 보고 싶은가배포 단위를 어떻게 가져가는가에 따라 달라집니다.

6. Next.js 모노레포에서는 무엇을 더 조심해야 할까?

Next.js 앱은 일반적인 라이브러리 build보다 영향 요소가 많습니다.

  • .next/** 출력물
  • 환경 변수
  • App Router 관련 빌드 결과
  • shared package transpilation

예를 들어 packages/ui가 바뀌면 아래처럼 여러 앱이 영향을 받을 수 있습니다.

packages/ui
  ├─> apps/web
  ├─> apps/admin
  └─> apps/docs

이럴 때 중요한 것은 "왜 다시 빌드됐지?"를 감으로 보지 않는 것입니다.

turbo run build --graph

이 명령으로 실제 task graph를 보고:

  • 어떤 패키지 변경이 전파됐는지
  • web build가 다시 돌았는지
  • docs까지 같이 포함됐는지

를 설명할 수 있어야 합니다.

즉, Next.js 모노레포에서는 캐시보다 먼저 graph를 설명할 수 있는 상태가 중요합니다.

7. shared package는 어디까지 쪼개는 게 좋을까?

이 질문은 실무에서 정말 자주 나옵니다.

처음에는 공통화가 좋아 보이기 때문에 아래처럼 계속 쪼개기 쉽습니다.

packages/
  ui/
  auth/
  api/
  hooks/
  utils/
  common/
  shared/

문제는 이렇게 되면 package 수는 많아지는데, 경계는 오히려 흐려질 수 있다는 점입니다.

좋지 않은 신호

  • utils 안에 도메인 로직이 들어감
  • shared가 사실상 아무거나 들어가는 폴더가 됨
  • commonshared 차이를 팀이 설명하지 못함
  • 공통 패키지 하나 바꾸면 모든 앱이 영향을 받음

비교적 안정적인 시작점

packages/
  ui/
  api/
  config-eslint/
  config-typescript/

즉, 초반에는 공통성이 명확한 패키지부터 시작하고, 정말 분리 필요성이 생길 때만 늘리는 편이 낫습니다.

8. 공통화는 왜 때로는 비용이 될까?

공통화는 보통 좋은 일처럼 보입니다. 하지만 Turborepo 운영에서는 항상 이 질문을 같이 해야 합니다.

  • 이 패키지를 분리하면 재사용이 늘어나는가
  • 아니면 단지 dependency fan-out만 넓어지는가

예를 들어 config-typescript 패키지는 편합니다.

{
  "extends": "@repo/config-typescript/base.json"
}

하지만 이 파일을 바꾸면 여러 앱의 타입 체크와 build가 같이 영향을 받을 수 있습니다.

즉, 공통 패키지는 편의와 함께 변경 반경도 가져옵니다.

이 관점은 ui, auth, api에도 똑같이 적용됩니다.

9. DX는 언제 좋아지고 언제 나빠질까?

Turborepo는 보통 초반 DX가 좋은 편입니다.

{
  "scripts": {
    "dev": "turbo run dev",
    "build": "turbo run build",
    "lint": "turbo run lint",
    "test": "turbo run test"
  }
}

루트 명령만 알아도 전체 흐름이 보이기 때문입니다.

하지만 운영이 복잡해지면 DX는 아래 지점에서 나빠질 수 있습니다.

  • 왜 cache miss가 났는지 설명이 안 될 때
  • 공통 패키지 의존성이 너무 넓을 때
  • turbo.json inputs/outputs가 실제 구조를 반영하지 못할 때
  • CI job 분리가 레포 구조와 맞지 않을 때

즉, Turborepo의 DX는 도구 자체보다 레포 구조와 task 관계가 얼마나 명확한가에 더 많이 좌우됩니다.

10. 운영 단계에서 자주 하는 실수는 무엇일까?

1. remote cache를 너무 빨리 붙인다

remote cache는 강력하지만, task 입력/출력이 부정확한 상태에서 먼저 붙이면 문제를 더 감추기도 합니다.

초기에는:

  • local cache로 먼저 안정성을 확인하고
  • 어떤 task가 자주 반복되는지 보고
  • 그다음 remote cache를 붙이는 편이 더 안전합니다

2. CI를 너무 일찍 잘게 쪼갠다

job을 세분화하면 좋아 보이지만, 실제로는:

  • 로그가 분산되고
  • 디버깅이 어려워지고
  • cache 전략도 복잡해질 수 있습니다

처음에는 단순하게 가고, 병목이 명확할 때만 나누는 편이 낫습니다.

3. shared package를 재사용성만 보고 늘린다

재사용 가능하다는 이유만으로 package를 늘리면, 장기적으로는 dependency fan-out과 cache invalidation 범위만 커질 수 있습니다.

4. graph를 안 보고 감으로 운영한다

레포가 커질수록 중요한 것은 사람 감이 아니라 graph입니다.

turbo run build --graph

이걸 자주 보고 "왜 이 task가 도는가"를 설명할 수 있어야 운영이 안정됩니다.

정리하면

Turborepo 운영 고도화의 핵심은 도구를 더 많이 붙이는 것이 아닙니다. 오히려 아래를 명확하게 만드는 쪽에 가깝습니다.

  • 어떤 task를 캐시할 것인가
  • 어떤 입력이 결과를 바꾸는가
  • CI를 어디서 나눌 것인가
  • shared package를 어디까지 공통화할 것인가

실무 기준으로 정리하면:

  • remote cache는 반복 작업이 충분히 많을 때 의미가 커지고
  • CI 최적화는 병목이 보일 때부터 나누는 편이 낫고
  • shared package 전략은 재사용성보다 영향 범위를 같이 봐야 합니다

중요한 것은 Turborepo를 "빠른 도구"로만 보는 것이 아니라, 레포의 실행 구조를 팀이 설명할 수 있게 만드는 도구로 보는 것입니다. 그 상태가 되면 CI와 캐시, 공통 패키지 운영도 훨씬 안정적으로 굴러갑니다.

다음 글에서 이어서 보기

여기까지는 remote cache, CI, shared package 전략을 운영 관점에서 정리했습니다. 다음 글에서는 한 단계 더 들어가서, 실제 운영 중 자주 만나는 cache miss, shared package 안티패턴, 경계 붕괴 문제를 원인과 대응 방식 중심으로 정리합니다.

같이 보면 좋은 글