핵심 요약
OpenAPI 스키마·코드 생성 없이 백엔드 타입을 그대로 프론트에서 호출. tRPC v11은 React Query v6와 결합되며 캐시·낙관적 업데이트·suspense까지 일관된 DX를 제공한다. 6개월 운영 결과 정리.
1. 5분 셋업
// server/router.ts
import { initTRPC } from '@trpc/server'
import { z } from 'zod'
const t = initTRPC.create()
export const appRouter = t.router({
user: t.router({
byId: t.procedure
.input(z.object({ id: z.number() }))
.query(({ input }) => db.user.findUnique({ where: { id: input.id } })),
update: t.procedure
.input(z.object({ id: z.number(), name: z.string() }))
.mutation(({ input }) => db.user.update({ where: { id: input.id }, data: input })),
}),
})
export type AppRouter = typeof appRouter
// client.tsx
const trpc = createTRPCReact<AppRouter>()
function User({ id }) {
const { data } = trpc.user.byId.useQuery({ id })
return <div>{data?.name}</div>
}
입력·출력 타입은 100% 자동 추론. tsc가 잡으면 런타임에도 안 깨짐.
2. v11에서 무엇이 바뀌었나
- 스트리밍 응답: 한 호출에서 여러 chunk (LLM 응답·진행률)
- FormData 입력: 파일 업로드 표준 지원
- Server Components 통합:
createTRPCNext+ RSC에서 직접 호출 - SSE 어댑터: WebSocket 없이 서버→클라 푸시
3. 스트리밍 — LLM 응답에 딱
// server
chat: t.procedure
.input(z.object({ prompt: z.string() }))
.subscription(async function* ({ input }) {
for await (const chunk of llm.stream(input.prompt)) {
yield chunk
}
})
// client
trpc.chat.useSubscription({ prompt }, {
onData: chunk => setText(prev => prev + chunk),
})
4. 에러 — 형태 표준화
throw new TRPCError({
code: 'FORBIDDEN',
message: '권한이 없습니다',
cause: { resource: 'project', id },
})
// 클라이언트
const { error } = trpc.x.useQuery(...)
if (error?.data?.code === 'FORBIDDEN') ...
error.data로 구조화 정보. Sentry에 그대로 보낼 수 있음.
5. 인증 미들웨어
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.user) throw new TRPCError({ code: 'UNAUTHORIZED' })
return next({ ctx: { ...ctx, user: ctx.user } })
})
const protectedProcedure = t.procedure.use(isAuthed)
6. React Query v6와 결합 — 캐시 키 무료
tRPC가 키를 자동 생성. queryClient.invalidateQueries(getQueryKey(trpc.user.byId, { id: 1 })) 같은 식.
const utils = trpc.useUtils()
await mutation.mutateAsync({ id, name })
await utils.user.byId.invalidate({ id })
7. 낙관적 업데이트
const update = trpc.user.update.useMutation({
onMutate: async (newData) => {
await utils.user.byId.cancel({ id: newData.id })
const prev = utils.user.byId.getData({ id: newData.id })
utils.user.byId.setData({ id: newData.id }, newData)
return { prev }
},
onError: (err, newData, ctx) => {
utils.user.byId.setData({ id: newData.id }, ctx?.prev)
},
})
8. 단점·트레이드오프
- 외부 노출 API에 부적합: 자사 풀스택 전용. 외부엔 OpenAPI/REST.
- 패키지 결합도: 백·프론트가 같은 레포여야 자연스러움. polyrepo는 codegen 필요.
- 버전 호환: 클라가 구 버전이고 서버가 신 버전이면 입력 스키마 변경에 깨짐. SemVer 정책 필요.
- 모바일: React Native·Swift/Kotlin은 자체 클라이언트 없음. REST 어댑터 별도.
9. 실측 — 사내 SaaS 6개월
| 지표 | 전 | 후 |
|---|---|---|
| API 타입 불일치 버그 | 월 5건 | 월 0건 |
| API 변경 → 프론트 반영 | 2~4시간 | 10분 |
| API 문서 작성 시간 | 월 6시간 | 0 (자동) |
| 번들 크기 (REST 클라이언트 제거) | — | -18kB |
10. 안티패턴
- "모든 API를 tRPC로" — 외부 노출은 REST/GraphQL 유지
- "입력 검증을 zod 없이" — 타입 안전성 깨짐, 반드시 zod/valibot
- "한 procedure에 너무 많은 로직" — 라우터/도메인 분리 유지
참고
- trpc.io/docs/v11
- tanstack.com/query/v6 — 변경점

댓글 0