๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
React/Nextjs

[Nextjs] react-query๋ฅผ ์ด์šฉํ•œ ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ Prefetch ๋ฐฉ๋ฒ•

by LasBe 2024. 9. 12.
๋ฐ˜์‘ํ˜•

๐Ÿ“’ react-query๋ฅผ ์ด์šฉํ•œ ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ Prefetch ๋ฐฉ๋ฒ•


Nextjs๋Š” ์„œ๋ฒ„์‚ฌ์ด๋“œ ๋ Œ๋”๋ง์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž์—๊ฒŒ ์™„์„ฑ๋œ HTML ํŽ˜์ด์ง€๋ฅผ ๊ฑด๋„ค์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ์™€ ๋”๋ถˆ์–ด ๋ฐ์ดํ„ฐ fetching์„ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์•„๋‹Œ ์„œ๋ฒ„์—์„œ ๋ฏธ๋ฆฌ ํ•œ ๋’ค, ๊ทธ ๊ฒฐ๊ณผ๋ฌผ์„ HTML์— ํฌํ•จ์‹œํ‚ฌ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ prefetching์ด ์ด๋ฃจ์–ด์งˆ ๊ฒฝ์šฐ ๊ฑฐ์น˜๋Š” ๋„คํŠธ์›Œํฌ ๋‹จ๊ณ„๊ฐ€ ์ ์–ด์ ธ ๋” ๋น ๋ฅด๊ณ , ์„œ๋ฒ„ ์•ˆ์—์„œ ํ˜ธ์ถœ-์‘๋‹ต์ด ์ด๋ฃจ์–ด์ง€๊ธฐ ๋•Œ๋ฌธ์— ๋” ์•ˆ์ „ํ•˜๋‹ค๋Š” ์ด์ ์„ ๋ˆ„๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿผ Nextjs์—์„œ react-query๋ฅผ ์ด์šฉํ•œ prefetch ๋ฐฉ๋ฒ•์„ ์†Œ๊ฐœํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

๐Ÿ“Œ QueryProvider ์„ธํŒ…

react query๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด QueryClientProvider๋ฅผ ์œ„ํ•œ ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์–ด์ค€ ๋’ค, layout.tsx์— ๊ฐ์•„์ค๋‹ˆ๋‹ค.

// components/Providers.tsx

"use client";

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

function makeQueryClient() {
  return new QueryClient();
}

let browserQueryClient: QueryClient | undefined = undefined;

function getQueryClient() {
  if (typeof window === "undefined") {
    return makeQueryClient();
  } else {
    if (!browserQueryClient) browserQueryClient = makeQueryClient();
    return browserQueryClient;
  }
}

export default function Providers({ children }: { children: React.ReactNode }) {
  const queryClient = getQueryClient();

  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
}
// app/layout.tsx

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

 

๋” ์ž์„ธํ•œ ์„ธํŒ… ๋ฐฉ๋ฒ•์€ ๋‹ค์Œ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•ด์ฃผ์„ธ์š”.

 

Server Rendering & Hydration | TanStack Query React Docs

This ad helps to keep us from burning out and rage-quitting OSS just *that* much more, so chill. ๐Ÿ˜‰

tanstack.com

 

๐Ÿ“Œ ํ˜ธ์ถœํ•  API Query ์ •๋ฆฌ

https://jsonplaceholder.typicode.com/

์ €๋Š” ์˜ˆ์ œ๋ฅผ ์œ„ํ•ด ์œ„์˜ mock api๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

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

type PostType = {
  userId: number;
  id: number;
  title: string;
  body: string;
};

export class PostService {
  static async getPost(id: number) {
    const response = await axios
      .get<PostType>(`https://jsonplaceholder.typicode.com/posts/${id}`)
      .then((data) => data);
    return response.data;
  }
  static async getPostList() {
    const response = await axios
      .get<PostType[]>(`https://jsonplaceholder.typicode.com/posts`)
      .then((data) => data);
    return response.data;
  }
}

export class PostQueryOption {
  static getPost(id: number) {
    return { queryKey: ["post", id], queryFn: () => PostService.getPost(id) };
  }
  static getPostList() {
    return { queryKey: ["post"], queryFn: () => PostService.getPostList() };
  }
}

export const usePost = (id: number) => {
  return useQuery(PostQueryOption.getPost(id));
};

export const usePostList = () => {
  return useQuery(PostQueryOption.getPostList());
};

Post๋ฅผ ๋‹จ์ผ๊ณผ ๋ฆฌ์ŠคํŠธ๋กœ ๋ถˆ๋Ÿฌ์˜ค๋Š” API๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

์–ด๋–ป๊ฒŒ ์‚ฌ์šฉ ๋ฐ ๋ถ„๋ฆฌํ•ด๋„ ์ƒ๊ด€์€ ์—†์ง€๋งŒ, ์ €๋Š” ํ•œ ํŒŒ์ผ์— ๊ณตํ†ต๋œ ๊ด€์‹ฌ์‚ฌ์— ๋Œ€ํ•œ API ํ˜ธ์ถœ ์ฝ”๋“œ ๋ฐ ์ฟผ๋ฆฌ๋ฅผ ์œ„์™€ ๊ฐ™์ด ์‚ฌ์ „ ์ •์˜ํ•ด ๋‘๊ณ  ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“Œ Prefetch ๊ณตํ†ต ์ฝ”๋“œ ๋ถ„๋ฆฌ

import { PostQueryOption } from "./usePost";
import { PostList } from "./PostList";
import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from "@tanstack/react-query";

export default async function Page() {
  const queryClient = new QueryClient();
  await queryClient.prefetchQuery(PostQueryOption.getPostList());
  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <PostList />
    </HydrationBoundary>
  );
}
// PostList.tsx

"use client";

import Link from "next/link";
import { usePostList } from "./usePost";

export const PostList = () => {
  const { data } = usePostList();
  return (
    <div>
      {!!data?.length &&
        data.map((item) => (
          <Link key={item.id} href={`/post/${item.id}`}>
            {item.title}
          </Link>
        ))}
    </div>
  );
};

์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ ์ธก์— ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ด๊ธฐ ์ „์— prefetch ํ•˜๊ธฐ ์œ„ํ•ด์„  ์œ„์—์„œ ์‚ฌ์šฉํ•œ 3๊ฐ€์ง€ ๊ธฐ๋Šฅ์— ๋Œ€ํ•ด ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

  1. queryClient.prefetchQuery
    ํŠน์ • ์ฟผ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฏธ๋ฆฌ ์„œ๋ฒ„์—์„œ ๋ถˆ๋Ÿฌ์™€์„œ ์บ์‹œ์— ์ €์žฅํ•˜๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.
    queryClient.prefetchQuery๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๋ฏธ๋ฆฌ ๊ฐ€์ ธ์˜ค๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.
  2. HydrationBoundary
    HydrationBoundary๋Š” ์„œ๋ฒ„์—์„œ ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ํด๋ผ์ด์–ธํŠธ์—์„œ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์ƒํƒœ๋ฅผ ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค.
  3. dehydrate
    ์„œ๋ฒ„์—์„œ ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ์ง๋ ฌํ™”ํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ๋กœ ์ „๋‹ฌํ•˜๊ณ , ํด๋ผ์ด์–ธํŠธ๊ฐ€ ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค๋‹ˆ๋‹ค.

์œ„ ๊ธฐ๋Šฅ๋“ค์˜ ๋™์ž‘ ๋ฐฉ์‹์„ ์š”์•ฝํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  1. ๋ฐ์ดํ„ฐ ๋ฏธ๋ฆฌ ํŒจ์นญ
    queryClient.prefetchQuery๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์„œ๋ฒ„์—์„œ ๊ฒŒ์‹œ๋ฌผ ๋ฆฌ์ŠคํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฏธ๋ฆฌ ๋ถˆ๋Ÿฌ์™€ ์บ์‹œ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
  2. ๋ฐ์ดํ„ฐ ์ง๋ ฌํ™”
    dehydrate(queryClient)๋ฅผ ํ†ตํ•ด ์„œ๋ฒ„์—์„œ ๋ฐ›์•„์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ์ง๋ ฌํ™”ํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ๋กœ ์ „๋‹ฌํ•  ์ค€๋น„๋ฅผ ํ•ฉ๋‹ˆ๋‹ค.
  3. ํ•˜์ด๋“œ๋ ˆ์ด์…˜
    ํด๋ผ์ด์–ธํŠธ์—์„œ๋Š” HydrationBoundary๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์„œ๋ฒ„์—์„œ ๋ฏธ๋ฆฌ ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ์žฌ์‚ฌ์šฉํ•˜์—ฌ ๋ถˆํ•„์š”ํ•œ ์ถ”๊ฐ€ ๋ฐ์ดํ„ฐ ํŒจ์นญ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ์œ„ ๋ฐฉ์‹๋Œ€๋กœ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด prefetch๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ์ค‘๋ณต ์ฝ”๋“œ๊ฐ€ ๋ฐœ์ƒํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ˜•ํƒœ์˜ ์ปดํฌ๋„ŒํŠธ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

import {
  dehydrate,
  FetchQueryOptions,
  HydrationBoundary,
  QueryClient,
} from "@tanstack/react-query";

type PrefetchBoundaryType = {
  children: React.ReactElement;
  options: FetchQueryOptions;
};

export default async function PrefetchBoundary({
  children,
  options,
}: PrefetchBoundaryType) {
  const queryClient = new QueryClient();
  await queryClient.prefetchQuery(options);
  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      {children}
    </HydrationBoundary>
  );
}
// After

import { PostQueryOption } from "./usePost";
import { PostList } from "./PostList";
import PrefetchBoundary from "./PrefetchBoundary";

export default async function Page() {
  return (
    <PrefetchBoundary options={PostQueryOption.getPostList()}>
      <PostList />
    </PrefetchBoundary>
  );
}

์—ฌ๊ธฐ์„œ ์ฃผ์˜ํ•  ์ ์€ prefetch ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ์— ๋ฐ˜๋“œ์‹œ async ํ‚ค์›Œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ ํŒจ์นญ์„ ์ˆ˜ํ–‰ํ•œ ํ›„์— ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋ง ํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ prefetch๋ฅผ ์‚ฌ์šฉํ•ด ๋กœ๋”ฉ ์‹œ๊ฐ„์„ ๋‹จ์ถ•ํ•˜๊ณ , ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๋” ๋‚˜์•„์ง€๊ฒŒ ์‹œ๋„ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

์ „์ฒด ์ฝ”๋“œ๋Š” ๊นƒํ—ˆ๋ธŒ์—์„œ ์ฐธ๊ณ  ๋ฐ”๋ž๋‹ˆ๋‹ค!

https://github.com/devlasbe/nextjs-playground/tree/main/app/2-prefetch

 

nextjs-playground/app/2-prefetch at main · LasBe-code/nextjs-playground

Nextjs ํ˜ธ๊ธฐ์‹ฌ ์ฒœ๊ตญ. Contribute to LasBe-code/nextjs-playground development by creating an account on GitHub.

github.com

 

๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€


์˜คํ”ˆ ์ฑ„ํŒ