SaaS Factory 모노레포 운영기 — pnpm + Turbo로 12개 서비스 묶기
pnpm workspaces와 Turborepo로 공용 패키지와 12개 서비스를 동시에 관리하며 얻은 실전 패턴을 공유합니다.
GRAXEL는 하나의 레포지토리 안에서 여러 개의 실제 서비스를 운영합니다. 포털, 내혜택, JobFit, K-Guide, 에이전트 등 현재 활성 서비스만 12개가 넘고, 이들이 packages/*에 있는 공용 모듈을 함께 씁니다. 이 구조를 pnpm workspaces + Turborepo 조합으로 운영하며 배운 점을 정리합니다.
왜 모노레포인가
서비스를 분리된 레포로 나누어 관리해본 경험도 있었지만, 결국 같은 코드(인증, 결제, UI, SEO)를 여러 번 복제하게 되는 함정에 매번 빠졌습니다. 특히 1인/소규모 팀에서는 "패키지를 npm에 publish해서 버전으로 관리"하는 오버헤드가 너무 큽니다. 그래서 선택한 것이 file: 프로토콜 기반 내부 패키지입니다.
// portal/package.json
{
"dependencies": {
"@saas-factory/auth": "workspace:*",
"@saas-factory/ui": "workspace:*",
"@saas-factory/i18n": "workspace:*"
}
}
공식 문서(pnpm workspaces)에 나오는 workspace:* 프로토콜 덕분에 packages/auth/nextjs의 변경이 포털을 재빌드하는 순간 반영됩니다. publish 과정이 아예 없습니다.
Turborepo 파이프라인
Turbo의 진가는 의존성 그래프 기반 병렬 빌드와 로컬 캐시입니다. turbo.json은 최소한으로만 유지합니다.
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
},
"lint": {},
"test": { "dependsOn": ["^build"] }
}
}
Turbo 공식 가이드에 따라 outputs를 명시해주지 않으면 캐시 효과가 떨어집니다. 처음엔 이걸 몰라서 "Turbo를 썼는데 왜 빠르지 않지?"라고 한참 고민했습니다.
TypeScript 경계 — noUncheckedIndexedAccess
공용 config에서 strict: true에 더해 noUncheckedIndexedAccess: true를 켰습니다. 배열 인덱스 접근이 항상 T | undefined가 되는 옵션인데, 초기에는 코드가 장황해져서 싫었지만 운영하면서 "런타임 undefined 폭발"이 거의 사라지는 효과를 체감했습니다.
ESLint Flat Config 중앙화
ESLint v9 flat config로 전환하면서 규칙을 @saas-factory/config-typescript 한 곳에 두고, 각 패키지는 그것을 import만 합니다.
// portal/eslint.config.js
import base from '@saas-factory/config-typescript/eslint.config.js';
export default [...base, { /* 포털 전용 오버라이드 */ }];
규칙 하나 바꾸면 모든 패키지가 동시에 반영된다는 점이 가장 큰 이점입니다.
서비스 생성 스크립트
새 서비스를 시작할 때마다 templates/service-nextjs를 복사하고 3군데(workspace, services 카탈로그, seed SQL)에 등록하는 과정을 반복해야 했는데, 이걸 pnpm create-service --non-interactive --name "…" --category ai-tools 명령으로 자동화했습니다. 한 번에 18~25개 파일이 생성되지만, 이후로는 pnpm --filter 새서비스 dev로 바로 띄울 수 있습니다.
가장 큰 함정들
- peerDependencies 미스매치 — React 19와 React 18이 패키지별로 섞이면서 훅이 두 번 호출되는 버그. 루트
package.json에서resolutions로 고정하는 것이 해결책이었다. - tsconfig paths — IDE에서는 인식되는데 빌드에서 깨지는 경우. Next.js 쪽은
transpilePackages에 공용 패키지를 나열해야 했다. - CI 캐시 — GitHub Actions에서
actions/setup-node의 pnpm 캐시만으로는 부족해서, Turbo의--cache-dir을 별도로 업로드하도록 바꿨다.
1인 개발자에게 이 구조가 맞는가
솔직히 "모노레포는 대기업용이다"라는 편견이 있었습니다. 그러나 6개월 이상 이 구조로 여러 서비스를 돌려본 결론은 오히려 1인일수록 모노레포가 유리하다는 것이었습니다. 공용 인증/결제/UI를 한 번 만들어두면 새 서비스는 하루 이틀이면 뼈대가 섭니다.
동일 맥락의 내용은 소개 페이지와 플랫폼 소개에서도 이어지고, 실제 서비스 운영 사례는 내혜택 RAG 운영기에서 보실 수 있습니다. 모노레포 구성에 대한 질문은 언제든 문의로 주세요.
공유하기
이어 읽으면 좋은 글
같은 주제와 태그를 기준으로 GRAXEL 운영 맥락을 더 깊게 볼 수 있는 글입니다.
1인 개발자 SaaS 모노레포 vs 멀티레포 — Graxel 운영 1년 후 다시 보는 결정
pnpm과 Turborepo로 구축한 모노레포 아키텍처가 1인 개발자에게 정말 정답이었을까요? 1년간의 뼈저린 운영 회고와 실패담.
Cloudflare Pages 무료 티어로 SaaS 시작하기 — 진짜 1년 비용 후기
1인 개발자가 Cloudflare Pages 무료 티어로 1년간 portal, myhyetaek 등 5개 서비스를 운영하며 지출한 실제 비용과 뼈아픈 실패담을 공개합니다.
Next.js 15 App Router + next-intl v4로 ko/en/ja 3개 언어 운영기
graxel.ai 자체를 한국어/영어/일본어 3개 언어로 운영하면서 배운 로케일 라우팅, SEO, hreflang 실전 패턴을 정리합니다.