스키마나 코드 생성 없이 타입세이프 API를 구축하고 사용하는 tRPC
(trpc.io)- tRPC는 풀스택 TypeScript 앱에서 TypeScript 추론을 공유해, 서버와 클라이언트 사이의 API 타입 불일치를 줄이는 도구임
- 서버 API를 바꾸면 클라이언트 코드에서 곧바로 TypeScript 오류와 자동완성으로 영향을 확인할 수 있어 리팩터링 부담이 줄어듦
- 별도 스키마나 코드 생성 단계 없이 동작해, 빌드 절차와 런타임 부하를 늘리지 않고 타입세이프 API를 구성할 수 있음
- React, Next.js, Express, Fastify, AWS Lambda, Solid, Svelte 등 여러 JavaScript 환경에 붙일 수 있는 어댑터를 제공함
- 기본 흐름은 프로시저 정의, HTTP 서버 생성,
AppRouter타입을 넘긴 클라이언트 연결이며, API 서버 코드를 SDK처럼 사용하는 개발 경험을 목표로 함
tRPC가 바꾸는 API 개발 방식
- tRPC는 풀스택 애플리케이션에서 서버와 클라이언트가 같은 TypeScript 타입 정보를 활용하도록 해 생산성을 높임
- 핵심은 엔드투엔드 타입세이프 API를 쉽게 만들고, 전통적인 API 계층에 드는 반복 작업을 줄이는 데 있음
- 서버 쪽 변경이 클라이언트 사용부에 영향을 주면 TypeScript가 오류를 알려주므로, 클라이언트·서버 경계의 타입 불일치를 빠르게 확인할 수 있음
개발자가 체감하는 주요 특징
-
자동 타입 안전성
- 서버 쪽 변경이 클라이언트 코드에 영향을 줄 때 TypeScript 오류로 드러남
-
빠른 개발자 경험
- tRPC에는 빌드나 컴파일 단계가 없음
- 코드 생성, 런타임 부하, 별도 빌드 절차 없이 사용할 수 있음
-
프레임워크 독립성
- JavaScript 프레임워크와 런타임 전반에서 사용할 수 있음
- 기존 프로젝트에도 쉽게 추가할 수 있음
-
자동완성
- API 서버 코드를 SDK처럼 다루는 경험을 제공함
- 엔드포인트 사용 시 타입 기반 힌트를 받을 수 있음
-
작은 클라이언트 풋프린트
- tRPC는 의존성이 없고 클라이언트 측 크기가 작음
-
포함된 어댑터
- React, Next.js, Express, Fastify, AWS Lambda, Solid, Svelte 등을 위한 어댑터를 제공함
기본 사용 흐름
- tRPC API는 먼저 프로시저(procedure) 를 정의하는 방식으로 시작함
- 프로시저는 백엔드를 구성하는 함수이며, 조합 가능하고 쿼리, 뮤테이션, 구독으로 만들 수 있음
- 여러 프로시저는 라우터에 묶임
-
프로시저 정의
- 예시에서는
initTRPC.create()로 tRPC 인스턴스를 만들고,router와publicProcedure를 구성함 greeting프로시저는name문자열을 입력으로 받고Hello ${input.name}형태의 문자열을 반환함- 입력 검증에는 Zod를 사용해 클라이언트 입력이 프로시저가 기대하는 형태와 정확히 일치하도록 함
- 파일 끝에서
export type AppRouter = typeof appRouter;로 라우터 타입을 내보내 프런트엔드 코드에서 사용할 수 있게 함
- 예시에서는
-
HTTP 서버 생성
createHTTPServer에appRouter를 전달해 tRPC 서버를 실행함- 예시에서는 API가 포트
3000에서 리스닝하도록listen(3000)을 호출함 - tRPC는 Next.js, Express, Fetch API 기반 환경, Fastify, AWS Lambda, vanilla Node HTTP 서버에 맞는 어댑터를 제공함
- Fetch API 기반 환경 예시로 Astro, Remix, SvelteKit, Cloudflare Workers가 포함됨
-
클라이언트 연결과 쿼리 실행
- 서버가 실행되면
createTRPCClient<AppRouter>로 클라이언트를 만들고 데이터를 쿼리할 수 있음 - 예시 클라이언트는
httpBatchLink를 사용해http://localhost:3000에 연결함 trpc.greeting.query({ name: 'John' })처럼 호출해 서버의greeting프로시저를 사용할 수 있음- 클라이언트를 만들 때
AppRouter타입을 전달하면 백엔드 API와 일치하는 TypeScript 자동완성과 IntelliSense를 코드 생성 없이 얻음
- 서버가 실행되면
시작 템플릿과 제작 의도
- 직접 시험해볼 수 있는 템플릿으로 Use this template이 제공됨
- tRPC 제작자는 전통적인 API 계층의 필요를 줄이고, 빠르게 반복 개발하면서도 앱이 깨지지 않는다는 확신을 주기 위해 tRPC를 만들었음
- tRPC는 선도적인 기술 팀과 여러 Fortune 500 기업에서 사용 중임
개발자 반응
- 여러 개발자 인용에서 tRPC는 코드 품질, 전달 속도, 개발자 만족도, 클라이언트·서버 경계의 리팩터링, 입력 검증, 타입화된 미들웨어 경험을 개선하는 도구로 평가됨
- TypeScript 모노레포에서는 plain REST나 GraphQL보다 더 단순하면서도 강한 타입을 제공하는 선택지로 받아들여짐
- Stripe API 페이로드를 서버에서 반환해도 React 컴포넌트에서 응답 데이터 타입을 받을 수 있는 사용 사례가 소개됨
댓글과 토론
Hacker News 의견들
-
지금 코드베이스에서 tRPC 제거를 진행 중인데, 강한 결합 때문에 악몽 같았음
주니어 개발자들이 인터페이스나 데이터 접근 패턴을 고민하지 않도록 유도하는 면도 있었고, Prisma에서 컴포넌트까지 그대로 이어지는 매핑이 생겼음
빠른 프로토타이핑에는 훌륭하지만, 나중에 코드베이스를 분리하려 하면 금세 막다른 골목에 갇힘- 스키마가 가진 간과된 장점이 바로 이것임. GraphQL 같은 스키마는 데이터 타입과 계약을 생각하게 만들고, 코드의 다른 부분을 맡은 사람들이 맞춰 일하기 좋게 해줌
TypeScript 밖의 언어로도 확장되기 때문에, 백엔드를 다른 언어로 옮기거나 Swift, Kotlin 같은 네이티브 모바일 클라이언트를 만들 때도 도움이 됨 - 회사에서 tRPC를 도입했는데, 조금만 미리 설계하니 코드 안에서 “강결합”될 만한 것들을 잘 분리해서 쓰고 있음
tRPC는 훌륭하지만 결국 백엔드와 프런트엔드 사이의 전송 계층일 뿐임
tRPC 내부 구조를 비즈니스 로직 깊숙이 쓰게 두는 건 입력, 스키마, 분리를 명확히 정의하는 컨트롤러나 라우터 계층이 없는 것만큼 나쁨
그래서 나중에 tRPC를 떠나더라도 비교적 간단할 것 같고, 예를 들어 하위 시스템 전체를 큐 위에서 돌리도록 옮기는 것도 어렵지 않을 듯함 - 문제는 tRPC가 아니라, 타입을 단지 타입을 붙이기 위해 쓰는 엔지니어들이 있는 것에 가까움. 어떤 도구를 써도 같은 문제가 생길 것임
이런 것에는 학습 곡선이 있고, 보통type FunctionIWroteTodayArgs = …같은 쓸모없고 아무것도 설명하지 않는 타입에서 시작함
몇 번의 반복을 거치며, 목표가 코드를 복제하는 게 아니라 도메인을 설명하고 재사용 가능하며 유익한 타입, 인터페이스, API를 만드는 것임을 점차 깨닫게 됨
그래서 tRPC를 걷어내기보다 팀과 함께 그 부분을 개선하는 게 맞다고 봄 - 결합이 악몽이라는 부분을 더 설명해주면 좋겠음
서버 쪽에서 HTTP 클라이언트를 선언하고 클라이언트 쪽에서 소비하는 게 왜 더 나쁜지 잘 모르겠음
우리는 서비스들을 만들고 웹 인터페이스나 CLI 같은 모든 소비자가 쓰는 패턴을 쓰는데, 이런 것들이 깨지지 않는다는 점은 예전에 본 어떤 방식보다 큰 개선이었음 - 작은 팀에서는 정반대의 경험을 하고 있고, 예전의 큰 팀에서도 잘 먹혔을 것 같음
주니어 개발자들이 인터페이스를 생각하지 않는다는 불만은 tRPC뿐 아니라 어떤 API를 쓰더라도 똑같이 나올 가능성이 큼
- 스키마가 가진 간과된 장점이 바로 이것임. GraphQL 같은 스키마는 데이터 타입과 계약을 생각하게 만들고, 코드의 다른 부분을 맡은 사람들이 맞춰 일하기 좋게 해줌
-
Notion에서는 tRPC와 비슷한 API 스타일을 써왔고, 그 API는 tRPC보다 4년쯤 먼저 있었음
TypeScript의 매핑된 타입으로 이런 건 쉽게 직접 만들 수 있음. 키는 API 이름이고 값은{ request, response }타입인 객체 타입을 만들면 됨
서버 쪽은 각 API 핸들러를APIs["addUser"]["request"]를 받아Promise를 반환하는 함수로 정의하고, 클라이언트는 같은 인자와 반환 타입을 가진 비동기 함수로 노출하면 됨
이 전략을 HTTPS 내부 API, WebSocket 기반 실시간 API, Electron과 Webview 사이의 IPC, iOS/Android 네이티브와 Webview 사이의 OS 웹뷰 IPC에 사용함
네이티브 API에서는 서버 쪽이 Swift나 Kotlin이라 요청/응답 타입을 TypeScript에서 손으로 다시 작성함. 언젠가는 자체 IDL이 있는 바이너리 형식으로 바꾸겠지만, 천천히 커지는 단일 크로스언어 API에는 Protobuf 같은 것의 개발자 경험 비용이 아직 가치 있어 보이지 않았음- 현재 프로젝트는 TypeScript 프런트엔드와 Python 백엔드 조합이고, OpenAPI 스키마를 진실의 원천으로 삼아 클라이언트 쪽에는 openapi-typescript-codegen [0]을 사용함
완벽하진 않지만 요청/응답 타입이 붙은 꽤 좋은 API 인터페이스를 제공함
또mockApi((request) => response)처럼 호출할 수 있는 10줄짜리 모의 API 래퍼를 만들었고, 모의 함수가 API를 올바르게 구현하는지 타입 검사하며 실제 API 함수와 똑같이 생긴 함수를 반환함
[0]: https://github.com/ferdikoomen/openapi-typescript-codegen - 이 방식이 맞음. tRPC는 단순히 타입을 추론하는 것보다 불필요한 복잡성을 더함
이런 접근을 채택한 잘 관리되고 홍보되는 라이브러리가 아직 나오지 않아서 자주 논의되지 않는 것 같음 - 이 과정에서 요청 데이터 검증은 어떻게 하는지 요령이 있는지 궁금함
tRPC, JSON Schema와 타입 생성, Zod 같은 것을 쓸 때 중요한 가치가 바로 그 부분임 - 샘플 코드를 보니 XHR 호출 위에 타입을 입힌 래퍼에 가까운 건지 궁금함
호출할 때 제네릭 인자를 직접 제공하라고까지 하는데, 의존성 트리를 정돈된 상태로 유지하려 할 때는 꽤 별로임 - 이 방식은 실제로 꽤 잘 동작함
https://github.com/mikew/transmission-material-ui/blob/maste...
https://github.com/mikew/transmission-material-ui/blob/maste...
- 현재 프로젝트는 TypeScript 프런트엔드와 Python 백엔드 조합이고, OpenAPI 스키마를 진실의 원천으로 삼아 클라이언트 쪽에는 openapi-typescript-codegen [0]을 사용함
-
tRPC를 아주 좋아함. TypeScript 전용 스택에서 개발자 경험을 한계까지 밀어붙인 점이 놀랍고, GraphQL 커뮤니티가 쿼리 언어의 한계와 절충을 인식하게 만들었음
동시에 tRPC는 정말 빠르게 유행 주기를 지나갔고, REST와 GraphQL에서 RPC로 대규모 이동이 일어나는 것 같지는 않음
다만 요즘 RPC에 대한 관심은 많아 보이고, 우리가 만든 BFF 프레임워크(https://wundergraph.com/)에서도 tRPC와 예전 NextJS의 아이디어를 일부 채택해 파일 기반 라우팅과 RPC를 결합했음
tRPC에 더해 각 작업마다 JSON Schema를 자동 생성하고, 전체 작업 집합에 대한 OpenAPI 명세도 생성함
RPC 엔드포인트 묶음을 OpenAPI 명세나 Postman 컬렉션으로 쉽게 공유할 수 있어서 사람들이 이 접근을 좋아함. HTTP 동사 논의도 거의 없고, 실질적으로는 쿼리, 변경, 구독만 있음
요즘 GraphQL, REST, RPC 스타일 API를 어떻게 쓰고 있는지, 몇 명이나 몇 팀이 API에 관여하는지 궁금함- Garph는 GraphQL용 tRPC 같은 것임: https://garph.dev
REST API 쪽에는 ts-rest(https://ts-rest.com), zodios(https://www.zodios.org), Hono(https://hono.dev)가 있음
팀에서 여러 언어를 쓴다면 Fern도 있음: https://www.buildwithfern.com - “GraphQL 커뮤니티가 쿼리 언어의 한계와 절충을 인식하게 만들었다”는 부분을 더 설명해주면 좋겠음
우리는 연합 스키마가 바뀔 때마다 GraphQL 타입을 생성하고, 파일을 저장할 때마다 쿼리와 변경의 응답 타입을 생성함 - 항상 눈에 띄는 건 동사와 경로가 꽤 사소한 세부사항이라는 점임
엔드포인트 소비자 생성 라이브러리로 쉽게 추상화할 수도 있는데, 그 외에 일반적인 웹 API와 비교해 실제로 무엇을 더 얻는지 모르겠음
어차피 각 백엔드 호출에서 해야 할 일은 그대로 수행해야 함
- Garph는 GraphQL용 tRPC 같은 것임: https://garph.dev
-
tRPC를 좋아함. 지금까지 본 풀스택 개발자 경험 중 단연 최고이고, 특히 Zod와 함께 쓰면 API가 정말 훌륭함
Zod와 tRPC는 TypeScript의 미래에서 중요한 프로젝트들이라고 보고, 앞으로 몇 년 동안 TypeScript 생태계 전반에 tRPC에서 영감을 받은 개발자 경험이 크게 피어날 것 같음
이미 tRPC의 DNA가 분명한 프로젝트로는 다른 사용 사례를 공략하는 Ping의 UploadThing(https://github.com/pingdotgg/uploadthing)과 우리 Lusat(https://github.com/lusatai/lusat)이 있음- Zod는 별로 좋아하지 않음. 타입과 제네릭 매개변수가 까다롭고 장황하며, 객체 제네릭에 데이터 타입을 넘길 수 없고 Zod 자체 스키마 타입을 속성으로 넘겨야 해서 지저분하고 직관적이지 않음
또 검증 오류의 타입이 어떤 스키마를 검사하느냐에 따라 달라지는 점도 성가심. 오류 처리가 예측 불가능해지고 예외 상황이 항상 너무 많음
개선 여지가 많음 - 최근 개인 프로젝트에서 tRPC와 Zod를 썼는데, 정말 훌륭한 경험이었다는 데 동의함
단위 테스트 작성도 훨씬 쉬워짐 - MongoDB 쪽에 이런 영역의 도구가 있는지 궁금함
큰 문제는 타입 안전한 CRUD와 데이터 마이그레이션인 것 같은데, Mongoose를 많이 언급하긴 하지만 타입 안전성 측면에서는 Zod/TypeScript보다 크게 후퇴하는 느낌임 - Zod는 정말 엄청나게 좋음
지금은 좀 과장해서 말하고 싶은 기분인데, 특정 프로그래밍 스타일에서는 코드베이스에 Zod를 추가하는 것이 TypeScript를 추가하는 것만큼 큰 이득이라고까지 말할 수 있음 - effect/core와 effect/schema도 추천함
스키마에서 시작해 타입이 있는 서비스를 만드는 패턴이 함수형 프로그래밍 쪽 성향의 사람들에게 잘 맞음
- Zod는 별로 좋아하지 않음. 타입과 제네릭 매개변수가 까다롭고 장황하며, 객체 제네릭에 데이터 타입을 넘길 수 없고 Zod 자체 스키마 타입을 속성으로 넘겨야 해서 지저분하고 직관적이지 않음
-
tRPC가 버전 차이와 마이그레이션을 어떻게 처리하는지 궁금함
필드에는 생명주기가 있음. 처음 도입될 때는 어떤 클라이언트나 서버도 그 필드를 모름
클라이언트와 서버는 한 번에 모두 재시작되지 않고, 같은 버전의 타입을 갖고 있지도 않음
데이터는 한 버전의 타입으로 쓰이고 다른 버전의 타입으로 읽히게 됨
모든 바이너리, 실행 중인 프로그램, 데이터가 업그레이드된다고 보장할 수 있고 영속 데이터가 없다면 문제가 아닐 수도 있음
하지만 여러 조직이 얽히는 순간 모든 앱과 서버가 최신 라이브러리 버전에 동기화되고, 다시 빌드되고, 재배포된다고 보장하기 어려움
정적 타입 검사는 버전 차이가 없다고 가정함. 바이너리 안의 모든 코드는 타입을 정의하는 같은 버전의 라이브러리로 빌드된 닫힌 세계임- 거의 “직접 해야 한다”에 가까움
Zod는 알 수 없는 필드를 허용할 수 있고, 그다음 선택 필드로 옮긴 뒤, 상황이 안정되면 필수 필드로 만들 수 있음
물론 tRPC가 해주는 것을 가장 원하는 층일수록 이런 종류의 문제를 생각할 가능성은 가장 낮음 - 그래서 웹 앱용 도구인 것임
- 거의 “직접 해야 한다”에 가까움
-
한 언어만 대상으로 한다면 스키마나 코드 생성이 필요하지 않은 건 당연함
-
tRPC를 약 5만 줄짜리 웹 애플리케이션 두 개에 사용했고 아주 좋아함. 개발자 경험이 훌륭함
다만 tRPC의 유행 시기는 이미 한동안 전에 지나갔고, RSC의 유행이 빠르게 따라잡은 느낌임
요즘은 tRPC처럼 안정적이고 좋은 해법이 있는데도 모두가 RSC를 쓸지 말지만 이야기함
RSC에 반대하는 건 아니지만 논의가 너무 많음. 요즘 애플리케이션을 만들 때 tRPC는 매우 실용적인 방식임
추가: RSC는 React Server Components이며, 자체적인 읽기/쓰기 데이터 철학을 함께 가져옴- RSC를 들어본 적이 없고 검색도 잘 안 됨. 링크가 있으면 좋겠음
- Royal Shakespeare Company가 RPC와 무슨 관련이 있는지 모르겠음
사람들이 모를 수 있는 약어는 정의하거나 링크를 붙여주면 좋겠음
-
몇몇 개인 프로젝트에서 tRPC와 Next.js를 써봤고 좋은 경험이었음
특히 Create T3 App(https://create.t3.gg/) 같은 미리 설정된 템플릿과 함께 쓰면 반복 속도를 이기기 어려움- Next 13과 서버 컴포넌트에서도 이제 제대로 동작하는지 궁금함
-
함께 일하던 개발자가 tRPC를 극찬하기 전까지는 몰랐는데, 알고 나니 너무 명백하게 좋은 것처럼 보였음
같이 T3 앱(tRPC, Next.js, Tailwind, TypeScript, Prisma)을 만들었고, 보고 싶다면 여기 있음: https://github.com/stytchauth/stytch-t3-example
TypeScript로 작업할 때 타입 안전한 API는 정말 큰 도움이 됨