React Server Components로 설정하기
이 문서는 'Classic' React Query 통합에 대한 문서입니다. 여전히 지원되지만, TanStack React Query를 사용하여 새로운 tRPC 프로젝트를 시작하는 권장 방식은 아닙니다. 대신 새로운 TanStack React Query Integration 사용을 권장합니다.
Next.js를 사용 중이신가요? 권장되는 접근 방식을 확인하려면 전용 Next.js App Router 설정 가이드를 참조하세요.
이 가이드는 Next.js App Router와 같은 React Server Components(RSC) 프레임워크에서 tRPC를 사용하는 방법에 대한 개요입니다. RSC 자체만으로도 tRPC가 해결하도록 설계된 많은 문제를 해결하므로, tRPC가 전혀 필요하지 않을 수 있습니다.
tRPC와 RSC를 통합하는 단 하나의 정답은 없습니다. 이 가이드를 시작점으로 삼아 프로젝트의 요구 사항과 선호에 맞게 조정하세요.
Server Actions와 함께 tRPC를 사용하는 방법을 찾고 있다면 Server Actions 가이드를 참조하세요.
진행하기 전에 React Query의 Advanced Server Rendering 문서를 읽고 다양한 서버 렌더링 유형과 피해야 할 함정을 이해하세요.
기존 프로젝트에 tRPC 추가하기
1. 의존성 설치
- npm
- yarn
- pnpm
- bun
- deno
npm install @trpc/server @trpc/client @trpc/react-query @tanstack/react-query@latest zod client-only server-only
yarn add @trpc/server @trpc/client @trpc/react-query @tanstack/react-query@latest zod client-only server-only
pnpm add @trpc/server @trpc/client @trpc/react-query @tanstack/react-query@latest zod client-only server-only
bun add @trpc/server @trpc/client @trpc/react-query @tanstack/react-query@latest zod client-only server-only
deno add npm:@trpc/server npm:@trpc/client npm:@trpc/react-query npm:@tanstack/react-query@latest npm:zod npm:client-only npm:server-only
2. tRPC 라우터 생성
trpc/init.ts에서 initTRPC 함수를 사용하여 tRPC 백엔드를 초기화하고 첫 번째 라우터를 생성합니다. 여기서는 간단한 "hello world" 라우터와 프로시저를 만들지만, tRPC API 생성에 대한 더 자세한 정보는 tRPC 정보를 위해 Quickstart guide와 Backend usage docs를 참조해야 합니다.
여기서 사용된 파일 이름은 tRPC에서 강제하지 않습니다. 원하는 파일 구조를 사용할 수 있습니다.
샘플 백엔드 보기
trpc/init.tstsimport {initTRPC } from '@trpc/server';import {cache } from 'react';export constcreateTRPCContext =cache (async () => {/*** @see: https://trpc.io/docs/server/context*/return {userId : 'user_123' };});// Avoid exporting the entire t-object// since it's not very descriptive.// For instance, the use of a t variable// is common in i18n libraries.constt =initTRPC .create ({/*** @see https://trpc.io/docs/server/data-transformers*/// transformer: superjson,});// Base router and procedure helpersexport constcreateTRPCRouter =t .router ;export constcreateCallerFactory =t .createCallerFactory ;export constbaseProcedure =t .procedure ;
trpc/init.tstsimport {initTRPC } from '@trpc/server';import {cache } from 'react';export constcreateTRPCContext =cache (async () => {/*** @see: https://trpc.io/docs/server/context*/return {userId : 'user_123' };});// Avoid exporting the entire t-object// since it's not very descriptive.// For instance, the use of a t variable// is common in i18n libraries.constt =initTRPC .create ({/*** @see https://trpc.io/docs/server/data-transformers*/// transformer: superjson,});// Base router and procedure helpersexport constcreateTRPCRouter =t .router ;export constcreateCallerFactory =t .createCallerFactory ;export constbaseProcedure =t .procedure ;
trpc/routers/_app.tstsimport {z } from 'zod';import {baseProcedure ,createTRPCRouter } from '../init';export constappRouter =createTRPCRouter ({hello :baseProcedure .input (z .object ({text :z .string (),}),).query ((opts ) => {return {greeting : `hello ${opts .input .text }`,};}),});// export type definition of APIexport typeAppRouter = typeofappRouter ;
trpc/routers/_app.tstsimport {z } from 'zod';import {baseProcedure ,createTRPCRouter } from '../init';export constappRouter =createTRPCRouter ({hello :baseProcedure .input (z .object ({text :z .string (),}),).query ((opts ) => {return {greeting : `hello ${opts .input .text }`,};}),});// export type definition of APIexport typeAppRouter = typeofappRouter ;
백엔드 어댑터는 프레임워크와 API 라우트 설정 방식에 따라 달라집니다. 다음 예제는 Next.js에서 fetch adapter를 사용하여 /api/trpc/*에 GET 및 POST 라우트를 설정합니다.
app/api/trpc/[trpc]/route.tstsimport {fetchRequestHandler } from '@trpc/server/adapters/fetch';import {createTRPCContext } from '../../../../trpc/init';import {appRouter } from '../../../../trpc/routers/_app';consthandler = (req :Request ) =>fetchRequestHandler ({endpoint : '/api/trpc',req ,router :appRouter ,createContext :createTRPCContext ,});export {handler asGET ,handler asPOST };
app/api/trpc/[trpc]/route.tstsimport {fetchRequestHandler } from '@trpc/server/adapters/fetch';import {createTRPCContext } from '../../../../trpc/init';import {appRouter } from '../../../../trpc/routers/_app';consthandler = (req :Request ) =>fetchRequestHandler ({endpoint : '/api/trpc',req ,router :appRouter ,createContext :createTRPCContext ,});export {handler asGET ,handler asPOST };
3. Query Client 팩토리 생성
QueryClient 인스턴스를 생성하는 함수를 내보내는 공유 파일 trpc/query-client.ts 파일을 생성합니다.
trpc/query-client.tstsimport {defaultShouldDehydrateQuery ,QueryClient ,} from '@tanstack/react-query';importsuperjson from 'superjson';export functionmakeQueryClient () {return newQueryClient ({defaultOptions : {queries : {staleTime : 30 * 1000,},dehydrate : {// serializeData: superjson.serialize,shouldDehydrateQuery : (query ) =>defaultShouldDehydrateQuery (query ) ||query .state .status === 'pending',},hydrate : {// deserializeData: superjson.deserialize,},},});}
trpc/query-client.tstsimport {defaultShouldDehydrateQuery ,QueryClient ,} from '@tanstack/react-query';importsuperjson from 'superjson';export functionmakeQueryClient () {return newQueryClient ({defaultOptions : {queries : {staleTime : 30 * 1000,},dehydrate : {// serializeData: superjson.serialize,shouldDehydrateQuery : (query ) =>defaultShouldDehydrateQuery (query ) ||query .state .status === 'pending',},hydrate : {// deserializeData: superjson.deserialize,},},});}
여기서 몇 가지 기본 옵션을 설정합니다:
staleTime: SSR을 사용할 때, 클라이언트에서 즉시 다시 가져오기를 피하기 위해 staleTime을 0보다 큰 기본값으로 설정하는 것이 일반적입니다.shouldDehydrateQuery: 쿼리를 탈수화(dehydrate)할지 여부를 결정하는 함수입니다. RSC 전송 프로토콜은 네트워크를 통한 Promise 수화(hydrate)를 지원하므로,defaultShouldDehydrateQuery함수를 확장하여 아직 대기 중인 쿼리도 포함하도록 합니다. 이를 통해 트리 상단의 서버 컴포넌트에서 사전 가져오기를 시작하고, 트리 하단의 클라이언트 컴포넌트에서 해당 Promise를 소비할 수 있습니다.serializeData및deserializeData(선택 사항): 이전 단계에서 data transformer를 설정했다면, 이 옵션을 설정하여 서버-클라이언트 경계를 넘어 Query Client를 수화할 때 데이터가 올바르게 직렬화되도록 합니다.
4. Client Components용 tRPC 클라이언트 생성
trpc/client.tsx 파일은 클라이언트 컴포넌트에서 tRPC API를 소비할 때의 진입점입니다. 여기에서 tRPC 라우터의 타입 정의를 가져오고 createTRPCReact를 사용하여 타입 안전한 훅을 생성합니다. 또한 이 파일에서 컨텍스트 프로바이더를 내보냅니다.
trpc/client.tsxtsx'use client';// ^-- to make sure we can mount the Provider from a server componentimport type {QueryClient } from '@tanstack/react-query';import {QueryClientProvider } from '@tanstack/react-query';import {httpBatchLink } from '@trpc/client';import {createTRPCReact } from '@trpc/react-query';importReact , {useState } from 'react';import {makeQueryClient } from './query-client';import type {AppRouter } from './routers/_app';export consttrpc =createTRPCReact <AppRouter >();letclientQueryClientSingleton :QueryClient ;functiongetQueryClient () {if (typeofwindow === 'undefined') {// Server: always make a new query clientreturnmakeQueryClient ();}// Browser: use singleton pattern to keep the same query clientreturn (clientQueryClientSingleton ??=makeQueryClient ());}functiongetUrl () {constbase = (() => {if (typeofwindow !== 'undefined') return '';if (process .env .VERCEL_URL ) return `https://${process .env .VERCEL_URL }`;return 'http://localhost:3000';})();return `${base }/api/trpc`;}export functionTRPCProvider (props :Readonly <{children :React .ReactNode ;}>,) {// NOTE: Avoid useState when initializing the query client if you don't// have a suspense boundary between this and the code that may// suspend because React will throw away the client on the initial// render if it suspends and there is no boundaryconstqueryClient =getQueryClient ();const [trpcClient ] =useState (() =>trpc .createClient ({links : [httpBatchLink ({// transformer: superjson, <-- if you use a data transformerurl :getUrl (),}),],}),);return (<trpc .Provider client ={trpcClient }queryClient ={queryClient }><QueryClientProvider client ={queryClient }>{props .children }</QueryClientProvider ></trpc .Provider >);}
trpc/client.tsxtsx'use client';// ^-- to make sure we can mount the Provider from a server componentimport type {QueryClient } from '@tanstack/react-query';import {QueryClientProvider } from '@tanstack/react-query';import {httpBatchLink } from '@trpc/client';import {createTRPCReact } from '@trpc/react-query';importReact , {useState } from 'react';import {makeQueryClient } from './query-client';import type {AppRouter } from './routers/_app';export consttrpc =createTRPCReact <AppRouter >();letclientQueryClientSingleton :QueryClient ;functiongetQueryClient () {if (typeofwindow === 'undefined') {// Server: always make a new query clientreturnmakeQueryClient ();}// Browser: use singleton pattern to keep the same query clientreturn (clientQueryClientSingleton ??=makeQueryClient ());}functiongetUrl () {constbase = (() => {if (typeofwindow !== 'undefined') return '';if (process .env .VERCEL_URL ) return `https://${process .env .VERCEL_URL }`;return 'http://localhost:3000';})();return `${base }/api/trpc`;}export functionTRPCProvider (props :Readonly <{children :React .ReactNode ;}>,) {// NOTE: Avoid useState when initializing the query client if you don't// have a suspense boundary between this and the code that may// suspend because React will throw away the client on the initial// render if it suspends and there is no boundaryconstqueryClient =getQueryClient ();const [trpcClient ] =useState (() =>trpc .createClient ({links : [httpBatchLink ({// transformer: superjson, <-- if you use a data transformerurl :getUrl (),}),],}),);return (<trpc .Provider client ={trpcClient }queryClient ={queryClient }><QueryClientProvider client ={queryClient }>{props .children }</QueryClientProvider ></trpc .Provider >);}
프로바이더를 애플리케이션 루트에 마운트합니다(예: Next.js를 사용할 경우 app/layout.tsx).
5. Server Components용 tRPC 콜러 생성
서버 컴포넌트에서 쿼리를 사전 가져오려면 tRPC 콜러를 사용합니다. @trpc/react-query/rsc 모듈은 React Query 클라이언트와 통합되는 createCaller 주위의 얇은 래퍼를 내보냅니다.
trpc/server.tsxtsximport 'server-only'; // <-- ensure this file cannot be imported from the clientimport {createHydrationHelpers } from '@trpc/react-query/rsc';import {cache } from 'react';import {createCallerFactory ,createTRPCContext } from './init';import {makeQueryClient } from './query-client';import {appRouter } from './routers/_app';// IMPORTANT: Create a stable getter for the query client that// will return the same client during the same request.export constgetQueryClient =cache (makeQueryClient );constcaller =createCallerFactory (appRouter )(createTRPCContext );export const {trpc ,HydrateClient } =createHydrationHelpers <typeofappRouter >(caller ,getQueryClient ,);
trpc/server.tsxtsximport 'server-only'; // <-- ensure this file cannot be imported from the clientimport {createHydrationHelpers } from '@trpc/react-query/rsc';import {cache } from 'react';import {createCallerFactory ,createTRPCContext } from './init';import {makeQueryClient } from './query-client';import {appRouter } from './routers/_app';// IMPORTANT: Create a stable getter for the query client that// will return the same client during the same request.export constgetQueryClient =cache (makeQueryClient );constcaller =createCallerFactory (appRouter )(createTRPCContext );export const {trpc ,HydrateClient } =createHydrationHelpers <typeofappRouter >(caller ,getQueryClient ,);
API 사용
이제 앱에서 tRPC API를 사용할 수 있습니다. 다른 React 앱처럼 클라이언트 컴포넌트에서 React Query 훅을 사용할 수 있고, 컴포넌트 트리 상단의 서버 컴포넌트에서 쿼리를 미리 가져와 RSC의 장점도 활용할 수 있습니다. 이 패턴은 일반적으로 로더로 구현하는 "render as you fetch"와 같습니다. useQuery나 useSuspenseQuery 훅에서 데이터가 필요해질 때까지 기다리지 않고 요청을 가능한 한 일찍 시작하는 방식입니다.
app/page.tsxtsximport {trpc ,HydrateClient } from '../trpc/server';import {ClientGreeting } from './client-greeting';export default async functionHome () {voidtrpc .hello .prefetch ();return (<HydrateClient ><div >...</div >{/** ... */}<ClientGreeting /></HydrateClient >);}
app/page.tsxtsximport {trpc ,HydrateClient } from '../trpc/server';import {ClientGreeting } from './client-greeting';export default async functionHome () {voidtrpc .hello .prefetch ();return (<HydrateClient ><div >...</div >{/** ... */}<ClientGreeting /></HydrateClient >);}
app/client-greeting.tsxtsx'use client';// <-- hooks can only be used in client componentsimport {trpc } from '../trpc/client';export functionClientGreeting () {constgreeting =trpc .hello .useQuery ();if (!greeting .data ) return <div >Loading...</div >;return <div >{greeting .data .greeting }</div >;}
app/client-greeting.tsxtsx'use client';// <-- hooks can only be used in client componentsimport {trpc } from '../trpc/client';export functionClientGreeting () {constgreeting =trpc .hello .useQuery ();if (!greeting .data ) return <div >Loading...</div >;return <div >{greeting .data .greeting }</div >;}
Suspense 활용
Suspense와 Error Boundaries를 사용하여 로딩 및 오류 상태를 처리하는 방식을 선호할 수 있습니다. useSuspenseQuery 훅을 사용하면 이를 구현할 수 있습니다.
app/page.tsxtsximport {trpc ,HydrateClient } from '../trpc/server';import {Suspense } from 'react';import {ErrorBoundary } from 'react-error-boundary';import {ClientGreeting } from './client-greeting';export default async functionHome () {voidtrpc .hello .prefetch ();return (<HydrateClient ><div >...</div >{/** ... */}<ErrorBoundary fallback ={<div >Something went wrong</div >}><Suspense fallback ={<div >Loading...</div >}><ClientGreeting /></Suspense ></ErrorBoundary ></HydrateClient >);}
app/page.tsxtsximport {trpc ,HydrateClient } from '../trpc/server';import {Suspense } from 'react';import {ErrorBoundary } from 'react-error-boundary';import {ClientGreeting } from './client-greeting';export default async functionHome () {voidtrpc .hello .prefetch ();return (<HydrateClient ><div >...</div >{/** ... */}<ErrorBoundary fallback ={<div >Something went wrong</div >}><Suspense fallback ={<div >Loading...</div >}><ClientGreeting /></Suspense ></ErrorBoundary ></HydrateClient >);}
app/client-greeting.tsxtsx'use client';import {trpc } from '../trpc/client';export functionClientGreeting () {const [data ] =trpc .hello .useSuspenseQuery ();return <div >{data .greeting }</div >;}
app/client-greeting.tsxtsx'use client';import {trpc } from '../trpc/client';export functionClientGreeting () {const [data ] =trpc .hello .useSuspenseQuery ();return <div >{data .greeting }</div >;}
서버 컴포넌트에서 데이터 가져오기
서버 컴포넌트에서 데이터에 접근해야 하는 경우, .prefetch()를 사용하는 대신 일반 서버 콜러와 마찬가지로 프로시저를 직접 호출할 수 있습니다. 이 방법은 쿼리 클라이언트와 분리되어 있으며 데이터를 캐시에 저장하지 않습니다. 따라서 서버 컴포넌트에서 데이터를 사용한 후 클라이언트에서 해당 데이터가 사용 가능할 것으로 기대할 수 없습니다. 이는 의도된 동작이며, 고급 서버 렌더링 가이드에서 자세히 설명되어 있습니다.
app/page.tsxtsximport {trpc } from '../trpc/server';export default async functionHome () {// Use the caller directly without using `.prefetch()`constgreeting = awaittrpc .hello ();// ^? { greeting: string }return <div >{greeting .greeting }</div >;}
app/page.tsxtsximport {trpc } from '../trpc/server';export default async functionHome () {// Use the caller directly without using `.prefetch()`constgreeting = awaittrpc .hello ();// ^? { greeting: string }return <div >{greeting .greeting }</div >;}