빠른 시작
이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다. 오류를 발견하셨나요? 문제 신고 →
tRPC는 REST와 GraphQL의 개념을 결합합니다. 둘 중 하나에 익숙하지 않다면 핵심 개념을 살펴보세요.
설치
tRPC는 여러 패키지로 분리되어 있어 필요한 부분만 설치할 수 있습니다. 각 패키지를 코드베이스의 적절한 영역에 설치해야 합니다. 이 빠른 시작 가이드에서는 간단하게 바닐라 클라이언트만 사용합니다. 프레임워크 가이드는 React 사용법과 Next.js 사용법을 참고하세요.
- tRPC는 TypeScript >=5.7.2 버전이 필요합니다
- 공식적으로 비엄격 모드를 지원하지 않으므로
tsconfig.json에서"strict": true를 사용할 것을 강력히 권장합니다
@trpc/server와 @trpc/client 패키지를 설치하는 것으로 시작합니다:
- npm
- yarn
- pnpm
- bun
- deno
npm install @trpc/server @trpc/client
yarn add @trpc/server @trpc/client
pnpm add @trpc/server @trpc/client
bun add @trpc/server @trpc/client
deno add npm:@trpc/server npm:@trpc/client
백엔드 라우터 정의하기
tRPC로 타입 안전한 API를 구축하는 단계를 차근차근 살펴보겠습니다. 시작으로 이 API에는 다음 TypeScript 시그니처를 가진 세 개의 엔드포인트가 포함될 것입니다:
tstype User = { id: string; name: string; };userList: () => User[];userById: (id: string) => User;userCreate: (data: { name: string }) => User;
tstype User = { id: string; name: string; };userList: () => User[];userById: (id: string) => User;userCreate: (data: { name: string }) => User;
1. 라우터 인스턴스 생성하기
먼저 tRPC 백엔드를 초기화합니다. 별도 파일에서 작업하고 전체 tRPC 객체 대신 재사용 가능한 헬퍼 함수를 내보내는 것이 좋은 관행입니다.
server/trpc.tstsimport {initTRPC } from '@trpc/server';/*** Initialization of tRPC backend* Should be done only once per backend!*/constt =initTRPC .create ();/*** Export reusable router and procedure helpers* that can be used throughout the router*/export constrouter =t .router ;export constpublicProcedure =t .procedure ;
server/trpc.tstsimport {initTRPC } from '@trpc/server';/*** Initialization of tRPC backend* Should be done only once per backend!*/constt =initTRPC .create ();/*** Export reusable router and procedure helpers* that can be used throughout the router*/export constrouter =t .router ;export constpublicProcedure =t .procedure ;
다음으로 appRouter라고 일반적으로 불리는 메인 라우터 인스턴스를 초기화합니다. 여기에 나중에 절차를 추가할 것입니다. 마지막으로 클라이언트 측에서 사용할 라우터 타입을 내보내야 합니다.
server/index.tstsimport {router } from './trpc';constappRouter =router ({// ...});// Export type router type signature,// NOT the router itself.export typeAppRouter = typeofappRouter ;
server/index.tstsimport {router } from './trpc';constappRouter =router ({// ...});// Export type router type signature,// NOT the router itself.export typeAppRouter = typeofappRouter ;
2. 쿼리 절차 추가하기
라우터에 쿼리 절차를 추가하려면 publicProcedure.query()를 사용하세요.
다음은 데이터베이스에서 사용자 목록을 반환하는 userList 쿼리 절차를 생성하는 예시입니다:
server/index.tstsimport {db } from './db';import {publicProcedure ,router } from './trpc';constappRouter =router ({userList :publicProcedure .query (async () => {// Retrieve users from a datasource, this is an imaginary databaseconstusers = awaitdb .user .findMany ();returnusers ;}),});
server/index.tstsimport {db } from './db';import {publicProcedure ,router } from './trpc';constappRouter =router ({userList :publicProcedure .query (async () => {// Retrieve users from a datasource, this is an imaginary databaseconstusers = awaitdb .user .findMany ();returnusers ;}),});
3. 입력 파서를 사용하여 절차 입력 검증하기
userById 절차를 구현하려면 클라이언트의 입력을 받아야 합니다. tRPC는 입력을 검증하고 파싱하기 위한 입력 파서를 정의할 수 있게 합니다. 사용자 정의 입력 파서를 만들거나 zod, yup, superstruct 같은 검증 라이브러리를 선택할 수 있습니다.
입력 파서는 publicProcedure.input()에 정의하며, 아래와 같이 리졸버 함수에서 접근할 수 있습니다:
- Vanilla
- Zod
- Yup
- Valibot
The input parser should be a function that validates and casts the input of this procedure. It should return a strongly typed value when the input is valid or throw an error if the input is invalid.
server/index.tstsconstappRouter =router ({// ...userById :publicProcedure // The input is unknown at this time. A client could have sent// us anything so we won't assume a certain data type..input ((val : unknown) => {// If the value is of type string, return it.// It will now be inferred as a string.if (typeofval === 'string') returnval ;// Uh oh, looks like that input wasn't a string.// We will throw an error instead of running the procedure.throw newError (`Invalid input: ${typeofval }`);}).query (async (opts ) => {const {input } =opts ;// Retrieve the user with the given IDconstuser = awaitdb .user .findById (input );returnuser ;}),});
server/index.tstsconstappRouter =router ({// ...userById :publicProcedure // The input is unknown at this time. A client could have sent// us anything so we won't assume a certain data type..input ((val : unknown) => {// If the value is of type string, return it.// It will now be inferred as a string.if (typeofval === 'string') returnval ;// Uh oh, looks like that input wasn't a string.// We will throw an error instead of running the procedure.throw newError (`Invalid input: ${typeofval }`);}).query (async (opts ) => {const {input } =opts ;// Retrieve the user with the given IDconstuser = awaitdb .user .findById (input );returnuser ;}),});
The input parser can be any ZodType, e.g. z.string() or z.object().
server.tstsimport {z } from 'zod';constappRouter =router ({// ...userById :publicProcedure .input (z .string ()).query (async (opts ) => {const {input } =opts ;// Retrieve the user with the given IDconstuser = awaitdb .user .findById (input );returnuser ;}),});
server.tstsimport {z } from 'zod';constappRouter =router ({// ...userById :publicProcedure .input (z .string ()).query (async (opts ) => {const {input } =opts ;// Retrieve the user with the given IDconstuser = awaitdb .user .findById (input );returnuser ;}),});
The input parser can be any YupSchema, e.g. yup.string() or yup.object().
server.tstsimport * asyup from 'yup';constappRouter =router ({// ...userById :publicProcedure .input (yup .string ().required ()).query (async (opts ) => {const {input } =opts ;// Retrieve the user with the given IDconstuser = awaitdb .user .findById (input );returnuser ;}),});
server.tstsimport * asyup from 'yup';constappRouter =router ({// ...userById :publicProcedure .input (yup .string ().required ()).query (async (opts ) => {const {input } =opts ;// Retrieve the user with the given IDconstuser = awaitdb .user .findById (input );returnuser ;}),});
The input parser can be any Valibot schema, e.g. v.string() or v.object().
server.tstsimport * asv from 'valibot';constappRouter =router ({// ...userById :publicProcedure .input (v .string ()).query (async (opts ) => {const {input } =opts ;// Retrieve the user with the given IDconstuser = awaitdb .user .findById (input );returnuser ;}),});
server.tstsimport * asv from 'valibot';constappRouter =router ({// ...userById :publicProcedure .input (v .string ()).query (async (opts ) => {const {input } =opts ;// Retrieve the user with the given IDconstuser = awaitdb .user .findById (input );returnuser ;}),});
본 문서의 남은 부분에서는 검증 라이브러리로 zod를 사용할 것입니다
4. 변형(mutation) 절차 추가하기
GraphQL과 유사하게 tRPC는 쿼리와 변형 절차를 구분합니다.
서버 측 절차의 작동 방식은 쿼리와 변형 간에 크게 다르지 않습니다. 메서드 이름이 다르고 클라이언트가 이 절차를 사용하는 방식이 변경되지만 그 외에는 모두 동일합니다!
라우터 객체에 새 속성으로 userCreate 변형을 추가해 보겠습니다:
server.tstsconstappRouter =router ({// ...userCreate :publicProcedure .input (z .object ({name :z .string () })).mutation (async (opts ) => {const {input } =opts ;// Create a new user in the databaseconstuser = awaitdb .user .create (input );returnuser ;}),});
server.tstsconstappRouter =router ({// ...userCreate :publicProcedure .input (z .object ({name :z .string () })).mutation (async (opts ) => {const {input } =opts ;// Create a new user in the databaseconstuser = awaitdb .user .create (input );returnuser ;}),});
API 서빙하기
이제 라우터를 정의했으므로 서빙할 수 있습니다. tRPC에는 다양한 어댑터가 있어 원하는 백엔드 프레임워크를 사용할 수 있습니다. 간단하게 하기 위해 standalone 어댑터를 사용하겠습니다.
server/index.tstsimport {createHTTPServer } from '@trpc/server/adapters/standalone';constappRouter =router ({// ...});constserver =createHTTPServer ({router :appRouter ,});server .listen (3000);
server/index.tstsimport {createHTTPServer } from '@trpc/server/adapters/standalone';constappRouter =router ({// ...});constserver =createHTTPServer ({router :appRouter ,});server .listen (3000);
See the full backend code
server/db.tststypeUser = {id : string;name : string };// Imaginary databaseconstusers :User [] = [];export constdb = {user : {findMany : async () =>users ,findById : async (id : string) =>users .find ((user ) =>user .id ===id ),create : async (data : {name : string }) => {constuser = {id :String (users .length + 1), ...data };users .push (user );returnuser ;},},};
server/db.tststypeUser = {id : string;name : string };// Imaginary databaseconstusers :User [] = [];export constdb = {user : {findMany : async () =>users ,findById : async (id : string) =>users .find ((user ) =>user .id ===id ),create : async (data : {name : string }) => {constuser = {id :String (users .length + 1), ...data };users .push (user );returnuser ;},},};
server/trpc.tstsimport {initTRPC } from '@trpc/server';constt =initTRPC .create ();export constrouter =t .router ;export constpublicProcedure =t .procedure ;
server/trpc.tstsimport {initTRPC } from '@trpc/server';constt =initTRPC .create ();export constrouter =t .router ;export constpublicProcedure =t .procedure ;
server/index.tstsimport {createHTTPServer } from "@trpc/server/adapters/standalone";import {z } from "zod";import {db } from "./db";import {publicProcedure ,router } from "./trpc";constappRouter =router ({userList :publicProcedure .query (async () => {constusers = awaitdb .user .findMany ();returnusers ;}),userById :publicProcedure .input (z .string ()).query (async (opts ) => {const {input } =opts ;constuser = awaitdb .user .findById (input );returnuser ;}),userCreate :publicProcedure .input (z .object ({name :z .string () })).mutation (async (opts ) => {const {input } =opts ;constuser = awaitdb .user .create (input );returnuser ;}),});export typeAppRouter = typeofappRouter ;constserver =createHTTPServer ({router :appRouter ,});server .listen (3000);
server/index.tstsimport {createHTTPServer } from "@trpc/server/adapters/standalone";import {z } from "zod";import {db } from "./db";import {publicProcedure ,router } from "./trpc";constappRouter =router ({userList :publicProcedure .query (async () => {constusers = awaitdb .user .findMany ();returnusers ;}),userById :publicProcedure .input (z .string ()).query (async (opts ) => {const {input } =opts ;constuser = awaitdb .user .findById (input );returnuser ;}),userCreate :publicProcedure .input (z .object ({name :z .string () })).mutation (async (opts ) => {const {input } =opts ;constuser = awaitdb .user .create (input );returnuser ;}),});export typeAppRouter = typeofappRouter ;constserver =createHTTPServer ({router :appRouter ,});server .listen (3000);
클라이언트에서 새 백엔드 사용하기
이제 클라이언트 측 코드로 이동하여 엔드투엔드 타입 안전성의 힘을 경험해 보겠습니다. 클라이언트에서 사용할 AppRouter 타입을 가져오면 구현 세부사항을 클라이언트에 노출시키지 않으면서도 시스템 전체에 완벽한 타입 안전성을 확보할 수 있습니다.
1. tRPC 클라이언트 설정
client/index.tstsimport {createTRPCClient ,httpBatchLink } from '@trpc/client';import type {AppRouter } from './server';// 👆 **type-only** import// Pass AppRouter as generic here. 👇 This lets the `trpc` object know// what procedures are available on the server and their input/output types.consttrpc =createTRPCClient <AppRouter >({links : [httpBatchLink ({url : 'http://localhost:3000',}),],});
client/index.tstsimport {createTRPCClient ,httpBatchLink } from '@trpc/client';import type {AppRouter } from './server';// 👆 **type-only** import// Pass AppRouter as generic here. 👇 This lets the `trpc` object know// what procedures are available on the server and their input/output types.consttrpc =createTRPCClient <AppRouter >({links : [httpBatchLink ({url : 'http://localhost:3000',}),],});
tRPC의 링크(Link)는 GraphQL의 링크와 유사하게 데이터가 서버로 전송되기 전에 데이터 흐름을 제어할 수 있게 해줍니다. 위 예제에서는 httpBatchLink를 사용하는데, 이 링크는 여러 호출을 자동으로 단일 HTTP 요청으로 묶어줍니다. 링크에 대한 더 깊은 활용법은 링크 문서를 참조하세요.
2. 쿼리 및 뮤테이션 실행
이제 trpc 객체를 통해 API 프로시저에 접근할 수 있습니다. 직접 사용해 보세요!
client/index.tsts// Inferred typesconstuser = awaittrpc .userById .query ('1');constcreatedUser = awaittrpc .userCreate .mutate ({name : 'sachinraja' });
client/index.tsts// Inferred typesconstuser = awaittrpc .userById .query ('1');constcreatedUser = awaittrpc .userCreate .mutate ({name : 'sachinraja' });
완전한 자동 완성
인텔리센스(Intellisense)를 열어 프론트엔드에서 API를 탐색해 보세요. 모든 프로시저 경로와 호출 방법이 그대로 제공되는 것을 확인할 수 있습니다.
client/index.tsts// Full autocompletion on your routestrpc .u ;
client/index.tsts// Full autocompletion on your routestrpc .u ;
직접 사용해 보세요!
다음 단계
선호하는 프레임워크에 tRPC를 설치하는 방법을 배우려면 예제 앱을 꼭 확인해 보시기 바랍니다.
기본적으로 tRPC는 Date 같은 복합 타입을 JSON 호환 형태(Date의 경우 string)로 매핑합니다. 이러한 타입의 무결성을 유지하려면 가장 간단한 방법은 superjson 사용하기를 데이터 변환기로 활용하는 것입니다.
tRPC는 React 프로젝트와 Next.js를 위해 특별히 설계된 고급 클라이언트 측 도구를 제공합니다.
-
React에서 사용하기 (클라이언트 측)
- React 통합 (권장) ->
@trpc/tanstack-react-query - React 통합 (기존) ->
@trpc/react-query - 확실하지 않다면
Recommended를 사용하세요
- React 통합 (권장) ->