'NextJS에서 별도의 상태관리가 필요한 가?'에 대한 고민이 계속 됐다.
그러던 중 회사 프로젝트에서 유지보수 해야할 업무(외주에서 진행하던)를 맡았고, 코드를 보는데 NextJS에서 React-query를 사용하고 있었다.
그동안 상태관리는 Redux, Recoil, jotai 정도만 접해봤고, react-query를 써볼 기회가 없었다(취업한 후에는 내 의지가 아니라고 할 수 있다. 하지만 사이드 프로젝트에서도 안써본 거는 내 의지 이긴하지만 그냥 내탓이 아니었으면 좋겠다는 변명....)
어쨌든 해당 업무 말고 원래 진행중이던 NextJS 프로젝트에서도 React-query를 사용해볼 겸, 유지보수 해야 할 코드도 볼 겸 공부를 시작한다.
오늘은 설치부터 필요한 각종 메서드와 옵션을 공부할 것이다! 뚜둔(강한 의지)
npm install @tanstack/react-query
app/layout.tsx
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</body>
</html>
);
}
const queryClient = new QueryClient();
-QueryClient 인스턴스는 모든 쿼리와 뮤테이션의 상태를 관리하며, 캐시와 관련된 설정을 포함한다.
-애플리케이션에서 데이터를 가져오고, 캐시를 관리하고, 쿼리의 생명주기를 제어하는 데 사용
<QueryClientProvider client={queryClient}>
-QueryClientProvider는 React Context API를 사용하여 애플리케이션의 하위 컴포넌트에 queryClient 인스턴스를 제공
-이 컴포넌트로 감싸진 모든 하위 컴포넌트는 useQuery, useMutation 등과 같은 React Query 훅을 사용할 수 있다.
export const fetchPosts = async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
};
import React from 'react';
import { useQuery } from '@tanstack/react-query';
import { fetchPosts } from '../api/posts';
const PostsPage = () => {
const { data, error, isLoading } = useQuery(['posts'], fetchPosts);
if (isLoading) {
return <div>Loading...</div>;
}
if (error instanceof Error) {
return <div>Error: {error.message}</div>;
}
return (
<div>
<h1>Posts</h1>
<ul>
{data.map((post: { id: number; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
};
export default PostsPage;
그래서?
첫번째, 메서드와 그 메서드의 사용방법을 먼저 확인해보자.
import { useQuery } from '@tanstack/react-query';
import { fetchPosts } from '../api/posts';
const Posts = () => {
const { data, error, isLoading } = useQuery(['posts'], fetchPosts);
if (isLoading) return <div>Loading...</div>;
if (error instanceof Error) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map((post: { id: number; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
};
query key(고유식별자)
쿼리함수
(React Query가 호출하여 데이터를 가져오고, 성공적으로 데이터를 가져오면 해당 데이터를 캐시에 저장)import { useMutation, useQueryClient } from '@tanstack/react-query';
import { createPost } from '../api/posts';
const CreatePost = () => {
const queryClient = useQueryClient();
const mutation = useMutation(createPost, {
onSuccess: () => {
// 데이터가 성공적으로 추가된 후 쿼리 재요청
queryClient.invalidateQueries(['posts']);
},
});
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
const data = { title: event.currentTarget.title.value };
mutation.mutate(data);
};
return (
<form onSubmit={handleSubmit}>
<input name="title" required />
<button type="submit">Create Post</button>
</form>
);
};
추가, 수정 또는 삭제
하는 비동기 작업을 정의export const createPost = async (newPost: { title: string }) => {
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newPost),
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json(); // 서버에서 반환된 새 게시물 데이터
};
useMutation의 옵션
onSuccess
: 뮤테이션이 성공적으로 완료되었을 때 호출되는 콜백 함수onError
: 뮤테이션이 실패했을 때 호출되는 콜백 함수onSettled
: 뮤테이션이 성공하거나 실패한 후 호출되는 콜백 함수onMutate
: 뮤테이션이 시작되기 전에 호출되는 콜백 함수사용예시
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { createPost } from '../api/posts';
const CreatePost = () => {
const queryClient = useQueryClient();
const mutation = useMutation(createPost, {
onMutate: () => {
// 뮤테이션이 시작되기 전에 호출
console.log('Mutation started');
},
onSuccess: (data) => {
console.log('Post created successfully!', data);
queryClient.invalidateQueries(['posts']);
},
onError: (error) => {
console.error('Error creating post:', error);
},
onSettled: () => {
// 성공 여부와 관계없이 호출
console.log('Mutation settled');
},
});
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
const data = { title: event.currentTarget.title.value };
//currentTarget: 이벤트가 바인딩된 요소를 참조, 이벤트가 발생한 폼 요소 자체를 가리
// const data = { title: event.target.title.value };
// event.target: 이벤트가 실제로 발생한 요소, 일반적으로 currentTarget과 같지만,
// 이벤트가 버블링되거나 캡처링되는 경우에는 다를 수 있음
mutation.mutate(data); // 뮤테이션 실행
};
return (
<form onSubmit={handleSubmit}>
<input name="title" required />
<button type="submit">Create Post</button>
</form>
);
};
어? useQueryClient
는 뭔데?
import { useQueryClient } from '@tanstack/react-query';
const SomeComponent = () => {
const queryClient = useQueryClient();
const handleRefresh = () => {
queryClient.invalidateQueries(['posts']);
};
return <button onClick={handleRefresh}>Refresh Posts</button>;
};
import { useInfiniteQuery } from '@tanstack/react-query';
import { fetchPosts } from '../api/posts';
const Posts = () => {
const {
data,
fetchNextPage,
hasNextPage,
isLoading,
} = useInfiniteQuery(['posts'], fetchPosts, {
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
return (
<div>
{isLoading && <div>Loading...</div>}
{data?.pages.map((page) =>
page.posts.map((post) => <div key={post.id}>{post.title}</div>)
)}
{hasNextPage && <button onClick={fetchNextPage}>Load More</button>}
</div>
);
};
useInfiniteQuery의 매개변수
getNextPageParam
: 다음 페이지를 요청하는 데 필요한 매개변수를 정의getNextPageParam: (lastPage, pages) => {
return lastPage.nextCursor; // lastPage에서 다음 커서를 가져옴
}
getPreviousPageParam
: 이전 페이지를 요청하는 데 필요한 매개변수를 정의getPreviousPageParam: (firstPage, pages) => {
return firstPage.previousCursor; // 첫 페이지에서 이전 커서를 가져옴
}
staleTime
: 쿼리 데이터가 신선하다고 간주되는 시간(밀리초). 이 시간 동안 쿼리는 재요청되지 않음cacheTime
: 쿼리 데이터가 캐시에 남아 있는 시간(밀리초). 이 시간이 지나면 데이터는 garbage collection에 의해 제거refetchOnWindowFocus
: 브라우저 창이 포커스를 받을 때 쿼리를 자동으로 다시 요청할지를 결정refetchOnReconnect
: 네트워크가 다시 연결될 때 쿼리를 자동으로 다시 요청할지를 결정enabled
: 쿼리의 활성화 여부를 결정하는 불리언 값. true일 경우에만 쿼리가 실행너무 많아서 이쯤에서 포기할까? 라는 생각 한 번 정도 해줘야함
import { useIsFetching } from '@tanstack/react-query';
const LoadingIndicator = () => {
const isFetching = useIsFetching();
return isFetching ? <div>Loading...</div> : null;
};
import { Hydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query';
const App = ({ dehydratedState }) => {
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>
<Hydrate state={dehydratedState}>
{/* Your application components */}
</Hydrate>
</QueryClientProvider>
);
};
중요한건 꺾이지 않는 마음이 아니라, 꺾여도 하는 마음이라고 누가 그랬는데 뭔소리야.
이미 하기 싫어 죽겠어가지고 아니 NextJS는 기본적으로 SSR이잖아? 그럼 useHydrate는 없어도 되는거 아녔어???라고 하나 정도 줄여보려고 했는데 아님.
// pages/index.js
import { Hydrate, QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
const fetchPosts = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!res.ok) throw new Error('Network response was not ok');
return res.json();
};
const PostsList = () => {
const { data, error, isLoading } = useQuery('posts', fetchPosts);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
};
const App = ({ dehydratedState }) => {
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>
<Hydrate state={dehydratedState}>
<PostsList />
</Hydrate>
</QueryClientProvider>
);
};
// 서버 사이드 렌더링을 위한 getServerSideProps
export async function getServerSideProps() {
const queryClient = new QueryClient();
// 데이터 미리 가져오기
await queryClient.prefetchQuery('posts', fetchPosts);
return {
props: {
dehydratedState: dehydrate(queryClient), // hydrate에 사용할 상태
},
};
}
export default App;
서버 사이드 렌더링을 위한 getServerSideProps
getServerSideProps
는 Next.js에서 서버 사이드 렌더링(SSR)을 구현하기 위한 함수// pages/index.js
import { Hydrate, QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
import { dehydrate } from '@tanstack/react-query';
const fetchPosts = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!res.ok) throw new Error('Network response was not ok');
return res.json();
};
const PostsList = () => {
const { data, error, isLoading } = useQuery('posts', fetchPosts);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
};
const App = ({ dehydratedState }) => {
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>
<Hydrate state={dehydratedState}>
<PostsList />
</Hydrate>
</QueryClientProvider>
);
};
// 정적 사이트 생성을 위한 getStaticProps
export async function getStaticProps() {
const queryClient = new QueryClient();
// 데이터 미리 가져오기
await queryClient.prefetchQuery('posts', fetchPosts);
return {
props: {
dehydratedState: dehydrate(queryClient), // hydrate에 사용할 상태
},
revalidate: 10, // 10초마다 재생성
};
}
export default App;
이 부분에 대해서는 따로 정리한 것이 있음. 아직 업로드를 하지 않은 것임. 그래서 그냥 이렇게 쓰이기 때문에 useHydrate가 NextJS에서도 사용되는거구나 정도로만 이해하고 넘어가세오.
혹시 눈치 챈 사람 있나?
지나오면서 QueryClient
설명을 했을때, invalidateQueries
메서드를 사용했다는 부분.
fetchQuery
const data = await queryClient.fetchQuery('posts', fetchPosts);
prefetchQuery
await queryClient.prefetchQuery('posts', fetchPosts);
invalidateQueries
queryClient.invalidateQueries('posts');
setQueryData
queryClient.setQueryData('posts', newData);
자, 이제 끝이 아니라. 시작임 아직 아무것도 하지 않았음. 나는 메서드만 봤을 뿐임. 하지만 내 맘은 아까 꺾임.
그렇지만 또 안할 수 없는 현실이 싫을 뿐 다음에는 작은 예시 혹은 프로젝트에 직접 적용해보겠씀. 이만 춍춍
프론트라 잘 모르지만 개 쩌는군…