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) 비교 매트릭스
| 차원 | tRPC | GraphQL |
|---|---|---|
| 본질 | TS 함수 시그니처 공유 RPC | Query language + schema |
| 계약 표현 | TS 타입 (자동 추론) | SDL (명시) |
| 언어 | TypeScript 전용 | 사실상 전부 |
| 모노레포 강제 | 사실상 필요 (import type) | 무관 |
| wire format | JSON (정해진 모양) | JSON (요청이 모양 결정) |
| 응답 모양 | 서버 결정 | 요청 결정 |
| codegen | 불필요 (직접 import) | 별도 단계 (graphql-codegen) |
| runtime overhead | 매우 적음 | 중간 (parse·validate·execute) |
| streaming | subscription 지원 (제한적) | subscription·@defer |
| federation | 없음 | 있음 |
| 외부 클라이언트 | 어려움 | 자연스러움 |
| introspection | TS 컴파일러 | 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와는 또 어떻게 다른가?