Turborepo를 이용해 Vercel에 여러 프로젝트를 배포하고 있었다. apps 디렉터리에서 웹을 개발하고, packages에 공통으로 사용하는 타입과 컴포넌트를 모아두는 구조다.
그런데 배포 과정에서 항상 packages에 있는 모듈을 찾지 못하는 오류가 발생했고, 단순히 재배포를 하면 해결되곤 했다.
이번 글에서는 이 오류의 원인을 분석하고 어떻게 해결했는지 정리해보겠다.
최초 배포 시의 build logs와 재배포 시의 build logs를 비교해보니 다음과 같은 차이가 있었다.
최초 배포 | 재배포 |
---|---|
Restored build cache from previous deployment | Skipping build cache, deployment was triggered without cache. |
@repo/types:build: cache hit, replaying logs ~~ | @repo/types:build: cache bypass, force executing ~~ |
최초 배포에서는 이전 빌드 캐시를 복원한 뒤, 해당 캐시가 hit 되어 재사용되었다. 반면, 재배포에서는 캐시를 아예 사용하지 않고 새롭게 빌드했기 때문에 문제가 발생하지 않았다.
그렇다면 Turberepo의 캐시는 무엇일까?
Turberepo에서는 빌드 속도를 향상 시키기 위하여 캐시를 사용한다. 작업이 캐시 대상이면 Turborepo는 최초 실행 때 만든 지문(해시)을 기준으로 다음 실행에서 이전 결과를 캐시에서 바로 복원한다.
Turborepo는 크게 두 가지를 캐시에 저장한다.
turbo.json의 outputs 키에 정의된 파일들이 여기에 해당한다.
캐시가 존재하면 Turborepo는 이 파일들을 캐시에서 복원한다.
{
"tasks": {
"build": {
// packages의 dist 디렉터리에 생성되는 모든 파일을 캐싱
"outputs": ["dist/**"]
}
}
}
빌드 및 실행 과정에서 남는 콘솔 로그가 캐시에 저장된다.
따라서 캐시가 hit 되면 실제 빌드를 다시 수행하지 않고, 로그만 “재생(replay)”할 수 있다.
나는 packages/types 패키지를 tsc로 빌드해 dist 폴더에 결과물을 생성하고, 이를 apps에서 불러와 사용하고 있었다.
{
"name": "@repo/types",
"scripts": {
"dev": "tsc --watch",
"build": "tsc"
},
"exports": {
"types": "./dist/index.d.ts"
},
"devDependencies": {
"@repo/typescript-config": "workspace:*"
}
}
하지만 기존 turbo.json에는 dist를 outputs로 지정하지 않았다.
{
"$schema": "https://turborepo.com/schema.json",
"ui": "tui",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": [".next/**", "!.next/cache/**"]
},
"lint": {
"dependsOn": ["^lint"]
},
"check-types": {
"dependsOn": ["^check-types"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
즉, 캐시는 hit 되었지만, outputs에 dist가 포함되지 않았으므로 캐시에서 복원할 때 dist 폴더가 존재하지 않았다.
이 때문에 apps에서 @repo/types를 불러올 때 타입을 찾지 못하는 문제가 발생한 것이다.
turbo.json에 dist/**를 outputs로 추가하여, dist 폴더도 캐시에 포함되도록 수정했다.
{
"$schema": "https://turborepo.com/schema.json",
"ui": "tui",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"lint": {
"dependsOn": ["^lint"]
},
"check-types": {
"dependsOn": ["^check-types"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
이를 통해 빌드가 한 번에 성공되는 것을 확인할 수 있다.
사실 이 오류를 처음 겪은 지는 벌써 1년 가까이 되었다. 하지만 그동안 원인을 깊이 파악하려 하지 않고, 단순히 재배포하도록 workflow를 작성해 임시로 해결하는 방식에 의존해왔다.
그러다 최근에야 문제를 제대로 파보기로 마음먹었고, 마침내 원인을 명확히 찾고 해결할 수 있었다.
이번 경험을 통해, 내가 사용하는 도구의 동작 원리를 제대로 모른 채 쓰는 것만큼 바보 같은 일도 없다는 사실을 뼈저리게 느꼈다. 앞으로는 단순히 AI에게 에러 해결을 맡기기보다, 원리를 이해하며 AI와 함께 페어 프로그래밍할 수 있는 역량을 키워나가야겠다고 다짐한다.