Next.js + tRPC + Prisma + TanStack Query, 아직 유효한 풀스택 타입-세이프 조합?
TL;DR
Server Action의 부상으로 tRPC가 한물갔다는 인식이 있지만, 실제로는 v11 릴리즈(2025.03)와 주간 70만+ npm 다운로드로 건재하다. 오히려 2024~2025년 연이어 터진 Next.js 보안 취약점(CVE-2025-29927, CVE-2025-66478 등)은 Server Action이 의존하는 미들웨어/RSC 프로토콜의 위험성을 드러냈고, 명시적 API 경계를 제공하는 tRPC의 아키텍처적 가치를 재확인시켜 주었다.
이번에 사내 프로젝트를 Next.js + tRPC + Prisma + TanStack Query + AWS RDS(PostgreSQL) 스택으로 단기간에 구축한 결과, DB 마이그레이션부터 클라이언트 화면까지 end-to-end 타입-세이프하고 레이어 분리가 명확한 애플리케이션을 빠르게 만들 수 있었다. 특히 LLM 페어 프로그래밍에서 각 레이어가 독립적이고 예측 가능한 패턴을 가져, AI가 정확한 코드를 생성하는 데 큰 도움이 되었다.
개요
배경: Server Action 시대에 tRPC를 선택한 이유
Next.js App Router와 함께 Server Action이 등장하면서 tRPC는 "더 이상 필요 없다"는 의견이 커뮤니티에서 종종 보인다. Dan Abramov가 Server Action을 "bundler feature로서의 tRPC"라고 표현한 것도 이런 흐름에 힘을 실어주었다.
하지만 실무에서 Server Action을 쓰다 보면 몇 가지 불편함이 체감된다.
1) 보안 우려가 현실로 드러났다.
2025년, Next.js에서 치명적인 CVE가 연달아 공개되었다.
- CVE-2025-29927 (CVSS 9.1):
x-middleware-subrequest헤더를 조작하면 미들웨어를 완전히 우회할 수 있는 취약점. Server Action의 인증/인가가 미들웨어에 의존하는 경우, 이 우회로 Server Action 엔드포인트가 그대로 노출되었다. - CVE-2025-66478 (CVSS 10.0): "React2Shell"로 불린 RSC Flight 프로토콜의 역직렬화 취약점. 인증 없이 원격 코드 실행(RCE)이 가능했으며, 공개 수 시간 내에 실제 공격이 관측되었다.
- CVE-2024-34351 (CVSS 7.5): Server Action의 리다이렉트 과정에서 Host 헤더를 조작해 SSRF가 가능한 취약점.
Next.js 공식 문서에서도 "Server Action을 만들고 export하면 기본적으로 공개 HTTP 엔드포인트가 생성된다"고 명시하고 있다. 인증, 인가, 입력 검증, rate limiting 등은 모두 개발자가 직접 추가해야 한다.
2) 레이어 분리가 어렵다.
Server Action은 컴포넌트 파일 안에 "use server"로 인라인 정의할 수 있어 편리하지만, 라우터 네임스페이스나 미들웨어 체인 같은 구조적 장치가 없다. 커뮤니티에서도 "Server Action을 네임스페이스로 그룹핑하기 어렵다"는 지적이 꾸준히 나온다. 오픈소스 문서 서명 플랫폼 Documenso는 Server Action에서 tRPC로 역마이그레이션하기도 했다.
3) 데이터 페칭에서의 한계.
Server Action은 모든 요청이 POST이므로 HTTP 캐싱이 불가능하고, 같은 페이지에서 여러 Server Action을 호출하면 병렬 실행이 안 되어 페이지 로드가 느려진다. TanStack Query가 제공하는 stale-while-revalidate, optimistic update, prefetch 같은 기능도 자연스럽게 쓸 수 없다.
이번 프로젝트의 제약 조건
이번에 만든 웹 애플리케이션은 다음과 같은 조건이 있었다.
- Next.js가 직접 AWS RDS(PostgreSQL)에 Prisma 어댑터를 통해 커넥션 풀을 만들어 연결해야 했다.
- 단기간 내에 완성해야 해서, 서버-클라이언트 간 타입을 프로젝트 단위로 빠르게 통일할 방법이 필요했다.
- 이전에는 Zod 스키마로 request/response 객체를 직접 검증하는 방식을 사용했지만, 이번엔 그 스키마 작성 시간조차 아끼고 싶었다.
- LLM과의 페어 프로그래밍을 적극 활용할 계획이었으므로, AI가 추적하기 쉬운 구조가 중요했다.
이런 조건들을 종합하니 tRPC + Prisma + TanStack Query 조합이 답이었다.
방법
타입 흐름: Prisma Schema → tRPC Router → TanStack Query Hook
이 스택의 핵심은 코드 생성이나 수동 타입 정의 없이 DB에서 UI까지 타입이 자동으로 흘러간다는 점이다.
[Prisma Schema] → prisma generate → [TypeScript 타입]
↓
[tRPC Router] ← ctx.prisma.post.findMany() 반환 타입 자동 추론
↓
[TanStack Query Hook] ← api.post.getAll.useQuery() → Post[] 타입 자동 완성
Prisma가 DB 스키마로부터 Post, Prisma.PostCreateInput, Prisma.PostWhereInput 등의 TypeScript 타입을 생성한다. tRPC 프로시저에서 ctx.prisma.post.findMany()를 호출하면, 그 반환 타입이 프로시저의 output 타입으로 자동 추론된다. 클라이언트에서 TanStack Query 훅을 호출하면 tRPC의 타입 추론을 통해 완전히 타이핑된 데이터를 받는다.
실천한 주요 패턴
- PrismaClient를 tRPC context로 전달: PrismaClient를 한 번만 초기화하고 context 객체에 붙여서 모든 프로시저에서 공유했다. Next.js 개발 환경에서는
globalThis에 저장하는 싱글턴 패턴을 적용해 핫 리로드 시 커넥션 고갈을 방지했다. - Zod validation은 tRPC 프로시저 레벨에서: 입력 검증을 Prisma에 도달하기 전에 tRPC의
.input()에서 Zod로 처리해, 잘못된 데이터가 DB 레이어까지 내려가지 않도록 했다. - tRPC v11의 새 TanStack Query 통합 사용: 기존 tRPC 클라이언트는
useQuery/useMutation을 래핑하는 방식이라 React hooks 규칙을 위반하는 문제가 있었다. v11의 새 통합은 TanStack Query의queryOptionsAPI를 네이티브로 사용해 React Compiler와도 호환된다. RouterOutputs헬퍼 타입 활용: 프론트엔드에서type User = RouterOutputs['user']['getById']형태로 서버 응답 타입을 추론해, 별도의 타입 정의 파일 없이도 컴포넌트에서 정확한 타입을 사용했다.
AWS RDS 연결 시 주의사항
Prisma로 AWS RDS에 연결할 때 한 가지 중요한 함정이 있다. AWS RDS Proxy는 Prisma와 함께 사용할 때 커넥션 풀링 이점이 없다. Prisma가 모든 쿼리에 prepared statement를 사용하기 때문에 RDS Proxy가 커넥션을 고정(pin)시켜 재사용이 안 된다. Prisma 공식 문서에서도 이를 명시하고 있다.
대안으로는 PgBouncer(같은 VPC의 EC2에서 transaction mode로 운영)나 Prisma Accelerate를 사용할 수 있다. PgBouncer 사용 시에는 connection URL에 pgbouncer=true를 설정하고, 마이그레이션용 별도 DIRECT_URL을 구성해야 한다.
이번 프로젝트에서는 Next.js가 Prisma의 @prisma/adapter-pg를 통해 직접 RDS에 붙는 구조를 사용했다. Prisma 7부터는 Rust 쿼리 엔진이 사라지고 TypeScript 기반 쿼리 컴파일러로 대체되면서 번들 크기가 약 90% 줄었다(14MB → 1.6MB). Pool 설정은 어댑터에서 직접 한다.
성과
1. DB migrate부터 화면까지 타입-세이프한 개발 경험
Prisma 스키마를 수정하고 prisma migrate dev를 실행하면, 타입 변경이 tRPC 라우터를 거쳐 프론트엔드 훅까지 자동으로 전파된다. 필드명을 바꾸거나 nullable을 변경하면 관련된 모든 곳에서 TypeScript 컴파일 에러가 발생해서, 런타임에 발견하는 대신 개발 시점에 잡을 수 있었다.
2. TanStack Query와의 궁합
tRPC v11의 새 TanStack Query 통합 덕분에 캐싱, invalidation, optimistic update가 자연스럽게 작동했다. 기존에 Zod + fetch로 직접 관리하던 서버 상태를 TanStack Query의 선언적 패턴으로 대체하니 보일러플레이트가 크게 줄었다. 특히 목록 페이지에서 필터링/정렬/페이지네이션을 처리할 때, tRPC의 query key가 TanStack Query의 캐시 키와 자연스럽게 맞물려 별도 key 설계가 필요 없었다.
3. LLM 페어 프로그래밍에 유리한 구조
이 스택이 AI 코딩 어시스턴트와 특히 잘 맞는다고 느꼈는데, 그 이유는 세 가지다.
레이어별 독립적 컨텍스트 제공이 가능하다. Prisma 스키마를 보여주면 데이터 모델링, tRPC 라우터를 보여주면 API 로직, TanStack Query 훅을 보여주면 UI 로직에 집중하게 할 수 있다. 각 레이어의 패턴이 일관적이므로 AI의 컨텍스트 윈도우를 효율적으로 사용할 수 있다.
타입 체계가 AI의 실수를 자동으로 잡아준다. GitHub의 연구에 따르면 LLM이 생성한 코드의 컴파일 에러 중 94%가 타입 체크 실패라고 한다. Prisma → tRPC → TanStack Query로 이어지는 end-to-end 타입 흐름이 이런 에러를 즉시 잡아준다. AI가 생성한 코드가 타입 체크를 통과하면, 상당 부분 정확한 코드라고 신뢰할 수 있는 "검증 루프"가 형성된다.
선언적이고 예측 가능한 패턴이다. Prisma의 선언적 스키마, tRPC의 라우터 프로시저 + Zod 검증, TanStack Query의 훅 패턴은 모두 정형화되어 있다. Prisma 공식 블로그에서도 PSL(Prisma Schema Language)이 LLM과 AI 도구가 스키마를 생성하고 수정하기 쉽도록 설계되었다고 밝히고 있다. 실제로 Prisma는 Cursor, Windsurf, GitHub Copilot 연동 가이드와 MCP 서버까지 공식 제공한다.
4. 단점: Prisma의 쿼리 복잡성
솔직히 불편했던 부분도 있다. WHERE 절이 복잡해지는 경우, 특히 여러 릴레이션을 넘나드는 필터링에서 Prisma의 쿼리 코드가 상당히 장황해진다. some, is, every 같은 nested relation 필터를 중첩하다 보면 가독성이 떨어진다. 이건 tRPC의 문제가 아니라 Prisma의 findMany 등이 JOIN이 많아질수록 복잡하게 보이는 특성이다.
다만 탈출구는 있다. Prisma의 TypedSQL(v5.19.0+)을 사용하면 .sql 파일에 직접 SQL을 작성하면서도 타입 안전성을 유지할 수 있다. 또한 relation load strategy에서 relationLoadStrategy: 'join' 옵션을 사용하면 PostgreSQL의 LATERAL JOIN을 활용해 별도 쿼리 대신 단일 JOIN 쿼리로 처리할 수도 있다. 커뮤니티의 합의는 "90%의 쿼리는 Prisma Client로, 나머지 10%의 복잡한 쿼리는 raw SQL 또는 TypedSQL로" 하는 것이 현실적이라는 것이다.
참고자료
tRPC
- Announcing tRPC v11 — v11 릴리즈 공식 블로그
- Introducing the new TanStack React Query integration — 새 TanStack Query 통합 소개
- Using Server Actions with tRPC — tRPC에서 Server Action을 함께 사용하는 방법
- @trpc/server npm — npm 패키지 (다운로드 수 확인)
Next.js 보안 취약점
- CVE-2025-29927 기술 분석 (ProjectDiscovery) — 미들웨어 우회 취약점
- Postmortem on Next.js Middleware bypass (Vercel) — Vercel 공식 포스트모템
- Security Advisory: CVE-2025-66478 (Next.js) — React2Shell 보안 권고
- React2Shell 취약점 AWS 분석 — CVE-2025-55182 실제 공격 사례
Server Action vs tRPC 비교
- Server Actions and Security — GitHub Discussion — Server Action 보안 우려 커뮤니티 논의
- Removing Server Actions → tRPC (Documenso) — Documenso의 Server Action → tRPC 역마이그레이션 사례
- Why I Migrated from Server Actions to tRPC (DEV.to) — 개발자 경험 비교
Prisma + AWS
- Caveats when deploying to AWS platforms (Prisma) — AWS 배포 시 주의사항
- Configure Prisma Client with PgBouncer — PgBouncer 설정 가이드
- Connection pool (Prisma) — 커넥션 풀 관리
Prisma 쿼리 & TypedSQL
- Announcing TypedSQL (Prisma) — TypedSQL 소개
- Database vs Application: Demystifying JOIN Strategies (Prisma) — JOIN 전략 비교
- Prisma Schema Language: The Best Way to Define Your Data — PSL의 AI 친화적 설계
LLM 페어 프로그래밍 & AI-friendly 아키텍처
- A developer's guide to designing AI-ready frontend architecture (LogRocket) — AI 친화적 프론트엔드 설계
- Why AI Is Pushing Us All Toward TypeScript (YUV.AI) — TypeScript와 AI의 시너지
- The AI-Friendly Tech Stack I Like Right Now — tRPC/TS/Postgres를 AI와 함께 사용한 실전 후기
'TIL' 카테고리의 다른 글
| [260223 TIL] 날짜 비교 시 UTC 타임존 문제 (0) | 2026.02.23 |
|---|---|
| [260117 TIL] Next Image와 Preload를 활용한 이미지 최적화 (0) | 2026.01.17 |
| [251227 TIL] RSC 에서 redirect 사용시 주의점 (0) | 2025.12.27 |
| [251214 TIL] React2Shell 사건 정리 (0) | 2025.12.14 |
| [251127 TIL] Next.js 이미지 최적화 정리 (0) | 2025.11.27 |