TanStack Start vs Next.js — 서버 경계 명시성과 타입 안전성으로 보는 2025 프레임워크 선택 기준
2025년 들어 Reddit과 DEV.to에 "Why I left Next.js for TanStack Start" 류의 글이 심심찮게 올라오기 시작했습니다. 처음엔 또 다른 프레임워크 노이즈려니 했는데, 읽다 보면 공감되는 지점이 꽤 있더라고요. Next.js App Router를 쓰면서 캐싱 동작이 예상과 달라 몇 시간씩 디버깅해본 경험, use client와 use server 경계에서 직렬화 오류로 당황했던 순간들 — 저도 겪어본 일이라 남 얘기가 아니었습니다.
Next.js를 실무에서 써본 경험이 있다면 이 글의 맥락을 훨씬 빠르게 잡을 수 있습니다. TanStack Start가 v1.0 정식 릴리스를 완료하면서 이제 "실험적 선택"이 아닌 진지한 대안으로 올라왔습니다. 새 프로젝트의 스택을 고민 중이거나 현재 쓰는 프레임워크에 불만이 쌓이고 있다면, 이 글이 그 결정에 필요한 구체적인 기준을 제시할 수 있을 것입니다. 두 프레임워크의 설계 철학 차이를 짚고, 어떤 상황에서 어느 쪽을 선택하면 좋을지 실제 코드와 함께 살펴봅니다.
핵심 개념
두 프레임워크의 서버 경계 철학
Next.js는 "서버 우선(Server-First)"입니다. App Router에서는 모든 컴포넌트가 기본적으로 RSC(React Server Component)이고, 클라이언트 인터랙션이 필요할 때 "use client"를 선언합니다.
RSC (React Server Components): 서버에서만 렌더링되는 React 컴포넌트. 클라이언트 번들에 포함되지 않아 번들 크기를 줄이고 초기 로딩 속도를 개선하지만,
useState나 이벤트 핸들러를 직접 사용할 수 없습니다.
TanStack Start는 반대 방향에서 접근합니다. SSR을 기본으로 지원하는 풀스택 프레임워크이지만, 렌더링 방식의 기본값이 클라이언트 인터랙티브입니다. 서버에서 실행해야 할 코드는 createServerFn으로 명시적으로 선언합니다. "마법 없는 프레임워크(No Magic Framework)"를 지향한다는 표현이 딱 맞습니다 — 코드만 봐도 이게 어디서 실행되는지 알 수 있습니다.
// Next.js Server Action — "use server" 지시어가 암묵적으로 서버임을 표시
"use server"
export async function createUser(data: FormData) {
const name = data.get("name")
await db.user.create({ data: { name } })
}// TanStack Start Server Function — .validator()까지 체이닝해야 완전한 타입 추론이 동작함
import { createServerFn } from "@tanstack/start"
import { z } from "zod"
export const createUser = createServerFn({ method: "POST" })
.validator(z.object({ name: z.string() }))
.handler(async ({ data }) => {
// data.name이 string임이 TypeScript 레벨에서 보장됨
await db.user.create({ data: { name: data.name } })
})언뜻 비슷해 보이지만 차이가 있습니다. TanStack Start의 createServerFn은 .validator()를 체이닝해야 완전한 타입 추론이 동작합니다. 클라이언트에서 이 함수를 호출할 때 name에 숫자를 넣으면 컴파일 단계에서 바로 오류가 납니다.
타입 안전 라우팅: 가장 체감되는 차이
솔직히 처음 TanStack Router를 써봤을 때 "이게 되나?" 싶었습니다. URL 파라미터가 자동으로 타입 추론되는 게 처음엔 낯설었거든요.
// TanStack Start — 라우트 파라미터가 자동 타입 추론
export const Route = createFileRoute("/users/$userId")({
component: UserDetail,
})
function UserDetail() {
const { userId } = Route.useParams() // userId: string — 별도 어노테이션 불필요
const { tab } = Route.useSearch() // search params도 동일하게 타입 안전
}// Next.js 15 기준 — params가 Promise 타입으로 변경됨
interface Props {
params: Promise<{ userId: string }>
}
export default async function UserDetail({ params }: Props) {
const { userId } = await params
}Next.js도 타입 지원을 제공하지만, v15에서 params가 Promise로 바뀌는 등 버전마다 API가 달라집니다. TanStack Router는 빌드 시점에 라우트 트리 전체를 분석해서 타입을 생성하기 때문에, 오타로 인한 버그가 런타임이 아닌 컴파일 단계에서 잡힙니다.
프레임워크 철학 비교
| 구분 | TanStack Start | Next.js |
|---|---|---|
| 기본 컴포넌트 타입 | 클라이언트 인터랙티브 (SSR 지원) | 서버 컴포넌트(RSC) |
| 서버 경계 선언 | 명시적 (createServerFn + .validator()) |
암묵적 지시어 ("use server") |
| 라우트 타입 | 빌드 시 자동 생성 | 수동 선언 필요 (v15: Promise params) |
| RSC 지원 | 로드맵 단계 (현재 미지원) | 기본 지원 |
| ISR | 미지원 | 지원 |
| 번들러 | Vite (Vinxi 추상화) | Turbopack (Next.js 15+) |
| 배포 | 플랫폼 비종속 | Vercel 최적화 |
실전 적용
예시 1: 복잡한 SaaS 대시보드 — TanStack Start가 빛나는 시나리오
여러 필터가 URL에 동기화되고, 중첩 레이아웃이 있는 관리자 패널을 생각해봅니다. 실무에서 정말 자주 맞닥뜨리는 상황인데, TanStack Start의 타입 안전 검색 파라미터가 여기서 진가를 발휘합니다. Next.js로 비슷한 대시보드를 만들 때 URL 파라미터 타입을 매번 수동으로 선언하면서 "이게 맞나?" 싶은 순간이 저도 많았거든요.
// routes/dashboard/users.tsx
import { createFileRoute, useNavigate } from "@tanstack/react-router"
import { z } from "zod"
const searchSchema = z.object({
page: z.number().default(1),
status: z.enum(["active", "inactive", "pending"]).optional(),
search: z.string().optional(),
})
export const Route = createFileRoute("/dashboard/users")({
validateSearch: searchSchema,
loaderDeps: ({ search }) => ({ search }),
loader: async ({ deps: { search } }) => {
return await fetchUsers(search) // Drizzle ORM 또는 Prisma로 DB 직접 쿼리
},
component: UsersPage,
})
function UsersPage() {
const users = Route.useLoaderData()
const search = Route.useSearch()
// 반환 타입: { page: number; status?: "active" | "inactive" | "pending"; search?: string }
const navigate = useNavigate()
return (
<div>
<UserFilters
currentStatus={search.status}
onStatusChange={(status) =>
navigate({ search: (prev) => ({ ...prev, status, page: 1 }) })
}
/>
<UserTable users={users} />
</div>
)
}| 코드 요소 | 역할 |
|---|---|
validateSearch |
Zod 스키마로 URL 파라미터 검증 및 기본값 설정 |
loaderDeps |
어떤 search param 변경이 재페치를 트리거하는지 명시 |
Route.useSearch() |
완전히 타입 추론된 search 파라미터 반환 |
navigate |
타입 안전하게 URL 업데이트 |
status 파라미터에 "deleted" 같은 허용되지 않는 값을 넣으면 TypeScript가 컴파일 시점에 잡아줍니다. 런타임에 API 오류로 이어지던 실수가 사라집니다.
예시 2: 콘텐츠 중심 이커머스 — Next.js가 유리한 시나리오
상품 목록 페이지처럼 자주 바뀌지 않지만 완전 정적도 아닌 콘텐츠에서 Next.js의 ISR은 강력합니다. 고트래픽 상품 페이지에서 매 요청마다 DB를 치지 않아도 되는 이 패턴은, TanStack Start에서는 현재 별도 캐시 레이어(CDN, Redis)를 직접 구성해야 합니다.
// app/products/[category]/page.tsx (Next.js 15 기준)
import { Suspense } from "react"
// 1시간마다 백그라운드에서 페이지 재생성
export const revalidate = 3600
// RSC — 클라이언트 번들에 포함되지 않음
// fetchProducts는 Prisma/Drizzle로 DB 직접 쿼리 (서버 전용)
async function ProductList({ category }: { category: string }) {
const products = await fetchProducts(category)
return (
<ul>
{products.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
)
}
export default async function CategoryPage({
params,
}: {
params: Promise<{ category: string }> // Next.js 15: params는 Promise
}) {
const { category } = await params
return (
<div>
<h1>{category}</h1>
<Suspense fallback={<ProductSkeleton />}>
<ProductList category={category} />
</Suspense>
</div>
)
}| 코드 요소 | 역할 |
|---|---|
export const revalidate = 3600 |
1시간마다 백그라운드 재생성 트리거 (ISR) |
RSC ProductList |
서버에서만 실행, DB 쿼리 결과가 클라이언트 번들에 포함 안 됨 |
Suspense |
스트리밍 렌더링으로 TTFB 개선 |
ISR (Incremental Static Regeneration): 정적 페이지를 빌드 시점에 생성하되, 설정한 주기 또는 on-demand로 백그라운드에서 갱신하는 전략. 캐시된 페이지를 즉시 서빙하면서도 콘텐츠를 최신 상태로 유지할 수 있습니다.
한 가지 주의할 점이 있습니다. Next.js 15부터 fetch의 캐싱 기본값이 no-store로 변경되었습니다. 이전 버전 코드를 마이그레이션하거나, 캐싱에 의존하던 페이지가 있다면 반드시 캐시 동작을 명시적으로 확인해야 합니다.
장단점 분석
TanStack Start 장점
실무에서 가장 체감이 큰 건 역시 개발 속도입니다. Vite 기반 HMR이 워낙 빠르다 보니 코드 수정 후 결과 확인이 거의 즉각적입니다. Platformatic이 진행한 React SSR 프레임워크 벤치마크에서 TanStack Start는 1,000 req/s 기준 평균 응답 시간 13ms 미만으로 테스트 프레임워크 중 최고 처리량을 기록하기도 했습니다.
| 항목 | 내용 |
|---|---|
| end-to-end 타입 안전성 | 라우트 파라미터부터 서버 함수 반환값까지 자동 타입 추론. 타입 어노테이션 없이도 TypeScript strict 모드 완전 활용 가능 |
| 명시적 설계 | createServerFn은 코드만 봐도 서버 실행임이 명확. 암묵적 마법 없음 |
| Vite 기반 개발 경험 | 로컬 서버 시작 2~3초, HMR 반응 속도가 현저히 빠름 |
| 배포 플랫폼 비종속 | Nitro 어댑터로 Cloudflare Workers, AWS Lambda, Node 등 자유롭게 전환 |
| 별도 API 레이어 불필요 | Server Functions가 타입 안전 API 레이어 역할. tRPC 없이도 충분한 경우가 많음 |
TanStack Start 단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 짧은 역사 | v1.0이 2025년 출시. 장기 프로덕션 검증 사례가 적음 | 커뮤니티 Discord 모니터링, 충분한 PoC 후 적용 |
| RSC 미지원 | Zero-bundle 서버 컴포넌트 패턴 불가 (로드맵 단계) | 번들 크기가 매우 민감한 프로젝트라면 Next.js 검토 |
| 작은 생태계 | 서드파티 플러그인, 커뮤니티 자료, AI 도구 학습 데이터 모두 적음 | 공식 문서와 GitHub Discussions 적극 활용 |
| ISR 미지원 | 콘텐츠 사이트의 점진적 재생성 불가 | 별도 캐시 레이어(CDN, Redis) 구성 필요 |
Next.js 장점
검증된 생태계의 무게감은 실제로 큽니다. AI 코딩 도구(GitHub Copilot, Cursor)의 학습 데이터가 풍부해 자동완성 품질이 높고, 팀 온보딩 때 참고할 자료도 압도적으로 많습니다.
| 항목 | 내용 |
|---|---|
| 검증된 생태계 | 수천 개 기업 프로덕션 사용, 풍부한 레퍼런스와 성숙한 서드파티 통합 |
| RSC + Suspense 최적화 | Zero-bundle 서버 컴포넌트로 초기 번들 크기 최소화 |
| ISR | 고트래픽 콘텐츠의 효율적인 캐시 갱신 전략 |
| AI 통합 성숙도 | Vercel AI SDK의 스트리밍 UI 패턴이 잘 정비되어 있음 |
| AI 보조 도구 품질 | GitHub Copilot, Cursor 등의 학습 데이터가 풍부해 코드 자동완성 품질이 높음 |
Next.js 단점 및 주의사항
개인적으로 가장 자주 부딪히는 건 캐싱입니다. 버전마다 기본 동작이 바뀌어 왔고, Next.js 15에서 fetch 기본값이 no-store로 변경된 것도 모르고 캐싱이 안 된다며 한참 디버깅한 경험이 있습니다.
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 복잡한 캐싱 모델 | fetch 캐싱 기본값이 v15에서 no-store로 변경. revalidate, no-store 계층 규칙이 직관적이지 않음 |
데이터 신선도가 중요한 페이지는 캐시 옵션을 항상 명시적으로 설정 |
| 서버/클라이언트 경계 혼란 | RSC와 클라이언트 컴포넌트 간 직렬화 제약이 잦은 오류 유발 | "use client" 경계를 가능한 leaf 컴포넌트에 위치시킴 |
| Vercel 종속 우려 | ISR, Edge Middleware 등 일부 기능이 Vercel 인프라에 최적화 | 자체 호스팅 시 동작 차이를 사전에 문서화하여 검토 |
| 느린 HMR | App Router + RSC 환경에서 root HMR이 800ms 이상 걸리는 케이스 보고 | 컴포넌트 분리와 로컬 캐시 최적화로 일부 완화 가능 |
HMR (Hot Module Replacement): 페이지 새로고침 없이 수정된 모듈만 교체하는 개발 서버 기능. TanStack Start와 Next.js 모두 HMR을 지원하지만, Vite 기반인 TanStack Start가 체감 속도에서 유리합니다.
실무에서 가장 흔한 실수
-
생태계 크기만 보고 Next.js를 선택하는 것: 팀의 주요 작업이 복잡한 URL 상태 관리와 타입 안전성이라면, 더 작은 생태계의 TanStack Start가 실제 생산성은 높을 수 있습니다. 도구 선택은 "인기"가 아닌 "프로젝트 적합성"으로 판단하는 편이 훨씬 낫습니다.
-
TanStack Start에서 RSC가 없다는 점을 간과하는 것: 번들 크기가 중요한 콘텐츠 사이트를 TanStack Start로 구축하려 하면 클라이언트에 과도한 JS가 내려가는 상황이 생길 수 있습니다. RSC가 필요한 시나리오인지 먼저 확인해보면 좋습니다.
-
Next.js의 캐싱을 기본값으로 신뢰하는 것: Next.js 15부터 fetch 캐싱 기본값이
no-store로 변경되었습니다. 데이터 신선도가 중요한 페이지라면cache: "no-store"나revalidate: 0을 명시적으로 설정하는 것이 안전합니다. 과거 버전에서 마이그레이션할 때 특히 주의해야 합니다.
마치며
두 프레임워크의 선택 기준은 결국 "서버와 클라이언트 경계를 얼마나 명시적으로 제어하고 싶은가"와 "콘텐츠 캐싱 전략이 얼마나 중요한가"로 압축됩니다.
아래 질문 흐름으로 스스로 판단해볼 수 있습니다:
- ISR이 필요한가? → Yes: Next.js 선택 / No: 다음 질문으로
- RSC 번들 최적화가 핵심인가? → Yes: Next.js 선택 / No: 다음 질문으로
- 배포 플랫폼이 Vercel 외로 고정되어 있는가? → Yes: TanStack Start 우위 / No: 다음 질문으로
- end-to-end 타입 안전성이 팀의 최우선 가치인가? → Yes: TanStack Start 선택
지금 바로 시작해볼 수 있는 3단계:
-
TanStack Start 체험:
pnpm create tanstack@latest로 기본 프로젝트를 생성할 수 있습니다. 공식 문서의 Start vs Next.js 비교 페이지를 함께 읽어보면 철학 차이가 바로 와닿습니다. -
소규모 PoC 진행: 현재 팀 프로젝트의 한 페이지를 TanStack Start로 재구현해보면 좋습니다. 타입 안전 라우팅이 실제 개발 경험에 어떤 영향을 미치는지 직접 체감할 수 있습니다.
-
위 결정 흐름 적용: ISR 필요 여부, RSC 번들 최적화 필요 여부, 배포 플랫폼 제약, 팀 학습 비용 — 이 네 가지를 프로젝트 초반에 명시적으로 검토해두면 좋습니다. 나중에 프레임워크를 교체하는 비용은 생각보다 큽니다.
참고 자료
- TanStack Start vs Next.js 공식 비교 | TanStack 공식 문서
- TanStack Start 프레임워크 비교 표 | TanStack 공식 문서
- Announcing TanStack Start v1 | TanStack 공식 블로그
- TanStack Start vs Next.js: Choosing the Right Full-Stack React Framework | LogRocket
- React SSR Framework Benchmark | Platformatic Blog
- TanStack Start vs Next.js: Choosing the Right React Framework in 2025 | Prototyp Digital
- Next.js vs TanStack Start (이커머스 관점) | Crystallize
- Why Developers Are Leaving Next.js for TanStack Start | Appwrite Blog
- Beyond Next.js — TanStack Start and the Future of Full-Stack React | DEV.to
- TanStack Start vs Next.js vs Remix 비교 | Makers' Den
- Build a Full-Stack SaaS App with TanStack Start | freeCodeCamp
- Next.js, React Router, TanStack: When to Use Each | The New Stack