npx next-forge@latest init)면 그 전체 골격이 내 컴퓨터에 복제된다.
@repo/* 패키지 뒤에 캡슐화된다. 그리고 그 모든 경계를 TypeScript 타입 + Zod 검증이 관통한다.
vercel/next-forge · 라이선스 MIT (© 2025 Vercel) · 언어 TypeScript · 버전 v6.0.2 · Next.js 16.1.6 · React 19.2.4 · Turborepo 2.8.14 · 패키지매니저 Bun 1.3.10 · ★ 약 7,000 · 원작 Hayden Bleasel, 현재 Vercel 공식 관리 · TrendShift Daily #19)
next-forge는 "Next.js로 SaaS를 만들 때 필요한 골격 전체를 미리 조립해 둔, 명령어 하나로 내 저장소에 복제되는 프로덕션급 모노레포 템플릿"이다. 마케팅 사이트, 로그인 뒤의 본 앱, 웹훅·크론을 받는 API 서버, 문서 사이트, 이메일 템플릿, 컴포넌트 작업실(Storybook)까지 — 실무 SaaS가 결국 갖게 되는 앱들을 처음부터 나눠 두고, 인증·결제·DB 같은 기능은 갈아끼울 수 있는 패키지로 분리해 둔다.
create-next-app으로 시작하는 건 빈 대지를 받는 것과 같다. 자유롭지만, 전기 배선(인증)·상하수도(DB)·도시가스(결제)·보안 시스템(방화벽)을 전부 내가 끌어와 연결해야 한다. 실무 SaaS 하나를 세우려면 이 "배관 공사"에만 몇 주가 걸린다.
next-forge는 그 배관이 이미 다 들어온 골조 주택을 준다. 벽에 콘센트(인증 훅)가 있고, 수도꼭지(DB 클라이언트)를 틀면 물이 나오고, 보안 패널(Arcjet)이 현관에 붙어 있다. 당신은 이제 벽지를 고르고 가구를 배치하는(=제품 기능을 만드는) 일부터 시작한다.
결정적 차이: 이 집의 설계도(소스코드)를 당신이 소유한다. 가스회사가 마음에 안 들면 배관을 뜯어 다른 회사로 바꿀 수 있다(=Clerk를 다른 인증으로 교체). 세 들어 사는 SaaS 보일러플레이트와 다른 점이다.
기술적으로 더 정확히 말하면, next-forge는 Turborepo가 관리하는 Bun 워크스페이스 모노레포다. apps/ 폴더에는 배포 가능한 앱 7개가, packages/ 폴더에는 앱들이 공유하는 패키지 20개가 들어 있다. 각 패키지는 @repo/이름 형태로 import되며(예: @repo/auth, @repo/database), 자기 환경변수를 Zod 스키마로 검증하는 keys.ts를 품고 있다. 즉 "기능 = 패키지 = 갈아끼울 수 있는 부품"이라는 등식이 프로젝트 전체를 관통한다.
첫째, Vercel 공식 프로젝트로 편입됐다. 원래 Hayden Bleasel 개인이 만든 next-forge가 지금은 Next.js를 만든 회사 Vercel의 공식 저장소(vercel/next-forge)로 관리된다. "Next.js 팀이 직접 권장하는 SaaS 구조"라는 신뢰가 붙는다. Vercel은 자체 아카데미에 "next-forge 프로덕션 패턴" 강의까지 두고 있다.
둘째, "설치하는 라이브러리"가 아니라 "복제되는 코드"다. 대부분의 프레임워크는 node_modules에 숨어 있어 내부를 못 고친다. next-forge는 init 순간 create-next-app의 --example 기능으로 저장소 전체를 내 프로젝트로 복사한다. 그 뒤로는 100% 내 코드 — 마음대로 뜯어고칠 수 있다. 이걸 요즘은 "fork-and-own(복제해서 소유)" 방식이라 부르고, shadcn/ui가 대중화한 흐름이다.
셋째, "의견이 강하지만(opinionated), 갈아끼울 수 있다". 각 기능이 최고급 프로바이더 하나로 기본 선택되어 있어 초보자는 고민 없이 시작한다. 동시에 각 프로바이더가 @repo/* 패키지 뒤에 캡슐화돼 있어, 나중에 "Clerk 대신 다른 인증", "Resend 대신 다른 이메일"로 바꾸는 마이그레이션 가이드까지 문서로 제공한다. "빠른 시작"과 "장기 유연성"을 동시에 노린 설계다.
보통의 보일러플레이트는 모든 API 키를 다 넣어야 앱이 겨우 켜진다. next-forge는 반대다. DB(DATABASE_URL) 하나만 있으면 뜨고, 나머지 결제·이메일·분석은 환경변수가 없으면 그 기능만 조용히 비활성된다(stripe?.prices.list()처럼 옵셔널 체이닝으로). "일단 돌려 보고, 필요한 것부터 하나씩 켠다"가 가능해 학습 곡선이 확 낮아진다.
| 항목 | next-forge | create-next-app / 일반 보일러플레이트 |
|---|---|---|
| 시작점 | 배관 완료된 SaaS 골격 | 빈 앱(CNA) 또는 관리 안 되는 템플릿 |
| 구조 | 모노레포 7앱·20패키지 | 단일 앱(대개) |
| 기능 통합 | 인증·결제·DB·이메일·보안·관측 미리 배선 | 직접 하나씩 붙여야 함 |
| 공급자 교체 | 패키지 캡슐화 + 가이드 제공 | 강결합이라 교체 어려움 |
| 키 없을 때 | 기능만 꺼짐(우아한 저하) | 대개 에러/크래시 |
| 소유권 | 내 저장소로 복제(fork-and-own) | 제각각 |
| 유지보수 | Vercel 공식 + update 명령 | 방치되기 쉬움 |
넷째, 타입 안전이 "선택"이 아니라 "강제"다. 환경변수조차 문자열로 대충 읽지 않고 @t3-oss/env-nextjs + Zod로 빌드 시점에 검증한다. 예컨대 Clerk 비밀키는 반드시 sk_로 시작해야 한다는 규칙까지 스키마에 박혀 있어, 오타 난 키로 배포되는 사고를 컴파일 단계에서 잡는다. 이런 "실무에서 실제로 데는 부분"을 미리 막아 둔 게 인기의 본질이다.
| 역할 | 도구 | 한 줄 설명 |
|---|---|---|
| 모노레포 오케스트레이터 | Turborepo 2.8 | 여러 앱·패키지의 build/test/dev를 캐시하고 병렬 실행. 안 바뀐 건 다시 안 돎 |
| 패키지 매니저 · 런타임 | Bun 1.3 | 설치·스크립트 실행이 빠른 올인원 런타임. 워크스페이스로 여러 패키지 연결 |
| 프레임워크 | Next.js 16 (App Router) | React 서버 컴포넌트 기반 풀스택 프레임워크 |
| UI 런타임 | React 19 | 서버 컴포넌트 · 액션 등 최신 기능 사용 |
| 언어 | TypeScript 5.9 | 앞뒤·패키지 경계까지 타입이 관통 |
| 린트 · 포맷 | Ultracite (Biome 2 래퍼) | ESLint+Prettier 대신 Rust 기반 초고속 통합 도구 |
| 테스트 | Vitest 4 | Vite 기반 초고속 유닛 테스트 |
| 환경변수 검증 | @t3-oss/env-nextjs + Zod | env를 스키마로 검증, 타입까지 부여 |
| 앱 | 포트 | 역할 |
|---|---|---|
| app | 3000 | 로그인 뒤의 본 애플리케이션 — 인증·DB 붙은 SaaS 본체 |
| web | 3001 | 마케팅 웹사이트 — CMS·SEO·블로그·다국어 |
| api | 3002 | 서버리스 API — Stripe 웹훅·크론 등을 수신 |
| 3003 | React Email 미리보기 서버(템플릿 개발용) | |
| docs | 3004 | 문서 사이트 — Mintlify 기반 |
| studio | 3005 | Prisma Studio — DB를 표로 보고 편집 |
| storybook | 6006 | 디자인 시스템 컴포넌트 작업실 |
| 패키지 | 기본 프로바이더 / 기술 | 역할 |
|---|---|---|
| auth | Clerk | 로그인·회원가입·세션·조직 |
| database | Prisma + Neon(서버리스 Postgres) | 타입세이프 ORM + 서버리스 DB 어댑터 |
| payments | Stripe (+ agent-toolkit) | 구독·결제·웹훅 |
| Resend + React Email | 트랜잭션 이메일 발송·템플릿 | |
| design-system | shadcn/ui + Radix (53개 UI 컴포넌트) | 다크모드 포함 공용 컴포넌트 라이브러리 |
| analytics | PostHog + Vercel Analytics + GA | 제품 분석 + 웹 분석 |
| observability | Sentry + Logtail(BetterStack) | 에러 추적·로깅·가동 모니터링 |
| security | Arcjet + nosecone | 봇 차단·앱 보안·보안 헤더 |
| rate-limit | Upstash Redis | 요청 속도 제한 |
| cms | BaseHub | 타입세이프 콘텐츠 관리(블로그·문서) |
| notifications | Knock | 인앱 알림 |
| feature-flags | Vercel flags + toolbar | 기능 플래그(A/B·점진 배포) |
| internationalization | Languine | 다국어(i18n) 번역 관리 |
| webhooks | svix | 아웃바운드 웹훅 발송 |
| storage | Vercel Blob | 파일 업로드·저장 |
| ai | AI SDK + OpenAI + streamdown | LLM 연동 유틸(스트리밍 응답) |
| collaboration | 실시간 협업 | 아바타·라이브 커서 등 |
| seo | 메타데이터·sitemap·JSON-LD | 검색엔진 최적화 유틸 |
| next-config | 공용 next.config | 모든 앱이 공유하는 Next 설정 |
| typescript-config | 공용 tsconfig | 모든 패키지가 상속하는 TS 설정 |
next-forge의 아키텍처를 한 문장으로 요약하면 "앱(apps)은 여러 패키지(packages)를 레고처럼 조립하고, 각 패키지는 외부 서비스를 감싸 타입세이프한 문 하나로 노출한다"이다. 그림으로 보면 이렇다.
next-forge에서 가장 자주 마주치는 파일이 keys.ts다. 각 패키지가 자기가 필요한 환경변수를 Zod 스키마로 선언하고, @t3-oss/env-nextjs가 이를 검증한다. 문자열을 그냥 process.env.X로 읽는 게 아니라, "이 값은 sk_로 시작해야 한다" 같은 규칙까지 코드에 박는다.
// packages/auth/keys.ts — 인증 패키지의 env 계약서
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const keys = () =>
createEnv({
// 키가 없어도 앱이 죽지 않게 검증 건너뛰기 스위치
skipValidation: process.env.SKIP_ENV_VALIDATION === "true",
server: {
// 서버 전용 비밀키 — 반드시 sk_ 로 시작(optional=없어도 됨)
CLERK_SECRET_KEY: z.string().startsWith("sk_").optional(),
},
client: {
// NEXT_PUBLIC_ 접두사 = 브라우저에 노출되는 공개키(pk_)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().startsWith("pk_").optional(),
},
runtimeEnv: { /* 실제 process.env 매핑 */ },
});
keys.ts는 각 부품에 붙은 "전원 규격 라벨"이다. "이 기기는 220V, 둥근 2핀 플러그"처럼 필요한 전기(환경변수)의 모양을 명시해 둔다. 규격이 안 맞는 플러그(오타 난 키)를 꽂으려 하면 빌드 단계에서 즉시 거부당한다 — 배포 후 런타임에 터지는 대신.
모든 프로바이더 클라이언트는 키가 없으면 null이 되고, 호출은 옵셔널 체이닝(?.)으로 감싼다. 그래서 Stripe 키를 안 넣어도 결제 기능만 조용히 꺼질 뿐, 앱 전체는 멀쩡히 돈다.
// 키가 있으면 Stripe 인스턴스, 없으면 undefined
export const stripe = env.STRIPE_SECRET_KEY
? new Stripe(env.STRIPE_SECRET_KEY)
: undefined;
// 호출부: stripe가 없으면 그냥 아무 일도 안 일어남(에러 X)
const prices = await stripe?.prices.list();
초보자가 20개 서비스 키를 다 발급받아야 앱이 뜨는 지옥을 피한다. DB 하나만 연결하면 일단 실행되고, 결제가 필요해지면 그때 Stripe 키만 추가하면 된다. 학습·프로토타이핑 단계에서 특히 강력하다.
Next.js App Router 규칙을 그대로 따른다. page.tsx·layout.tsx는 항상 서버 컴포넌트라, DB·비밀키·서버 전용 API를 브라우저로 내보내지 않고 직접 다룰 수 있다. 버튼 클릭 같은 상호작용이 필요한 부분만 'use client'를 붙인 별도 파일로 분리한다.
'use client' 표시)는 브라우저에서 실행돼 클릭·입력 같은 상호작용을 담당한다. next-forge는 "기본은 서버, 필요할 때만 클라이언트"를 규칙으로 삼는다.turbo.json이 "무엇을 어떤 순서로, 무엇을 캐시하며 돌릴지"를 정의한다. 예컨대 build는 의존 패키지의 build가 먼저 끝나야(^build) 하고, test도 통과해야 실행된다. 한 번 돌린 결과는 캐시돼, 안 바뀐 패키지는 다시 빌드하지 않는다 — 모노레포가 커져도 빌드가 느려지지 않는 비결이다.
// turbo.json (요약)
{
"tasks": {
"build": {
"dependsOn": ["^build", "test"], // 의존성 build + 자기 test 후
"outputs": [".next/**", "!.next/cache/**"] // 캐시할 산출물
},
"dev": { "cache": false, "persistent": true } // dev는 캐시 X, 계속 실행
}
}
npx next-forge@latest init의 속을 열어 보면 의외로 담백하다. commander로 명령을 파싱하고 @clack/prompts로 예쁜 대화형 질문을 던진 뒤, 실제 복제는 create-next-app의 --example에 next-forge 저장소 URL을 넘겨 처리한다. 그 후 패키지매니저 설정 보정 → .env 파일 생성 → 의존성 설치 → Prisma 빌드 → git 초기화를 순서대로 밟는다.
흥미로운 건 update 명령도 있다는 점이다. 특정 버전 사이의 파일 변경분을 가져와 이미 만든 프로젝트를 최신 next-forge 구조로 따라잡게 돕는다. "복제해서 소유"의 약점(원본이 발전해도 내 프로젝트는 낡음)을 보완하는 장치다.
패키지는 대개 세 조각이면 충분하다. 기능을 내보내는 index.ts(x), env를 검증하는 keys.ts, 정체를 밝히는 package.json. 이 패턴이 20개 패키지 전체에 반복되므로 하나만 이해하면 나머지는 자동으로 읽힌다.
packages/auth/
├─ package.json # name: "@repo/auth", deps: @clerk/nextjs …
├─ keys.ts # CLERK_SECRET_KEY 등 env를 Zod로 검증
├─ index.ts # auth(), currentUser() 등 기능 export
├─ client.ts # 'use client' — 브라우저용 훅/컴포넌트
└─ components/ # 로그인 버튼 등 UI
@repo/auth를 import하면 npm에서 받아오는 게 아니라 옆 폴더의 소스를 바로 쓴다. package.json에 "@repo/auth": "workspace:*"라 적으면 "이건 워크스페이스 내부 패키지"라는 뜻. 그래서 패키지를 고치면 앱에 즉시 반영된다.여러 앱이 공용 코드를 어떻게 나눠 갖는지, 순환 참조 없이 의존성을 어떻게 계층화하는지 배운다. turbo.json의 dependsOn·outputs를 읽으며 "빌드 캐시"가 실제로 어떻게 작동하는지 감을 잡을 수 있다.
실습 아이디어: packages/에 @repo/utils를 새로 만들어 앱 두 곳에서 import해 보기. 그 후 turbo run build를 두 번 돌려 "두 번째는 캐시로 즉시 끝남"을 눈으로 확인.
실무에서 "키 오타 하나로 프로덕션이 죽는" 사고를 구조적으로 막는 법. keys.ts를 여러 개 열어 보며 server / client / runtimeEnv 3분할과 NEXT_PUBLIC_ 접두사의 의미를 익힌다.
실습 아이디어: 새 패키지에 keys.ts를 만들고, 일부러 규칙에 안 맞는 값을 넣어 빌드가 막히는 걸 확인.
왜 Clerk를 앱 곳곳에서 직접 부르지 않고 @repo/auth로 한 번 감쌌는지 이해하면, "의존성 역전"과 "결합도 낮추기"를 실물로 체득한다. 이게 나중에 프로바이더를 갈아끼울 수 있게 하는 핵심이다.
실습 아이디어: @repo/analytics가 PostHog를 어떻게 감쌌는지 읽고, 호출부가 프로바이더 이름을 전혀 모른다는 점을 확인.
app/ 안에서 서버 컴포넌트가 DB를 직접 조회하고, 폼 제출을 서버 액션으로 처리하는 최신 패턴을 실제 코드로 본다. "언제 'use client'를 붙여야 하는가"의 감을 잡는 데 최고의 교재다.
버튼·다이얼로그 같은 UI를 앱마다 만들지 않고 @repo/design-system 한 곳에 모아 모든 앱이 같은 룩앤필을 공유하는 법. components.json(new-york 스타일, neutral 베이스, lucide 아이콘)을 읽으며 shadcn 구성을 익힌다.
실습 아이디어: npx shadcn@latest add [컴포넌트] -c packages/design-system로 새 컴포넌트를 추가하고 두 앱에서 동시에 써 보기.
schema.prisma로 모델을 정의하고 bun run migrate(format→generate→push)로 DB에 반영하는 흐름. 서버리스 환경에서 connection pooling을 어댑터(@prisma/adapter-neon)로 어떻게 푸는지도 볼 수 있다.
next-forge는 특별한 하드웨어가 필요 없다. 계산·저장·분석 같은 무거운 일은 전부 외부 서비스(Neon·Vercel·Sentry…)가 클라우드에서 처리하므로, 개발자 컴퓨터는 코드를 편집하고 개발 서버를 띄울 정도면 된다.
| 항목 | 요구사항 | 메모 |
|---|---|---|
| Node.js | 20+ 권장 (engines는 18+) | 런타임 기반 |
| 패키지매니저 | Bun 권장 (npm/yarn/pnpm 가능) | init에서 선택 가능 |
| 데이터베이스 | PostgreSQL (Neon 권장) | DATABASE_URL만 필수 |
| Stripe CLI | 로컬 웹훅 테스트용 | 결제 개발 시 |
| Mintlify CLI | 문서 앱 로컬 실행용 | docs 작업 시 |
| 메모리/디스크 | 일반 개발용 노트북 수준 | 모노레포라 node_modules는 다소 큼 |
앱 7개 + 패키지 20개가 각자 의존성을 가지므로 첫 install과 첫 build는 시간이 걸린다. 대신 Turborepo 캐시 덕분에 두 번째부터는 빠르다. 또한 필수는 DB뿐이라, 처음엔 나머지 키를 비워 두고 가볍게 시작하는 걸 권한다.
배포의 핵심 개념은 "앱 하나 = Vercel 프로젝트 하나"다. app·web·api를 각각 별도 프로젝트로 만들고, 각 프로젝트의 루트 디렉토리를 apps/app·apps/web·apps/api로 지정한다. 환경변수는 프로젝트별로 넣거나 Vercel의 팀 공용 환경변수를 쓴다. 그러면 app.내도메인.com, www.내도메인.com, api.내도메인.com처럼 서브도메인으로 나뉜 실서비스가 된다.
npx next-forge@latest init으로 새 프로젝트를 만들고, packages/database/.env에 Neon에서 만든 DATABASE_URL만 넣은 뒤 bun run migrate → bun run dev. 브라우저로 3000·3001 포트를 열어 본 앱과 마케팅 사이트가 동시에 뜨는 걸 확인한다.
배우는 것: 모노레포가 여러 앱을 한 번에 실행하는 감각, "DB만 있으면 돈다"는 우아한 저하.
apps/web의 히어로 문구를 내 서비스 이름으로 바꾸고, npx shadcn@latest add badge -c packages/design-system로 Badge 컴포넌트를 추가해 app과 web 양쪽에서 import해 본다.
배우는 것: 디자인 시스템 중앙화 — 한 곳에 추가하면 모든 앱이 공유한다.
packages/database/prisma/schema.prisma에 Post 모델(제목·본문)을 추가하고 bun run migrate를 실행한다. 그 후 studio 앱(apps/studio, 포트 3005)을 띄워 표에서 직접 행을 넣어 본다.
배우는 것: 스키마-우선 DB 워크플로우, generate/push 흐름, Prisma Studio 활용.
Clerk에서 키를 발급받아 apps/app/.env.local에 넣고(pk_·sk_), 로그인해야만 볼 수 있는 페이지를 하나 만든다. 키를 넣기 전과 후의 동작 차이(기능 꺼짐 → 켜짐)를 관찰한다.
배우는 것: env로 기능을 켜는 우아한 저하의 실제, 서버 컴포넌트에서의 인증 처리.
packages/에 @repo/pdf 같은 새 패키지를 만들어 index.ts+keys.ts+package.json 3종 세트를 갖추고 앱에서 import한다. 여력이 되면 @repo/analytics의 프로바이더를 PostHog에서 다른 도구로 호출부를 건드리지 않고 교체해 본다.
배우는 것: 패키지 아키텍처의 확장·교체 — next-forge 설계 철학의 최종 시험.
| 주차 | 주제 | 핵심 목표 |
|---|---|---|
| 1주차 | Next.js App Router · RSC | 서버/클라이언트 컴포넌트 경계, 서버 액션, 라우팅. apps/app 코드 읽기 |
| 2주차 | 모노레포 · Turborepo · Bun | 워크스페이스 개념, turbo.json 파이프라인, 빌드 캐시 실습 |
| 3주차 | 타입세이프 env · 패키지 캡슐화 | t3-env+Zod, keys.ts 패턴, 프로바이더 감싸기(결합도) |
| 4주차 | 인증 · DB · 결제 | Clerk 세션, Prisma+Neon 마이그레이션, Stripe 웹훅(apps/api) |
| 5주차 | 관측 · 보안 · 분석 | Sentry 에러추적, Arcjet 봇차단·rate-limit, PostHog 이벤트 |
| 6주차 | 배포 · 프로바이더 교체 | 앱별 Vercel 프로젝트 배포, 공급자 마이그레이션, update 명령 |
next-forge는 "큰 그림 → 부품" 순서로 접근하는 게 좋다. 처음부터 20개 패키지를 다 파려 하지 말고, 먼저 apps/app 하나가 어떻게 도는지만 따라가라. 그다음 그 앱이 import하는 패키지를 하나씩 열어 보면, "왜 이렇게 나눴는가"가 자연스럽게 이해된다. tree(구조)를 먼저, leaf(구현)를 나중에.
| 키워드 | 뜻 |
|---|---|
| Turborepo | 모노레포의 build/test/dev를 캐시하고 병렬 실행하는 오케스트레이터 |
| 모노레포(monorepo) | 여러 앱·라이브러리를 한 저장소에서 함께 관리하는 방식 |
| 워크스페이스 / workspace:* | 저장소 내부 패키지끼리 로컬에서 직접 참조하게 묶는 기능 |
| @repo/* | next-forge 내부 공유 패키지의 이름 접두사(예: @repo/auth) |
| App Router | Next.js의 최신 라우팅 방식(app/ 폴더 기반) |
| RSC(서버 컴포넌트) | 서버에서만 실행돼 HTML을 보내는 컴포넌트 — DB·비밀키 안전 |
| 'use client' | 이 파일은 브라우저에서 실행되는 클라이언트 컴포넌트라는 표시 |
| t3-env + Zod | 환경변수를 스키마로 검증하고 타입을 부여하는 조합 |
| graceful degradation | 키가 없으면 기능만 꺼지고 앱은 계속 도는 "우아한 성능 저하" |
| fork-and-own | 라이브러리로 설치하지 않고 소스를 복제해 내가 소유·수정하는 방식 |
| Prisma / Neon | 타입세이프 ORM / 서버리스 PostgreSQL |
| Clerk · Stripe · Resend | 기본 인증 · 결제 · 이메일 프로바이더 |
| Arcjet | 봇 차단·rate-limit·보안 헤더를 다루는 앱 보안 서비스 |
| shadcn/ui | 복사해서 소유하는 컴포넌트 방식의 UI 라이브러리(Radix 기반) |
| Ultracite / Biome | ESLint+Prettier를 대체하는 Rust 기반 초고속 린트·포맷 |
| Mintlify · BaseHub · svix | 문서 사이트 · 타입세이프 CMS · 웹훅 발송 서비스 |