본문으로 건너뛰기
버전: 11.x

Next.js App Router로 설정하기

노트

서버 렌더링의 다양한 유형과 피해야 할 함정을 이해하려면 TanStack React Query의 Advanced Server Rendering 문서를 읽는 것을 권장합니다.

graphql
.
├── src
│ ├── app
│ │ ├── api
│ │ │ └── trpc
│ │ │ └── [trpc]
│ │ │ └── route.ts # <-- tRPC HTTP handler
│ │ ├── layout.tsx # <-- mount TRPCReactProvider
│ │ └── page.tsx # <-- server component
│ ├── trpc
│ │ ├── init.ts # <-- tRPC server init & context
│ │ ├── routers
│ │ │ ├── _app.ts # <-- main app router
│ │ │ ├── post.ts # <-- sub routers
│ │ │ └── [..]
│ │ ├── client.tsx # <-- client hooks & provider
│ │ ├── query-client.ts # <-- shared QueryClient factory
│ │ └── server.tsx # <-- server-side caller
│ └── [..]
└── [..]
graphql
.
├── src
│ ├── app
│ │ ├── api
│ │ │ └── trpc
│ │ │ └── [trpc]
│ │ │ └── route.ts # <-- tRPC HTTP handler
│ │ ├── layout.tsx # <-- mount TRPCReactProvider
│ │ └── page.tsx # <-- server component
│ ├── trpc
│ │ ├── init.ts # <-- tRPC server init & context
│ │ ├── routers
│ │ │ ├── _app.ts # <-- main app router
│ │ │ ├── post.ts # <-- sub routers
│ │ │ └── [..]
│ │ ├── client.tsx # <-- client hooks & provider
│ │ ├── query-client.ts # <-- shared QueryClient factory
│ │ └── server.tsx # <-- server-side caller
│ └── [..]
└── [..]

기존 Next.js App Router 프로젝트에 tRPC 추가하기​

1. 의존성 설치​

npm install @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod client-only server-only
AI 에이전트

AI 코딩 에이전트를 사용하는 경우, 더 나은 코드 생성을 위해 tRPC 스킬을 설치하세요:

bash
npx @tanstack/intent@latest install
bash
npx @tanstack/intent@latest install

2. tRPC 라우터 생성​

trpc/init.ts에서 initTRPC 함수를 사용하여 tRPC 백엔드를 초기화하고 첫 번째 라우터를 생성합니다. 여기서는 간단한 "hello world" 라우터와 프로시저를 만들 것입니다. tRPC API 생성에 대한 더 자세한 정보는 Quickstart guide 및 Backend usage docs를 참고하세요.

trpc/init.ts
ts
import { initTRPC } from '@trpc/server';
 
/**
* This context creator accepts `headers` so it can be reused in both
* the RSC server caller (where you pass `next/headers`) and the
* API route handler (where you pass the request headers).
*/
export const createTRPCContext = async (opts: { headers: Headers }) => {
// const user = await auth(opts.headers);
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
.context<Awaited<ReturnType<typeof createTRPCContext>>>()
.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;
trpc/init.ts
ts
import { initTRPC } from '@trpc/server';
 
/**
* This context creator accepts `headers` so it can be reused in both
* the RSC server caller (where you pass `next/headers`) and the
* API route handler (where you pass the request headers).
*/
export const createTRPCContext = async (opts: { headers: Headers }) => {
// const user = await auth(opts.headers);
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
.context<Awaited<ReturnType<typeof createTRPCContext>>>()
.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;

trpc/routers/_app.ts
ts
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;
trpc/routers/_app.ts
ts
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;

3. API 라우트 핸들러 생성​

App Router에서는 tRPC 요청을 처리하기 위해 fetch adapter를 사용합니다. GET과 POST를 모두 내보내는 라우트 핸들러를 생성하세요:

app/api/trpc/[trpc]/route.ts
ts
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({ headers: req.headers }),
});
 
export { handler as GET, handler as POST };
app/api/trpc/[trpc]/route.ts
ts
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({ headers: req.headers }),
});
 
export { handler as GET, handler as POST };
노트

App Router는 Pages Router에서 사용되는 Next.js 전용 어댑터 대신 fetch adapter(fetchRequestHandler를 통해)를 사용합니다. 이는 App Router의 라우트 핸들러가 Web 표준 Request 및 Response 객체에 기반하기 때문입니다.

4. Query Client 팩토리 생성​

QueryClient 인스턴스를 생성하는 함수를 내보내는 공유 파일 trpc/query-client.ts를 생성하세요.

trpc/query-client.ts
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,
},
},
});
}
trpc/query-client.ts
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,
},
},
});
}

여기서 몇 가지 기본 옵션을 설정합니다:

  • staleTime: SSR을 사용할 때, 클라이언트에서 즉시 다시 가져오기를 피하기 위해 staleTime을 0보다 큰 값으로 설정하는 것이 일반적입니다.
  • shouldDehydrateQuery: 쿼리를 탈수화(dehydrate)할지 여부를 결정하는 함수입니다. RSC 전송 프로토콜은 네트워크를 통한 프롬리스(hydrating)를 지원하므로, defaultShouldDehydrateQuery 함수를 확장하여 아직 대기 중인 쿼리도 포함하도록 합니다. 이를 통해 트리 상단의 서버 컴포넌트에서 사전 가져오기를 시작하고, 더 아래쪽의 클라이언트 컴포넌트에서 해당 프롬리스를 소비할 수 있습니다.
  • serializeData 및 deserializeData (선택 사항): 이전 단계에서 data transformer를 설정했다면, 이 옵션을 설정하여 서버-클라이언트 경계를 넘어 쿼리 클라이언트를 수화(hydrate)할 때 데이터가 올바르게 직렬화되도록 보장합니다.

5. Client Components용 tRPC 클라이언트 생성​

trpc/client.tsx 파일은 클라이언트 컴포넌트에서 tRPC API를 소비할 때의 진입점입니다. 여기에서 tRPC 라우터의 타입 정의를 가져오고 createTRPCContext를 사용하여 타입 안전한 훅을 생성합니다. 또한 이 파일에서 컨텍스트 프로바이더도 내보냅니다.

trpc/client.tsx
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 client 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>
);
}
trpc/client.tsx
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 client 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>
);
}

애플리케이션의 루트 레이아웃에 프로바이더를 마운트하세요:

app/layout.tsx
tsx
import { TRPCReactProvider } from '~/trpc/client';
 
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<TRPCReactProvider>{children}</TRPCReactProvider>
</body>
</html>
);
}
app/layout.tsx
tsx
import { TRPCReactProvider } from '~/trpc/client';
 
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<TRPCReactProvider>{children}</TRPCReactProvider>
</body>
</html>
);
}

6. Server Components용 tRPC 콜러 생성​

서버 컴포넌트에서 쿼리를 사전 가져오려면 라우터에서 프록시를 생성합니다. 라우터가 별도의 서버에 있는 경우 클라이언트를 전달할 수도 있습니다.

trpc/server.tsx
tsx
import 'server-only'; // <-- ensure this file cannot be imported from the client
 
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
import { headers } from 'next/headers';
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: async () =>
createTRPCContext({
headers: await headers(),
}),
router: appRouter,
queryClient: getQueryClient,
});
 
// If your router is on a separate server, pass a client instead:
// createTRPCOptionsProxy({
// client: createTRPCClient({ links: [httpLink({ url: '...' })] }),
// queryClient: getQueryClient,
// });
trpc/server.tsx
tsx
import 'server-only'; // <-- ensure this file cannot be imported from the client
 
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
import { headers } from 'next/headers';
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: async () =>
createTRPCContext({
headers: await headers(),
}),
router: appRouter,
queryClient: getQueryClient,
});
 
// If your router is on a separate server, pass a client instead:
// createTRPCOptionsProxy({
// client: createTRPCClient({ links: [httpLink({ url: '...' })] }),
// queryClient: getQueryClient,
// });

7. API 요청 수행​

준비가 완료되었습니다! 이제 서버 컴포넌트에서 쿼리를 사전 가져오고 클라이언트 컴포넌트에서 이를 사용할 수 있습니다.

Server Component에서 사전 가져오기​

app/page.tsx
tsx
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
import { getQueryClient, trpc } from '~/trpc/server';
import { ClientGreeting } from './client-greeting';
 
export default async function Home() {
const queryClient = getQueryClient();
void queryClient.prefetchQuery(
trpc.hello.queryOptions({
text: 'world',
}),
);
 
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<ClientGreeting />
</HydrationBoundary>
);
}
app/page.tsx
tsx
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
import { getQueryClient, trpc } from '~/trpc/server';
import { ClientGreeting } from './client-greeting';
 
export default async function Home() {
const queryClient = getQueryClient();
void queryClient.prefetchQuery(
trpc.hello.queryOptions({
text: 'world',
}),
);
 
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<ClientGreeting />
</HydrationBoundary>
);
}

Client Component에서 데이터 사용​

app/client-greeting.tsx
tsx
'use client';
 
// <-- hooks can only be used in client components
import { useQuery } from '@tanstack/react-query';
import { useTRPC } from '~/trpc/client';
 
export function ClientGreeting() {
const trpc = useTRPC();
const greeting = useQuery(trpc.hello.queryOptions({ text: 'world' }));
if (!greeting.data) return <div>Loading...</div>;
return <div>{greeting.data.greeting}</div>;
}
app/client-greeting.tsx
tsx
'use client';
 
// <-- hooks can only be used in client components
import { useQuery } from '@tanstack/react-query';
import { useTRPC } from '~/trpc/client';
 
export function ClientGreeting() {
const trpc = useTRPC();
const greeting = useQuery(trpc.hello.queryOptions({ text: 'world' }));
if (!greeting.data) return <div>Loading...</div>;
return <div>{greeting.data.greeting}</div>;
}
팁

이를 더 간결하게 만들기 위해 prefetch 및 HydrateClient 헬퍼 함수를 생성할 수 있습니다:

trpc/server.tsx
tsx
export function HydrateClient(props: { children: React.ReactNode }) {
const queryClient = getQueryClient();
return (
<HydrationBoundary state={dehydrate(queryClient)}>
{props.children}
</HydrationBoundary>
);
}
 
export function prefetch<T extends ReturnType<TRPCQueryOptions<any>>>(
queryOptions: T,
) {
const queryClient = getQueryClient();
if (queryOptions.queryKey[1]?.type === 'infinite') {
void queryClient.prefetchInfiniteQuery(queryOptions as any);
} else {
void queryClient.prefetchQuery(queryOptions);
}
}
trpc/server.tsx
tsx
export function HydrateClient(props: { children: React.ReactNode }) {
const queryClient = getQueryClient();
return (
<HydrationBoundary state={dehydrate(queryClient)}>
{props.children}
</HydrationBoundary>
);
}
 
export function prefetch<T extends ReturnType<TRPCQueryOptions<any>>>(
queryOptions: T,
) {
const queryClient = getQueryClient();
if (queryOptions.queryKey[1]?.type === 'infinite') {
void queryClient.prefetchInfiniteQuery(queryOptions as any);
} else {
void queryClient.prefetchQuery(queryOptions);
}
}

그런 다음 다음과 같이 사용할 수 있습니다:

tsx
import { HydrateClient, prefetch, trpc } from '~/trpc/server';
import { ClientGreeting } from './client-greeting';
 
function Home() {
prefetch(trpc.hello.queryOptions({ text: 'world' }));
 
return (
<HydrateClient>
<ClientGreeting />
</HydrateClient>
);
}
tsx
import { HydrateClient, prefetch, trpc } from '~/trpc/server';
import { ClientGreeting } from './client-greeting';
 
function Home() {
prefetch(trpc.hello.queryOptions({ text: 'world' }));
 
return (
<HydrateClient>
<ClientGreeting />
</HydrateClient>
);
}

Suspense 활용하기​

Suspense와 Error Boundaries를 사용하여 로딩 및 오류 상태를 처리하는 방식을 선호할 수 있습니다. useSuspenseQuery 훅을 사용하면 이를 구현할 수 있습니다.

app/page.tsx
tsx
import { HydrateClient, prefetch, trpc } from '~/trpc/server';
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { ClientGreeting } from './client-greeting';
 
export default async function Home() {
prefetch(trpc.hello.queryOptions());
 
return (
<HydrateClient>
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Suspense fallback={<div>Loading...</div>}>
<ClientGreeting />
</Suspense>
</ErrorBoundary>
</HydrateClient>
);
}
app/page.tsx
tsx
import { HydrateClient, prefetch, trpc } from '~/trpc/server';
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { ClientGreeting } from './client-greeting';
 
export default async function Home() {
prefetch(trpc.hello.queryOptions());
 
return (
<HydrateClient>
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Suspense fallback={<div>Loading...</div>}>
<ClientGreeting />
</Suspense>
</ErrorBoundary>
</HydrateClient>
);
}
app/client-greeting.tsx
tsx
'use client';
 
import { useSuspenseQuery } from '@tanstack/react-query';
import { useTRPC } from '~/trpc/client';
 
export function ClientGreeting() {
const trpc = useTRPC();
const { data } = useSuspenseQuery(trpc.hello.queryOptions());
return <div>{data.greeting}</div>;
}
app/client-greeting.tsx
tsx
'use client';
 
import { useSuspenseQuery } from '@tanstack/react-query';
import { useTRPC } from '~/trpc/client';
 
export function ClientGreeting() {
const trpc = useTRPC();
const { data } = useSuspenseQuery(trpc.hello.queryOptions());
return <div>{data.greeting}</div>;
}

서버 컴포넌트에서 데이터 가져오기​

서버 컴포넌트에서 데이터에 접근해야 하는 경우, 서버 콜러를 생성하여 직접 사용하는 것을 권장합니다. 이 방식은 쿼리 클라이언트와 분리되어 있으며 데이터를 캐시에 저장하지 않습니다. 즉, 서버 컴포넌트에서 데이터를 사용한 후 클라이언트에서 해당 데이터가 사용 가능할 것으로 기대할 수 없습니다. 이는 의도된 동작이며, Advanced Server Rendering 가이드에서 자세히 설명되어 있습니다.

trpc/server.tsx
tsx
import { headers } from 'next/headers';
import { createTRPCContext } from './init';
import { appRouter } from './routers/_app';
 
// ...
export const caller = appRouter.createCaller(async () =>
createTRPCContext({ headers: await headers() }),
);
trpc/server.tsx
tsx
import { headers } from 'next/headers';
import { createTRPCContext } from './init';
import { appRouter } from './routers/_app';
 
// ...
export const caller = appRouter.createCaller(async () =>
createTRPCContext({ headers: await headers() }),
);
app/page.tsx
tsx
import { caller } from '~/trpc/server';
 
export default async function Home() {
const greeting = await caller.hello();
const greeting: { greeting: string; }
 
return <div>{greeting.greeting}</div>;
}
app/page.tsx
tsx
import { caller } from '~/trpc/server';
 
export default async function Home() {
const greeting = await caller.hello();
const greeting: { greeting: string; }
 
return <div>{greeting.greeting}</div>;
}

서버와 클라이언트 컴포넌트 양쪽에서 데이터를 사용해야 하고, Advanced Server Rendering 가이드에 설명된 트레이드오프를 충분히 이해하는 경우에만, prefetch 대신 fetchQuery를 사용하여 서버에서 데이터를 처리하고 클라이언트로 하이드레이션할 수 있습니다:

app/page.tsx
tsx
import { getQueryClient, HydrateClient, trpc } from '~/trpc/server';
import { ClientGreeting } from './client-greeting';
 
export default async function Home() {
const queryClient = getQueryClient();
const greeting = await queryClient.fetchQuery(trpc.hello.queryOptions());
 
// Do something with greeting on the server
 
return (
<HydrateClient>
<ClientGreeting />
</HydrateClient>
);
}
app/page.tsx
tsx
import { getQueryClient, HydrateClient, trpc } from '~/trpc/server';
import { ClientGreeting } from './client-greeting';
 
export default async function Home() {
const queryClient = getQueryClient();
const greeting = await queryClient.fetchQuery(trpc.hello.queryOptions());
 
// Do something with greeting on the server
 
return (
<HydrateClient>
<ClientGreeting />
</HydrateClient>
);
}

다음 단계​