Nx를 알아보자: project graph 중심으로 보는 모노레포 운영
이 글은 Nx 시리즈 1편입니다.
- 2편: Nx 워크스페이스 시작하기: project.json, target, executor, inferred tasks
- 3편: Nx affected와 캐시로 CI 줄이기: Turborepo remote cache와 비교
- 4편: Nx 운영 고도화: module boundaries, generators, 그리고 Turborepo가 비워둔 자리
전에 Turborepo 시리즈를 정리하면서, 모노레포 도구가 결국 "워크스페이스 위에 태스크 실행과 캐시 레이어를 얹는 것"이라는 이야기를 했습니다. Turborepo는 그 역할을 아주 단순하게, 빠르게 해줍니다.
그런데 레포가 커지면 도구에 거는 기대가 조금씩 달라집니다. "더 빨리 돌려줘"에서 "이 변경이 누구한테 영향을 주는지, 경계가 새고 있지는 않은지 같이 봐줘"로 질문이 바뀝니다. Nx는 바로 그 지점을 노리는 도구입니다.
그래서 이 시리즈는 Nx를 기능 나열로 소개하기보다, Turborepo와 무엇을 다르게 바라보는지를 기준으로 정리하려고 합니다. 이미 Turborepo를 써봤거나 Turborepo vs Nx 비교 글을 읽은 분이라면, 그 비교를 한 단계 더 깊게 들어간다고 생각하면 됩니다.
한눈에 보면
- Turborepo의 출발점은
package graph(package manager가 아는 의존성)이고, Nx의 출발점은project graph(워크스페이스를 프로젝트 단위로 해석한 그래프)입니다. - 이 차이 하나가 affected 분석, 경계 규칙, 코드 생성까지 전부로 이어집니다.
- Nx는 "스크립트를 빨리 돌리는 도구"보다 워크스페이스를 하나의 시스템으로 운영하는 도구에 가깝습니다.
- 그만큼 초반 학습량은 많지만, 최근 Nx는 inferred tasks로 설정을 많이 줄이는 방향으로 움직였습니다.
- 결론을 미리 말하면, 앱 몇 개면 Turborepo, 앱·라이브러리 관계가 복잡해지면 Nx가 자연스럽습니다.
조금 더 자세히 보면 이렇습니다.
| 항목 | Turborepo | Nx |
|---|---|---|
| 기본 모델 | package graph + task graph | project graph + task graph |
| 시작 감각 | 기존 workspace 위에 얹는다 | 워크스페이스를 프로젝트로 재해석한다 |
| 캐시 | 강함 (task 입출력 기반) | 강함 (named input + project graph 기반) |
| 변경 영향 | filter/변경 기반 실행 | affected를 운영 모델로 내재화 |
| 코드 생성 | 별도 도구에 의존 | generators 내장 |
| 경계 통제 | 팀 규칙·리뷰에 의존 | module boundary rule로 강제 가능 |
| 학습 곡선 | 낮음 | 초반엔 높음, 이후 운영 생산성으로 전환 |
한 줄로 줄이면, Turborepo는 "빠른 실행 레이어", Nx는 "운영 체계" 입니다. 이 시리즈는 그 "운영 체계"가 실제로 어떤 모양인지를 봅니다.
같은 레포, 다른 시선
말로만 하면 추상적이니 같은 구조를 두 도구의 눈으로 봐보겠습니다. 흔한 프론트엔드 모노레포입니다.
repo/
apps/
web/ # 사용자 서비스 (Next.js)
admin/ # 운영자 서비스 (Next.js)
packages/
ui/ # 공통 UI
auth/ # 인증 도메인
api/ # API client
utils/ # 공통 유틸Turborepo가 보는 방식: package graph
Turborepo는 package manager가 이미 알고 있는 의존성 관계를 그대로 씁니다. package.json의 dependencies에 @repo/ui가 있으면 그게 곧 엣지입니다.
package graph
apps/web ----> packages/ui ----> packages/utils
apps/admin --> packages/ui ----> packages/utils
apps/admin --> packages/auth --> packages/api이 위에서 build, lint, test task를 엮어 실행 순서를 잡고, 입력이 같으면 캐시를 재사용합니다. 단순하고, 그래서 빠르게 안착합니다. mental model이 "내가 이미 아는 의존성 + task 순서"라서 설명할 게 거의 없습니다.
Nx가 보는 방식: project graph
Nx도 같은 의존성을 보지만, 한 단계 더 들어갑니다. 패키지를 단순 노드가 아니라 프로젝트로 보고, 코드 안의 import까지 정적으로 분석해 그래프를 만듭니다.
project graph
web -----> ui ------> utils
web -----> auth ----> api
admin ---> ui ------> utils
admin ---> auth ----> api겉보기엔 비슷해 보이지만 차이가 있습니다.
package.json에 명시 안 된 의존이라도 실제import가 있으면 그래프에 잡습니다.- 각 프로젝트에
tag를 붙여 "이건 web 도메인", "이건 shared"처럼 의미를 부여할 수 있습니다. - 이 그래프가 affected 계산, 경계 규칙, 캐시 입력 산정의 공통 기반이 됩니다.
즉 Turborepo의 그래프는 "실행 순서를 잡기 위한 그래프"에 가깝고, Nx의 그래프는 "레포를 이해하고 통제하기 위한 그래프"에 가깝습니다. 같은 그림이지만 쓰임이 다릅니다.
그래프를 직접 눈으로 보는 명령도 둘 다 있지만, 무게가 다릅니다.
# Turborepo: 이 task가 어떤 순서로 도는지
turbo run build --graph
# Nx: 이 프로젝트가 누구를 참조하고, 무엇이 affected인지
nx graphnx graph는 단순 시각화가 아니라, 실무에서 "왜 admin까지 다시 도는가"를 추적하는 디버깅 도구로 더 자주 씁니다. 이 부분은 3편에서 affected와 함께 깊게 봅니다.
Nx를 이루는 네 가지 감각
Turborepo의 핵심을 package graph / task graph / caching / filter로 정리했었습니다. Nx도 네 가지로 잡으면 감이 잡힙니다.
1. project graph
방금 본 그것입니다. Nx의 거의 모든 기능이 이 그래프 위에서 동작합니다. Turborepo에서 "package graph가 토대"였다면, Nx에서는 project graph가 더 적극적인 토대입니다.
2. target
Turborepo는 각 패키지의 package.json scripts를 task로 봅니다. Nx는 프로젝트가 가진 실행 단위를 target이라고 부릅니다. build, serve, lint, test 같은 것들이죠.
// apps/web/project.json
{
"name": "web",
"targets": {
"build": { "executor": "@nx/next:build" },
"serve": { "executor": "@nx/next:server" },
"lint": { "executor": "@nx/eslint:lint" },
"test": { "executor": "@nx/jest:jest" },
},
}scripts와 target의 가장 큰 차이는, target은 그냥 명령 문자열이 아니라 입력·출력·의존 관계를 갖는 구조화된 단위라는 점입니다. 그래서 캐시와 affected가 더 정교하게 붙습니다.
3. executor
target이 "무엇을 한다"라면, executor는 "어떻게 한다"입니다. @nx/next:build는 Next.js 빌드를, @nx/eslint:lint는 ESLint 실행을 캡슐화합니다.
Turborepo에는 이 레이어가 없습니다. Turborepo는 그냥 next build를 직접 부르고, 그게 Turborepo의 단순함이자 장점입니다. 반대로 Nx는 executor를 통해 "Next.js 프로젝트라면 보통 이렇게 빌드한다"는 표준을 도구가 들고 있습니다. 표준화에 유리하지만, 한 겹 추상화가 더 있다는 뜻이기도 합니다.
다만 최근 Nx는 이 부분을 많이 가볍게 만들었습니다. inferred tasks를 쓰면
next.config.js나jest.config.ts같은 설정 파일만 보고 target을 자동으로 추론합니다.project.json에 일일이 executor를 적지 않아도 되죠. 이건 2편에서 다룹니다.
4. affected
이게 Nx의 시그니처입니다. Turborepo가 filter와 캐시로 "안 바뀐 건 캐시 재사용"을 한다면, Nx는 project graph를 기준으로 "이 변경이 영향을 주는 프로젝트만" 골라냅니다.
nx affected -t build,lint,test --base=origin/main --head=HEADpackages/auth만 바뀌었다면, 그래프를 따라 auth → admin, auth를 쓰는 다른 앱만 추려서 돌립니다. 영향 안 받는 docs는 아예 후보에서 빠집니다. Turborepo에서도 변경 기반 실행과 캐시로 비슷한 효과를 내지만, Nx는 affected를 CI 운영의 1급 시민으로 올려둔다는 점이 다릅니다.
changed: packages/auth
packages/auth
├─> apps/admin (affected)
└─> apps/web (affected)
apps/docs (not affected → skip)
affected targets
- admin:build, admin:test
- web:build, web:test그래서 무엇이 갈리는가
여기까지가 1편의 핵심입니다. 두 도구의 차이는 결국 출발점 하나에서 갈립니다.
- Turborepo는 "이미 있는 의존성을 빠르게 실행한다" 에서 출발합니다.
- Nx는 "워크스페이스를 프로젝트 그래프로 재해석하고, 그 위에서 실행·영향·경계·생성을 같이 다룬다" 에서 출발합니다.
그래서 같은 기능(캐시, 변경 기반 실행)도 결이 다릅니다.
| 질문 | Turborepo | Nx |
|---|---|---|
| 무엇을 다시 돌릴지 어떻게 아나 | 입력 해시가 바뀐 task | project graph 기반 affected |
| 의존 관계는 어디서 오나 | package.json 의존성 | package.json + 코드 import 분석 |
| 경계가 새는 걸 막을 수 있나 | 직접은 아님 (팀 규칙) | module boundary lint로 강제 |
| 새 라이브러리는 어떻게 만드나 | 손으로 (혹은 별도 도구) | nx g generator로 표준 생성 |
이 표의 오른쪽 열들이 바로 이 시리즈에서 한 편씩 풀어볼 주제입니다.
그럼 Nx가 항상 정답인가
아닙니다. 1편을 닫으면서 균형을 잡자면, Nx의 강점은 전부 레포가 충분히 클 때 의미가 생깁니다.
- 앱이 1~2개, shared package 몇 개라면 project graph의 정교함은 과합니다.
- affected의 가치는 "안 바뀐 프로젝트가 많을 때" 큽니다. 프로젝트가 적으면 그냥 다 돌려도 됩니다.
- module boundary rule은 경계가 새기 시작할 만큼 도메인이 많아야 의미가 있습니다.
이 규모에 도달하지 않았다면 Turborepo가 더 빠르고 깔끔한 선택입니다. Nx는 "모노레포라서 쓰는 도구"가 아니라, 모노레포의 운영 복잡도가 사람 머리를 넘어서기 시작할 때 그 복잡도를 시스템으로 옮기는 도구입니다.
정리하면
- Turborepo와 Nx는 같은 후보군이지만, package graph냐 project graph냐라는 출발점에서 갈립니다.
- Nx의 핵심은
project graph / target / executor / affected네 가지이고, 이게 캐시·경계·생성으로 확장됩니다. - Nx는 실행 속도뿐 아니라 레포를 구조적으로 이해하고 통제하는 감각이 강합니다.
- 대신 초반 학습량이 있고, 작은 레포에는 과할 수 있습니다. 최근 inferred tasks로 그 부담은 많이 줄었습니다.
다음 편에서는 추상적인 개념에서 내려와서, 실제로 Nx 워크스페이스를 어떻게 시작하고 project.json·target·executor·inferred tasks가 파일 단위로 어떻게 생겼는지 봅니다.
