Turborepo - compiled package vs source import(JIT), 무엇을 선택해야 할까?
31 Aug 2026

해당 블로그를 개발하며 겪었던 문제를 하나 정리해보려고 한다.
👉 UI 패키지를 수정해도 상위 앱에 변경이 반영되지 않는 문제였다.
공용 패키지 구성 방식
이 문제를 이해하려면
Turborepo 내에서, 먼저 공용 패키지를 사용하는 방식부터 정리할 필요가 있다.
1. compiled package (build 기반)
패키지를 build하여 dist를 생성
앱에서는 build 결과물을 import
``` json
{
"main" : "dist/index.js",
"types" : "dist/index.d.ts"
}👉 안정적이고 캐시 활용이 가능함
👉 대신 개발 환경에 공용 패키지의 build 과정이 필요함
2. source import 방식 (JIT 방식)
패키지를 build 하지 않고 source로 직접 import
앱의 bundler가 직접 처리
json
{
"main": "src/index.ts",
"types" : "src/index.ts"
}👉 dev 경험이 좋고 즉시 반영됨
👉 대신 turborepo cache 활용이 제한됨
내가 선택한 구조
나는 저 두가지 중 하나를 선택해서 trade-off를 만드는 것 보다 설정을 잘 건들면 두가지의 이점을 모두 누릴 수 있지 않을까? 라는 생각이 들었다.
따라서 👉패키지의 성격에 따라 다른 전략을 선택했다.
1. hooks → source import (JIT)
hooks 패키지는
- react
- react-dom
정도의 가벼운 dependency만 가지고 있었다.
이 경우
- build 비용이 크지 않고
- 변경 시 즉시 반영되는 것이 더 중요하다고 판단했다.
그래서
👉 source import (JIT) 방식으로 구성했다.
2. ui -> compiled package
반면 UI 패키지는 달랐다.
- vanilla-extract
- editor
- 다양한 UI 라이브러리
와 같이 dependency가 무거웠다.
이 경우
- 매번 앱에서 다시 빌드등의 task를 갱신하는 방식은 비효율적이고
- turborepo cache를 활용하는 것이 더 유리하다고 판단했다.
그래서
👉 compiled package 방식으로 구성했다.
문제의 시작
이 구조 자체는 합리적인 선택이었다.
하지만 예상치 못한 문제가 발생했다.
👉 UI 패키지를 수정해도,
상위 앱에 변경이 즉시 반영되지 않았다.
성능을 위해 compiled package를 선택했지만
👉 그 대가로 DX(개발 경험)가 크게 떨어졌다.
이 문제를 해결하기 위해
- compiled package를 유지하면서 DX를 개선할 것인지
- 아예 source import로 전환할 것인지
두 가지 방향을 고민하게 되었다.
해결 방안 고민
이 문제를 해결하기 위해 두가지 선택지를 고민했다
1. source import로 통일하기
UI 패키지까지 모두 sourcei mport 방식으로 바꾸는 방법이다.
👉 장점
변경 시 즉시 반영
별도 build 필요 없음
개발 경험이 매우 좋음
👉 단점
UI 패키지의 dependency가 무거움
앱 빌드 시 매번 함께 번들링 됨
turborepo cache 활용 어려움
👉결론
개발 경험은 좋지만,
프로젝트가 커질수록 build 비용이 커진다.
2. compiled package 유지 + watch build(with 활용)
여러 방법을 고민한 끝에
compiled 패키지 구조는 유지하되
개발 경험을 개선하는 방향을 선택해봤다.
핵심은 단순하다.
UI 패키지를 "항상 최신 상태로 build 하도록 하기"
가장 먼저 시도한 것은 UI 패키지의 build 방식 변경이다.
먼저 UI 패키지의 `dev` 스크립트를 단순 개발 서버가 아니라
`build --watch` 형태로 동작하도록 변경했다.
이렇게 하면 UI 패키지에서 변경이 발생할 때마다
dist 결과물이 자동으로 다시 생성된다.
그리고 상위 앱에서는 turbo.json 의 dependsOn의 설정을 통해 ^dev를 전달받도록 구성했다.
즉, 상위 앱의 dev를 실행하면 UI 패키지의 watch build도 함께 실행되고,
그 결과
👉 UI 패키지 변경 -> dist 갱신 -> 상위 앱 반영
이라는 흐름으로 DX가 개선되길 기대했다.
하지만 실제로는 기대처럼 동작하지는 않았다.
turborepo의 dev task는 build처럼 종료되는 작업이 아니라
지속적으로 실행되는 task(persistent task)이기 때문에,
dependsOn을 통해 하위 패키지의 task를 실행하면서
동시에 이를 persistent하게 유지하는 구조를 기대하는 방식으로 함께 구성하기 어려웠다.
하지만 이 문제는 turborepo의 with 옵션을 통해 해결할 수 있었다.
with 옵션을 활용한 방식
turborepo는 dependsOn외에도
with옵션을 통해 task를 함께 실행할 수 있다.
이 옵션은 의존성 기반 실행이 아니라
👉 특정 task를 "같이 실행"하도록 강제하는 방식이다.
예를 들어
{
"tasks": {
"dev": {
"with": ["@ui#dev"],
"cache": false,
"persistent": true
}
}
}이렇게 설정하면
상위 앱의 dev 실행 시 UI 패키지의 dev (watch build)도 함께 실행된다.
이 방식으로 변경한 이후
UI 패키지 -> watch build 유지
App -> dev server 실행
이 두 작업이 동시에 실행되며
겉보기에 정상적인 동작을 하는것 처럼 보였다.
하지만 여기서 문제가 또 발생했다.
바로 실행 순서가 보장되지 않는다는 점이었다.
with 옵션을 통해 UI 패키지의 build watch와
App의 dev 서버를 함께 실행할 수는 있었지만,
👉 Build 결과물이 준비되기 전에 dev 서버가 먼저 실행되는 상황이 발생했다.
즉,
build는 먼저 "시작"되지만
dev는 build가 "완료되기를 기다리지 않는다"
따라서
👉 초기 실행시 dist가 준비되지 않은 상태로 앱이 실행되며 런타임 오류가 발생했다.
빌드시에는 compiled package, dev 환경에선 JIT(최종 선택)
이번엔 "실행 순서"가 아니라
"참조 대상 자체를 바꾸는 방식"으로 접근하게 되었다.
이 문제를 해결하기 위해
👉환경에 따라 패키지 소비 방식을 분리하는 방법을 다시 시도하게 되었다.
build 시 -> compiled package (dist/cache 활용)
dev 시 -> source import (JIT 활용)
처음에는 이 방식이 설정이 복잡하고 유지보수 비용이 클 것 같아 부담스럽게 느껴졌다.
하지만 구조를 다시 생각해보면
👉 import 경로는 그대로 유지한 채
👉 실제 참조 대상만 환경에 따라 바꾸면 되지 않을까?
어떻게 가능했을까?
핵심은 두가지였다.
각각의 app bundler의 resolver 설정
index 파일 기반 tsconfig path를 통한 import 경로 토일
예를 들어
dev 환경에서는
src/index.tsbuild 환경에서는
dist/index.js
를 바라보도록 설정하되,
👉 코드 상의 import 경로는 동일하게 유지했다.
즉,
import { Button } from "@boo/ui"이 코드는 그대로 유지하며
실제로 어떤 파일을 참조할지는 환경에 따라 결정되도록 구성했다.
실험은
하위 패키지를 먼저 build하고
src에서 TARGET이라는 변수를
src로 exportdist로 들어가 index.js에 TARGET 변수를
BUILD로 변경하여 export이후 상위 앱의 build 결과와 dev 결과를 비교 진행하였고
실험은 성공적이였다.
vite/react 환경에 적용하기
// vite.config.ts
const publicAlias = {
"@": path.resolve(__dirname, "./src"), // public alias 설정 무시해도 됨
};
const isDev = process.env.NODE_ENV !== "production";
const alias = {
...publicAlias,
...(isDev
? { "@boo/ui": path.resolve(__dirname, "../../packages/ui/src/index.ts") }
// dev runtime 환경 상대 경로로 직접 소스 코드와 연결
: {}), // prod 환경 ui 패키지 export 환경 기반 동작
};
// ...vite 설정들
resolve: {
alias,
},
```
```json
//tsconfig.json
{
// ... 설정들
"compilerOptions": {
"paths": {
"@/*": ["./src/*"], // public alias 설정 무시해도 됨
"@boo/ui": ["../../packages/ui/src/index.ts"]
// dev, idea 환경 상대 경로로 직접 소스 코드와 연결
}
}
}next 환경에서
해당 환경에서 빌드 alias를 어떤식으로 잡는지 처음에 헤매었다.
next.js config의 webpack 설정을 건들여보기도, turbopack으로 바꾸어 설정을 건들여보기도 했지만
자꾸 연결한 상대 경로의 소스코드를 bundler에 포함하여 직접 빌드하는 상황이 일어나 캐싱의 이점을 잡지 못하고 있었다.
하지만 추후 알게 된 사실은 path 기반을 tsconfig.json을 기반으로 잡는것을 알게되었고 이를 next config 환경에서 분기가 가능하다고 알게 되었다.
따라서 tsconfig를 tsconfig.buid.json과 tsconfig.json으로 나누어 관리하였다.
// next.config.ts
const nextConfig: NextConfig = {
// ... other options...
typescript: {
tsconfigPath: isProd ? "tsconfig.build.json" : "tsconfig.json",
},
};
```
```json
// dev환경인 tsconfig.json
{
// ... 설정들
"compilerOptions": {
"paths": {
"@/*": ["./src/*"], // public alias 설정 무시해도 됨
"@boo/ui": ["../../packages/ui/src/index.ts"]
// dev, idea 환경 상대 경로로 직접 소스 코드와 연결
}
}
}
```
```json
// prod 환경인 tsconfig.build.json
// dev환경인 tsconfig.json
{
// ... 설정들
"compilerOptions": {
"paths": {
"@/*": ["./src/*"] // public alias 설정 무시해도 됨
// 상대경로 삭제하여 하위 패키지에 적힌 export 기반 build
}
}
}
```결과
실행 순서에 맞추는 대신, 환경에 따라 참조 대상을 분리하는 것이 더 안정적인 해결 방식이었다.
이 방식으로 변경한 ㄷ이후
dev 환경에서는 source를 직접 참조하며 빠른 HMR과 즉시 반영을 얻을 수 있었고
build 환경에서는 dist를 사용하여 안적적인 cache 활용이 가능해졌다.
결국 DX와 성능을 동시에 만족시키는 구조를 만들 수 있었다.
🚀 빌드 성능 변화
컴포넌트 갯수가 8개 정도의 초기 ui 패키지의 간단한 빌드 성능만 비교를 해보면 다음과 같다.
[JIT]

[COMPILE]

절대적인 차이는 크지 않지만,
프로젝트 초기 시작의 작은 규모에서도 약 10% 이상의 개선이 있었다.
특히 중요점은 단순한 시간 감소보다도,
turborepo 캐시가 정상적으로 동작하기 시작했다는 점이다.
이 구조는 프로젝트 규모가 커질수록
더 큰 빌드 성능 차이를 만들어낼 가능성이 높은것 같다.
하지만 이 방식에도 trade-off는 존재한다.
이 구조는
bundler 설정에 의존하고
빌드 환경별 환경 분리 및 설정이 필요하다.
특히 plugin들이 다양하게 추가되는 환경에서 세팅의 시간이 걸리고
팀원들의 환경에 대한 이해가 필요하다.
즉 구조가 환경에 영향을 받는다는 단점이 있다.
하지만 현재 프로젝트에서
복잡성을 감수하더라도 얻는 DX와 성능 이점이 더 크다고 판단했다.
결국 이번 선택은
source import와 compiled package 중 하나를 고르는 것이 아니라
두 방식을 상황에 맞게 조합하는 방향이였다.
물론 내가 모르는 더 단순하고 우아한 방식이 존재할 수도 있다.
하지만 현재 기준에서는
이 구조가 DX와 성능을 가장 균형있게 만족시킨 선택이였다.
#framework #architecture #troubleshooting #performance #infra
