2025-09-20 15:31
- 
React Query는 단순한 데이터 패칭 라이브러리를 넘어선 비동기 상태 관리자이다.
 - 
복잡한 로딩, 에러 처리, 캐싱, 동시성 문제 등 서버 상태 관리를 자동으로 해결해 개발 효율을 극대화한다.
 - 
이로써 개발자는 데이터 관리에 대한 고민을 덜고 핵심 로직에 집중하여 ‘5시 퇴근’을 현실로 만들 수 있다.
 
1. 기술의 부상 그 이면의 철학 5시 퇴근 규칙
왜 어떤 기술은 갑자기 폭발적인 인기를 얻고 또 다른 기술은 역사 속으로 사라지는가. 여기에는 다양한 이유가 있겠지만, 그 기저에는 하나의 공통된 원리가 존재한다. 바로 ‘5시 퇴근 규칙’이다. 이 규칙은 문제를 해결하는 추상화의 수준이 점차 높아져서, 평범한 개발자가 더 이상 그 문제에 대해 고민하지 않고 곧바로 퇴근할 수 있을 만큼 쉬워질 때 해당 기술이 대중화된다는 이론이다.
React Query가 바로 이 이론의 산 증인이다. 유타주의 한 개발자가 만든 이 라이브러리는 현재 전 세계 React 애플리케이션 6개 중 1개에서 사용되며, 매주 330만 회가 넘는 다운로드를 기록하고 있다. 이 엄청난 성장세의 원동력은 바로 개발자가 데이터 관리에 대한 끝없는 고민에서 벗어나, 5시에 퇴근할 수 있도록 도와주었기 때문이다. 그렇다면 과연 React Query는 어떤 문제를 해결해 주었을까. 이 문제를 이해하기 위해 먼저 React의 기본부터 되짚어볼 필요가 있다.
2. 리액트의 기본 원리 복습
2.1. UI 라이브러리로서의 정체성
가장 근본적인 형태로, React는 사용자 인터페이스를 구축하기 위한 라이브러리다. 그 정신은 ‘View는 Application State의 함수’라는 단순한 공식으로 요약된다. 즉, 개발자는 애플리케이션의 상태(State)가 어떻게 변하는지에만 집중하면, React가 알아서 그에 맞춰 UI를 업데이트해 준다.
2.2. 컴포넌트 기반의 구성
이러한 React의 철학은 컴포넌트라는 개념을 통해 구현된다. 컴포넌트는 특정 UI 조각의 시각적 표현뿐만 아니라, 그와 관련된 상태와 로직을 함께 캡슐화한다. 마치 함수를 조합해 새로운 값을 만들어내듯이, 컴포넌트를 조합해 복잡한 UI를 손쉽게 구축할 수 있다. 이는 React의 가장 강력한 장점이자 개발자가 가장 익숙한 부분이다.
3. 훅스 시대의 데이터 갈증
초기 React는 UI 조합에 강점을 가졌지만, UI와 직접 관련 없는 로직을 재사용하고 조합하는 데는 어려움이 있었다. 이 문제를 해결하기 위해 등장한 것이 바로 React Hooks다. 컴포넌트가 UI의 재사용성을 높여주듯, 훅스는 비-UI 로직의 재사용성을 혁신적으로 개선했다.
useState, useEffect, useContext 등 다양한 내장 훅스가 제공되었지만, 웹 애플리케이션 구축에 있어 가장 흔한 기능인 ‘데이터 가져오기(Data Fetching)’를 위한 전용 훅은 포함되지 않았다. 그 결과, 개발자들은 useEffect와 useState를 조합하여 데이터를 가져오는 로직을 직접 구현해야 했다. 간단해 보이는 이 조합이 바로 복잡성의 시작이었다.
4. useEffect 데이터 패칭의 숨겨진 함정들
겉보기에는 매우 간단한 useEffect 기반의 데이터 패칭 코드는 사실 실제 프로덕션 환경에서 수많은 문제를 야기한다. 튜토리얼 코드에서는 다루지 않지만, 이 숨겨진 문제들은 개발자의 퇴근을 막는 주된 원인이 된다.
4.1. 로딩과 에러 상태 관리의 부재
가장 먼저 맞닥뜨리는 문제는 로딩과 에러 상태의 관리다. 데이터를 가져오는 동안 사용자에게 로딩 상태를 보여주지 않거나, 실패했을 때 에러를 처리하지 않으면 사용자 경험은 최악이 된다. 이는 흔히 ‘누적 레이아웃 변경(Cumulative Layout Shift)‘이나 ‘무한 스피너’와 같은 심각한 UX 문제를 초래한다. 해결을 위해서는 isLoading과 isError 같은 추가적인 상태 변수를 도입해야만 한다.
4.2. 보이지 않는 버그 경쟁 조건 (Race Condition)
더욱 심각한 문제는 비동기 요청의 특성에서 비롯되는 ‘경쟁 조건’이다. useEffect는 의존성 배열이 바뀔 때마다 실행된다. 사용자가 빠르게 버튼을 여러 번 클릭해 ID를 변경하면, fetch 요청이 여러 번 발생한다. 이때 각 요청의 응답이 도착하는 순서는 보장되지 않는다. 예를 들어, ID 2에 대한 요청이 ID 1에 대한 요청보다 먼저 완료된다면, UI는 잠시 ID 2의 데이터를 보여주다가, 뒤늦게 도착한 ID 1의 응답으로 인해 다시 ID 1의 데이터를 보여주는 현상이 발생한다. 이는 사용자에게 혼란을 주고 데이터 불일치 버그를 유발한다.
이 문제를 해결하기 위해서는 useEffect의 ‘정리 함수(Cleanup Function)‘를 활용해야 한다. useEffect가 다음 의존성 변화로 인해 다시 실행되거나 컴포넌트가 언마운트될 때 호출되는 이 함수를 이용해 이전 요청의 응답을 무시하도록 코드를 작성하는 방법이다.
useEffect(() => {
  let ignore = false;
  fetchData(id).then(data => {
    if (!ignore) {
      // 최신 요청일 경우에만 상태 업데이트
      setData(data);
    }
  });
 
  // 정리 함수: 다음 요청이 발생하면 이전 요청 무시
  return () => {
    ignore = true;
  };
}, [id]);이처럼 복잡한 로직을 추가해야 비로소 기본적인 버그를 해결할 수 있게 된다.
5. 추상화의 여정 커스텀 훅스와 전역 상태
useEffect의 지옥에서 벗어나기 위해 개발자들은 데이터 패칭 로직을 커스텀 훅으로 추상화하는 방법을 택했다. 하지만 이 또한 완벽한 해결책은 아니었다.
5.1. 데이터 중복 문제의 발생
커스텀 훅을 사용해도 컴포넌트마다 데이터를 개별적으로 가져오므로, 동일한 데이터를 여러 컴포넌트에서 사용하면 요청이 중복되어 발생한다. 이로 인해 불필요한 네트워크 트래픽과 서버 부하가 발생하고, 사용자에게 여러 개의 로딩 스피너가 독립적으로 보여지는 비효율적인 상황이 벌어진다.
5.2. 전역 상태 관리 Context의 도입
이를 해결하기 위해 개발자들은 데이터를 상위 컴포넌트나 Context로 이동시켜 여러 컴포넌트가 공유하도록 만들었다. 데이터를 전역 상태에 저장함으로써, 한 번 가져온 데이터를 메모리에 캐시하고 다른 컴포넌트에서 재사용할 수 있게 된다. 이는 데이터 중복 문제를 해결하고 예측 가능한 데이터 흐름을 만들어내는 획기적인 발전이었다.
6. Context의 한계와 새로운 고민
하지만 Context를 사용한 데이터 캐싱 방식 또한 새로운 문제들을 야기했다.
6.1. 비효율적인 업데이트와 리렌더링
Context는 동적인 데이터 배포에 최적화된 도구가 아니다. Context의 값이 변경되면 해당 Context를 사용하는 모든 하위 컴포넌트가 리렌더링된다. 만약 여러 개의 API 응답을 하나의 Context에 저장한다면, 특정 데이터 하나만 바뀌어도 관련 없는 다른 컴포넌트까지 불필요하게 리렌더링되는 성능 문제를 겪게 된다.
6.2. 캐시 무효화의 어려움
Context 기반 캐시의 가장 큰 문제는 ‘캐시 무효화’다. 서버 데이터는 언제든 외부에서 변경될 수 있으므로, 클라이언트의 캐시는 최신 상태를 반영하도록 주기적으로 무효화하고 업데이트해야 한다. 하지만 Context만으로는 이러한 캐시 정책(예: 데이터를 얼마 동안 신선한 상태로 유지할 것인지, 언제 다시 데이터를 가져올 것인지)을 관리하는 것이 매우 복잡하고 어렵다. 이로 인해 개발자는 수동으로 캐시를 관리하는 지루하고 버그가 많은 작업을 해야 했다.
7. 상태의 이분법 클라이언트 상태와 서버 상태
위와 같은 문제들은 하나의 중요한 깨달음으로 이어진다. 바로 모든 상태를 동일하게 취급해서는 안 된다는 것이다. 모든 상태는 크게 두 가지로 분류된다.
| 구분 | 동기적 상태 (Client State) | 비동기적 상태 (Server State) | 
|---|---|---|
| 소유권 | 클라이언트 소유 (개발자가 직접 제어) | 서버 소유 (여러 사용자 및 서버 프로세스에 의해 변경 가능) | 
| 가용성 | 즉시 사용 가능 (메모리에 상주) | 즉시 사용 불가능 (네트워크 요청 필요) | 
| 특성 | 항상 최신 상태를 보장 | 언제든지 ‘부실한(stale)’ 상태가 될 수 있음 | 
| 관리 방법 | useState, useReducer, Redux, Zustand 등 | React Query, SWR, Apollo Client 등 | 
클라이언트 상태는 예측 가능하고, 통제하기 쉬우므로 기존의 상태 관리 라이브러리로 충분히 해결할 수 있다. 하지만 서버 상태는 이와 완전히 다른 특성을 가지므로, 서버 상태만을 위한 특별한 관리자가 필요했다.
8. 리액트 쿼리 진짜 정체성의 탄생
이러한 배경 속에서 React Query가 등장한다. 하지만 많은 사람들이 오해하는 것과 달리, React Query는 단순한 ‘데이터 패칭 라이브러리’가 아니다. 데이터를 가져오는 행위 자체는 쉬운 부분이고, 진정으로 어려운 것은 그 데이터를 ‘관리하는 것’이다.
React Query의 진짜 정체성은 서버 상태 관리자(Async State Manager)이다. 개발자는 그저 React Query에게 ‘이 URL에서 데이터를 가져와달라’고 요청하면 된다. React Query는 개발자가 제공한 Promise를 받아 그 결과를 애플리케이션의 필요에 따라 적절하게 분배하고 관리한다.
8.1. 약속 (Promise) 기반의 작동 방식
React Query는 데이터 패칭 로직 자체를 직접 구현하지 않는다. 개발자가 useQuery 훅에 비동기 함수(Promise를 반환하는 함수)를 제공하면, React Query는 그 함수를 호출하고 반환된 데이터를 관리한다.
8.2. 핵심 기능 자동화
React Query는 다음의 복잡한 작업들을 자동으로 처리한다.
- 
지능적인 캐싱: 한 번 가져온 데이터는 캐시하고, 동일한 데이터가 필요할 때 네트워크 요청 없이 캐시된 데이터를 즉시 제공한다.
 - 
백그라운드 동기화: 사용자가 브라우저 탭을 다시 활성화하거나 네트워크가 재연결될 때, 자동으로 최신 데이터를 백그라운드에서 다시 가져와 캐시를 업데이트한다.
 - 
데이터 무효화: 데이터가 변경(Mutation)될 때, 관련 캐시를 자동으로 ‘부실’ 상태로 만들고, 필요에 따라 다시 가져오게 유도한다.
 - 
로딩 및 에러 상태 관리:
isPending,isError,data와 같은 상태를 훅이 자동으로 반환해주므로, 개발자가 직접 상태 변수를 만들 필요가 없다. - 
중복 요청 제거 (Deduplication): 동일한 쿼리가 여러 컴포넌트에서 동시에 요청될 때, 하나의 요청만 발생시키고 그 결과를 모든 컴포넌트에 공유한다.
 - 
자동 재시도: 네트워크 오류 등으로 요청이 실패했을 때, 설정된 횟수만큼 자동으로 재시도한다.
 
9. 리액트 쿼리로 5시 퇴근하기
이제 React Query가 왜 ‘5시 퇴근 규칙’의 상징이 되었는지 명확해진다. useEffect와 useState를 사용해 직접 구현해야 했던 로딩, 에러, 경쟁 조건 처리, 캐싱, 데이터 무효화와 같은 끝없는 복잡성들은 이제 React Query라는 훌륭한 전문가에게 완전히 맡겨진다. 개발자는 더 이상 이러한 ‘더러운’ 작업에 대해 고민하지 않아도 된다.
React Query는 단순히 코드 몇 줄을 줄여주는 것이 아니라, 개발자의 정신적 부담을 덜어주는 라이브러리다. 복잡한 비동기 데이터 흐름을 우아하게 추상화함으로써, 개발자는 서버 상태에 대한 깊은 고민 없이 애플리케이션의 핵심 비즈니스 로직과 사용자 경험에만 온전히 집중할 수 있게 되었다. 이것이 바로 React Query가 수많은 개발자에게 ‘5시 퇴근’의 자유를 선사한 비결이다.