
React-Query를 사용해야 하는 이유와 등장 배경을 Why Reacy Query의 내용을 바탕으로 정리하고, 이를 간단한 코드 예제로 알아보겠습니다.
지금은
TanStack-Query로 이름이 바뀜
import { useState, useEffect } from "react";
export default function App() {
const [id, setId] = useState(1);
const [pokemon, setPokemon] = useState(null);
const handlePrevious = () => setId((id) => (id > 1 ? id - 1 : id));
const handleNext = () => setId((id) => id + 1);
useEffect(() => {
const handleFetchPokemon = async () => {
setPokemon(null);
const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`);
const json = await res.json();
setPokemon(json);
};
handleFetchPokemon();
}, [id]);
if (!pokemon) return null;
return (
<>
<img width="475px" height="475px" src={pokemon.sprites.front_default} />
<div className="button-group">
<button name="previous" onClick={handlePrevious}>
←
</button>
<button name="next" onClick={handleNext}>
→
</button>
</div>
</>
);
}
React를 처음 배웠을 때 위와같이 데이터를 가져와서 화면에 보여줬을겁니다. 하지만 이 코드에는 3가지 문제점이 있습니다.
- 로딩처리 문제
- 에러처리 문제
- race condition 문제
로딩처리를 하지 않으면 발생하는 가장 큰 문제점은 Layout shift입니다.

위와 같이 서버에서 데이터를 가져오는 도중에 UI가 일그러지는게 대표적인 현상입니다.
이를 해결하기 위한 방법 중 널리 사용되는게 loading상태를 만드는 것입니다.
const [isLoading, setIsLoading] = useState(true); // 로딩 상태 추가
useEffect(() => {
const handleFetchPokemon = async () => {
setPokemon(null);
setIsLoading(true);
const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`);
const json = await res.json();
setPokemon(json);
setIsLoading(false);
};
handleFetchPokemon();
}, [id]);
if (isLoading) return <LoadingCard />; // 로딩상태일 때 보여줄 컴포넌트
아래 gif를 보면 isLoading이 true일 때 빈 카드를 보여줘서 layout shift를 방지한 모습입니다.

서버 요청이 항상 성공하지는 않습니다. 통신 실패 시 무한 로딩 등 심각한 문제가 발생할 수 있습니다. 이를 해결하기 위해 isError 상태를 추가하고 try-catch를 활용하여 에러를 처리합니다.
const [isError, setIsError] = useState(false); // 에러 상태 추가
useEffect(() => {
const handleFetchPokemon = async () => {
setPokemon(null);
setIsLoading(true);
setIsError(false);
try { // try catch로 에러상태 관리
const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`);
if (!res.ok) throw new Error(`Error fetching Pokemon #${id}`);
const json = await res.json();
setPokemon(json);
setIsLoading(false);
} catch (error) {
setIsError(true);
setIsLoading(false);
}
};
handleFetchPokemon();
}, [id]);
if (isError) return <div>에러가 발생했습니다.</div>; // 에러가 발생했을 때 보여줄 컴포넌트
여기까지는 대부분 겪어봤고 해결해봤을 내용입니다. 그런데 한가지 문제가 남아있습니다.
바로 Race Conditoin을 어떻게 처리할지 입니다.
Race Condition이란 데이터 패칭 요청이 연속적으로 발생할 때, 응답의 순서가 뒤바뀌며 예상치 못한 결과를 초래하는 문제입니다. 이를 방지하려면 useEffect의 clean-up 함수를 사용해야 합니다.
예를 들어 어떤 데이터를 가져오는 버튼을 빠르게 2번 연속 눌렀다고 가정해봅시다.
좀 더 자세히 말하면 첫번째 데이터를 받아오는 중에 다른 데이터를 가져오는 버튼을 눌렀을 경우입니다.
이 경우 화면에 어떤 데이터가 렌더링될지 확신할 수 없습니다. 서버나 네트워크 상태에 따라 요청이 처리되는 순서가 달라질 수 있기 때문입니다. 요청 순서와 관계없이 마지막 요청만 화면에 렌더링되도록 처리해야 합니다.
아래는 쉽게 그림으로 표현한 gif입니다.


->버튼을 눌러 포켓몬 데이터를 여러번 보내면 일어나는 일입니다.현재 문제를 정리해보고 해결방법을 생각해보겠습니다.
문제 정리
- 개발자도구 네트워크탭을 보면
{;}는 데이터 패칭요청입니다. 모두 200으로 성공했지만png부분에서 에러가 나고있음.- 총 6번의 요청이 있었지만 포켓몬 사진을 렌더링 해주는 곳에서도 이상해씨->이상해풀->꼬부기 순서로 부자연스럽게 화면이 렌더링 됨.
해결방법
- 해결방법은 간단합니다. 가장 마지막에 요청한 데이터만 읽어들이고 화면에 렌더링 해주는 것입니다. 이러면 모든 문제가 해결됩니다.
- 이런 경우 useEffect의 clean-up 함수를 활용하여 이전 요청을 무효화하고, 마지막 요청만 유효하게 처리해야 합니다.
clean up함수에 대해서는 여기서 자세히 설명하진 않겠습니다. 간단히 말하면 컴포넌트가unmount될 때 실행되는 함수입니다. 간단한 설명과 예제 링크입니다. https://react.dev/reference/react/useEffect#updating-state-based-on-previous-state-from-an-effect
useEffect(() => {
let ignore = false;
const handleFetchPokemon = async () => {
setPokemon(null);
setIsLoading(true);
setIsError(null);
try {
const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`);
if (ignore) {
return;
}
if (res.ok === false) {
throw new Error(`Error fetching pokemon #${id}`);
}
const json = await res.json();
setPokemon(json);
setIsLoading(false);
} catch (e) {
setIsError(e.message);
setIsLoading(false);
}
};
handleFetchPokemon();
return () => { // 주목
ignore = true;
};
}, [id]);
편의상 useEffect부분만 작성했습니다. 핵심은 ignore라는 변수가 어떻게 쓰이고 있는가? 입니다.
ignore 변수 생성 - 이 변수는 현재 요청이 마지막 요청인지 즉 유효한 요청인지 확인하는 변수입니다.
새 요청이 발생하면 useEffect의 clean-up 함수가 실행되어 ignore 값을 true로 설정합니다.
이전 요청이 실행되던 중이라도, 결과를 반영하려는 시점에 ignore가 true면 해당 요청의 결과를 무시합니다.
결국, 마지막 요청의 결과만 화면에 렌더링되며 Race Condition 문제가 해결됩니다.
이전 요청 자체가 중단되거나 서버와의 연결이 취소된다는게 아닙니다. 클라이언트 측에서 요청의 결과를 반영하지 않는 방식으로 처리하는 것입니다. (setState로 상태값을 변경하지 않는다는 의미임)
이를 통해 Race Condition 문제를 해결하며, "마지막 요청만 유효하다"는 비즈니스 로직을 구현할 수 있습니다.

위 gif를 보면 이제, 데이터 요청을 빠르게 연속해서 발생시키더라도, 이전 요청은 무효화되고 마지막 요청만 정확히 반영되는 것을 확인할 수 있습니다.
여기까지는 데이터 패치를 중심으로 React-Query를 사용하지 않았을 때 발생하는 문제를 살펴보았습니다.
다음 글에서는 데이터를 활용하는 과정에서 React-Query를 사용하지 않을 경우 발생하는 문제를 알아보겠습니다.