React-Query를 사용해야하는 이유 - 1

이태관·2025년 1월 18일

Why-React-Query

목록 보기
1/2
post-thumbnail

React-Query를 사용해야 하는 이유와 등장 배경을 Why Reacy Query의 내용을 바탕으로 정리하고, 이를 간단한 코드 예제로 알아보겠습니다.

지금은 TanStack-Query로 이름이 바뀜


기본적인 React의 데이터 패칭 방법

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 문제

1. 로딩처리 문제

로딩처리를 하지 않으면 발생하는 가장 큰 문제점은 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를 보면 isLoadingtrue일 때 빈 카드를 보여줘서 layout shift를 방지한 모습입니다.


2. 에러처리 문제

서버 요청이 항상 성공하지는 않습니다. 통신 실패 시 무한 로딩 등 심각한 문제가 발생할 수 있습니다. 이를 해결하기 위해 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>; // 에러가 발생했을 때 보여줄 컴포넌트

3. Race Condition

  • 여기까지는 대부분 겪어봤고 해결해봤을 내용입니다. 그런데 한가지 문제가 남아있습니다.
    바로 Race Conditoin을 어떻게 처리할지 입니다.

  • Race Condition이란 데이터 패칭 요청이 연속적으로 발생할 때, 응답의 순서가 뒤바뀌며 예상치 못한 결과를 초래하는 문제입니다. 이를 방지하려면 useEffect의 clean-up 함수를 사용해야 합니다.

예를 들어 어떤 데이터를 가져오는 버튼을 빠르게 2번 연속 눌렀다고 가정해봅시다.

좀 더 자세히 말하면 첫번째 데이터를 받아오는 중에 다른 데이터를 가져오는 버튼을 눌렀을 경우입니다.

이 경우 화면에 어떤 데이터가 렌더링될지 확신할 수 없습니다. 서버나 네트워크 상태에 따라 요청이 처리되는 순서가 달라질 수 있기 때문입니다. 요청 순서와 관계없이 마지막 요청만 화면에 렌더링되도록 처리해야 합니다.

아래는 쉽게 그림으로 표현한 gif입니다.

실제 페이지를 보며 이 문제를 더 자세히 분석해보겠습니다.

  • 위 gif는 네트워크 속도를 일부로 낮춘다음 ->버튼을 눌러 포켓몬 데이터를 여러번 보내면 일어나는 일입니다.

현재 문제를 정리해보고 해결방법을 생각해보겠습니다.

문제 정리

  1. 개발자도구 네트워크탭을 보면 {;}는 데이터 패칭요청입니다. 모두 200으로 성공했지만 png 부분에서 에러가 나고있음.
  2. 총 6번의 요청이 있었지만 포켓몬 사진을 렌더링 해주는 곳에서도 이상해씨->이상해풀->꼬부기 순서로 부자연스럽게 화면이 렌더링 됨.

해결방법

  • 해결방법은 간단합니다. 가장 마지막에 요청한 데이터만 읽어들이고 화면에 렌더링 해주는 것입니다. 이러면 모든 문제가 해결됩니다.
  • 이런 경우 useEffect의 clean-up 함수를 활용하여 이전 요청을 무효화하고, 마지막 요청만 유효하게 처리해야 합니다.
  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라는 변수가 어떻게 쓰이고 있는가? 입니다.

코드 흐름

  1. ignore 변수 생성 - 이 변수는 현재 요청이 마지막 요청인지 즉 유효한 요청인지 확인하는 변수입니다.

  2. 새 요청이 발생하면 useEffect의 clean-up 함수가 실행되어 ignore 값을 true로 설정합니다.

  3. 이전 요청이 실행되던 중이라도, 결과를 반영하려는 시점에 ignoretrue면 해당 요청의 결과를 무시합니다.

  4. 결국, 마지막 요청의 결과만 화면에 렌더링되며 Race Condition 문제가 해결됩니다.

🚨주의할 점🚨

  • 이전 요청 자체가 중단되거나 서버와의 연결이 취소된다는게 아닙니다. 클라이언트 측에서 요청의 결과를 반영하지 않는 방식으로 처리하는 것입니다. (setState로 상태값을 변경하지 않는다는 의미임)

  • 이를 통해 Race Condition 문제를 해결하며, "마지막 요청만 유효하다"는 비즈니스 로직을 구현할 수 있습니다.


위 gif를 보면 이제, 데이터 요청을 빠르게 연속해서 발생시키더라도, 이전 요청은 무효화되고 마지막 요청만 정확히 반영되는 것을 확인할 수 있습니다.


여기까지는 데이터 패치를 중심으로 React-Query를 사용하지 않았을 때 발생하는 문제를 살펴보았습니다.

다음 글에서는 데이터를 활용하는 과정에서 React-Query를 사용하지 않을 경우 발생하는 문제를 알아보겠습니다.

0개의 댓글