이번 시간에는 Next.js 13에서 React Query SSR 을 적용하는 방법에 대해 알아보도록 하겠습니다. 아. 이왕 하는김에 React Query가 무엇이고 왜 사용하는지 간단하게 알아보고 넘어가도록 하겠습니다.
React Query는 정말 유명한 라이브러리 입니다. 여러 테크 블로그에서 Redux같은 전역 상태 도구를 버리고 React Query로 전환했다는 글이 있을 정도니까요. 뭐... 그만큼 React Query는 이미 많은 곳에서 사용 중이라는 것입니다.
“Powerful asynchronous state management for TS/JS, React, Solid, Vue and Svelte” - 참고
TS/JS, React, Solid, Vue, Svelte를 위한 강력한 비동기 상태 관리 도구 라고 합니다.
“Toss out that granular state management, manual refetching and endless bowls of async-spaghetti code. TanStack Query gives you declarative, always-up-to-date auto-managed queries and mutations that directly improve both your developer and user experiences.”
해석) 세분화된 상태관리, 수동 refetching, 끝없는 비동기 스파게티 코드들을 버려라. React Query는 개발자 및 사용자 경험을 직접 개선하는 선언적(declarative)이고 항상 최신의 자동 관리 queries와 mutations를 제공합니다.
참고 : https://tanstack.com/query/latest/docs/react/overview
“TanStack Query (FKA React Query) is often described as the missing data-fetching library for web applications, but in more technical terms, it makes fetching, caching, synchronizing and updating server state in your web applications a breeze.”
해석) React Query를 흔히 누락된(?) 데이터 fetching 라이브러리로 설명되지만, 더 전문적인 용어로는 웹 애플리케이션의 서버 상태 가져오기, 캐싱, 동기화 및 업데이트하는 작업을 간편하게 수행할 수 있습니다.
대부분의 핵심 프레임워크는 데이터를 fetching하거나 updating하는 독점적인 방법을 제공하고 있지 않습니다. 이러한 이유로 개발자들은 캡슐화해서 구축하거나 자체적인 데이터 fetching 방법을 개발합니다. 이는 일반적으로 컴포넌트 기반 상태와 사이드 이펙트를 함께 사용하거나, 앱 전체에 비동기 데이터를 저장하고 제공하기 위해 보다 범용적인 상태 관리 라이브러리를 사용하는 것을 의미합니다. (Redux나 Recoil 같은 상태 도구를 말하는건가?)
대부분의 상태 관리 라이브러리는 클라이언트 상태를 사용하는데 유용하지만 비동기 또는 서버 상태를 사용하는데는 그다지 적합하지 않습니다. 서버 상태가 완전히 다르기 때문입니다. (음… 일리 있어… 🧐)
우선 서버 상태는 다음은 특징이 있습니다.
애플리케이션에서 서버 상태의 특성을 파악하면 다음과 같은 문제가 발생합니다. 도전에 직면하게 됩니다.
만약 당신이 이 목록에 압도되지 않았다면, 그것은 당신이 이미 모든 서버 상태 문제를 해결했다는 것을 의미하고 상을 받을 자격이 있다는 것을 의미합니다. 하지만, 만약 여러분이 대다수의 사람들과 같다면, 여러분은 아직 이러한 도전들의 전부 또는 대부분을 해결하지 못했고 우리는 단지 표면을 긁고 있을 뿐입니다!
React Query는 서버 상태를 관리하기 위한 최고의 라이브러리 중 하나입니다. 제로 구성으로 즉시 사용할 수 있으며, 애플리케이션 성장에 따라 원하는 대로 사용자 지정할 수 있습니다. 리액트 쿼리를 사용하면 서버 상태의 까다로운 문제와 장애물을 극복하고 당신이 앱 데이터를 제어하기 전에 제어할 수 있습니다.
좀 더 기술적인 측면에서, React Query는 다음과 같은 기능을 제공합니다:
💡 결국 React Query는 서버 상태 동기화를 위한 라이브러리라고 보면 됩니다. 기존 전역상태 관리 라이브러리는 전부 클라이언트 위주이고, React Query는 전역상태 라이브러리 필요 없이 서버 상태를 동기화해주는 거.
참고 : https://tanstack.com/query/latest/docs/react/quick-start
React Query에서 가장 중요한 3가지 부분에 대해 예시를 들고 있습니다.
import {
useQuery,
useMutation,
useQueryClient,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
import { getTodos, postTodo } from '../my-api'
// Create a client
const queryClient = new QueryClient()
function App() {
return (
// Provide the client to your App
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
)
}
function Todos() {
// Access the client
const queryClient = useQueryClient()
// Queries
const query = useQuery({ queryKey: ['todos'], queryFn: getTodos })
// Mutations
const mutation = useMutation({
mutationFn: postTodo,
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
return (
<div>
<ul>
{query.data?.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
<button
onClick={() => {
mutation.mutate({
id: Date.now(),
title: 'Do Laundry',
})
}}
>
Add Todo
</button>
</div>
)
}
render(<App />, document.getElementById('root'))
이미지 출처 : [LIVE] React Query와 상태관리 :: 2월 우아한테크세미나
참고 : https://tanstack.com/query/latest/docs/react/guides/important-defaults
React Query 를 제대로 이해하기 위해서는 staleTime, cacheTime에 대해 알아야 합니다.
@tanstack/react-query-devtools 를 설치해보면 아래처럼 상태 변화를 볼 수 있습니다.
그렇다면 staleTime을 5분으로 설정해보면 어떻게 될까요? 오. refetch 되지 않습니다. Last Updated도 그대로 인 것을 알 수 있습니다. 이런식으로 캐시 옵션을 조정할 수 있습니다. 다만 무리하게 캐시를 사용하게 되면 사용자가 최신 데이터를 못 볼 수도 있으니 적절한 조치를 취해야 할 것입니다.
그 외에도 아래 3가지 옵션에 대해 알아놓으면 좋습니다. 3개 옵션 전부 기본값은 true 입니다. stale 되었을 때 해당 이벤트가 발생한다면 refetch 하게 됩니다.
해당 쿼리 옵션은 전역에서 설정할 수 있고, 개별 쿼리에서도 설정이 가능합니다. 저는 보통 전역으로 refetchOnWindowFocus는 false로 해놓고 시작합니다. 저걸 true로 사용하다보면 너무 많이 요청되는거 같아서 그렇습니다.
그 외 retry 및 retryDelay 옵션도 있습니다. 리액트 쿼리가 실패할 때 기본적으로 3번은 다시 요청이 일어나게 되는데 앞선 retry 기본값이 3이기 때문입니다.
참고 : https://tanstack.com/query/v4/docs/react/guides/ssr#using-the-app-directory-in-nextjs-13
React Query 공식 문서를 참고해보면 next.js 13에서 어떻게 사용하라고 나온 가이드가 있습니다. 이대로 한번 진행해보도록 하겠습니다.
initialData
또는 <Hydrate>
를 사용하는 두 가지 prefetching 방식 모두 app Directory 내에서 사용할 수 있습니다.
initialData
: 서버 컴포넌트에서 데이터 prefetch 및 클라이언트 컴포넌트로 initialData prop 를 전달하는 방법
<Hydrate>
: 서버에서 쿼리를 prefetch 하고 캐시를 dehydrate 한 후, <Hydrate>
로 클라이언트에게 rehydrate 해줍니다.
initialData
에 비해 더 많은 설정이 요구됩니다.저는 이 중에서 Hydrate 방식을 사용하도록 하겠습니다. 보통 권장하기로는 <Hydrate>
를 사용하는게 일반적인거 같습니다.
react-query 패키지에서 제공하는 hooks는 context에서 queryClient를 검색해야 합니다. 컴포넌트 트리를 로 QueryClientProvider로 래핑하고 QueryClient 인스턴스를 전달합니다.
React Hooks는 Next.js 클라이언트 컴포넌트에서만 사용 가능하니 "use client;"를 맨 앞에 적어줍니다.
// Provider.tsx
"use client";
import React from "react";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
type Props = {
children: React.ReactNode;
};
function Providers({ children }: Props) {
const [client] = React.useState(
new QueryClient({
defaultOptions: { // react-query 전역 설정
queries: {
refetchOnWindowFocus: false,
retry: false,
},
},
})
);
return (
<QueryClientProvider client={client}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
export default Providers;
그리고 나서 Providers 로 래핑해주면 react-query 기본 설정이 다 되었습니다.
// layout.tsx
import Providers from "@/components/Providers";
import "./globals.css";
export const metadata = {
title: "React Query SSR",
description: "React Query SSR 실습",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
<Providers>{children}</Providers>
</body>
</html>
);
}
<Hydrate>
QueryClient의 request-scoped 싱글톤 인스턴스를 만듭니다. 이렇게 하면 서로 다른 사용자 요청 간에 데이터가 공유되지 않고 요청당 한 번만 쿼리 클라이언트를 만들 수 있습니다.
// app/getQueryClient.jsx
import { QueryClient } from '@tanstack/react-query'
import { cache } from 'react'
const getQueryClient = cache(() => new QueryClient())
export default getQueryClient
prefetched queries를 사용하는 클라이언트 컴포넌트보다 component tree의 높은 위치에 있는 서버 컴포넌트에서 데이터를 가져옵니다.
dehydrate
를 사용하여 쿼리 캐시로부터 프리페치된 쿼리의 dehydrate 된 상태를 가져옵니다. 🤔<Hydrate>
을 사용할 수 있습니다.// app/posts/hydratedPosts.jsx
import { dehydrate, Hydrate } from '@tanstack/react-query'
import getQueryClient from './getQueryClient'
export default async function HydratedPosts() {
const queryClient = getQueryClient()
await queryClient.prefetchQuery(['posts'], getPosts)
const dehydratedState = dehydrate(queryClient)
return (
<Hydrate state={dehydratedState}>
<Posts />
</Hydrate>
)
}
참고: TypeScript는 현재 비동기 서버 컴포넌트 요소를 사용할 때 타입 오류가 발생합니다. 임시 해결 방법으로 다른 컴포넌트 내부에서 이 컴포넌트를 호출할 때 {/* @ts-expect-error Server Component */}
을(를) 사용하십시오.
간단하게 jsonplaceholder API를 사용해서 post를 호출해서 사용해보겠습니다.
// app/posts/getPosts.ts
export interface Post {
userId: number;
id: number;
title: string;
body: string;
}
export async function getPosts() {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
const posts = (await response.json()) as Post[];
return posts;
}
서버 렌더링(SSR) 중에 <Hydrate>
내 클라이언트 컴포넌트에서 nested useQuery 호출은 state property에서 제공되는 prefetch 된 데이터에 액세스할 수 있습니다.
밑에서는 비교를 위해 하나는 HydratedPosts에서 사용한 querykey와 동일한 키를 가진 useQuery를 사용했고, 또 다른 하나는 동일하지 않은 키를 가진 useQuery를 사용했습니다.
// app/posts/posts.tsx
"use client";
import { useQuery } from "@tanstack/react-query";
import { getPosts } from "./getPosts";
export default function Posts() {
// This useQuery could just as well happen in some deeper child to
// the "HydratedPosts"-component, data will be available immediately either way
const { data } = useQuery({ queryKey: ["posts"], queryFn: getPosts });
// This query was not prefetched on the server and will not start
// fetching until on the client, both patterns are fine to mix
const { data: otherData } = useQuery({
queryKey: ["posts-not-ssr"],
queryFn: getPosts,
});
return (
<div className="flex">
<div>
<h2 className="text-lg text-center font-bold mb-6">
React Query SSR 적용
</h2>
<ul>
{data?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
<div>
<h2 className="text-lg text-center font-bold mb-6">
React Query SSR 적용 안함
</h2>
<ul>
{otherData?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
</div>
);
}
결과를 보면 왼쪽이 제대로 SSR이 적용된 것을 알 수 있고, 오른쪽은 SSR이 적용이 안된 CSR 방식으로 처리된 것을 알 수 있습니다. 이는 위에 코드 주석에도 나와있는 것처럼 서버에서 prefetch를 해서 dehydrate한 다음, 클라이언트에게 rehydrate를 해주는지 안해주는지 차이에서 비롯됩니다.
그렇다면 뭐가 더 좋을까요? 만약 해당 데이터가 검색 엔진 최적화(SEO)에 중요하거나 사용자에게 초반에 미리 보이는게 좋다면 SSR을 적용하면 될 거 같습니다. 하지만 서버에서 prefetch 하는 시간이 오래걸리게 되면 그만큼 페이지에서는 아무것도 보여줄 수 없을 것입니다. 따라서 오래 걸리는 작업은 차라리 오른쪽 처럼 CSR 방식대로 사용하는게 나아보입니다. 그래야 로딩 중이라는 표시라도 해줄 수 있기 때문이죠.
이번 시간에는 React Query란 무엇이고 왜 사용하는지 배워봤습니다. 그리고 간단 사용법과 Next.js 13 버전에서 React Query SSR 적용하는 방법도 알아봤습니다. 처음에 봤을때 prefetch, dehydrate 이런 개념을 잘 몰라서 어려웠던 경험이 있었는데 그래도 이제는 어느정도 이해가 되는거 같습니다. 가면 갈수록 어려워지는 프론트엔드... 😂
감사합니다 너무 도움이 됐어요 🤩