OPEN HYPER STEP
← 목록으로 (stack-analysis)
STACK-ANALYSIS · 52 / 90
stack-analysis
CHAPTER 52 / 90
읽기 약 2
FUNCTION

tRPC: 타입 안전 API의 미래


핵심 개념

tRPC v11·router·procedure·미들웨어·React Query 통합 — 풀스택 TS.

본문

tRPC 셋업

BASH📋 코드 (1줄)
pnpm add @trpc/server @trpc/client @trpc/react-query @tanstack/react-query zod

서버 — Router

TYPESCRIPT📋 코드 (85줄)
// server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';

const t = initTRPC.context<Context>().create();

// 미들웨어 — 인증
const isAuthed = t.middleware(({ ctx, next }) => {
  if (!ctx.userId) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }
  return next({ ctx: { ...ctx, userId: ctx.userId } });
});

export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(isAuthed);


// server/routers/post.ts
import { z } from 'zod';
import { router, publicProcedure, protectedProcedure } from '../trpc';

export const postRouter = router({
  list: publicProcedure
    .input(z.object({
      limit: z.number().min(1).max(100).default(20),
      cursor: z.string().optional(),
    }))
    .query(async ({ input, ctx }) => {
      const items = await ctx.db.post.findMany({
        take: input.limit + 1,
        cursor: input.cursor ? { id: input.cursor } : undefined,
        orderBy: { createdAt: 'desc' },
      });

      const nextCursor = items.length > input.limit
        ? items.pop()!.id : null;

      return { items, nextCursor };
    }),

  byId: publicProcedure
    .input(z.string())
    .query(async ({ input, ctx }) => {
      const post = await ctx.db.post.findUnique({ where: { id: input } });
      if (!post) throw new TRPCError({ code: 'NOT_FOUND' });
      return post;
    }),

  create: protectedProcedure
    .input(z.object({
      title: z.string().min(1).max(200),
      content: z.string().min(1),
    }))
    .mutation(async ({ input, ctx }) => {
      return ctx.db.post.create({
        data: { ...input, authorId: ctx.userId },
      });
    }),

  update: protectedProcedure
    .input(z.object({
      id: z.string(),
      title: z.string().min(1).max(200).optional(),
      content: z.string().min(1).optional(),
    }))
    .mutation(async ({ input, ctx }) => {
      const post = await ctx.db.post.findUnique({ where: { id: input.id } });
      if (!post) throw new TRPCError({ code: 'NOT_FOUND' });
      if (post.authorId !== ctx.userId) {
        throw new TRPCError({ code: 'FORBIDDEN' });
      }
      const { id, ...data } = input;
      return ctx.db.post.update({ where: { id }, data });
    }),
});


// 통합
export const appRouter = router({
  post: postRouter,
  user: userRouter,
});

export type AppRouter = typeof appRouter;

클라이언트 (Next.js)

TYPESCRIPT📋 코드 (26줄)
// lib/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server';

export const trpc = createTRPCReact<AppRouter>();


// app/providers.tsx
'use client';
import { httpBatchLink } from '@trpc/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();
const trpcClient = trpc.createClient({
  links: [httpBatchLink({ url: '/api/trpc' })],
});

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    </trpc.Provider>
  );
}

사용 — 완벽한 타입 안전

TYPESCRIPT📋 코드 (43줄)
function PostList() {
  // 자동 완성 + 타입 추론
  const { data, isLoading } = trpc.post.list.useQuery({ limit: 20 });

  if (isLoading) return <Skeleton />;

  return (
    <ul>
      {data?.items.map(post => (
        // post의 타입 = 서버 정의와 100% 일치
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}


function CreatePost() {
  const utils = trpc.useUtils();
  const createMutation = trpc.post.create.useMutation({
    onSuccess: () => {
      // 캐시 무효화
      utils.post.list.invalidate();
    },
  });

  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      const form = new FormData(e.currentTarget);
      createMutation.mutate({
        title: form.get('title') as string,
        content: form.get('content') as string,
      });
    }}>
      <input name="title" />
      <textarea name="content" />
      <button disabled={createMutation.isPending}>
        {createMutation.isPending ? '저장 중...' : '저장'}
      </button>
    </form>
  );
}

tRPC vs GraphQL vs REST

📋 코드 (25줄)
tRPC:
✅ 풀스택 TS — 100% 타입 안전
✅ 학습 곡선 낮음 (REST와 유사)
✅ 코드 생성 불필요
❌ TS 전용 (다른 언어 클라이언트 어려움)
❌ 공개 API 부적합


GraphQL:
✅ 다양한 클라이언트
✅ 선택적 페치
❌ 셋업 복잡
❌ 캐싱 어려움


REST:
✅ 표준·생태계
✅ HTTP 캐시
❌ 타입 안전 X (별도 작업)


결론:
- 사내 풀스택 TS → tRPC (강력 추천)
- 공개 API → REST + OpenAPI
- 다양한 클라이언트 → GraphQL

다음 모듈

CH.53~60 "인프라 기초" — Vercel·AWS·CI/CD.


AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude

무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6

내 코드의 tRPC 부분을 분석해서
실전 분석 + 개선 우선순위를 알려줘.
ChatGPT

무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro

tRPC 관련 인기 라이브러리/패턴 5개를
비교 분석해서 패턴 추출를 알려줘.
Gemini

무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro

내 프로젝트 전체에서 tRPC
최적화 가능 위치를 보고해줘.
Grok

무료: Grok 4.1 / SuperGrok $30/mo

2026년 한국 백엔드 시장의
tRPC 트렌드를 솔직히 알려줘.

⭐ 이것만 기억하세요
tRPC: 타입 안전 API의 미래 이 3가지만 확실히 잡으세요
1.tRPC = 풀스택 TS의 타입 안전 끝판왕 — 코드 생성 불필요
2.Zod 스키마 + procedure = 검증·타입·문서 통합
3.TanStack Query 통합으로 캐싱·invalidate 표준


공유하기
진행도 52 / 90