태그 turborepo모노레포빌드 캐시turbo.jsonpnpm workspace캐시 무효화
전체 글 보기

turborepo

Turborepo가 캐시 적중이라며 아무것도 만들지 않았습니다

Turborepo가 FULL TURBO 62ms로 성공을 알렸는데 빌드 산출물은 0개였습니다. 원인은 outputs 설정 한 줄이었고, 그 앞에는 71일간 아무도 모른 채 멈춰 있던 빌드가 있었습니다.

2026년 8월 11일 4분

Turborepo를 쓰는 모노레포에서 빌드 캐시를 점검하다가 두 가지를 찾았습니다. 하나는 71일 동안 루트 빌드 명령이 아예 실행된 적이 없었다는 것이고, 다른 하나는 그걸 고친 뒤에도 캐시가 성공을 알리면서 파일을 하나도 만들지 않았다는 것입니다.

둘 다 에러를 내지 않았습니다. 그래서 오래갔습니다.

실패는 조용했다

Turborepo를 붙인 모노레포의 루트 package.json에는 보통 이런 스크립트가 들어갑니다. 이 저장소도 같았습니다.

{ "scripts": { "build": "turbo run build" } }

돌려보니 이렇게 나왔습니다.

$ npx turbo build --filter=blog
x Failed to add workspace "dreamstore" from "apps/dreamstore/package.json",
| it already exists at "apps/dreamstore-app-master/package.json"

앱 두 개가 package.jsonname을 똑같이 dreamstore로 쓰고 있었습니다. 하나는 현재 앱이고 하나는 마이그레이션 참고용으로 남겨둔 옛 코드였습니다. 옛 코드를 워크스페이스로 들여온 커밋은 2026년 6월 1일자였습니다. 측정한 날이 8월 11일이니 71일입니다.

그동안 이걸 아무도 못 챈 이유가 있습니다.

pnpm은 워크스페이스를 경로로 식별합니다. 이름이 겹쳐도 pnpm install은 멀쩡히 됩니다. 그리고 앱을 빌드할 때는 pnpm --filter blog build처럼 개별로 돌리고 있었습니다. 그 경로는 turbo를 거치지 않습니다.

즉 매일 쓰는 명령은 다 잘 됐고, 아무도 안 쓰는 명령 하나만 죽어 있었습니다. .turbo 캐시 디렉토리가 아예 존재하지 않았던 게 그 방증입니다. 한 번이라도 성공했다면 만들어졌을 폴더입니다.

Turborepo는 outputs에 적힌 것만 캐시한다

이름을 고치니 turbo가 돌았습니다. 그런데 끝에 경고가 붙었습니다.

WARNING  no output files found for task blog#build.
Please check your `outputs` key in `turbo.json`

turbo.json을 열어보니 이랬습니다.

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

create-turbo가 만들어주는 Next.js 기본값입니다. 그런데 이 저장소에서 빌드 산출물을 만드는 워크스페이스는 이렇습니다.

워크스페이스빌드산출물
블로그astro builddist/
랜딩vite builddist/
공용 패키지tscdist/
개인 프로젝트 하나next build.next/

.next/**는 네 개 중 하나만 맞았습니다. 그것도 곁다리로 붙여둔 개인 프로젝트였습니다.

이게 이 함정의 고약한 점입니다. 설정이 완전히 엉터리였다면 눈에 띄었을 겁니다. 하나는 맞아서, 훑어보면 그럴듯해 보입니다.

62밀리초 만에 성공했다고 말하는 빈 빌드

여기서 실제로 무슨 일이 벌어지는지 확인해봤습니다. 빌드 결과 폴더를 지우고 다시 돌렸습니다.

$ rm -rf apps/blog/dist
$ npx turbo build --filter=blog
blog:build: cache hit, replaying logs 7ee581b375db050c
Cached:    1 cached, 1 total
  Time:    62ms >>> FULL TURBO

$ ls apps/blog/dist
(아무것도 없음)

캐시 적중이라고 했고, FULL TURBO라고 했고, 62밀리초 만에 끝났습니다. 그리고 만들어진 파일은 0개입니다.

Turborepo가 캐시하는 것은 두 가지입니다. 작업의 로그와, outputs에 적어준 파일들입니다. 로그는 저장돼 있으니 그대로 재생됩니다. 파일은 애초에 저장 대상이 아니었으니 복원할 게 없습니다.

그러니까 캐시는 정상 동작했습니다. “아무것도 저장하지 말라”고 시킨 대로 아무것도 저장하지 않았고, 복원할 때도 아무것도 복원하지 않았습니다.

문제는 이게 성공한 빌드와 로그가 똑같다는 것입니다. cache hitFULL TURBO는 잘 됐을 때 나오는 바로 그 문구입니다.

고친 뒤에 다시 재봤다

outputs를 실제 산출물에 맞췄습니다.

"outputs": ["dist/**", ".next/**", "!.next/cache/**"]

네 워크스페이스의 결과 폴더를 전부 지우고 다시 돌렸습니다.

캐시 적중 시간복원된 파일
고치기 전62ms FULL TURBO0개
고친 뒤132ms FULL TURBO412개

412개는 블로그 79, 랜딩 122, 공용 패키지 26, 개인 프로젝트 185을 더한 수입니다. 경고도 사라졌습니다.

70밀리초 차이가 그 412개를 디스크에 쓰는 값입니다. 바꿔 말하면, 고치기 전의 62밀리초는 아무 일도 안 해서 빨랐던 것입니다.

빠른 것과 되는 것을 구분하는 법

캐시 도구를 볼 때 로그만 보면 안 된다는 게 결론입니다. 캐시의 목적은 빨라지는 게 아니라 결과를 다시 얻는 것인데, 로그는 앞쪽만 보여줍니다.

확인은 한 줄로 됩니다.

rm -rf <산출물> && <빌드> && ls <산출물>

캐시 적중이라고 하면서 폴더가 비어 있으면 그 캐시는 껍데기입니다. 공식 문서의 Configuring tasks 문서도 outputs에 적히지 않은 파일은 캐시되지 않는다고 명시하고 있습니다.

한 가지 더 있습니다. create-turbo로 시작했다면 turbo.json이 Next.js를 전제로 깔려 있습니다. Astro, Vite, tsc, Expo 어느 쪽이든 산출물 경로가 다르므로, 첫날에 한 번은 열어봐야 합니다.

남는 질문

정직하게 적어두면, 이 작업으로 CI가 얼마나 빨라졌는지는 모릅니다. 이 저장소의 배포는 turbo를 거치지 않고 호스팅 쪽이 프로젝트별로 빌드합니다. 71일 동안 손해 본 시간도 모릅니다. 아무도 그 명령을 안 썼기 때문에 실제 손해는 0이었을 수도 있습니다.

확인한 것은 이것뿐입니다. 빌드 오케스트레이터가 71일 동안 안 돌고 있었고, 돌게 만든 뒤에도 캐시가 빈 껍데기였고, 둘 다 로그로는 구분되지 않았습니다.

도구를 붙였다는 것과 도구가 일하고 있다는 것은 다른 이야기입니다. 전자는 설정 파일이 증명하고, 후자는 산출물이 증명합니다.

2026년 8월 11일 기준으로 turbo 2.9.14에서 확인했습니다.

이전 글 리액트 네이티브 모달은 항상 위에 뜹니다 - 신규 사용자 소프트락