Next.js 에서 React-query를 사용하여 데이터를 prefetch 하고 사용하는 법에 대해 알아보자.
npm install @tanstack/react-query@4
// 또는 v3 버전은
npm install react-query
먼저 react-query의 사용법에 대해 알기 위해 가장 많이 쓰이는 useQuery, useMutation 훅 에 대해 알아보자.
GET
요청과 같은 Read 작업을 할 때 사용되는 Hook이다.import { useQuery } from "react-query";
// 주로 사용되는 3가지 return값 외에도 더 많은 return 값들이 있다.
const { data, isLoading, error } = useQuery(queryKey, queryFn, options)
QueryKey
를 기반으로 데이터 캐싱을 관리한다.// 문자열
useQuery('todos', ...)
// 배열
useQuery(['todos', '1'], ...)
const { data, isLoading, error } = useQuery(['todos', id], () => axios.get(`http://.../${id}
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]));
React-Query docs를 참고하면 더 많은 옵션들을 볼 수 있다.
enabled (boolean)
// id가 존재할 때만 쿼리 요청을 한다.
const { data } = useQuery(
['todos', id],
() => fetchTodoById(id),
{
enabled: !!id,
}
);
retry (boolean | number | (failureCount: number, error: TError) => boolean)
staleTime (number | Infinity)
cacheTime (number | Infinity)
refetchOnMount (boolean | "always")
refetchOnWindowFocus (boolean | "always")
refetchOnReconnect (boolean | "always")
onSuccess ((data: TDdata) => void)
onError ((error: TError) => void)
onSettled ((data?: TData, error?: TError) => void)
initialData (TData | () => TData)
React-Query docs를 참고하면 더 많은 반환값들을 볼 수 있다.
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>
)
}
const data = useMutation(API 호출 함수, 콜백);
React-Query docs를 참고하면 더 많은 옵션들을 볼 수 있다.
mutationFn (variables: TVariables) => Promise<TData>
onMutate: (variables: TVariables) => Promise<TContext | void> | TContext | void
optimistic update
사용 시 유용한 함수이다.onSuccess: (data: TData, variables: TVariables, context?: TContext) => Promise<unknown> | voi
onError: (err: TError, variables: TVariables, context?: TContext) => Promise<unknown> | void
onSettled: (data: TData, error: TError, variables: TVariables, context?: TContext) => Promise<unknown> | void
React-Query docs를 참고하면 더 많은 반환값들을 볼 수 있다.
mutate: (variables: TVariables, { onSuccess, onSettled, onError }) => void
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>
);
}
initialData
: 서버 컴포넌트에서 데이터 prefetch 및 클라이언트 컴포넌트로 initialData prop 를 전달하는 방법<Hydrate>
: 서버에서 쿼리를 prefetch 하고 캐시를 dehydrate 한 후, <Hydrate>
로 클라이언트에게 rehydrate 해주는 방법💬 Hydration?
서버 사이드에서 먼저 정적인 페이지(HTML)를 렌더링하고, JS 코드가 모두 로드되면 이 HTML에 이벤트 핸들러 등을 붙여 동적인 페이지를 만드는 과정을 hydration이라 말한다. hydration을 직역하면 '수분 공급'이라는 뜻인데, 즉 건조한 HTML에 수분(인터랙션, 이벤트 핸들러 등)을 공급하여 동적인 페이지를 만들어나가는 과정을 말한다.
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
를 넘겨줘야한다.dataUpdatedAt
이나 refetch가 필요한 시점에 대한 것을 결정한다.queryClient
에 dehydrate
하는 것을 지원한다.서버에서의 query 캐싱 지원 및 hydration 설정을 위해서는 먼저 app.tsx
에 설정이 필요하다.
QueryClient
instance를 생성한다.<QueryClientProvider>
로 감싸고 client instance에 넘겨준다.<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>
);
}
QueryClient
instance를 생성한다.prefetchQuery
메서드를 사용해 데이터를 prefetch해오고 완료되기까지 기다린다.dehydrate
메서드를 사용하고, dehydratedState
prop을 통해 이를 페이지에 넘겨준다.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 기본 편