[Next.js] React-Query 사용하기 (with SSR/SSG)

문지은·2023년 10월 31일
2

Next.js - Page Router

목록 보기
10/11
post-thumbnail

React Query

  • 데이터 Fetching, Caching, 동기화, 서버 데이터 업데이트 등을 쉽게 만들어 주는 라이브러리
  • 자동 캐싱 및 동기화 기능으로 데이터를 여러 컴포넌트가 의존하는 경우 여러번의 요청을 방지하고 동시에 데이터를 가져올 수 있도록 한다.
  • 페이지네이션 및 무한 스크롤과 같은 기능을 쉽게 구현할 수 있도록 내장된 기능을 제공한다.
  • 서버 측 렌더링과 하이드레이션을 지원하여 앱이 서버에서 렌더링되는 환경에서도 원활하게 작동하도록 한다.

Next.js 에서 React-query를 사용하여 데이터를 prefetch 하고 사용하는 법에 대해 알아보자.

설치

  • SSR & Next.js 환경에서 사용하기 위해서는 react-query v4 이하의 버전을 설치해야 한다. (2023.11 기준)
npm install @tanstack/react-query@4

// 또는 v3 버전은
npm install react-query
  • devtools 설치 시 의존성 문제가 생겨서 나는 v3을 사용하였다.

먼저 react-query의 사용법에 대해 알기 위해 가장 많이 쓰이는 useQuery, useMutation 훅 에 대해 알아보자.

useQuery

  • GET 요청과 같은 Read 작업을 할 때 사용되는 Hook이다.
  • 첫번째 파라미터로 QueryKey가 들어가고 두번째 파라미터로 비동기 함수(api호출 함수)가 들어간다.
  • return값은 api의 성공여부, 실패여부, api return값을 포함한 객체이다.
  • useQuery는 비동기로 작동한다. 즉, 한 컴포넌트에 여러 개의 useQuery가 있다면 하나가 끝나고 다음 useQuery가 실행되는 것이 아닌 두개의 useQuery가 동시에 실행된다. 그러므로 여러개의 비동기 query가 있다면 useQuery보다는 useQueries를 사용하는 것이 좋다.
  • 혹은 Query Options에서 enabled를 사용하면 useQuery를 동기적으로 사용할 수 있다.
import { useQuery } from "react-query";

// 주로 사용되는 3가지 return값 외에도 더 많은 return 값들이 있다.
const { data, isLoading, error } = useQuery(queryKey, queryFn, options)

Query Key

  • QueryKey를 기반으로 데이터 캐싱을 관리한다.
    • 다른 컴포넌트에서도 해당 키를 사용하면 호출 가능하다.
  • 쿼리키는 문자열 또는 배열로 지정할 수 있다.
  • 쿼리키를 배열로 지정하면 배열의 0번 값은 string으로 다른 컴포넌트에서 부를 키를 넣어주면 되고 1번째 값은 query함수의 파라미터로 전달될 값을 넣어주면 된다.
// 문자열
useQuery('todos', ...)

// 배열
useQuery(['todos', '1'], ...)
  • 쿼리가 변수에 의존하는 경우에는 Query Key에도 해당 변수를 추가해주어야 한다.
const { data, isLoading, error } = useQuery(['todos', id], () => axios.get(`http://.../${id}

Query Functions

  • useQuery의 두번째 인자에는 promise를 반환하는 함수를 넣어주어야 한다.
  • 다음의 방식들로 작성할 수 있다.
useQuery('todos', fetchTodos);

useQuery(['todos', todoId], () => fetchTodoById(todoId));

useQuery(['todos', todoId], async () => {
   const data = await fetchTodoById(todoId);
   return data
 });

useQuery(['todos', todoId], ({ queryKey }) => fetchTodoById(queryKey[1]));

Options

React-Query docs를 참고하면 더 많은 옵션들을 볼 수 있다.

enabled (boolean)

  • 쿼리가 자동으로 실행되지 않게 설정하는 옵션이다.
  • enabled: true이면 특정 조건이 trued 때만 실행
// id가 존재할 때만 쿼리 요청을 한다.
const { data } = useQuery(
  ['todos', id],
  () => fetchTodoById(id),
  {
    enabled: !!id,
  }
);

retry (boolean | number | (failureCount: number, error: TError) => boolean)

  • default : 3회
  • 실패한 쿼리를 재시도하는 옵션이다.
  • true 로 설정하면 쿼리 실패시 무한 재시도하고 false 로 설정하면 재시도를 하지 않는다.

staleTime (number | Infinity)

  • default : 0
  • 해당 시간이 지나면 stale 상태가 된다.
  • 데이터가 fresh 상태로 유지되는 (stale 상태가 되기까지의) 시간이다.fresh 상태에서는 다시 mount 되어도 fetch가 실행되지 않는다.

cacheTime (number | Infinity)

  • default : 5분
  • inactive 상태인 캐시 데이터가 메모리에 남아있는 시간이다. 이 시간이 지나면 캐시 데이터는 가비지 컬렉터에 의해 메모리에서 제거된다.

refetchOnMount (boolean | "always")

  • default : true
  • 데이터가 stale 상태일 경우 마운트 시 마다 refetch를 실행하는 옵션이다.
  • always 로 설정하면 마운트 시 마다 매번 refetch 를 실행한다.

refetchOnWindowFocus (boolean | "always")

  • default : true
  • 데이터가 stale 상태일 경우 윈도우 포커싱 될 때 마다 refetch를 실행하는 옵션이다.
  • always 로 설정하면 항상 윈도우 포커싱 될 때 마다 refetch를 실행한다는 의미이다.

refetchOnReconnect (boolean | "always")

  • default : true
  • 데이터가 stale 상태일 경우 재 연결될 때 refetch를 실행하는 옵션이다.
  • always 도 위에 두 옵션 처럼 쿼리가 매번 재 연결될 때 마다 refetch를 실행한다.

onSuccess ((data: TDdata) => void)

  • 쿼리 성공 시 실행되는 함수이다.
  • 매개변수 data는 성공 시 서버에서 넘어오는 response 값이다.

onError ((error: TError) => void)

  • 쿼리 실패 시 실행되는 함수이다.
  • 매개변수로 에러 값을 받을 수 있다.

onSettled ((data?: TData, error?: TError) => void)

  • 쿼리가 성공하면 성공한 데이터가 전달되거나, 실패하면 에러가 전달 될 때 실행되는 함수이다.
  • 매개변수로 성공 시엔 성공 데이터, 실패 시에는 에러가 전달된다.

initialData (TData | () => TData)

  • initialData 를 설정하면 쿼리 캐시의 초기 데이터로 사용된다. (쿼리가 아직 생성되지 않았거나 캐시되지 않았을 때)
  • staleTime 이 설정되지 않은 경우 초기 데이터는 기본적으로 stale 상태로 간주한다.

Returns

React-Query docs를 참고하면 더 많은 반환값들을 볼 수 있다.

  • status
    • idle: 초기 상태
    • loading: 데이터 fetching 중일 때 상태. (isFetching === true)
    • error: 데이터 fetch에 실패한 상태.
    • success: 데이터 fetch에 성공한 상태.
  • isIdle
  • isLoading
  • isFetching
  • isSuccess
  • isError
  • isStale
  • data: 응답받은 데이터
  • error: 실패 정보
  • refetch: 수동으로 데이터 refetch를 실행하는 함수. stale이나 cache같은 설정들이 무시되고 무조건 다시 데이터를 fetching한다.

사용 예시

function Todos() {
 const { isLoading, isError, data, error } = useQuery('todos', fetchTodoList)
  
  if (isLoading) {
    return <span>Loading...</span>
  }
 
  if (isError) {
    return <span>Error: {error.message}</span>
  }
 
  // We can assume by this point that `isSuccess === true`
  return (
    <ul>
      {data.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  )
 }

useMutation

  • 데이터 변경 및 삭제 하는 메서드
  • POST, PUT, DELETE와 같은 변경 및 수정 작업을 할 때 사용되는 훅이다.
const data = useMutation(API 호출 함수, 콜백);

Options

React-Query docs를 참고하면 더 많은 옵션들을 볼 수 있다.

mutationFn (variables: TVariables) => Promise<TData>

  • Required
  • 비동기 작업을 수행하고 프로미스를 반환하는 함수이다. (쉽게 말해 api 요청하는 함수)
  • variables 는 mutate가 전달하는 객체이다.

onMutate: (variables: TVariables) => Promise<TContext | void> | TContext | void

  • onMutate 는 mutation 함수가 실행되기 전에 실행되고 mutation 함수가 받을 동일한 변수가 전달된다.
  • optimistic update 사용 시 유용한 함수이다.

onSuccess: (data: TData, variables: TVariables, context?: TContext) => Promise<unknown> | voi

  • onSuccess 는 mutation 이 성공하고 결과를 전달할 때 실행된다.

onError: (err: TError, variables: TVariables, context?: TContext) => Promise<unknown> | void

  • onError 는 mutation 이 error 를 만났을 때 실행된다.

onSettled: (data: TData, error: TError, variables: TVariables, context?: TContext) => Promise<unknown> | void

  • onSettled 는 mutation 이 성공해서 성공한 데이터 또는 error가 전달될 때 실행된다. (성공하든 실패하든 아무튼 결과가 전달된다)

Returns

React-Query docs를 참고하면 더 많은 반환값들을 볼 수 있다.

mutate: (variables: TVariables, { onSuccess, onSettled, onError }) => void

  • mutate 를 호출해서 mutation 을 실행시킬 수 있다.
  • variables 는 mutationFn 에 전달하는 객체이다.
  • onSuccess, onSettled, onError 는 useMutation option에 설명한 것과 동일하다.

사용 예시

import { useMutation } from "react-query";
import axios from 'axios';

interface TodoType {
  id: number;
  todo: string;
}

const addTodo = async (newTodo: TodoType): Promise<TodoType> => {
  const { data } = await axios.post<TodoType>(`/todos`, newTodo);
  return data;
};

const { mutate, isLoading, isError, error, isSuccess } = useMutation(addTodo);

export default function App() {
  return (
    <div>
      {
        isLoading ? (
          'Adding todo...'
        ) : (
        <>
          {isError && <p>error: {error.message}</p>}
            
          {isSuccess && <p>Todo added!</p>}
            
          <button
            onClick={() => {
              mutate({ id: 1, todo: 'useMutation 블로그 작성하기' })
            }}
          >
               작성 완료
          </button>
        </>
        )
      }
    </div>
  );
}

Using Next.js

  • React Query는 SSR(또는 SSG) 환경에서 다음 두 가지 Prefetching 방식을 제공한다.
    • initialData : 서버 컴포넌트에서 데이터 prefetch 및 클라이언트 컴포넌트로 initialData prop 를 전달하는 방법
    • <Hydrate> : 서버에서 쿼리를 prefetch 하고 캐시를 dehydrate 한 후, <Hydrate> 로 클라이언트에게 rehydrate 해주는 방법

💬 Hydration?
서버 사이드에서 먼저 정적인 페이지(HTML)를 렌더링하고, JS 코드가 모두 로드되면 이 HTML에 이벤트 핸들러 등을 붙여 동적인 페이지를 만드는 과정을 hydration이라 말한다. hydration을 직역하면 '수분 공급'이라는 뜻인데, 즉 건조한 HTML에 수분(인터랙션, 이벤트 핸들러 등)을 공급하여 동적인 페이지를 만들어나가는 과정을 말한다.

Using initialData

  • Next.js의 getStaticProps나 getSererSideProps 함수를 통해 fetch한 데이터를 useQuery의 initialData 옵션을 통해서도 넘겨줄 수 있다.
export async function getStaticProps() {
	const posts = await getPosts()  
	return { props: { posts } }
}

function Posts(props) {  
	const { data } = useQuery(['posts'], getPosts, { initialData: props.posts })
  // ...
}

initialData를 활용하는 방식은 설정할 것이 적고 몇몇 케이스에서는 가장 빠른 솔루션일 수 있겠지만, 전체 접근 방식과 비교했을 때 몇 가지 트레이드오프가 존재한다.

  • 만일 useQuery를 컴포넌트 트리 깊숙이 존재하는 컴포넌트에서 호출한다면, initialData를 그 지점까지 넘겨줘야 한다.
  • 만일 useQuery를 같은 query로 여러 위치에서 호출한다면, 호출한 지점 모두에 initialData를 넘겨줘야한다.
  • 해당 query가 서버로부터 fetch된 정확한 시점을 알 수 있는 방법이 없어서, 페이지가 로드된 시점을 기반으로 dataUpdatedAt이나 refetch가 필요한 시점에 대한 것을 결정한다.

Using Hydration

  • React Query는 Next.js 서버에서 여러 개의 query를 prefetch하고 그 query들을 queryClientdehydrate하는 것을 지원한다.
  • 즉, 서버는 페이지 로드 시 즉시 사용할 수 있는 마크업을 미리 렌더링할 수 있으며, JS를 사용할 수 있게 되면 React Query는 라이브러리 자체의 기능으로 이러한 query들을 업그레이드하거나 hydrate할 수 있다.
  • 이 기능 중에는 query들이 서버에서 렌더링된 이후로 클라이언트에서 stale한 상태가 되었을 때 refetch해오는 것도 포함된다.
  • 나는 프로젝트에 Hydration 방식을 사용하였다.

app.tsx 설정하기

서버에서의 query 캐싱 지원 및 hydration 설정을 위해서는 먼저 app.tsx 에 설정이 필요하다.

  • app, instance ref (또는 React 상태) 내에 새로운 QueryClient instance를 생성한다.
    • 이렇게 하면 컴포넌트 라이프사이클 당 QueryClient를 오직 한 번만 생성하여 데이터가 서로 다른 사용자와 요청 간에 공유되지 않는다.
  • app 컴포넌트를 <QueryClientProvider>로 감싸고 client instance에 넘겨준다.
  • app 컴포넌트를 <Hydrate> 로 감싸고 pageProps의 dehydratedState prop을 넘겨준다.
  • ReactQueryDevtools 태그를 넣으면 개발모드로 프로젝트 실행 시 Data Fetching State 를 쉽게 확인할 수 있다. (꽃모양 아이콘 생김)
import { Hydrate, QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';

export default function App({ Component, pageProps }: AppProps) {
  const [queryClient] = useState(() => new QueryClient());
  return (
    <QueryClientProvider client={queryClient}>
      <Hydrate state={pageProps.dehydratedState}>
            <Component {...pageProps} />
      </Hydrate>
      <ReactQueryDevtools />
    </QueryClientProvider>
  );
}

SSR/SSG 페이지에서 데이터 prefetch 하기

  • 각 페이지의 request 별로 새로운 QueryClient instance를 생성한다.
    • 이렇게 하면 서로 다른 사용자와 요청 간에 데이터가 공유되지 않는다.
  • client의 prefetchQuery 메서드를 사용해 데이터를 prefetch해오고 완료되기까지 기다린다.
  • query cache를 dehydrate하기 위해 dehydrate 메서드를 사용하고, dehydratedState prop을 통해 이를 페이지에 넘겨준다.
  • 페이지 컴포넌트에서 useQuery를 사용하여 prefetch 된 데이터를 사용할 수 있다.
    • prefetch 시 작성한 key 값과 동일한 key를 작성해 주어야 한다.
import { QueryClient, useQuery } from 'react-query';
import { dehydrate } from 'react-query/hydration';

export async function getStaticProps() {
	const queryClient = new QueryClient()
	await queryClient.prefetchQuery('posts', getPosts)
	return {
		props: {
			dehydratedState: dehydrate(queryClient),
		},
	}
}

function Posts() {
	// 이 useQuery는 "Posts" 페이지에 대한 더 깊은 자식 요소에서 사용될 수도 있으며, data는 어느 쪽에서 사용되든 즉시 사용할 수 있다.
	const { data } = useQuery(['posts'], getPosts)

	// 이 query는 서버에서 prefetch된 것이 아니며 클라이언트에서 시작할 때까지 fetch하지 않는다. 
	// 두 가지 패턴(서버에서 prefetch, 클라이언트에서 fetch)은 혼합될 수 있다.
	const { data: otherData } = useQuery(['posts-2'], getPosts)
	
	// ...
}

References

SSR | TanStack Query Docs
[한글 번역] React Query - SSR (Using Next.js)
[React Query] react-query 시작하기
Next.js 13에서 React Query SSR 적용하는 방법
[React Query] 리액트 쿼리 useMutation 기본 편

profile
코드로 꿈을 펼치는 개발자의 이야기, 노력과 열정이 가득한 곳 🌈

0개의 댓글