React Query 도입 시, 왜 상태 관리와 아키텍처도 함께 바꿔야 할까?

Sigrid Jin
25 min read6 days ago

--

React 애플리케이션에 React Query를 도입하면 데이터 패칭과 상태 관리가 크게 쉬워집니다. 하지만 단순히 React Query만 추가한다고 끝이 아닙니다. 기존 컨테이너 컴포넌트의 상태 관리 방식데이터 처리 계층도 함께 재고해야 합니다. 그래야만 React Query의 이점을 극대화하고, UI와 비즈니스 로직을 깔끔하게 분리할 수 있습니다.

이번 글에서는 React Query 도입 시 고려해야 할 상태 관리 요소, 데이터 핸들링 계층을 분리하는 이유와 장점, UI와 비즈니스 로직을 분리하는 실용 패턴을 살펴보겠습니다. 또한 실제 프로젝트 사례에서 발생한 문제와 해결 과정을 소개하고, 주니어 프론트엔드 개발자가 이러한 변화를 실무에 적용할 때 고려할 점과 점진적인 개선 전략을 제안해보도록 하겠습니다.

React Query 도입 시 함께 고려해야 할 상태 관리 요소

우선 React Query의 역할을 명확히 할 필요가 있습니다. React Query는 기본적으로 서버 상태(server state) 를 관리하는 라이브러리입니다. 서버와 클라이언트 간의 비동기 데이터 페칭, 캐싱, 동기화 등을 쉽게 해주죠. 반면 Redux나 MobX 같은 것은 클라이언트 상태(client state) 를 관리하는 도구입니다. 즉 React Query는 단순한 데이터 패칭 도구가 아니라, 비동기 상태 관리자로서 전역적인 서버 상태 캐시를 제공해주는 것입니다.

React Query 도입 후에는 기존 전역 상태 관리자(Redux) 의 부담이 크게 줄어듭니다. 서버에서 불러온 데이터 대부분은 이제 React Query가 관리하므로, 개발자가 직접 전역으로 관리해야 할 데이터가 매우 적어집니다. 실제로 TanStack Query 공식 문서에서도 “비동기 코드를 React Query로 모두 옮기고 나면 남는 진짜 글로벌 상태는 극소수”라고 언급합니다. 따라서 남은 것은 테마나 UI 모드처럼 클라이언트에 국한된 상태뿐일 가능성이 높습니다.

React Query를 도입하면, 과거에 Redux-Saga나 Thunk로 API 호출 결과를 전역 상태에 저장하고 관리하던 코드를 대체하게 됩니다. 예전에는 API 요청을 디스패치하고, 로딩 중임을 표시하고, 응답을 저장하고, 오류 처리를 하는 일련의 보일러플레이트가 있었을 겁니다. React Query는 이런 패턴을 몇 줄의 코드로 대체하여 캐싱까지 자동으로 처리해줍니다. 예를 들어, useQuery 훅 하나면 로딩 여부(isLoading), 오류(error), 데이터(data) 상태를 모두 관리해주기 때문에, 수동으로 useState로 로딩 플래그를 두거나 할 필요가 없습니다.

React Query를 도입할 때 흔히 실수하는 것이 데이터를 이중으로 관리하는 겁니다. 이미 React Query가 서버 데이터를 들고 있는데, 그것을 다시 Redux나 Context에 넣어 중복 저장하거나, 혹은 여전히 이전 방식의 useEffect + useState 패턴을 남겨두는 경우가 있죠. 이는 권장되지 않습니다. React Query의 권장 사항은 가능한 한 서버 상태를 다른 상태 관리 도구에 동기화하지 말라는 것입니다. 그렇게 하면 오히려 상태 소스가 두 군데가 되어 일관성 문제가 생길 수 있습니다.

React Query 도입 시에는 기존 전역 상태에서 서버 데이터 관련 부분을 걷어내거나 대폭 축소하고, React Query의 단일 소스를 참조하도록 리팩터링해야 합니다. 예컨대 React Query로 가져온 리스트를 굳이 Redux store에 다시 넣지 말고, 필요한 곳에서 useQuery로 동일한 키의 데이터를 구해서 쓰는 식입니다. React Query의 캐시는 전역적으로 공유되므로, 다른 컴포넌트에서 같은 키로 useQuery를 쓰면 똑같은 데이터를 바로 얻을 수 있습니다.

그렇다고 Redux나 Context 같은 게 완전히 불필요해졌다는 뜻은 아닙니다. React Query는 로컬/클라이언트 상태 관리까지 담당하지는 않습니다. 여전히 UI 상태유저 인터랙션에 의한 즉각적인 상태 등은 다른 도구로 관리해야 할 수 있습니다. 예를 들어 “다크 모드 설정”이나 “현재 열려있는 모달 창 여부” 같은 것은 서버와 무관한 상태이고, 이런 것까지 React Query로 관리하려고 하면 오용입니다. 이런 상태는 Context API나 경량 전역 상태 라이브러리(Recoil, Zustand 등)를 함께 사용하는 것이 좋습니다. 실제로 React Query + 원하는 전역 상태 라이브러리 조합이 많이 쓰이고, 공식 문서나 커뮤니티에서도 “React Query로 서버 상태를 관리하고, 남은 전역 상태는 최소화해서 필요한 경우 Recoil 등을 써라”라는 조언이 있습니다.

React Query를 도입할 때 캐싱 전략도 함께 고민해야 합니다. 예컨대 얼마나 오래 데이터를 신선하게 유지할지(staleTime), 언제 다시 패칭할지 등을 결정해야 하죠. 기존에는 컴포넌트 마운트 때마다 API를 호출했다면, 이제는 React Query의 캐시 기간을 조정하여 불필요한 재호출을 줄일 수 있습니다. 상황에 따라 실시간 동기화가 중요하면 refetchOnWindowFocus 같은 옵션을 켜둘 수 있고, 데이터가 잘 변하지 않는다면 staleTime을 크게 잡아 잦은 재호출을 피할 수도 있습니다. 이런 설정들은 전역 상태 관리 전략의 일부로 간주해야 합니다. React Query를 제대로 활용하려면, 어떤 데이터는 언제 새로고침되어야 하는지 팀 내에서 합의하고 적용하는 것이 중요합니다.

요약하면, React Query 도입은 곧 애플리케이션의 상태 관리 전략을 재편하는 일과 같습니다. 서버로부터 가져온 비동기 데이터는 React Query에게 맡기고, 기존 전역 상태(또는 컨테이너 컴포넌트의 상태)는 역할을 축소하거나 제거해야 합니다. 그리고 남은 클라이언트 상태에 대해서만 별도의 관리 방법을 적용하는 식으로 책임을 분리해야 합니다. 이러한 변화를 통하지 않으면 React Query를 도입하더라도 이점이 반감되고, 오히려 상태 관리가 이원화되어 혼란을 야기할 수 있습니다.

데이터 핸들링 계층을 분리해야 하는 이유와 장점

React Query를 도입하는 것과 별개로, 현대적인 React 아키텍처에서는 데이터 핸들링 계을 UI와 분리하는 것이 중요합니다. 여기서 데이터 핸들링 계층이란, 데이터를 가져오고 가공하고 상태를 결정하는 비즈니스 로직 부분을 말합니다. 전통적으로는 이 역할을 컨테이너 컴포넌트나 Redux의 이펙트(Saga 등)가 맡아왔고, UI 컴포넌트는 props로 완료된 데이터만 받아서 표시하는 방식이 많았습니다.

UI를 다루는 코드와 데이터 로직을 다루는 코드를 분리하면 각각의 관심사(concern)이 명확해집니다. 한 파일이나 컴포넌트가 둘 다 책임질 때 발생하는 복잡성을 줄이고, 코드를 더 이해하기 쉽게 만들어줍니다. 예를 들어 예전의 “컨테이너 vs 프레젠테이셔널 컴포넌트” 패턴은 이런 분리를 극단적으로 적용한 사례인데, 프레젠테이셔널 컴포넌트는 오로지 UI 렌더링만 담당하고 props로 주어진 데이터만 표시했기 때문에, 책임이 명확하고 재사용성이 높았죠. 이러한 분리를 통해 명확한 디커플링이 이루어지고, 한 부분의 변경이 다른 부분에 영향을 최소화하게 됩니다.

데이터 fetching이나 변환 로직을 한 곳(커스텀 훅 또는 별도 모듈)에 몰아두면, 여러 컴포넌트에서 그 로직을 공유할 수 있습니다. 같은 API 호출이나 동일한 데이터 가공이 필요할 때 코드를 복붙하지 않고 재사용하게 되어 중복을 줄일 수 있습니다. 예를 들어 useTodos라는 훅을 만들어 두면, 리스트를 필요로 하는 어떤 컴포넌트라도 이를 호출해서 동일한 데이터를 얻고, 또 React Query 캐시까지 공유할 수 있습니다. 이렇게 하면 데이터의 단일 소스를 유지할 수 있어 일관성이 높아집니다.

애플리케이션의 데이터 소스가 변경되거나 비즈니스 로직이 변경되더라도, 이를 처리하는 계층이 분리되어 있다면 UI 레이어를 건드리지 않고도 수정이 가능합니다. 예를 들어 REST API에서 GraphQL로 변경되거나, API 스펙이 바뀌는 경우, 데이터 핸들링 계층의 코드만 수정하면 됩니다. UI 컴포넌트들은 여전히 동일한 props나 훅 인터페이스를 통해 데이터를 받으므로 내부 변경이 외부에 전파되지 않습니다. 이는 시스템을 더 안정적으로 만들고, 부분적인 리팩토링을 수월하게 해줍니다.

비즈니스 로직이 UI와 분리되어 있으면 단위 테스트를 작성하기도 수월합니다. UI 컴포넌트는 주로 렌더링 결과를 스냅샷 테스트하거나, 간단한 상호작용만 확인하면 되고, 복잡한 로직은 별도의 모듈이나 훅을 테스트하면 됩니다. 예를 들어 API 응답을 가공하는 함수나 커스텀 훅을 분리해 두면, 해당 부분은 React 컴포넌트 렌더링 없이도 독립적으로 테스트할 수 있습니다. 반면에 로직이 컴포넌트에 섞여 있으면 UI를 렌더링하면서 모든 상태 변화를 테스트해야 해 복잡해지죠. 실제로 한 리팩토링 사례에서는 데이터 fetch 함수를 컴포넌트에서 분리하고, 도메인 엔티티와 DTO(Data Transfer Object)를 분리하는 등의 단계를 거쳐 비즈니스 로직을 컴포넌트에서 떼어냈더니, UI 코드가 서버나 데이터와 격리되고 비즈니스 로직이 UI 프레임워크에 독립적이 되어 테스트 가능성이 크게 향상되었다는 보고가 있습니다.

데이터 계층이 분리되어 명확하면, 성능 최적화 지점을 찾기도 쉽습니다. 예를 들어 데이터 fetching 로직을 모아두면 캐싱 전략을 전역적으로 조절하기 수월하고, 불필요한 재렌더를 유발하는 데이터 흐름을 파악하기 쉬워집니다. React Query 자체도 동일한 쿼리를 두 군데에서 쓰면 한 번만 네트워크 요청을 보내고, 결과를 공유하는 최적화를 해주는데, 만약 각각이 분리되지 않고 여기저기 흩어져 있었다면 이런 최적화 혜택을 놓치거나 중복 호출을 일으키기 쉬웠을 것입니다.

한마디로, 데이터 핸들링 계층의 분리는 더 나은 아키텍처를 위한 필수 요소입니다. React Query 도입과 맞물려 이 부분을 개선하면, UI는 가벼워지고 필요한 데이터와 콜백만 받아서 쓰는 순수 뷰가 되며, 데이터 관리 로직은 한 곳에 모여 체계적으로 동작하게 됩니다. 결과적으로 유지보수가 쉬워지고, 새로운 요구사항에도 탄력적으로 대응할 수 있는 코드베이스가 됩니다.

UI와 비즈니스 로직을 분리하는 실용적인 패턴

실무에서 UI 코드와 데이터 로직을 어떻게 분리할 수 있을까요? 과거부터 사용되어 온 컨테이너-프레젠테이셔널 컴포넌트 패턴과, 최근 선호되는 커스텀 훅 활용 방안을 중심으로 살펴보겠습니다.

컨테이너 & 프레젠테이셔널 컴포넌트 패턴은 리액트 초기 시절부터 사용된 패턴으로, 컴포넌트를 역할에 따라 두 가지로 나눕니다. 프레젠테이셔널 컴포넌트(presentational component) 는 말 그대로 UI를 표현하는 데 집중합니다. 레이아웃, 스타일, 사용자 입력 처리(그러나 그 입력을 어떻게 처리할지는 모름) 등 시각적인 부분만 담당하고, 자체적으로 상태를 갖지 않거나 UI 상태만 갖습니다. 대신 필요한 데이터나 콜백은 모두 props로 받아옵니다.

이에 반해 컨테이너 컴포넌트(container component) 는 데이터를 가져오고 가공하며 상태를 관리하는 부분에 집중합니다. 이 컨테이너가 API 호출을 하거나, React Query의 useQuery를 호출하거나, 전역 상태를 구독해 필요한 정보를 얻은 뒤, 그 결과를 프레젠테이셔널 컴포넌트에 props로 넘겨주는 것입니다. 이렇게 하면 프레젠테이셔널 컴포넌트는 오직 props에 의존하여 렌더링되므로 순수함수처럼 동작하게 되고, 컨테이너는 비즈니스 로직에만 전념하게 됩니다. 이 패턴은 명확한 역할 분리를 통해 재사용성과 테스트 용이성을 높여주지만, 컴포넌트를 쌍으로 만들어야 하고 계층이 깊어지는 단점도 있습니다.

함수형 컴포넌트와 훅(Hooks)의 등장 이후로는, UI와 로직 분리에 커스텀 훅을 활용하는 방식이 인기를 얻고 있습니다. 사실상 컨테이너 컴포넌트의 역할을 굳이 컴포넌트로 만들지 않고, 훅으로 치환하는 접근입니다. 예를 들어 DashboardContainer 컴포넌트를 만들었던 대신 useDashboardData라는 훅을 만들고, 내부에서 React Query나 필요한 상태 관리를 수행하도록 합니다. 그리고 UI 컴포넌트인 Dashboard (프레젠테이셔널 역할)에서는 이 훅을 호출하여 데이터와 메서드를 얻습니다.

훅을 사용하면 별도의 컴포넌트 계층을 추가하지 않으면서도 로직을 분리할 수 있고, 필요하면 여러 UI 컴포넌트에서 이 훅을 재사용할 수도 있습니다. React Query와도 궁합이 좋아서, 커스텀 훅 내부에서 useQuery를 호출해두면 원하는 데이터 페칭 로직을 한 군데로 모으는 효과도 있습니다. 아울러 훅 자체는 UI를 직접 렌더링하지 않으므로, 테스트 시 훅을 호출해서 리턴 값을 확인하거나, 필요한 경우 훅을 감싸는 로직을 모킹(mocking)하는 식으로 비즈니스 로직만 따로 검증할 수도 있습니다.

만약 애플리케이션 전역에서 혹은 트리 깊숙한 곳의 여러 컴포넌트가 공유해야 하는 상태나 로직이 있다면, 리액트 Context API를 사용해보는 것도 하나의 패턴입니다. 컨텍스트를 하나의 상태 관리 계층으로 보고, 그 안에 데이터 fetching 로직이나 상태 변경 로직을 넣어 Provider로 하위 트리에 공급하는 겁니다. Consumer 격인 컴포넌트들은 여전히 UI만 담당하고, 필요한 데이터나 액션은 useContext로 받아 씁니다.

다만 컨텍스트는 남발하면 앱 성능에 영향이 있을 수 있고, React Query 같은 라이브러리와도 적절히 섞어 써야 하기 때문에 신중한 적용이 필요합니다. 앞서 언급한 대로, React Query로 관리되는 서버 상태를 굳이 컨텍스트로 다시 제공할 필요는 없지만, React Query를 쓰지 않는 극히 로컬한 상태(테마, 폼 입력 일시 저장 등)에 대해서 컨텍스트를 사용할 수 있습니다.

최근 UI 라이브러리들(headless UI, Downshift 등)에서 채택하는 Headless Component 개념도 UI/로직 분리의 한 형태입니다. 렌더링은 사용자가 알아서 하고, 라이브러리는 로직만 제공하는 식인데,화면에 아무 UI도 가지지 않는 컨테이너 컴포넌트를 훅이나 render prop으로 제공하는 패턴이라 볼 수 있습니다. 예를 들어 children-as-a-function 패턴으로 { data, error, isLoading } => ...UI... 형태를 제공하는 컴포넌트를 만들 수도 있지만, React Query 자체가 훅 형태로 이 역할을 하기 때문에 헤드리스 컴포넌트보다는 커스텀 훅 쪽이 더 직접적으로 사용됩니다.

중요한 점은, 항상 완벽하게 분리하는 것이 정답은 아니라는 것입니다. 경우에 따라 약간의 로직은 UI 컴포넌트 안에 넣어도 괜찮습니다. React Query의 useQuery 훅을 컴포넌트에서 바로 호출하는 것도 때로는 문제없습니다. Mark Erikson(리덕스 메인테이너)이 말했듯이, 훅을 어디서나 쓰게 되면서 컨테이너/프레젠테이션 패턴이 예전만큼 꼭 필요하지 않게 되기도 했습니다. 실제로 모든 것이 트레이드오프이고, 프로젝트 상황에 따라 적절한 균형을 잡아야 합니다.

예를 들어 정말 단순한 UI 버튼 컴포넌트가 자신의 내부 상태로 UI 효과를 주는 건 문제없지만, 그 버튼이 클릭될 때 데이터를 가져와야 한다면 그건 별도의 훅이나 컨테이너가 담당하게 하는 식으로 선을 긋는 게 좋겠죠. 반대로 대시보드 화면처럼 복잡한 페이지는 컨테이너/훅을 활용해 분리하는 편이 유지보수에 이롭습니다. 즉, 분리가 이득을 보는 부분에 한해 적용하고, 오버엔지니어링이 되지 않도록 주의하는 현실적인 접근이 필요합니다.

정리하면, UI와 비즈니스 로직 분리를 위한 패턴은 컨테이너 컴포넌트 패턴커스텀 훅 패턴이 대표적이며, 상황에 따라 컨텍스트나 기타 기법을 조합합니다. React Query를 사용할 때도 이러한 패턴을 활용하면 훨씬 깔끔한 구조로 데이터를 다룰 수 있습니다. 컨테이너/훅에서 React Query의 useQuery를 호출해 서버 데이터를 얻고 가공한 뒤, UI 컴포넌트는 그것을 props나 훅 반환값으로 받아서 표시만 하는 흐름을 만들어 보세요. 이렇게 하면 앞서 말한 다양한 장점을 실질적으로 누리게 될 것입니다.

실제 사례 문제점과 해결 과정

Redux와 컨테이너 컴포넌트 기반으로 데이터 패칭을 처리하는 웹이 있습니다. 각 페이지마다 컨테이너 컴포넌트가 useEffect로 API를 호출하고, 로딩 상태와 응답 데이터를 useState나 Redux store에 저장한 뒤, 하위 프레젠테이션 컴포넌트에 전달하는 구조였죠. 시간이 지나면서 API 호출 중복, 상태 일관성 문제(한 페이지에서 갱신된 데이터를 다른 페이지에서 바로 못 쓰는 등)가 늘어났고, 코드량도 방대해졌습니다. 이를 해결하기 위해 React Query를 도입하기로 결정했습니다.

처음에는 “기존 코드를 크게 안 건드리면서 React Query를 붙여보자” 는 접근을 했습니다. 예를 들어, Redux로 관리되던 몇몇 데이터에 대해 React Query의 useQuery로 데이터 fetch를 대체하고, 응답을 받아 곧바로 Redux 액션을 디스패치하여 store를 갱신하도록 한 것입니다. 또는 컨테이너 컴포넌트에서 useEffect로 호출하던 API를 useQuery로 바꿨지만, 여전히 예전의 isLoading 상태나 error 상태 관리 로직을 일부 남겨두기도 했습니다. 이렇게 절충안을 시도한 이유는, 한 번에 구조를 모두 바꾸면 위험하니 점진적으로 하려는 의도였는데, 오히려 새로운 혼란과 버그가 생겨났습니다.

가장 큰 문제는 이중 상태중복 호출이었습니다. 일부 컴포넌트는 React Query로 데이터를 가져오면서도 결과를 다시 로컬 state에 넣어 관리하고 있었는데, 개발자들이 둘 중 어느 것을 소스로 삼아야 할지 혼란을 겪었습니다. 어느 곳은 useQuerydata를 직접 쓰고, 또 어느 곳은 여전히 전역 store에 있는 값을 구독하는 식으로 섞여 있었죠. 그 결과 같은 데이터에 대해 두 번 API 호출이 일어나는 경우도 있었고, React Query 쪽 캐시만 갱신되고 기존 store는 안 갱신되어 UI에 오래된 데이터가 남는 버그도 발생했습니다. 실제로이슈 중 하나는 어떤 목록 데이터를 편집한 후 화면에 반영되지 않는 현상이었는데, 원인을 추적해보니 업데이트 뮤테이션은 React Query로 처리했지만 정작 화면은 이전에 Redux store에서 가져온 데이터를 보여주고 있어서 벌어진 일이었습니다.

또 다른 문제는 불필요한 복잡도 유지였습니다. React Query 도입 전의 컨테이너 컴포넌트들은 대부분 isLoading, hasError, data와 같은 state와 이를 제어하는 effect를 가지고 있었는데, React Query 도입 후 이 로직을 상당 부분 걷어낼 수 있었습니다. 그러나 초기 도입 시에는 혹시 모를 상황에 대비한다며 옛 코드를 주석 처리만 해두거나, 새로운 방식과 함께 조건부로 남겨둔 경우가 있었어요. 이것이 코드베이스에 죽은 코드나 다름없는 형태로 남아 가독성을 해쳤습니다.

구체적으로 어떤 조치를 취했는지 살펴보도록 하죠.

각 페이지의 컨테이너 컴포넌트에서 더 이상 useState로 로딩/에러/데이터를 관리하지 않도록 변경했습니다. 대신 해당 컴포넌트 최상단에서 React Query의 useQuery를 호출해 필요한 데이터를 얻고, isLoading, error 등을 바로 받아 씁니다. 그리고 과거에 useEffect 안에서 했던 API 호출 및 상태 업데이트 코드는 모두 삭제했습니다. 덕분에 컨테이너 컴포넌트의 코드가 절반 이하로 줄었고, 의존성도 감소했습니다. 이제 컨테이너는 React Query 훅의 thin wrapper 정도 역할만 남게 되었죠.

Redux store에서 서버 데이터와 관련된 슬라이스(slice)를 아예 제거하거나, 필요한 일부 경우를 제외하고는 더 이상 사용하지 않도록 했습니다. 예를 들어 유저 프로필 정보처럼 앱 여러 곳에서 필요했던 데이터도, useUserProfileQuery 같은 훅을 만들어 필요한 곳에서 호출하게 수정했습니다. Redux를 통해 프로필을 가져오던 옛 코드는 제거했고, 해당 컴포넌트들이 모두 새로운 훅을 사용하도록 일괄 변경했습니다. 이렇게 하니 Redux 액션/리듀서/사가 등 부수 코드가 대폭 줄었고, 동일한 데이터를 두 군데에 저장하지 않게 되어 일관성 문제가 사라졌습니다.

흩어져 있던 API 호출 로직을 한데 모으고, React Query 훅으로 래핑하는 작업도 했습니다. 예전에는 각 페이지 컴포넌트 파일에서 API 엔드포인트 URL과 axios 호출이 있었다면, 이를 /apis 혹은 /services 디렉토리에 모아 API 모듈을 만들었습니다. 그런 다음 React Query 훅들은 이 API 모듈을 사용하도록 했습니다. 이 계층 분리를 통해, 나중에 API가 변경돼도 훅 내부만 고치면 되고, 컴포넌트 코드는 손댈 필요가 없도록 했습니다. 또한 여러 컴포넌트에서 같은 훅을 재사용하니 캐시 공유도 원활해졌습니다. 동일 쿼리키를 쓰면 자동으로 캐시를 공유하고, 중복 호출을 막아주는 React Query의 특성을 적극 활용하는 거죠.

위의 변경을 한꺼번에 모든 곳에 적용한 것은 아닙니다. 우선 문제가 두드러진 일부 핵심 흐름(위의 프로필 업데이트 시 즉시 반영 문제 등)부터 고쳤습니다. 특정 버그를 해결하기 위해 프로필 조회/수정 관련 부분을 먼저 React Query 중심으로 재구현하고, QA를 거쳐 배포했습니다. 버그가 해결되자, 팀은 같은 원인이 있을 법한 다른 부분도 차례로 점검하여 비슷한 패턴의 개선을 진행했습니다. 이러한 점진적 리팩토링 덕분에 큰 사고 없이 단계별로 전환할 수 있었습니다. 한 번에 대규모 변화를 주면 새로운 버그가 생길 위험이 크지만, 기능 단위로 하나씩 고쳐나가고 결과를 확인하면서 진행하니 안정성이 확보됐습니다.

리팩토링과 함께, 팀 코드 컨벤션도 업데이트했습니다. 예를 들어 앞으로 새로운 기능을 개발할 때 데이터 가져오기 로직은 반드시 React Query를 사용하고, 절대 Redux에 넣지 않는다거나 useEffect로 API 호출하지 않는다는 원칙을 명문화했습니다. 또 컴포넌트는 가능하면 훅을 활용해 필요한 데이터만 받아서 사용할 것 (컨테이너/프레젠테이션 분리 권장) 등의 가이드를 공유했습니다. 주니어 개발자들도 이러한 원칙 하에서 코드 작성과 리뷰를 받도록 해서, 일관된 패턴으로 코드베이스가 정돈되도록 했습니다.

이러한 개선 과정을 거치고 나니 몇 가지 긍정적인 효과가 나타났습니다. 우선, 문제로 보고되었던 데이터 미반영 버그들이 해소되었습니다. React Query의 캐시와 상태를 신뢰하면서, 한 소스의 데이터만 바라보게 하니 업데이트 누락이나 이중 업데이트 문제가 없어졌습니다. 또한 API 호출 수가 줄고 응답 속도가 개선되었습니다.

이전에는 페이지 전환 시마다 같은 데이터를 중복 fetch하던 것이 React Query 캐시 덕분에 필요할 때만 불러오게 되었고, staleTime을 조정하여 적절히 최신 데이터만 가져오도록 했습니다. 코드 구조 측면에서는, 컨테이너 컴포넌트의 수와 복잡도가 감소하여 대부분의 화면이 “데이터 훅 + UI 컴포넌트” 조합으로 단순화되었습니다. Redux 관련 보일러플레이트도 크게 줄어들어, 신규 개발자가 프로젝트 구조를 파악하기 훨씬 수월해졌습니다. 팀원들 사이에도 “이제는 어느 파일에서 데이터를 가져오는지 바로 알겠다”는 피드백이 있을 정도로 가독성과 명시성이 좋아졌습니다.

이 사례에서 알 수 있듯이, React Query 도입 효과를 극대화하려면 기존 패턴과의 과감한 결별이 필요합니다. 처음에는 보수적으로 같이 물려서 쓰려 했던 것이 오히려 해가 되었고, 일관된 아키텍처로 정리하니 비로소 React Query의 장점이 온전히 발휘되었습니다. 이 과정에서 중요한 것은 한 번에 무조건 다 바꾸기보다, 문제를 트래킹하며 단계적으로 개선한 점입니다. 다음 장에서 이런 점진적 개선 전략을 더 일반화하여 정리해보겠습니다.

주니어 개발자를 위한 실무 적용 팁과 점진적 개선 전략

먼저 프로젝트에서 다루는 상태들을 목록으로 나열해 보고, 이게 서버에서 오는 데이터인지, 순전히 클라이언트에서 생성되는 상태인지 구분해보세요. 서버에서 오는 데이터라면 React Query로 관리하는 게 적합합니다. 반면 UI상의 토글 상태나 입력값처럼 클라이언트에만 존재하는 것은 기존 방법( useState나 Context)을 유지하면 됩니다. 이 경계를 명확히 그어야 이후 단계가 수월해집니다. (혼동해서 React Query를 전역 상태처럼 쓰려 들면 곤란합니다.

한꺼번에 전부 갈아엎기보다, 적용하기 쉬운 부분부터 점진적으로 도입해보세요. 예를 들어 비교적 독립적인 기능이나 화면 하나를 골라서 Redux/Context 대신 React Query로 데이터 불러오기를 하도록 수정합니다. 리스트 페이지 하나를 선택해, 거기에 필요한 데이터 fetching을 useQuery로 바꾸고 관련 로컬 상태 관리 코드를 제거해 보는 식입니다. 그리고 나서 앱을 구동해 기존 동작과 동일하게 잘 작동하는지, 성능이나 사용자 경험이 나아졌는지 확인합니다. 이렇게 성공 사례를 작은 범위에서 만들어보고 나면, 자신감도 붙고 동료들에게도 공유하기 쉬워집니다.

React Query를 사용할 때 팀 내 통일된 패턴을 만드는 것이 좋습니다. 예를 들어 “모든 서버 데이터 요청은 /hooks 디렉토리의 커스텀 훅으로 만든다”거나, “쿼리 키 작명 규칙은 ___로 한다”, “프레젠테이셔널 컴포넌트에서는 훅 호출 외에 API 호출을 직접 하지 않는다” 등의 약속입니다. 이런 패턴을 문서화하거나 코드 리뷰 때 강조하면, 주니어 개발자들도 갈피를 잡기 쉽고 실수 확률이 줄어듭니다. 컨테이너-프레젠테이션 분리를 계속 유지할지, 아니면 훅 기반으로 갈지도 팀에서 합의하세요. 둘 다 장단점이 있으므로 프로젝트 성격과 팀원 숙련도에 맞게 선택하면 됩니다.

리팩토링 중간 단계에서는 한동안 옛 방식과 새 방식이 공존할 수밖에 없습니다. 이때 호환성을 잘 관리해야 합니다. 예를 들어 동일한 데이터를 두 가지 경로(Redux와 React Query)로 가져오는 부분이 생기지 않도록 주의해야 합니다. 만약 불가피하게 일정 기간 두 군데에서 가져와야 한다면, 데이터 불일치를 방지하기 위한 임시 방편(React Query 쪽에서 갱신 시 Redux도 갱신해주는 브릿지 코드 등)을 마련할 수도 있습니다. 하지만 가능하면 그런 상황을 빨리 없애는 것이 좋습니다. 주니어 개발자라면 시니어들과 상의하여 언제 어떤 부분을 완전히 전환할지 계획을 세우고 따르는 것이 안전합니다.

React Query를 실제 써보면, isLoading, isError, data 같은 기본 제공 외에도 staleTime, cacheTime, refetchOnMount다양한 옵션이 존재합니다. 처음엔 모든 걸 알 필요는 없지만, 적어도 기본 동작을 이해하고, 요구사항에 따라 옵션을 설정하는 걸 두려워하지 마세요. 예를 들어 아주 자주 바뀌는 데이터가 아니면 staleTime을 늘려서 캐시를 더 활용하고, 반대로 실시간성이 중요하면 refetchInterval을 쓴다든지 하는 식입니다. 그리고 React Query Devtools와 같은 도구를 사용하면 쿼리 캐시 상태를 시각적으로 볼 수 있어서 학습에 도움이 됩니다. 주니어라고 해도 Devtools를 켜서 현재 쿼리들이 어떻게 관리되는지 살펴보면 감이 빨리 잡힐 것입니다.

새로운 방식으로 바꾼 부분은 꼭 기능 테스트를 해보세요. 특히 기존 방식에서 전환한 화면은, 이전에 없던 edge case 버그가 생기지 않았는지 확인해야 합니다. 예를 들어 캐싱 때문에 즉시 안 보이는 업데이트가 있진 않은지, 병렬 요청이 잘 처리되는지 등. 가능하면 사용자 시나리오 테스트크로스 브라우징도 해서 React Query 훅이 잘 동작하는지 확인합니다 (대부분 잘 되지만 네트워크 offline 등 특이 상황도 한 번씩 점검하면 좋습니다. 또한 서비스 릴리즈 후에도 모니터링을 통해 에러가 늘어나지 않았는지 살펴보세요. React Query 도입 후 서버 에러 처리 방식이 바뀌었기 때문에, 중앙 에러 로깅에 변화가 있을 수 있습니다. 이러한 꼼꼼한 검증 과정을 거치면서 점진적으로 확장하면 안전하게 전환할 수 있습니다.

새로운 라이브러리와 패턴을 적용할 때 혼자 모든 걸 완벽히 해내기 어렵습니다. 주니어라면 더더욱, 시니어 개발자나 팀 리더의 피드백을 자주 받는 게 중요합니다. React Query 적용이나 컨테이너 분리와 관련해 의문이 들면 질문하고, 본인이 짠 코드에 대해 리뷰를 받을 때 열린 마음으로 개선점을 수용하세요. 이렇게 하면 실무에서 더욱 빠르게 성장할 수 있을 뿐 아니라, 프로젝트에도 안정감을 줍니다. 팀 내에 React Query 경험자가 없다면, 공식 문서의 Best Practices나 커뮤니티 사례를 함께 살펴보며 스스로 리뷰하는 것도 좋은 대안입니다.

마지막으로, 점진적 개선에서 잊지 말아야 할 것은 장기적인 목표를 놓치지 않는 것입니다. 중간에 업무 압박이나 예기치 않은 어려움 때문에 한동안 전환 작업이 멈출 수도 있지만, 왜 이걸 도입하려 했는지를 주기적으로 상기하세요. 팀이 함께 더 나은 코드를 유지관리하고 기능 개발 속도를 높이려는 큰 그림이 있었을 겁니다. 주니어 개발자라도 이런 맥락을 이해하고 있으면 우선순위 판단이나 의사소통에서 주도적으로 임할 수 있습니다. 그리고 최종적으로 모든 주요 부분이 새로운 아키텍처로 전환되면, 그 성취를 팀과 함께 공유하고 느껴보세요.

맺으며

React Query를 도입하는 것은 단순히 새로운 라이브러리를 쓰는 문제가 아니라, 프론트엔드 애플리케이션의 데이터 관리 방식 전반을 재고하는 계기가 됩니다. 컨테이너 컴포넌트의 상태 관리 방식과 서버 데이터 처리 계층을 현대화함으로써, 우리는 더 일관되고 견고한 구조를 얻을 수 있습니다. 핵심은 관심사의 분리 원칙을 적용하여 UI와 비즈니스 로직을 적절히 나누고, React Query를 서버 상태의 단일 소스로 활용하는 것입니다. 이를 통해 중복된 코드와 상태 불일치를 줄이고, 유지보수성과 성능을 모두 향상시킬 수 있습니다.

참고 자료

--

--

No responses yet