https://www.youtube.com/watch?v=ED2H_y6dmC8
56:15
- Setup tRPC v11
- Create a procedure with Prisma API
- Explore tRPC client-side
- Explore tRPC server-side
- Explore tRPC server + client (prefetch)
- Push to GitHub
- Create a new branch
- Create a new PR
- Review & merge
https://trpc.io/docs/client/tanstack-react-query/server-components
Set up with React Server Components | tRPC
This guide is an overview of how one may use tRPC with a React Server Components (RSC) framework such as Next.js App Router.
trpc.io
tRPC v11과 Next.js App Router를 통합하기
패키지 설치
- @trpc/server, @trpc/client: tRPC의 핵심 라이브러리
- @trpc/tanstack-react-query: React Query와 tRPC를 통합하는 어댑터
- @tanstack/react-query: 강력한 서버 상태 관리 라이브러리
- zod: 타입 안전한 스키마 검증 라이브러리
- client-only, server-only: Next.js에서 클라이언트/서버 코드 분리를 강제하는 유틸리티
💡 안토니오와 버전의 큰차이가 없어보여서 이번엔 그냥 넘어갔다가 그냥 다시 맞춰줌....ㅋㅋㅋ
"@tanstack/react-query": "^5.90.2",
"@trpc/client": "^11.6.0",
"@trpc/server": "^11.6.0",
"@trpc/tanstack-react-query": "^11.6.0",
"client-only": "^0.0.1",
npm install @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod client-only server-only
tRPC 초기화 설정
src/trpc/init.ts 생성
- createTRPCContext: tRPC에서 사용할 컨텍스트를 생성. 여기에는 인증 정보, 데이터베이스 연결 등을 포함할 수 있다.
- cache(): React의 cache 함수로 감싸서 동일한 요청 내에서 컨텍스트가 재사용되도록 한다.
import { initTRPC } from '@trpc/server';
import { cache } from 'react';
export const createTRPCContext = 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.
const t = initTRPC.create({
/**
* @see https://trpc.io/docs/server/data-transformers
*/
// transformer: superjson,
});
// Base router and procedure helpers
export const createTRPCRouter = t.router;
export const createCallerFactory = t.createCallerFactory;
export const baseProcedure = t.procedure;
라우터 생성
src/trpc/routers/_app.ts
- baseProcedure: tRPC의 기본 프로시저로, 모든 API 엔드포인트의 기본이 된다.
- .input(): Zod 스키마를 사용해 입력값을 검증. 타입 안전성을 보장한다.
- .query(): 데이터를 조회하는 프로시저다. (데이터 변경은 .mutation() 사용)
- AppRouter 타입 export: 클라이언트에서 타입 안전하게 API를 호출할 수 있게 한다.
import { z } from 'zod';
import { baseProcedure, createTRPCRouter } from '../init';
export const appRouter = createTRPCRouter({
hello: baseProcedure
.input(
z.object({
text: z.string(),
}),
)
.query((opts) => {
return {
greeting: `hello ${opts.input.text}`,
};
}),
});
// export type definition of API
export type AppRouter = typeof appRouter;
API Route 핸들러 설정
src/app/api/trpc/[trpc]/route.ts
- fetchRequestHandler: Next.js App Router의 Route Handler와 tRPC를 연결하는 어댑터다.
- [trpc] 동적 라우트: 모든 tRPC 요청을 /api/trpc/* 경로로 처리한다.
- GET과 POST 모두 export: tRPC는 쿼리에는 GET, 뮤테이션에는 POST를 사용한다.
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { createTRPCContext } from '@/trpc/init';
import { appRouter } from '@/trpc/routers/_app';
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext: createTRPCContext,
});
export { handler as GET, handler as POST };
Query Client 설정
- staleTime: 30 * 1000: 데이터가 30초 동안 신선한(fresh) 상태로 유지. 이 시간 내에는 재요청하지 않는다.
- dehydrate: 서버에서 가져온 데이터를 클라이언트로 전달할 때의 설정이다.
- shouldDehydrateQuery: pending 상태의 쿼리도 dehydrate하도록 설정하여 서버에서 시작된 요청이 클라이언트에서 계속될 수 있게 한다.
- superjson은 나중에 설치할 계획이다. (Date, Map, Set 등 복잡한 타입을 직렬화할 때 사용)
src/trpc/query-client.ts
import {
defaultShouldDehydrateQuery,
QueryClient,
} from '@tanstack/react-query';
// import superjson from 'superjson';
export function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 30 * 1000,
},
dehydrate: {
// serializeData: superjson.serialize,
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) ||
query.state.status === 'pending',
},
hydrate: {
// deserializeData: superjson.deserialize,
},
},
});
}
클라이언트 Provider 설정
- 'use client': 이 파일은 클라이언트 컴포넌트다.
- browserQueryClient: 브라우저에서는 싱글톤 패턴으로 QueryClient를 재사용한다. React Suspense 중에 클라이언트가 재생성되는 것을 방지한다.
- getUrl(): 환경에 따라 올바른 API URL을 반환한다. (로컬, Vercel 배포 등)
- httpBatchLink: 여러 tRPC 요청을 하나의 HTTP 요청으로 배치 처리하여 성능을 최적화한다.
- useState(() => ...): tRPC 클라이언트를 초기화할 때 한 번만 생성되도록 한다.
src/trpc/client.tsx
'use client';
// ^-- to make sure we can mount the Provider from a server component
import type { QueryClient } from '@tanstack/react-query';
import { QueryClientProvider } from '@tanstack/react-query';
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import { createTRPCContext } from '@trpc/tanstack-react-query';
import { useState } from 'react';
import { makeQueryClient } from './query-client';
import type { AppRouter } from './routers/_app';
export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();
let browserQueryClient: QueryClient;
function getQueryClient() {
if (typeof window === 'undefined') {
// Server: always make a new query client
return makeQueryClient();
}
// Browser: make a new query client if we don't already have one
// This is very important, so we don't re-make a new client if React
// suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}
function getUrl() {
const base = (() => {
if (typeof window !== 'undefined') return '';
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
return 'http://localhost:3000';
})();
return `${base}/api/trpc`;
}
export function TRPCReactProvider(
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 c lient on the initial
// render if it suspends and there is no boundary
const queryClient = getQueryClient();
const [trpcClient] = useState(() =>
createTRPCClient<AppRouter>({
links: [
httpBatchLink({
// transformer: superjson, <-- if you use a data transformer
url: getUrl(),
}),
],
}),
);
return (
<QueryClientProvider client={queryClient}>
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
{props.children}
</TRPCProvider>
</QueryClientProvider>
);
}
Layout에 Provider 추가
- 앱 전체를 TRPCReactProvider로 감싸서 모든 컴포넌트에서 tRPC를 사용할 수 있게 한다.
src/app/layout.tsx
<TRPCReactProvider>
{children}
</TRPCReactProvider>
서버 사이드 tRPC 설정
- 'server-only': 이 파일이 클라이언트에서 import되면 빌드 에러가 발생. 서버 코드 보호를 위한 안전장치.
- cache(makeQueryClient): 동일한 요청 내에서 QueryClient를 재사용.
createTRPCOptionsProxy: 서버 컴포넌트에서 tRPC를 사용할 수 있게 해주는 프록시 객체를 생성한다.
주석 처리된 부분: tRPC 라우터가 별도 서버에 있을 때 사용하지만, 현재는 같은 앱 내에 있으므로 불필요하다.
src/trpc/server.tsx
import 'server-only'; // <-- ensure this file cannot be imported from the client
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
import { cache } from 'react';
import { 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 const getQueryClient = cache(makeQueryClient);
export const trpc = createTRPCOptionsProxy({
ctx: createTRPCContext,
router: appRouter,
queryClient: getQueryClient,
});
// 안필요함 삭제
// If your router is on a separate server, pass a client:
// createTRPCOptionsProxy({
// client: createTRPCClient({
// links: [httpLink({ url: '...' })],
// }),
// queryClient: getQueryClient,
// });
Prisma와 tRPC 통합
- prisma.user.findMany(): Prisma ORM을 사용해 데이터베이스에서 모든 사용자를 조회한다.
- 이제 tRPC를 통해 타입 안전하게 데이터베이스에 접근할 수 있다.
src/trpc/routers/_app.ts 수정하기
import { baseProcedure, createTRPCRouter } from '../init';
import prisma from '@/lib/db';
export const appRouter = createTRPCRouter({
getUsers: baseProcedure
.query(() => {
return prisma.user.findMany()
}),
});
// export type definition of API
export type AppRouter = typeof appRouter;
Set up with React Server Components | tRPC
This guide is an overview of how one may use tRPC with a React Server Components (RSC) framework such as Next.js App Router.
trpc.io
서버 컴포넌트에서 데이터 가져오기
- createCaller: 서버 사이드에서 직접 tRPC 프로시저를 호출할 수 있는 caller 객체를 생성한다.
- HTTP 요청 없이 직접 함수를 호출하므로 매우 빠르다.
src/trpc/server.tsx
export const caller = appRouter.createCaller(createTRPCContext);
src/app/page.tsx - 서버 컴포넌트 방식(느림 안씀)
- 서버 컴포넌트에서 caller를 사용하면 HTTP 오버헤드 없이 데이터를 가져올 수 있다.
- 초기 페이지 로드가 매우 빠르다
import { caller } from "@/trpc/server"
const Page = async() =>{
const users = await caller.getUsers()
return(
<div className="min-h-screen min-w-screen flex items-center justify-center">
{JSON.stringify(users)}
</div>
)
}
export default Page

⚠️ 클라이언트 컴포넌트로 변경 시 성능 이슈
- 클라이언트 컴포넌트에서는 HTTP 요청을 통해 데이터를 가져와야 한다.
- 초기 렌더링 시 데이터가 없어 깜빡임이 발생한다.
- 네트워크 레이턴시로 인해 전반적으로 느리다.
"use client"
import { useTRPC } from "@/trpc/client"
import { useQuery } from "@tanstack/react-query";
const Page = () =>{
const trpc = useTRPC();
const {data:users} = useQuery(trpc.getUsers.queryOptions())
return(
<div className="min-h-screen min-w-screen flex items-center justify-center">
{JSON.stringify(users)}
</div>
)
}
export default Page
Props로 데이터 전달하는 방법 (권장하지 않음)
- 클라이언트 컴포넌트는 최종 데이터만 가지고 있다.
- React Query의 캐싱, refetch 등의 기능을 사용할 수 없다.
- 데이터를 다시 가져올 방법이 없어 유연성이 떨어진다.
src/app/client.tsx
"use client"
export const Client = ({users}:{users: Record<string, any>[]}) => {
return(
<div>
Client component : {JSON.stringify(users)}
</div>
)
}
src/app/page.tsx
import { caller } from "@/trpc/server"
import { Client } from "./client"
const Page = async() =>{
const users = await caller.getUsers()
return(
<div className="min-h-screen min-w-screen flex items-center justify-center">
<Client users={users}/>
</div>
)
}
export default Page

최적의 해결책: Prefetch + Hydration (추천!)
핵심 동작 원리:
- 서버에서 Prefetch:
- queryClient.prefetchQuery()로 서버에서 데이터를 미리 가져온다.
- 데이터가 React Query 캐시에 저장된다.
- Dehydration :
- dehydrate(queryClient): 서버의 QueryClient 상태를 직렬화 가능한 형태로 변환한다.
- 이 상태가 HTML과 함께 클라이언트로 전송된다.
- Hydration :
- HydrationBoundary: 클라이언트에서 dehydrated 상태를 받아 QueryClient를 복원한다.
- 클라이언트 컴포넌트가 마운트될 때 이미 데이터가 캐시에 있어 즉시 사용 가능하다.
- Suspense Query:
- useSuspenseQuery: 데이터가 캐시에 있으면 즉시 반환하고, 없으면 Suspense를 트리거한다.
- 하지만 prefetch 덕분에 데이터가 이미 있어서 깜빡임 없이 렌더링된다.
장점:
- 초고속 로딩: 서버에서 미리 데이터를 가져와 깜빡임이 없다.
- React Query 기능 유지: refetch, invalidation, 캐싱 등 모든 기능을 사용할 수 있다.
- 타입 안전성: tRPC의 end-to-end 타입 안전성을 그대로 유지한다.
- 유연성: 클라이언트에서 필요할 때 데이터를 다시 가져올 수 있다.
src/app/page.tsx - Prefetch 방식
import { getQueryClient, trpc } from "@/trpc/server"
import { Client } from "./client"
import { dehydrate, HydrationBoundary } from "@tanstack/react-query"
const Page = async() =>{
const queryClient = getQueryClient()
void queryClient.prefetchQuery(trpc.getUsers.queryOptions())
return(
<div className="min-h-screen min-w-screen flex items-center justify-center">
<HydrationBoundary state={dehydrate(queryClient)}>
<Client/>
</HydrationBoundary>
</div>
)
}
export default Page
src/app/client.tsx
"use client"
import { useTRPC } from "@/trpc/client"
import { useSuspenseQuery } from "@tanstack/react-query"
export const Client = () => {
const trpc = useTRPC();
const {data: users} = useSuspenseQuery(trpc.getUsers.queryOptions())
return(
<div>
Client component : {JSON.stringify(users)}
</div>
)
}
새로고침을 해보니 호출 속도가 엄청 빠르다.
tRPC의 프리패치를 활용하고 있으며, TanStack Query 클라이언트의 hydration 기능으로
깜빡이지 않고 잘 된다.
https://trpc.io/docs/client/tanstack-react-query/server-components#leveraging-suspense
Set up with React Server Components | tRPC
This guide is an overview of how one may use tRPC with a React Server Components (RSC) framework such as Next.js App Router.
trpc.io
Suspense로 로딩 상태 처리
- <Suspense fallback={<p>Loading...</p>}>: 로딩 중일 때 fallback UI를 보여준다.
- prefetch 덕분에 대부분의 경우 fallback이 표시되지 않지만, 만약의 경우를 대비한 안전장치.
- 네트워크가 느리거나 데이터가 클 때 유용.
import { getQueryClient, trpc } from "@/trpc/server"
import { Client } from "./client"
import { dehydrate, HydrationBoundary } from "@tanstack/react-query"
import { Suspense } from "react"
const Page = async() =>{
const queryClient = getQueryClient()
void queryClient.prefetchQuery(trpc.getUsers.queryOptions())
return(
<div className="min-h-screen min-w-screen flex items-center justify-center">
<HydrationBoundary state={dehydrate(queryClient)}>
<Suspense fallback={<p>Loading...</p>}>
<Client/>
</Suspense>
</HydrationBoundary>
</div>
)
}
export default Page
최종 선택: 앞으로 사용할 구조!
앞으로도 이렇게 클라이언트 컴포넌트로 넘겨줄 예정이다.
// 서버 컴포넌트에서 prefetch
void queryClient.prefetchQuery(trpc.getUsers.queryOptions())
// HydrationBoundary로 감싸기
<HydrationBoundary state={dehydrate(queryClient)}>
<Client />
</HydrationBoundary>
// 클라이언트에서 useSuspenseQuery 사용
const {data: users} = useSuspenseQuery(trpc.getUsers.queryOptions())
이 패턴을 선택한 이유:
- 서버 사이드 렌더링의 성능 이점
- 클라이언트의 인터랙티브한 기능
- React Query의 모든 기능 활용
- 깔끔한 코드 분리
- 최고의 사용자 경험 (깜빡임 없음)
'Clone Coding' 카테고리의 다른 글
| N8N & Zapier - Theme & styling (0) | 2025.12.31 |
|---|---|
| N8N & Zapier - Authentication (0) | 2025.12.31 |
| N8N & Zapier - Database & ORM (0) | 2025.12.27 |
| N8N & Zapier - setup (0) | 2025.12.26 |
| N8N & Zapier - 작업 할 내용 (0) | 2025.12.26 |