tRPC 실전 가이드 — 타입 안전한 API 만들기
tRPC로 타입 안전한 API를 만드는 방법. Next.js 통합, Zod 검증, React Query 연동까지.
한 줄 요약: tRPC는 API 스키마 정의 없이 TypeScript 타입만으로 프론트엔드와 백엔드 간 완전한 타입 안전성을 보장하는 RPC 라이브러리다. Next.js 풀스택 프로젝트에서 REST/GraphQL 없이 타입 오류를 컴파일 단계에서 잡을 수 있다.
tRPC는 "타입 안전한 API"를 위해 별도의 스키마 언어나 코드 생성 없이, 서버에서 정의한 TypeScript 타입이 클라이언트에 자동으로 전파되는 방식이다. REST API의 단순함과 GraphQL의 타입 안전성을 결합하면서도 설정 복잡도는 훨씬 낮다. Next.js App Router와의 통합이 특히 매끄러워 풀스택 TypeScript 프로젝트의 표준 선택지로 자리잡고 있다.
tRPC 핵심 개념과 작동 원리
tRPC의 핵심은 서버에서 정의한 Router 타입을 클라이언트에서 그대로 참조하는 방식이다. 별도의 스키마 파일(.graphql, .proto)이나 코드 생성 단계가 없다. 서버 함수의 입력/출력 타입이 TypeScript 추론으로 클라이언트에 전달된다.
- Router: 여러 프로시저를 묶는 단위. Express 라우터와 유사한 개념.
- Procedure: query(읽기), mutation(쓰기), subscription(실시간) 세 종류.
- Context: 각 요청에서 사용 가능한 공유 데이터 (인증 정보, DB 클라이언트 등).
- Middleware: 인증 체크, 로깅 등을 절차 실행 전 처리하는 레이어.
- React Query 통합: 클라이언트에서 자동으로 useQuery, useMutation 훅으로 변환.
Next.js App Router 통합 셋업
tRPC v11과 Next.js 14+ App Router를 기준으로 초기 셋업 과정을 단계별로 정리한다.
Bash — 패키지 설치npm install @trpc/server @trpc/client @trpc/react-query @tanstack/react-query zod npm install -D typescript @types/node
TypeScript — tRPC 초기화 (src/server/trpc.ts)import { initTRPC, TRPCError } from '@trpc/server' import { ZodError } from 'zod' // Context 타입 정의 export interface Context { userId?: string db: { /* DB 클라이언트 */ } } const t = initTRPC.context<Context>().create({ // Zod 에러를 클라이언트 친화적 형식으로 변환 errorFormatter({ shape, error }) { return { ...shape, data: { ...shape.data, zodError: error.cause instanceof ZodError ? error.cause.flatten() : null, }, } }, }) // 공개 프로시저 (인증 불필요) export const router = t.router export const publicProcedure = t.procedure // 인증 미들웨어 const isAuthed = t.middleware(({ ctx, next }) => { if (!ctx.userId) { throw new TRPCError({ code: 'UNAUTHORIZED' }) } return next({ ctx: { userId: ctx.userId } }) }) // 인증된 프로시저 export const protectedProcedure = t.procedure.use(isAuthed)
TypeScript — 라우터 정의 (src/server/routers/post.ts)import { z } from 'zod' import { router, publicProcedure, protectedProcedure } from '../trpc' import { TRPCError } from '@trpc/server' // 입력 스키마 const createPostInput = z.object({ title: z.string().min(1).max(100), content: z.string().min(10), published: z.boolean().default(false), }) export const postRouter = router({ // 게시글 목록 조회 (query) list: publicProcedure .input(z.object({ limit: z.number().min(1).max(50).default(10) })) .query(async ({ input, ctx }) => { // ctx.db에서 실제 DB 쿼리 return { posts: [], total: 0 } }), // 게시글 상세 조회 byId: publicProcedure .input(z.object({ id: z.string().cuid() })) .query(async ({ input, ctx }) => { const post = null // ctx.db에서 조회 if (!post) throw new TRPCError({ code: 'NOT_FOUND', message: '게시글을 찾을 수 없습니다' }) return post }), // 게시글 생성 (mutation, 인증 필요) create: protectedProcedure .input(createPostInput) .mutation(async ({ input, ctx }) => { const post = { id: 'generated-id', ...input, authorId: ctx.userId } return post }), // 게시글 삭제 delete: protectedProcedure .input(z.object({ id: z.string().cuid() })) .mutation(async ({ input, ctx }) => { return { success: true } }), })
Next.js API 핸들러와 클라이언트 설정
TypeScript — App Router API 핸들러 (app/api/trpc/[trpc]/route.ts)import { fetchRequestHandler } from '@trpc/server/adapters/fetch' import { appRouter } from '@/server/routers/_app' import { createContext } from '@/server/context' const handler = (req: Request) => fetchRequestHandler({ endpoint: '/api/trpc', req, router: appRouter, createContext: () => createContext(req), }) export { handler as GET, handler as POST }
TypeScript — 클라이언트 설정 (src/utils/trpc.ts)import { createTRPCReact } from '@trpc/react-query' import { httpBatchLink } from '@trpc/client' import type { AppRouter } from '@/server/routers/_app' export const trpc = createTRPCReact<AppRouter>() export function getTRPCClient() { return trpc.createClient({ links: [ httpBatchLink({ url: '/api/trpc', // 요청 배칭 활성화 (기본값) maxURLLength: 2048, }), ], }) }
React 컴포넌트에서 tRPC 훅 사용
tRPC의 React Query 통합으로 서버 프로시저가 자동으로 useQuery와 useMutation 훅으로 변환된다. 타입이 서버 정의에서 그대로 전파되므로 별도 타입 선언이 필요 없다.
TypeScript/React — 클라이언트 컴포넌트에서 사용'use client' import { trpc } from '@/utils/trpc' import { useState } from 'react' export function PostList() { // useQuery — 자동 타입 추론 const { data, isLoading, error } = trpc.post.list.useQuery({ limit: 10 }) // useMutation — 입력/출력 타입 자동 추론 const createPost = trpc.post.create.useMutation({ onSuccess: () => { // 목록 자동 갱신 utils.post.list.invalidate() }, }) const utils = trpc.useUtils() const [title, setTitle] = useState('') const [content, setContent] = useState('') if (isLoading) return <div>로딩 중...</div> if (error) return <div>에러: {error.message}</div> return ( <div> <form onSubmit={(e) => { e.preventDefault() createPost.mutate({ title, content }) }} > <input value={title} onChange={(e) => setTitle(e.target.value)} placeholder='제목' /> <textarea value={content} onChange={(e) => setContent(e.target.value)} placeholder='내용' /> <button type='submit' disabled={createPost.isPending}> {createPost.isPending ? '저장 중...' : '게시글 작성'} </button> </form> <ul> {data?.posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> </div> ) }
미들웨어와 Zod 검증 심화
tRPC 미들웨어는 프로시저 실행 전/후 로직을 삽입하는 방식이다. 인증, 로깅, 레이트 리미팅 등을 미들웨어로 분리해 관심사를 분리할 수 있다.
TypeScript — 로깅 미들웨어와 레이트 리밋 예제import { initTRPC, TRPCError } from '@trpc/server' const t = initTRPC.context<Context>().create() // 로깅 미들웨어 const withLogging = t.middleware(async ({ path, type, next }) => { const start = Date.now() const result = await next() const durationMs = Date.now() - start console.log(`[${type}] ${path} - ${durationMs}ms - ${result.ok ? 'OK' : 'ERROR'}`) return result }) // 레이트 리밋 미들웨어 (인메모리, 프로덕션에선 Redis 사용) const rateLimitMap = new Map<string, number[]>() const withRateLimit = t.middleware(({ ctx, next }) => { const key = ctx.userId ?? 'anonymous' const now = Date.now() const windowMs = 60_000 // 1분 const limit = 100 const timestamps = (rateLimitMap.get(key) ?? []).filter((t) => now - t < windowMs) if (timestamps.length >= limit) { throw new TRPCError({ code: 'TOO_MANY_REQUESTS', message: '요청 한도 초과' }) } rateLimitMap.set(key, [...timestamps, now]) return next() }) // 미들웨어 체이닝 export const loggedProcedure = t.procedure.use(withLogging) export const rateLimitedProcedure = t.procedure.use(withLogging).use(withRateLimit)
참고 자료:
- tRPC 공식 문서: trpc.io/docs
- Next.js + tRPC 예제: github.com/trpc/trpc examples
- create-t3-app (tRPC 포함 풀스택 보일러플레이트): create.t3.gg