코드노트

React-Query의 SSR 사용방법에 대한 고민 본문

Code note/codenote

React-Query의 SSR 사용방법에 대한 고민

코드노트 2023. 6. 5. 04:23

이번 프로젝트에서 Next.js를 사용하기로 했다.
 
Next를 사용하기로 한 이유는?

- 현재 프로젝트는 생각보다 규모가 크다. 페이지도 많다. 간편하게 사용할 수 있는 next 라우팅을 사용하기로 했다.
- SEO 최적화, 프론트엔드에서 중요하게 작용하는! 메타데이터를 쉽게 설정할 수 있고, 페이지별로 지정할 수 있기 때문에 안쓸이유가 없었다.
- 리액트세계에만 있었던 사람이라면 Next세계로 들어오는 순간 다양한 렌더링 방식에서도 감탄을 하게 되는거 같다. 초기에 긴 로딩시간이 해결되지 않았다면 정적 파일을 제공하여 사이트의 성능도 높이고 로딩 속도도 높일 수 있다.
- 프레임워크임에도 React 기반으로 어려움없이 사용할 수 있다. 그외 등등...

 
아무튼!
 
이번 글에 주제인 React-Query를 사용하기로 기술스택을 고민하며 확정했다.
여기서 문제가 발생했다.
생각지도 못한 Next.js 13에서의 React-Query SSR구현...
 
- 찾고 찾아도 나오지않는 레퍼런스들.. 13이라 그런지 아직 Next.js 13 + React-query의 내용들이 많이 없었다. 있었지만 이해부족..
- gpt chat 똥멍청이다

- 가장 좋은건 역시나 공식문서!

SSR | TanStack Query Docs

React Query supports two ways of prefetching data on the server and passing that to the queryClient. Prefetch the data yourself and pass it in as initialData

tanstack.com

- 강사님, 멘토님 공식문서를 보라는 이유가 있어... 아무리 찾아도 안나오는거 공식문서를 찾아보니 답이 있었다.
- 앞으로는 무적권 공식문서 -> 구글링 -> gpt선생님...
 
 

SSR

- React-Query는 서버에서 데이터를 미리 가져와 queryClient에 전달하는 2가지 방법을 지원한다.
 

props pre-fetch,

- 데이터를 직접 프리패치하며 초기값으로 전달한다.
- 쉽게 사용이 가능하지만 props drilling이 생긴다.
- 동일한 쿼리를 사용하여 여러 클라이언트 구성요소에 props 전달
- 페이지가 로드된 시점을 기준으로 리패치
 

hydrate pre-fetch

- 설정해야하는것들이 많다.
- props drilling없이 자식 컴포넌트에서 사용할 수 있다.
- 서버에서 프리패치된 시점을 기준으로 리패치


Hydrate

- next를 하면서도 알게되었지만 리액트쿼리에서도 사용된다.
- 서버 사이드 렌더링 시에 초기 데이터 상태를 클라이언트로 전달하여 클라이언트에서도 동일한 데이터를 사용할 수 있도록 해준다.
- 서버에서 직렬화된 상태를 전달받은 클라이언트에서 Hydrate 컴포넌트를 사용하여 해당 상태를 역직렬화하고 리액트쿼리의 queryClient를 초기화한다. 이를 통해서 클라이언트는 서버에서 초기 데이터를 사용할 수 있다.

* 리액트쿼리는 클라이언트 사이드 렌더링을 위한 라이브러리임을 잊지 말자.

 
- React-query 에서의 Hydrate 컴포넌트는 서버 사이드 렌더링과 클라이언트의 초기 데이터 전달을 효율적으로 관리하는데 사용!


props pre-fetch

"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { PropsWithChildren, useState } from "react";

export default function ReactQueryProvider({ children }: PropsWithChildren) {
  const [queryClient] = useState(() => new QueryClient());
  return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}

- QueryClient 구성요소로 트리를 감싸고 QueryClientProvider 인스턴스를 전달

// app/layout.jsx
import ReactQueryProvider from "./ReactQueryProvider";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <ReactQueryProvider>{children}</ReactQueryProvider>
      </body>
    </html>
  );
}

- RootLayout 컴포넌트에서 body안에 children을 감싸준다.
 

import Post from "./Post";
import { getPosts } from "./api/post";

export default async function Home() {
  const initialData = await getPosts();

  return (
    <>
      {/* @ts-expect-error Async Server Component */}
      <Post posts={initialData} />
    </>
  );
}

- initialData 초기값을 지정해준다. 
- 초기값을 props로 내려준다.
 

"use client";
import React from "react";
import { getPosts } from "./api/post";
import { useQuery } from "@tanstack/react-query";
import PostList, { Post } from "./PostList";

async function Post(props: PostProps) {
  const { data } = useQuery<Post[]>({
    queryKey: ["posts"],
    queryFn: getPosts,
    initialData: props.posts,
  });

  return (
    <div>
      <PostList data={data} />
    </div>
  );
}

export default Post;

- 상위에 컴포넌트에서 초기 데이터를 받아와 useQuery에서 초기값을 props로 지정해준다.
 

import React from "react";

export interface Post {
  id: number;
  title: string;
  body: string;
}

function PostList({ data }: { data: Post[] }) {
  console.log("나는 서버");

  return (
    <div>
      {data.map(post => (
        <div key={post.id}>
          <h1>{post.title}</h1>
          <p>{post.body}</p>
        </div>
      ))}
    </div>
  );
}

export default PostList;

 
 

- props drilling이 발생하며, console.log도 클라이언트에서 사용되는걸 볼 수 있다.
- 초기값을 서버에서 불러오고 그 뒤로는 클라이언트에서 데이터가 리패치된다.( staleTime 등을 사용하여 리패치 조정 가능 )
 
 


hydrate pre-fetch

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

const getQueryClient = cache(() => new QueryClient());
export default getQueryClient;

- 먼저 QueryClient 요청 범위 내에서 인스턴스를 만든다.
 

import React from "react";
import { getPosts } from "./api/post";
import { Hydrate, dehydrate } from "@tanstack/react-query";
import PostList, { Post } from "./PostList";
import getQueryClient from "./getQueryClient";


async function Post() {
  const queryClient = getQueryClient();
  await queryClient.prefetchQuery(["posts"], getPosts);
  const dehydratedState = dehydrate(queryClient);

  return (
    <Hydrate state={dehydratedState}>
      <PostList />
    </Hydrate>
  );
}

export default Post;

- getQuertClient함수를 통해 queryClient 인스턴스를 만든 후,
prefetchQuery로 미리 "posts" 쿼리키와 getPosts API를 요청하여 데이터를 불러온다.
- dehydrate함수를 통하여 queryClient를 직렬화 한다. (저장 가능한 형태로 변환하는 함수)
 

"use client";
import { useQuery } from "@tanstack/react-query";
import React from "react";
import { getPosts } from "./api/post";

function PostList() {
  const { data } = useQuery({
    queryKey: ["posts"],
    queryFn: getPosts,
    staleTime: 60000,
  });
  
  return (
    <div>
      {data?.map(post => (
        <div key={post.id}>
          <h1>{post.title}</h1>
          <p>{post.body}</p>
        </div>
      ))}
    </div>
  );
}

export default PostList;

- 그 후 하위 컴포넌트에서 useQuery를 통해 미리 불러온 데이터를 사용하기 때문에 SSR로 사용가능해진다. ( staleTime 등을 사용하여 리패치 조정 가능 




- props로 전달하지 않고 queryKey에 data가 저장되어있는걸 볼 수 있다.


이번 프로젝트에서 Next.js 13을 사용하면서 막히는부분들이 많이 있지만 새로운것들을 많이 알게되는거 같아서 재미있는거 같다.
이제 시작인데 이번 프로젝트를 끝날때쯤에는 조금 더 성장할 수 있기를...