Skip to main content

Command Palette

Search for a command to run...

React Query, 왜 이제는 거의 필수일까?

Updated
•6 min read•View as Markdown
D

프론트엔드 개발자 doit입니다.

직접 써보고 느낀 서버 상태 관리의 필요성
이제는 React Query로 서버 상태를 관리하는것이 거의 필수가 된 거 같아, React Query 없이 하면 얼나 불편할까 몸소 경험해보고자..? React Query 없이 구현해보고자 했습니다.


🔙 과거 방식: 전통적인 API 요청 방식

const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [data, setData] = useState(null);

useEffect(() => {
  const fetchData = async () => {
    try {
      setLoading(true);
      const res = await getList(id);
      setData(res);
    } catch (err) {
      setError(err);
    } finally {
      setLoading(false);
    }
  };

  fetchData();
}, [id]);

이런 식으로 데이터를 불러오고 useState에 저장하면 끝!
딱히 문제 없어 보이지만..

useState + useEffect로 다 하려니까 너무 반복됐어요

이 세 가지는 거의 필수처럼 따라붙습니다:

const [loading, setLoading] = useState(true);
const [data, setData] = useState(null);
const [error, setError] = useState(null);

그리고 항상 이런 식으로 useEffect 안에서 fetch 호출하고 상태 바꾸고...
데이터 하나 불러오는데 코드가 너무 길어지고, 다른 컴포넌트에서도 쓰려면 또 복붙해야 했습니다.

  • 컴포넌트마다 비슷한 요청/상태 로직 반복

  • 동일한 요청도 컴포넌트마다 따로 실행

  • 응답 결과를 공유하거나 캐시하는 건 불가능

❓ 그럼 Redux로 공유하면 되지 않나요? 맞아요. 그래서 실제로 많은 프로젝트에서 Redux로 서버 상태까지 관리했었습니다.

⚙️그럼 전역 상태로 빼면 되지!" → Redux

그래서 상태를 Redux로 빼보기로 했습니다.
데이터를 한 번 불러오고 store에 저장하면 전역에서 관리하니까 어디서든 쓸 수 있고, 다른 컴포넌트에선 useSelector()로 꺼내 쓰면 끝이니까요. 그런데…


1. 액션 타입 정의 (actions/types.ts)

export const FETCH_USERS_REQUEST = 'FETCH_USERS_REQUEST';
export const FETCH_USERS_SUCCESS = 'FETCH_USERS_SUCCESS';
export const FETCH_USERS_FAILURE = 'FETCH_USERS_FAILURE';

2. 액션 크리에이터 (actions/users.ts)

export const fetchUsersRequest = () => ({ type: FETCH_USERS_REQUEST });
export const fetchUsersSuccess = (data) => ({ type: FETCH_USERS_SUCCESS, payload: data });
export const fetchUsersFailure = (error) => ({ type: FETCH_USERS_FAILURE, error });

3. 비동기 함수 (thunks/users.ts)

import {
  fetchUsersRequest,
  fetchUsersSuccess,
  fetchUsersFailure
} from '../actions/users';

export const fetchUsers = () => async (dispatch) => {
  dispatch(fetchUsersRequest());
  try {
    const res = await fetch('/api/users');
    const data = await res.json();
    dispatch(fetchUsersSuccess(data));
  } catch (err) {
    dispatch(fetchUsersFailure(err.message));
  }
};

4. 리듀서 (reducers/users.ts)

import {
  FETCH_USERS_REQUEST,
  FETCH_USERS_SUCCESS,
  FETCH_USERS_FAILURE
} from '../actions/types';

const initialState = {
  data: [],
  loading: false,
  error: null
};

export function userReducer(state = initialState, action) {
  switch (action.type) {
    case FETCH_USERS_REQUEST:
      return { ...state, loading: true, error: null };
    case FETCH_USERS_SUCCESS:
      return { ...state, loading: false, data: action.payload };
    case FETCH_USERS_FAILURE:
      return { ...state, loading: false, error: action.error };
    default:
      return state;
  }
}

5. 컴포넌트에서 사용 (components/UserList.tsx)

import { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchUsers } from '../thunks/users';

export function UserList() {
  const dispatch = useDispatch();
  const { data, loading, error } = useSelector((state) => state.users);

  useEffect(() => {
    if (!data || data.length === 0) {
        dispatch(fetchUsers());
    }
  }, [data]);

  if (loading) return <p>로딩 중...</p>;
  if (error) return <p>에러 발생: {error}</p>;

  return (
    <ul>
      {data.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

서버 요청 하나 처리하려고…

  • 액션 타입 만들고

  • 액션 생성자 만들고

  • thunk 만들어서 비동기 처리하고

  • 리듀서에 로직 넣고

  • 컴포넌트에서 디스패치하고, 셀렉터 쓰고…

하나의 요청을 처리하는데 진짜 50줄 가까이 되는 코드를 쓰고 있었어요.
게다가 요청 중복, 캐싱, stale 처리도 직접 해야 하고…


🧠 Redux의 한계

서버 상태는 클라이언트 상태랑 다르다!

구분클라이언트 상태서버 상태
내가 만든 상태O (ex. 모달, 토글)X (서버에서 옴)
변경 주체내가 바꿈서버가 바꿈
stale 가능성거의 없음항상 있음
캐싱 필요XO
공유 필요드물게자주

useState나 Redux는 원래 내가 직접 바꾸는 상태를 위한 도구라서,
서버에서 받아오는 데이터를 다루기엔 뭔가 억지로 맞춰 쓰는 느낌이었어요.

문제설명
🔁 요청 중복 처리 안 됨같은 API 요청도 중복 실행됨
🧠 상태 구조가 무거움액션, 리듀서, thunk까지 다 만들어야 함
🛠️ 갱신/동기화 직접 처리stale 처리나 refetch는 수동으로 구현해야 함
🧱 단순 API 요청에도 보일러플레이트 쌓임요청 하나에 최소 50줄 필요

즉, 가능은 했지만 모든 로직을 직접 구현해야했기에..

시대서버 상태 관리 방식설명
🔙 예전Redux + thunk/saga다 직접 만들었음
🔁 과도기Redux Toolkit QueryRTK에서 자체적인 캐싱 기능 추가
🔥 현재React Query, SWR서버 상태를 전문 도구로 관리

💡 React Query의 등장

React Query는 딱 이 문제를 해결해주는 도구예요.

"서버 상태는 클라이언트 상태랑 다르니까, 따로 관리하자!"

그래서 React Query는 다음을 자동화합니다:

기능역할
✅ 캐싱같은 요청 반복 방지
✅ 공유여러 컴포넌트에서 결과 공유
✅ 구독데이터 변경 시 자동 갱신
✅ stale 관리최신성 판단 및 갱신 트리거
✅ refetch 제어수동/자동 갱신 모두 지원

직접 해보니 알겠더라고요.
내가 매번 하던 귀찮은 일들을 React Query가 다 해주는구나 하는 걸..!


React Query는 위의 복잡한 상태 관리 없이도 서버 데이터를 useState처럼 쉽게 다룰 수 있게 해줍니다:

const { data, error, isLoading } = useQuery(['posts'], fetchPosts);

React Query는 어떻게 우리가 요청한 서버 데이터를 자동으로 추적하고 관리할 수 있을까?


🔍 React Query의 두 가지 핵심 원리

1️⃣ 서버 데이터 상태를 추적한다

React Query는 데이터를 요청하고 상태를 전역 캐시에 저장합니다:

const queryCache = {
  'todos': {
    status: 'idle',     // 'loading' | 'success' | 'error'
    data: null,
    error: null,
    observers: []
  }
};
  • 요청 전: status = 'idle'

  • 요청 중: status = 'loading'

  • 성공 시: status = 'success', data = ...

  • 실패 시: status = 'error', error = ...


2️⃣ 누가 이 데이터를 보고 있는지 기억하고, 바뀌면 다시 그려준다

React Query는 이 데이터를 쓰고 있는 컴포넌트의 리렌더링 함수를 observers 배열에 저장해둡니다.

function useQuery(key, fetchFn) {
  const [, forceUpdate] = useState(0); // 현재 컴포넌트를 다시 그리는 함수

  useEffect(() => {
    const update = () => forceUpdate(n => n + 1); 
    queryCache[key].observers.push(update);

    return () => {
      // 컴포넌트가 사라지면 등록 해제
      queryCache[key].observers = queryCache[key].observers.filter(cb => cb !== update);
    };
  }, []);

  if (!queryCache[key].data) {
    fetchFn()
      .then(data => {
        queryCache[key].data = data;
        queryCache[key].status = 'success';
        queryCache[key].observers.forEach(fn => fn()); // 모든 컴포넌트 리렌더링
      })
      .catch(err => {
        queryCache[key].error = err;
        queryCache[key].status = 'error';
        queryCache[key].observers.forEach(fn => fn());
      });
  }

  return queryCache[key];
}

🛠 이게 어떻게 작동하는 걸까?

React는 상태가 바뀌면 무조건 컴포넌트를 다시 리랜더링합니다.
커스텀 훅 내부에서도 useState, useReducer 등으로 상태를 바꾸면, 그 훅을 사용한 컴포넌트는 당연히 리렌더링됩니다. React Query는 이 원리를 이용해서 "컴포넌트를 강제로 다시 그리게 만들고 싶을 때 setState를 사용하여 forceUpdate가 강제 리랜더링하도록 만든 업데이트 함수를 만들어줍니다.

그래서 forceUpdate()를 실행하면 그 컴포넌트만 다시 그림!
React Query는 이 update() 함수를 observers 배열에 저장해두었다가,
데이터가 바뀌면 이걸 전부 실행해서 자동 리렌더링이 일어나게 하는 거예요.


✅ 정리: React Query의 작동 원리는?

개념역할
useState컴포넌트 리렌더링 기능 (setState) 제공
useEffect컴포넌트가 mount/unmount될 때 리액션
observers데이터를 사용하는 컴포넌트의 리렌더 함수들을 저장
fetchFn데이터를 불러오는 함수
queryCache서버 상태 + 구독자(observer)를 저장하는 전역 상태 저장소

🎯 결론

React Query는 useState + useEffect 기반의 옵저버 패턴을 활용하여
서버 상태를 자동으로 추적하고, 관련된 컴포넌트만 리렌더링해주는 똑똑한 라이브러리입니다.

6 views

More from this blog

Agent Skill로 README 자동화하기

저는 요즘 업무에서 AI 에이전트를 적극적으로 활용하여 반복적인 업무를 최대한 AI로 자동화하기 위해 공부 중인데요. 이번에 했던 작업 중 하나가 README 파일 생성 자동화입니다. AI에게 README를 써달라고 하면 꽤 그럴싸한 결과물이 나옵니다. 하지만 여러 프로젝트에 반복하다 보면 프롬프트가 모호할 때마다 매번 다른 방식으로 문제를 풀기 때문에 들

Mar 31, 20268 min read8

배열을 처리하는 세 가지 방법 (chaining, reduce, for loop)

개발을 하다 보면 배열 데이터를 가공할 일이 정말 많은데요.저는 평소에는 filter, map 같은 메서드 체이닝 방식을 주로 사용합니다. 읽기 쉽고, 데이터 흐름이 단계별로 명확하게 보이기 때문이죠. 하지만 어떠한 계기로(?) 인해 이런 의문을 가지게 되었습니다.. "만약 데이터가 수천만 건으로 늘어난다면? 배열을 여러 번 순회하는 체이닝 방식이 성능 병목을 일으키지는 않을까?" 혹은 "reduce 하나로 합치는 게 베스트일까?" 이 질문을 ...

Feb 4, 20263 min read19

[React] 좋은 추상화란 무엇인가(2) 잘못된 추상화는 중복보다 비싸다

지난 글을 보면 성급했던 재사용 컴포넌트 경험으로 고생을 한 적이 있었는데요.(아직 안 보셨다면? [1편: 좋은 추상화란 무엇인가(1)?] ) 애초에 왜 이런 고생을 했을까를 생각해 보면이유는 단순했습니다. 중복 코드가 거슬리니 바로 추상화를 해버린 것이었죠..똑같은 실수를 반복하지 않기 위해 저만의 기준이 필요하다고 느꼈습니다. 이에 대해 이미 깊이 고민하셨던 Cher Scarlett님과 Kent C. Dodds님의 글을 보면서, 제가 취해야...

Dec 16, 20253 min read25

[React] 좋은 추상화란 무엇인가(1) ? 유연한 컴포넌트 설계를 위한 제어의 역전

재사용을 위해 만든 컴포넌트가, 시간이 지날수록 오히려 재사용하기 어려워진 경험이 있으신가요? 저는 최근 프로젝트에서 SearchFilter 컴포넌트가 그러한데요 프로젝트에는 여러 목록 페이지가 존재했고, 페이지마다 검색 조건이 제각각이었습니다. 그래서 처음에는 Configuration 배열만 넘기면 알아서 그려주는 컴포넌트를 만들었습니다. // ❌ 초기의 설정 기반 접근 (Configuration) const filterFields = [ ...

Dec 9, 202510 min read36

React 시대의 함수형 프로그래밍

바닐라 JS처럼 개발자가 DOM과 상태 변경을 직접 제어하던 시기에는 불변성·순수 함수 같은 함수형 프로그래밍 철학이 지금처럼 중요하지는 않았습니다. 하지만 React(Vue/Redux/SWR/Recoil 등)처럼 선언적·상태 기반 UI가 등장하면서 상태를 “데이터 흐름의 스냅샷”으로 다뤄야 했고, 그때부터 함수형 프로그래밍이 중요해지기 시작했습니다. 🔥 바닐라 JS에서는 왜 FP가 별로 중요하지 않았을까? 바닐라 JS 시대의 UI 개발은...

Dec 8, 20255 min read35
D

Dlog!

39 posts

배우고, 나누며, 함께 성장하는 개발자 🚀