avatar
nine_devlog

tRPC with Next.js 14: Client Side Setting

바로 개발 경험 정상화
tRPCnextjs
2 months ago
·
7 min read

1909

이전 게시글에서 이어집니다.

본격적인 서버단 로직 작성에 앞서 테스트 등을 위해 Client쪽 세팅도 진행해보자.

client side

비교적 client side에는 설정할 것이 많지 않지만, 서버쪽과 유기적으로 연결이 되다 보니 Provider와 Client에서 사용하는 객체같은게 혼동되기 쉬운 부분들이 있다.

tRPC Client

말 그대로 클라이언트에서 사용할 tRPC clent를 createTRPC...를 이용해 생성해주어야 한다. tRPC에는 axios의 interceptor와 비슷한 개념인 link가 있는데, 클라이언트를 생성할 때 tRPC에서 제공하는 link를 사용해 클라이언트에서 요청을 할 때 동작할 로직을 구성한다.

실질적으로 요청이 발생하는 link는 httpLink, httpBatchLink로 서버에서 지정해준 url과 함께 fetch를 넣어주면 된다. 요청을 보낼 때 기본으로 포함되어야 하는 헤더와 같은 설정들도 이 곳에서 가능하다. 이 플젝에서는 헤더에 토큰을 넣어서 검증하는 방식으로 사용할거라 해당 헤더에 대한 내용을 포함하였다.

이 때 주의할 점은 React Query를 사용할 거라면 createTRPCReact@trpc/react-query에서 가져와야 한다는 점이다. 이렇게 생성된 클라이언트는 ReactQuery로만 사용이 가능하기 때문에 당연하게도 서버 컴포넌트에서는 사용할 수 없다. 만약 React Query를 사용하지 않거나 사용할 수 없는 로직에서 요청을 하려면 createTRPCClient로 별도의 클라이언트를 생성하면 된다.

trpcClient.ts

import type { AppRouter } from "@/server/routes/appRouter"; // 서버에서 작성한 AppRouter 타입도 불러온다.
import {
  createTRPCClient,
  getFetch,
  httpBatchLink,
  loggerLink,
} from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
import superjson from "superjson";

// react-query와 함께 사용할 클라이언트
export const trpcReact = createTRPCReact<AppRouter>();
const trpcClient = trpcReact.createClient({
  links: [
    loggerLink({
      enabled: () => true,
    }),
    httpBatchLink({
      url: "/api",
      transformer: superjson,
      headers() {
        return {
          Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
        };
      },
      fetch: async (input, init?) => {
        const fetch = getFetch();
        return await fetch(input, {
          ...init,
          credentials: "include",
        });
      },
    }),
  ],
});

// Provider에 주입하기 위해 함수 형태로 감싼다.
export const trpcClientForProvider = () => trpcClient;

// "use client"를 사용하지 않는 tsx 파일 등에 사용할 별도 클라이언트
export const trpc = createTRPCClient<AppRouter>({
  links: [
    httpBatchLink({
      url: "/api",
      transformer: superjson,
      headers() {
        return {
          Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
        };
      },
      fetch: async (input, init?) => {
        const fetch = getFetch();
        return await fetch(input, {
          ...init,
          credentials: "include",
        });
      },
    }),
  ],
});

Query Client

React Query를 사용하기 위한 클라이언트는 일반적으로 사용하는 것과 크게 다르지 않다. 기본으로 설정해주어야 하는 부분들이 있다면 여기에 작성해주면 된다.

trpcClient.ts

import { QueryClient } from "@tanstack/react-query";

export const queryClient = new QueryClient({
  defaultOptions: { queries: { staleTime: 5 * 1000 } },
});

Provider

다음은 tRPC를 클라이언트 사이드에서 사용하기 위해 Provider 컴포넌트를 작성한다. 사실 이렇게 별도 컴포넌트를 만들 필요가 없을 수도 있겠지만, 보통 이런 라이브러리를 위해 적용해야 하는 Provider의 경우 일반적으로 layout.tsx에 적용되어야 한다. 하지만 layout 파일에서 metadata 등을 설정하려면 반드시 server component여야 하므로 이렇게 별도 컴포넌트로 작성하였다.

TRPCProvider.tsx

"use client";

import { queryClient } from "@/utils/queryClient";
import { trpcClientForProvider, trpcReact } from "@/utils/trpcClient";
import { QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ReactNode, useState } from "react";

export const TRPCProvider = ({ children }: { children: ReactNode }) => {
  const [trpcClient] = useState(trpcClientForProvider);
  return (
    // 아까 생성한 trpcClient와 queryClient를 Provider에 넣어주고 QueryClientProvider도 함께 감싸준다.
    <trpcReact.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {children}
        <ReactQueryDevtools />
      </QueryClientProvider>
    </trpcReact.Provider>
  );
};

layout.tsx

import { TRPCProvider } from "@/components/providers/TRPCProvider";
import { Metadata, Viewport } from "next";

export const metadata: Metadata = {...};

export default function Layout({ children }: { children: React.ReactNode }) {
  return <TRPCProvider>{children}</TRPCProvider>;
}

실제 사용 예시

위의 설정을 모두 마쳤다면 실제 컴포넌트나 페이지에서는 아래처럼 사용 가능하다. data에 들어오는 type도 알아서 지정해주고, 서버에서 작성한 route도 모두 객체 내의 메서드 형태로 작성할 수 있어 개발경험이 아주아주아주많이 개선되었다. 일일히 문자열 형태로 endpoint를 작성하거나 별도 API 함수를 사용하지 않아도 되고, intellisense도 아주 잘 작동하고 있어 개인적으로 아주개큰감동 하는 중.

page.tsx

"use client";

import { trpcReact } from "@/utils/trpcClient";

const Page = () => {
  const { data: user } = trpcReact.user.getUser.useQuery();
  return (
    <>
      <div key={user?.id}>
        <div>{user?.id}</div>
        <div>{user?.username}</div>
        <div>{user?.email}</div>
        <div>{user?.role}</div>
        <div>{user?.createdAt.toDateString()}</div>
      </div>
    </>
  );
};

export default Page;

다음 포스팅에서는 서버에서 어떤 식으로 실질적인 코드를 작성하는지, middleware 작성을 위해 겪었던 고초를 다뤄볼까 한다.


- 컬렉션 아티클






뭐가 됐든 뭐가 되어서 뭐든 합니다.