카카오 엔터프라이즈 5기 먀옹팀의 프로젝트 PPLOG의 프론트엔드 기본 세팅 과정입니다.
TanStack Query는 JS 애플리케이션에서 서버 상태를 관리하고 비동기 데이터를 쉽게 가져오고, 캐싱하며, 동기화하는 데에 특화된 라이브러리입니다.
TanStack Query는 다음과 같은 기능을 합니다.
- 데이터 페칭
- TanStack Query로 데이터를 쉽게 가져올 수 있습니다. 데이터 요청과 함께 로딩 상태, 에러 상태를 관리합니다.
- 캐싱
- 한 번 가져온 데이터는 캐시에 저장이 되고, 동일한 데이터 요청이 발생할 때 서버로 다시 요청하지 않고 캐시된 데이터를 반환합니다. 또한, 자동으로 캐시를 새로고침하는 기능도 제공합니다.
- 자동 새로고침
- 데이터의 유효기간(TTL)을 설정하여 일정 시간이 지나면 데이터를 다시 가져오거나, 네트워크 상태가 변경될 때 자동으로 데이터를 다시 불러올 수 있습니다.
- 데이터 동기화
- 여러 컴포넌트에서 동일한 데이터를 필요로 할 때, 중앙에서 관리하여 중복 요청을 방지하고, 데이터가 동기화되도록 합니다.
- 쿼리 인밸리데이션(Query Invalidation)
- 데이터가 변경된 후 캐시된 데이터를 무효화하고, 최신 데이터를 가져오도록 트리거할 수 있습니다.
✌리액트 애플리케이션은 보통 두 가지 상태를 관리합니다.
로컬 상태
서버 상태
여기서 서버 상태는 다음과 같은 특징이 있습니다.
이러한 서버 상태 관리를 수동으로 관리하면 코드가 매우 복잡해져, TanStack Query를 사용합니다.

다음 명령어를 사용해 TanStack Query를 npm install합니다.
npm install @tanstack/react-query
그리고 최상위 파일에 다음과 같이 작성하여 애플리케이션 전체를 감싸줍니다.
NextJS 14를 사용중이므로 루트 디렉토리의 layout.tsx파일에 다음과 같이 작성합니다.
import { QueryClient, QueryClientProvider } from '@tanstack/react-qeury';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvier client={queryClient}>
<Component />
</QueryClientProvider>
);
}
QueryClientProvider로 애플리케이션 전체를 감싸면 TanStack Query에서 제공하는 기능을 사용할 수 있게 됩니다.
아래는 TanStack Query를 사용해서 서버 상태 관리를 진행하는 예제들입니다.
기본적인 TanStack Query 사용 예제는 서버에서 데이터를 가져오는 것입니다. 여기서는 비동기 API 호출을 처리하는 방식과 쿼리 키를 사용하여 데이터를 캐싱하고, 다시 호출할 때 효율적으로 처리하는 방법을 보여줍니다. axios를 사용해서 데이터를 페칭했습니다.
const fetchUserData = async () => {
const response = await axios.get('get/users');
return response.data;
};
function UserList() {
const { data, isLoading, isError, error } = useQuery(['users'], fetchUserData);
if (isLoading) return <div>로딩 중...</div>;
if (isError) return <div>에러 발생: {error.message}</div>;
return (
<p>
{data.map(user => {
<div key={user.id}>{user.name}</div>
))}
</p>
);
}
여기선 useQuery를 사용해 fetchUserData 함수를 호출합니다. 이 때 ['users']는 쿼리의 고유한 키로 사용되며, 해당 키를 통해 데이터가 캐싱됩니다.
데이터 페칭 시 파라미터가 필요한 경우입니다. 사용자 ID를 파라미터로 상세 정보를 가져오는 예제입니다.
const fetchUserId = async (userId) => {
const response = await axios.get(`get/users/${userId}`);
return response.data;
};
function UserDetail({ userId }) {
const { data } = useQuery(['user', userId], () => fetchUserId(userId));
return (
...
)
}
여기서 useQuery(['user', userId])는 고유한 쿼리 키로 userId에 따라 다른 데이터를 캐싱합니다. 사용자 ID가 변경되면, 쿼리는 해당 ID에 맞는 데이터를 가져오며, 캐시된 데이터를 활용할수도 있습니다.
useQuery로 데이터를 가져올 때, 쿼리가 캐싱되므로 데이터를 다시 가져오지 않고 캐시된 데이터를 사용할 수 있습니다. 그러나! 사용자가 데이터를 업데이트한 후, 기존의 캐시된 데이터를 무효화해야 할 경우가 있습니다. 이럴 때 queryClient.invalidateQueries를 사용합니다.
const fetchLists = async () => {
const response = await axios.get('/get/lists');
return response.data;
};
const deleteList = async (id) => {
return axios.delete(`/delete/${id}`);
};
function MyLists() {
const queryClient = useQueryClient();
const { data } = useQuery(['lists'], fetchLists);
const mutation = useMutation(deletePost, {
onSuccess: () => {
// 성공적으로 삭제 후, 캐시 무효화하여 데이터를 새로 가져옴
queryClient.invalidateQueries(['posts']);
}
});
return (
<div>
{data.map(list => (
<div key={list.id}>
{list.tile}
<button onClick={()=> mutation.mutate(list.id)}>삭제</button>
</div>
))}
</div>
);
}
여기서 mutation.mutate(list.id)는 특정 리스트를 삭제하며, 삭제가 완료되면 queryClient.invalidateQueries(['lists'])를 통해 기존에 캐시된 리스트 목록을 무효화하고 다시 데이터를 가져옵니다.
만약 각 함수가 다른 컴포넌트상에 존재한다고 해도, 쿼리 키를 통해 다른 컴포넌트에서 캐시 무효화를 적용할 수 있습니다.
이는 QueryClient로 애플리케이션을 감싸 중앙에서 캐시를 관리해 데이터를 동기화 시키므로 가능합니다.
TanStack Query는 데이터의 유효기간을 설정할 수 있으며, 유효기간이 지나면 자동으로 데이터를 다시 가져올 수 있습니다. 사용자가 직접 데이터를 새로고침할 수도 있습니다.
const fetchTodos = async () => {
const response = await axios.get('/get/todos');
return response.data;
};
function TodoList() {
const { data, refetch } = useQuery(['todos'], fetchTodos, {
refetchOnWindowFocus: false, // 창에 포커스가 맞춰졌을 때 자동 새로고침 비활성화
});
return (
<div>
<button onClick={refetch}>새로고침</button>
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</div>
);
}
여기서 refetchOnWindowFocus: false는 창에 포커스가 다시 맞춰졌을 때 자동으로 데이터를 새로고침하지 않도록 설정합니다. refetch 함수를 사용해 사용자가 직접 데이터를 새로고침할 수 있도록 구현했습니다.
데이터가 많을 경우 페이징 기능도 구현할 수 있습니다. 이때 쿼리 키와 쿼리 함수에 페이지 번호를 넘겨주면 됩니다.
const fetchPostsByPage = async (page) => {
const response = await axios.get(`/get/posts?_page=${page}&_limit=10`);
return response.data;
};
function PaginatedPosts() {
const [page, setPage] = useState(1);
const { data } = useQuery(['posts', page], () => fetchPostsByPage(page), {
keepPreviousData: true, // 이전 페이지 데이터를 유지하며 새로운 데이터를 로드
});
return (
<div>
<ul>
{data.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
<div>
<button onClick={() => setPage(old => Math.max(old -1, 1))} disabled={page === 1}>
이전 페이지
</button>
<button onClick={() => setPage(old => old + 1)} disabled={data.length < 10}>
다음 페이지
</button>
</div>
</div>
);
}
여기선 keepPreviousData: true 옵션을 사용해 페이지 전환 시 새로운 데이터가 로딩될 때까지 이전 페이지 데이터를 유지합니다. 페이지 번호는 page 상태로 관리되며, useQuery의 쿼리키에 포함시켜 페이징을 구현합니다.
TanStack Query에는 정말 많은 속성들이 존재합니다. 그 중 주요 속성 몇 가지를 적어봅니다(그래도 많음).
useQuery훅은 데이터를 서버에서 가져오고 이를 관리하는 역할을 합니다.
const queryKey = ['todos']; // 쿼리 키
const fetchTodos = async () => {
const response = await fetch('/api/todos');
return response.json();
};
const { data, isLoading } = useQuery(queryKey, fetchTodos, { enabled: userId !== null });
const { data} = useQuery(queryKey, fetchTodos, { staleTime : 1000 * 60 });
const { data } = useQuery(queryKey, fetchTodos, { cacheTime: 1000 * 60 * 5 });
true입니다.const { data } = useQuery(queryKey, fetchTodos, { refetchOnWindowFocus: false });
const { data } = useQuery(queryKey, fetchTodos, {refetchInterval: 1000 * 10 });
const { data } = useQuery(queryKey, fetchTodos, { retry: 2 }); // 2회 재시도
const { data } = useQuery(queryKey, fetchTodos, { onSuccess: (data) => console.log('성공'); });
const { data } = useQuery(queryKey, fetchTodos, { onError: (error) => console.error('에러 발생'); });
useMutation은 데이터르르 생성, 수정, 삭제할 때 사용하는 훅입니다. useQuery와 다르게 서버에서 데이터를 변경하는 작업에 적합합니다.
const addTodo = async (newTodo) => {
return axios.post('/api/todos', newTodo);
};
const mutation = useMutation(addTodo, {
onSuccess: () => {
queryClient.invalidateQueries('todos'); // 'todos' 쿼리 무효화
},
});
const mutation = useMutation(addTodo, {
onError: (error) => console.error('에러 발생:', error),
});
const mutation = useMutation(addTodo, { retry: 1 }); // 1회 재시도
새로운 todo를 추가하는 함수 예제
function AddTodo() {
const queryClient = useQueryClient();
const mutation = useMutation(addTodo, {
onSuccess: () => {
queryClient.invalidateQueries('todos'); // 투두 쿼리 무효화
},
});
const handleAdd = () => {
mutation.mutate({ title: '새로운 할 일' });
};
return <button onClick={handleAdd}>추가</button>;
}
QueryClient는 TanStack Query의 전역 상태 관리 객체입니다. 쿼리를 무효화하거나 데이터를 수동으로 갱신하는 작업을 합니다.
queryClient.invalidateQueries('todos'); // todos 쿼리 무효화
queryClient.setQueryData('todos', (oldData) => [...oldData, newTodo]);
낙관적 업데이트 : 사용자가 서버에 데이터를 보내고, 서버의 응답을 기다리지 않고도 사용자 인터페이스(UI)를 즉시 업데이트하는 기법
qeuryClient.refetchQueries('todos');
TanStack Query는 서버 상태를 관리하는 데 매우 유용한 도구이지만, 모든 API 호출에 사용하지 않고 상황에 따라 적절히 사용합니다.
- 비동기 데이터 페칭
- API에서 데이터를 가져올 때 유용합니다. 특히, 데이터를 자주 가져오고, 로딩 상태나 에러 처리 등 상태 관리가 필요한 경우에 적합합니다.
- 정기적인 데이터 새로 고침이 필요한 경우
- 실시간으로 데이터를 업데이트하거나 유효기간이 있는 데이터를 주기적으로 갱신해야 할 때 사용합니다.
- 서버 상태 관리
- 클라이언트에서 여러 컴포넌트가 같은 데이터를 사용하거나, 서버 상태와 클라이언트 상태를 동기화하는 것이 필요할 때 적합합니다.
TanStack Query를 사용하지 않을 때!
- 단순 POST/PUT/DELETE 요청
- 서버에 단순 데이터 요청을 전송할 때, TanStack Query를 사용하지 않고 직접적으로 API를 호출하는 것이 적합합니다. 그러나 요청 이후 데이터를 갱신해야 하는 경우에는
useMutation을 사용할 수 있습니다.
- 짧은/일회성 요청
- 사용자의 상호작용이 적고, 단순 데이터 요청의 경우에도 직접적인 API 호출이 더 적합합니다.
- 실시간 업데이트가 필요 없는 경우
- 캐싱이나 자동 새로고침이 필요 없는 API 요청이라면 TanStackQuery를 사용할 필요가 없을 수 있습니다.
즉 데이터 페칭(특히 GET 요청)과 같은 경우에는 TanStack Query를 사용하고,
단순한 POST/PUT/DELETE 요청에는 별도의 HTTP 요청 도구(axios)를 사용할 수 있습니다.