🔷 GraphQL8. 이론 & 대안04 — GraphQL vs tRPC

04 — GraphQL vs tRPC

한 줄 답: tRPC는 TypeScript 모노레포 안에서 서버 함수의 타입을 그대로 클라이언트로 import하는 도구다 — schema 없이 end-to-end type safety. GraphQL은 언어/팀/플랫폼이 갈라질 때의 일반화된 도구. TS 풀스택 모노레포에서는 tRPC가 더 가볍고, 그 경계를 벗어나면 GraphQL이 더 일반화되어 있다.


Why — 왜 이 비교가 2022년 이후 자주 등장하나

tRPC는 2021년 Alex Johansson이 만들었고, 2022~2023년 Next.js + TS 생태계에서 폭발적으로 컸다. “GraphQL의 schema 부담 없이 type-safe API”라는 가벼움이 매력. 그러나 왜 GraphQL이 처음에 schema를 만들었는지를 이해 못 하면 잘못된 비교가 된다.

흔한 잘못된 프레임진실
”tRPC가 GraphQL의 진화형”다른 trade-off — tRPC는 언어 종속을 받아들이고 schema를 버린다
”GraphQL은 무거우니 tRPC가 낫다”무거움의 대가가 있다 — 다언어, 외부 client, codegen, federation
”둘 다 type safety다”type safety의 경계가 다름 — tRPC는 컴파일 타임 같은 process, GraphQL은 런타임 + 다언어

How — 두 도구가 어떻게 다른가

1) 비교 매트릭스

차원tRPCGraphQL
본질TS 함수 시그니처 공유 RPCQuery language + schema
계약 표현TS 타입 (자동 추론)SDL (명시)
언어TypeScript 전용사실상 전부
모노레포 강제사실상 필요 (import type)무관
wire formatJSON (정해진 모양)JSON (요청이 모양 결정)
응답 모양서버 결정요청 결정
codegen불필요 (직접 import)별도 단계 (graphql-codegen)
runtime overhead매우 적음중간 (parse·validate·execute)
streamingsubscription 지원 (제한적)subscription·@defer
federation없음있음
외부 클라이언트어려움자연스러움
introspectionTS 컴파일러spec 의무
잘 맞는 자리TS 풀스택 모노레포 (Next.js)다언어·다팀·외부 노출

2) tRPC의 구조

// server/routers/user.ts
export const userRouter = router({
  getById: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input }) => {
      return await db.user.findUnique({ where: { id: input.id } });
    }),
});
 
export type AppRouter = typeof appRouter;
// client/index.ts
import type { AppRouter } from '../server/routers';
import { createTRPCProxyClient } from '@trpc/client';
 
const trpc = createTRPCProxyClient<AppRouter>({ url: '/api/trpc' });
 
const user = await trpc.user.getById.query({ id: '1' });
// user의 type은 *자동 추론* — 서버 코드를 직접 import 했으니까

핵심: 서버의 return type을 클라이언트가 직접 import한다. schema 정의 단계가 없다. TS 컴파일러가 곧 타입 동기화 메커니즘.

3) GraphQL과의 핵심 차이

  • tRPC: TS 컴파일러가 모든 type 동기화를 책임짐. 서버와 클라이언트가 같은 monorepo에 있어야 함. 다른 언어면 못 씀.
  • GraphQL: SDL이 언어 독립적 contract. 서버는 Go, 클라이언트는 Swift여도 SDL을 통해 type 안전.

→ tRPC는 경계 안에서 빠르고 가볍다. GraphQL은 경계를 넘는 데 강하다.

4) 응답 모양의 결정권

// tRPC — 서버가 결정한 User 전부
const user = await trpc.user.getById.query({ id: '1' });
// → { id, name, email, createdAt, posts: [...], ... }
# GraphQL — 클라이언트가 선택
query { user(id: "1") { name } }
# → { user: { name: "Alice" } }

tRPC는 over-fetching에 약하다. 서버가 한 procedure에 큰 객체를 return하면 전부 내려온다. 회피하려면 procedure를 화면별로 쪼개야 함 — REST 패턴 회귀. GraphQL은 클라이언트가 잘라낸다.

5) Schema의 부재가 주는 trade-off

Schema 없음의 장점 (tRPC)Schema 없음의 단점
시작 비용 0 — 함수만 만들면 끝서버 코드를 모르는 외부 클라이언트는 못 씀
컴파일 타임에 모든 type check다른 언어/팀과 계약 공유 불가
codegen 단계 없음introspection이 TS 컴파일러 한정
작은 번들 (런타임 schema 없음)API 문서 자동화가 어려움 (Zod로 부분 가능)
refactor가 즉각 클라이언트에 전파됨breaking change너무 쉽게 일어남

What — 실제 사용 시나리오

시나리오 A — 1인 풀스택 Next.js 프로젝트

선택: tRPC. 이유 — 한 코드베이스, TS 한 언어, 클라이언트가 하나. schema overhead 없이 함수처럼 API 호출.

const user = await trpc.user.getById.query({ id: '1' });  // 자동 type

시나리오 B — 스타트업의 모바일 + 웹 + Admin

처음엔 tRPC로 시작했다가…

  • iOS 앱 추가 → tRPC 안 됨 (Swift은 TS 타입 import 불가)
  • Android 앱 추가 → 또 막힘
  • 외부 파트너 API 노출 → 또 막힘

대안: GraphQL로 전환하거나, 모바일 부분만 GraphQL을 추가.

tRPC의 첫 한계는 보통 플랫폼 확장 시점에 온다.

시나리오 C — 마이크로프런트엔드 (다팀)

각 팀이 독립 배포하고 각자의 type을 가지고 싶다.

  • tRPC: 모든 팀이 같은 monorepo에 있어야 type import가 가능. 강제됨.
  • GraphQL: 각 팀이 subgraph를 만들고 federation으로 합침. 팀 경계 자연스러움.

다팀이면 GraphQL의 federation이 압승.

시나리오 D — 외부 개발자에게 노출

  • tRPC: 불가능에 가까움. 클라이언트가 서버 TS 코드에 access 해야 함.
  • GraphQL: SDL 한 장이 곧 문서. introspection으로 SDK 자동 생성.

공용/파트너 API는 GraphQL (or REST).


What-if — 잘못 선택하면

1) iOS 앱이 곧 추가될 줄 알면서 tRPC 선택

→ 6개월 뒤 전환 비용이 더 큼. 대응: 플랫폼 확장 가능성이 있으면 GraphQL.

2) GraphQL을 1인 풀스택 Next.js에 도입

→ schema 작성 + codegen + Apollo 세팅 — 과한 overhead. 대응: TS 한 언어 + 한 클라이언트면 tRPC가 가벼움.

3) tRPC를 다팀 마이크로서비스에 도입

→ monorepo 강제 + type import 의존 — 팀 독립성 파괴. 대응: 팀 경계가 명확하면 GraphQL federation.

4) tRPC의 procedure를 그래프처럼 짜려 함

→ tRPC는 함수 묶음 — 관계 traversal이 안 됨. N 호출 회귀. 대응: 진짜 그래프가 필요하면 GraphQL.

5) “tRPC는 가벼우니 schema 안 만들어도 된다”

→ Zod schema는 runtime validation 책임이라 어쨌든 정의해야 함. schema 없음type 안전 한정. 대응: tRPC도 Zod schema는 거의 필수 — schema 부담을 과소평가하면 안 됨.


Insight — 흥미로운 이야기

”tRPC는 GraphQL의 비판에서 태어났다”

Alex Johansson은 tRPC를 만들기 전 GraphQL을 Next.js에 도입하면서 schema 작성 → codegen → 클라이언트 type import의 4단계가 TS 단일 언어에서는 낭비라고 느꼈다. “TS 컴파일러가 이미 type을 알고 있는데, 왜 또 SDL을 적냐”는 의문이 출발점. 결과: TS 컴파일러가 곧 contract인 RPC.

이 통찰은 정확하다 — 하지만 경계 안에서만. 경계를 넘는 순간 다시 SDL 같은 contract가 필요해진다.

”Theo의 영상이 만든 폭발”

YouTuber Theo(t3.gg)가 2022년 “Why I switched from GraphQL to tRPC” 영상을 올린 뒤 채택률이 폭발했다. 이 영상이 정확히 지적한 것 — 작은 풀스택 TS 프로젝트에서 GraphQL의 overhead는 실제로 과하다. 다만 그 영상의 결론을 모든 규모로 일반화하는 건 잘못. Theo 본인도 후속 영상에서 “tRPC는 모든 자리의 답이 아니다”라고 정정했다.

”Schema vs Inferred Type — 종교 전쟁”

함수형 진영(특히 Haskell·Elm)은 명시적 schema를 선호한다 — type을 적는 것 자체가 설계다. TS 진영(특히 React·Next)은 inferred type을 선호 — type 적기를 줄이는 게 생산성. tRPC와 GraphQL의 비교는 결국 이 철학 차이의 거울이다.

”Connect-RPC가 가운데 자리”

Buf의 Connect(2022)는 .proto + 자동 codegen + HTTP/1.1 호환을 노린다. tRPC의 가벼움 + gRPC의 다언어 + GraphQL의 schema-driven — 세 가지의 교집합을 목표로 한다. 아직 시장에서 자리를 잡진 않았지만, 세 도구 사이의 빈 공간을 보여준다.


요약 + 다이어그램

tRPC는 경계 안에서 가장 가볍고, GraphQL은 경계 너머에서 가장 일반적이다. TS 모노레포 1개 클라이언트면 tRPC, 다언어/다팀/외부 노출이면 GraphQL. 경계가 어디까지 확장될 가능성이 결정의 핵심.

다음 문서: 05-graphql-vs-rpc.mdx — JSON-RPC·OpenRPC 같은 전통 RPC와는 또 어떻게 다른가?