본문 바로가기
Clone Coding

N8N & Zapier - tRPC 설정

by zzuny-code 2025. 12. 29.
반응형

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;

 

 

https://trpc.io/docs/client/tanstack-react-query/server-components#getting-data-in-a-server-component

 

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 (추천!)

핵심 동작 원리:

  1. 서버에서 Prefetch:
    • queryClient.prefetchQuery()로 서버에서 데이터를 미리 가져온다.
    • 데이터가 React Query 캐시에 저장된다.
  2. Dehydration :
    • dehydrate(queryClient): 서버의 QueryClient 상태를 직렬화 가능한 형태로 변환한다.
    • 이 상태가 HTML과 함께 클라이언트로 전송된다.
  3. Hydration :
    • HydrationBoundary: 클라이언트에서 dehydrated 상태를 받아 QueryClient를 복원한다.
    • 클라이언트 컴포넌트가 마운트될 때 이미 데이터가 캐시에 있어 즉시 사용 가능하다.
  4. 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())

 

이 패턴을 선택한 이유:

  1. 서버 사이드 렌더링의 성능 이점
  2. 클라이언트의 인터랙티브한 기능
  3. React Query의 모든 기능 활용
  4. 깔끔한 코드 분리
  5. 최고의 사용자 경험 (깜빡임 없음)

반응형

'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