본문 바로가기
Backend2026년 5월 9일7분 읽기

tRPC v11 + React Query v6 — 풀스택 타입 안전성의 끝판왕

YS
김영삼
조회 1008
tRPC v11 + React Query v6 — 풀스택 타입 안전성의 끝판왕

핵심 요약

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

아직 댓글이 없습니다.
Ctrl+Enter로 등록