Frontend is 실전

[React] React-Query로 데이터 통신 해보기

킹경후 2025. 1. 10. 19:34

최근 면접을 준비하며, React-Query에 대한 질문을 많이 받았었다. 

그런데 마침 다음주에 면접 보는 기업에서 React-Query를 사용한다기에

면접 준비도 할 겸, 공부도 할 겸, 과거 진행했던 프로젝트를 리팩토링 할 겸 사용해 보니 생각보다 아주 편리해서 한번 소개해 보고자 한다.

 

사실 그동안 React-Query가 서버 통신 라이브러리인 줄로만 알았는데, 공부해 보니 HTTP 통신을 담당하는 라이브러리가 아니라, HTTP 통신을 통해 받아온 데이터를 캐싱하고 관리해 주는 라이브러리였다!

 

과거 진행했던 전기차 지도(CPM) 프로젝트를 기반으로 설명하도록 하겠다.

아주아주 기본적인 기능만 사용했기 때문에, 좀 더 자세한 탐구는 공부를 더 하고 써보도록 하도록,.,.,.,.어쩌구,.,.


1. 설치

$ npm install @tanstack/react-query
$ yarn add @tanstack/react-query

 

사실 devtools도 있는데, 나는 아직 자세한 기능을 사용해 보진 않아서 굳이 설치하진 않았다.

만약 실시간으로 데이터의 fetching 상황이나 업데이트를 확인해 보고 싶은 사람은 devtools도 함께 설치하면 되겠다.

 

$ npm install @tanstack/react-query-devtools
$ yarn add @tanstack/react-query-devtools

 


2. 사용법

먼저 상태 관리 라이브러리들 처럼 Provider를 이용해 컴포넌트를 감싸줘야 한다.

보통 index.js의 <App /> 컴포넌트 상위에 적용하지만, 사용하는 곳은 자유.

나는 스테레오타입으로 index.js에 적용해 주었다.

 

//index.js

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import { store, persistor } from "./redux/store";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const client = new QueryClient();
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <Provider store={store}>
    <QueryClientProvider client={client}> {/*Provider로 감싸주기*/}
      <PersistGate loading={null} persistor={persistor}>
        <App />
      </PersistGate>
    </QueryClientProvider>
  </Provider>
);

 

QueryClient 클래스는 캐싱에 쓰이는 데이터, 즉 '캐시'를 관리하는 클래스이다.

우리가 캐시에 접근할 때, 직접 캐시에 접근하기 보다는 QueryClient 클래스를 이용해 접근한다고 보면 되겠다.

여튼 최상위 컴포넌트 위에 이 Provider를 적용해 주자

 

그 다음은 데이터를 페칭해 올 컴포넌트로 이동한다.

 

const [data, setData] = useState([]);
const [loading, setIsLoading] = useState(true)

  useEffect(() => {
    // AWS 설정
    AWS.config.update({
      region: process.env.REACT_APP_region,
      accessKeyId: process.env.REACT_APP_accessKeyId,
      secretAccessKey: process.env.REACT_APP_secretAccessKey
    });

    // S3 객체 생성
    const s3 = new AWS.S3();
 
    // S3에서 파일 읽기
    const params = { Bucket: 'chargers-list/chargers', Key: 'chargers.json' };

    s3.getObject(params, (err, result) => {
      if (err) {
        console.error('Error fetching data from S3:', err);
      } else {
        const fileContent = result.Body.toString('utf-8');
        const parsedData = JSON.parse(fileContent);
        setData(parsedData);
      }
    });
    setIsLoading(false)
  }, []);
  const category = useMemo(() => [...new Set(data.map(el => el.main_category))], [data])
  const town = useMemo(() => [...new Set(data.map(el => el.town))], [data])

 

 

위 코드는 기존의 데이터 페칭 코드이다. 

AWS S3에 저장된 json 파일을 가져와 읽었고, 데이터를 읽는 로직을 useEffect에 담아 최초 렌더링시에만 데이터를 받아오도록 했다.

생명주기와 state를 통해 데이터를 페칭해 오기 떄문에 코드가 길어지고, 많은 라이브러리를 import 해와야 하는 불편함이 있었다.

또한, 서버에 저장된 데이터의 양이 많기 때문에 리런데링이 일어날 때 마다 category와 town을 업데이트 한다면 성능이 매우매우 나빠질 것을 우려해서 memo까지 사용했다.

 

여기까지만 봐도 데이터 하나를 받아오기 위해 수많은 Hook이 동원된다는 것을 볼 수 있다,.,.,,!

React-Query는 단 하나의 라이브러리로 Hook들의 기능을 대체할 수 있는 전지전능(아님)한 능력을 가졌다.

 

먼저, React-Query에서 데이터를 GET 해오는데 사용하는 useQuery의 구조를 살펴보자.

 

const { data, isLoading, isError } = useQuery({
	queryKey: 쿼리 키,
	queryFn: 실행할 함수,
});

 

useQuery에서는 페칭해 올 데이터인 data, 로딩 여부를 나타내는 isLoading, 에러 여부를 나타내는 isError를 불러올 수 있다.

isLoading을 이용해 로딩 페이지를 구현하거나, isError를 이용해 에러 페이지를 구현할 수 있다.

참고로 두 변수를 모두 불러와놓고 사용하지 않으면 data가 undefined가 되어 버리는 문제가 발생했다.

그러므로 로딩, 에러 로직도 착실하게 구현해 주도록 하자.

 

React-Query의 캐싱 데이터는  QueryClient 내에서 key: value 형태, 즉 '해시' 형태로 존재하는데, 이때 useQuery에 쓰이는 queryKey가 이 해시의 키 역할을 한다.

따라서 페칭하려는 데이터마다 각각 고유한 queryKey를 사용해 주어야 한다.

참고로 이 queryKey는 string[] 타입이므로, 배열 안에 key를 담아 사용해야 한다.

 

queryFn은 데이터를 페칭할 함수를 넣어주면 된다.

외부 함수를 넣던 직접 작성하건 상관은 없으나, 비동기 처리를 위해 반드시 promise 객체를 리턴하는 함수(Promise, async 등)를 넣어줘야 한다.

 

말로만 들으면 어려울 수 있으니, 실제로 사용했던 코드를 보며 익혀보도록 하자.


3. 적용

먼저 AWS에서 데이터를 페칭해 오는 로직을 외부 함수로 빼버렸다.

//getData.js

import AWS from "aws-sdk";

//promise 객체를 리턴하도록 async로 작성
const getData = async () => {
  try {
    AWS.config.update({
      region: process.env.REACT_APP_region,
      accessKeyId: process.env.REACT_APP_accessKeyId,
      secretAccessKey: process.env.REACT_APP_secretAccessKey,
    });

    // S3 객체 생성
    const s3 = new AWS.S3();

    // S3에서 파일 읽기
    const params = { Bucket: "chargers-list/chargers", Key: "chargers.json" };
    // await와 promise()로 결과 데이터를 promise 객체로 변환
    const result = await s3.getObject(params).promise();
    const fileContent = result.Body.toString("utf-8");
    const parsedData = JSON.parse(fileContent);
    return parsedData;
  } catch (err) {
    console.log(err);
  }
};
export default getData;

 

그리고 데이터를 사용할 컴포넌트에서 useQuery를 불러와 로직을 처리해 주었다.

 

import { useQuery } from "@tanstack/react-query";
import getData from "../functions/getData";

const { data, isLoading, isError } = useQuery({
    queryKey: ["all"],
    queryFn: getData,
});

 if (isLoading) return <Loading>로딩중</Loading>;
if (isError) return <Error>에러가 발생했습니다!</Error>;

const category = [...new Set(data.map((el) => el.main_category))];
const town = [...new Set(data.map((el) => el.town))];

 

로딩중에는 로딩 컴포넌트를, 에러가 발생한다면 에러 컴포넌트를 리턴해주도록 했다.

에러가 없거나 로딩이 끝난 상태라면, 페칭한 데이터를 활용해서 컴포넌트 렌더링 또는 데이터 처리를 진행하면 된다.

 

나는 데이터를 받아오는(GET) 작업만 했기 때문에, 서버의 데이터와 클라이언트의 데이터가 다른 데이터를 바라볼 일이 없다.

그렇기 때문에 클라이언트에서는 useMemo를 사용할 필요 없이, 캐싱된 data를 바로 사용해 주면 된다. 이 얼마나 편리한가.

 

 

데이터가 아주아주 잘 넘어오는 모습을 확인 할 수 있다.

 

 

에러가 발생했을 때는 이렇게 에러 페이지를 리턴해주게 된다.

로딩 페이지는 제대로 나오긴 하는데, 너무 휙 지나가서 어떻게 스크린샷이나 GIF를 만들 수가 없었다 ,,.,,,,


React-Query에 대해 탐구하며 정말 매력적인 라이브러리라는 생각이 들었다.

지금은 간단한 리팩토링 단계여서 GET 정도밖에 못해봤지만, 다른 프로젝트를 리팩토링 하거나 진행하게 된다면 다른 메서드들도 경험해 보고 싶다.