/logo.png

[번역] Thinking in React Query

[번역] Thinking in React Query
2025. 1. 16.

이 글은 TkDodo의 Thinking in React Query 를 번역한 글입니다.

안녕하세요 여러분 👋, 오늘 이 자리에 있게 되어 영광입니다. 오늘 이야기 하고자 하는 바는…여러분의 신발끈을 바로 잡는 것에 대해서 입니다.

대부분의 사람들이 신발끈을 묶는 데에 있어서 정확한 방법과 잘못된 방법의 차이를 구분해내지 못합니다. 두 방법은 언뜻 보기에 비슷해 보여도 전자의 매듭은 꽉 묶여있지만 후자의 매듭은 걸을때 풀리기 십상입니다. 이는 여러분의 삶을 변화시킬 수 있는 작은 변화일 것 입니다.

리액트 쿼리를 사용할 때도 작은 변화가 큰 차이를 만들어낼 수 있는 몇가지 상황들이 존재하곤 합니다.

제가 2020년경에 오픈소스 커뮤니티 활동을 한창 시작했을때 이러한 상황들을 발견하곤 했습니다. 여러 플랫폼들을 통해서 질의응답을 하는 활동은 제가 오픈소스 활동을 시작하게 된 아주 훌륭한 방법이였습니다. 여러분들이 이러한 문제들을 해결해주면 사람들은 고마워하고 감사해하며, 저 역시 이전에는 마주치지 못했던 문제들을 질문으로 받아보면서 많은 것들을 배워 볼 수 있었습니다.

이러한 활동 덕에 리액트 쿼리에 대해 매우 잘 알게 되었고, 그와 동시에 여러 질문들에서 공통된 패턴들을 파악할 수 있었습니다. 질문해주신 분들 중 많은 분들이 리액트 쿼리가 무엇인지 또는 무엇을 하는지에 대해 오해하고 있었고 이러한 오해는 약간의 다른 생각들을 갖도록 했습니다.

다시 돌아와서, 오늘 제가 이야기하고자 하는 바는 리액트 쿼리를 이해하는데 있어서 더 나은 사고방식을 가질 수 있는 3가지 요소들 입니다. 신발 끈을 바로 매는 것 처럼 이번 시간을 통해 여러분들은 리액트 쿼리를 쉽지만 정확하게 이해할 수 있게 될 것입니다.

자 이제 본격적으로 “**React Query적으로 사고”**할 수 있는 요소들에 대해 알아봅시다

1. React Query is NOT a data fetching library

첫 번째 요점은 놀랄 수도 있지만 사실입니다. “React에서 data fetching 위한 누락된 부분”으로 묘사되는 경우가 많지만, React Query는 fetching 라이브러리가 아닙니다. 간단히 살펴보면 다음과 같이 데이터 불러오기를 수행하지 않습니다.

useQuery({
	queryKey : ["issues"],
	queryFn : () => axios.get("/issues").then((res) => res.data)
})

표준 리액트 쿼리 예제를 보면, 사용하려면 두 가지를 제공해야 한다는 것을 알 수 있습니다:

  • 리액트 쿼리가 데이터를 저장할 고유한 쿼리키
  • 데이터를 검색해야 할 때마다 실행될 쿼리 함수

물론 컴포넌트에서 해당 훅을 사용하여 데이터와 쿼리가 있을 수 있는 다양한 상태를 렌더링할 수 있지만, queryFn을 다시 한 번 간단히 살펴보면, 이 예제에서는 axios로 구현된 것을 볼 수 있습니다. 하지만 요점은 이것이 바로 data fetching 라이브러리입니다. React Query는 어떻게 하든 상관없습니다.

오직 이행된 약속을 반환하는지 아니면 거부된 약속을 반환하는지에만 관심을 갖습니다.

사실 (그리고 이것은 라이브러리 maintainer로서 하는 말이지만) API가 비공개이기 때문에 복제본을 보여줄 수 없다는 문제를 제기하는 경우, 데이터 가져오기 없이 queryFn을 구현하는 가장 간단한 방법이라고 말할 가능성이 높습니다:

우리가 하는 것은 확인된 Promise를 반환하는 것뿐입니다. 물론 React Query는 axios, fetch 또는 graphql-request와 같은 데이터 불러오기 라이브러리와 매우 잘 어울리며, 이들은 모두 Promise를 생성하기 때문입니다.

React Query가 데이터를 가져오지 않는다는 것을 이해하면 데이터 가져오기와 관련된 모든 종류의 질문이 사라질 것입니다. 다음과 같은 질문들 말입니다:

데이터 가져오기와 관련된 모든 질문에는 일반적으로 동일한 답이 있습니다:

  • 리액트 쿼리로 baseURL을 정의하려면 어떻게 해야 하나요?
  • 리액트 쿼리로 응답 헤더에 액세스하려면 어떻게 해야 하나요?
  • 리액트 쿼리로 그래프QL 요청을 하려면 어떻게 해야 하나요?

리액트 쿼리는 상관없습니다! 그냥 어떻게든 Promise를 반환해 주세요.

좋아요, 일단 그 정보를 얻었으니 이제 물어보는 것은 당연한 일입니다:

React Query가 데이터 불러오기 라이브러리가 아니라면, 그것은 무엇일까요? 이 질문에 대한 제 대답은 항상 그렇습니다:

An Async State Manager

비동기 상태 관리자. 이제 “비동기 상태”가 무엇을 의미하는지 이해하는 것이 중요합니다.

2020년 5월에 React Query의 창시자인 태너 린슬리가 It’s Time to Break up with your “Global State” 라는 멋진 강연을 했습니다.

이 강연은 오늘날에도 여전히 시사하는 바가 크므로 아직 시청하지 않으셨다면 꼭 시청해 보세요.

요점은 우리가 오랫동안 state를 필요한 부분으로 나눠서 살아왔다는 것입니다. 하나의 컴포넌트에만 필요한 것일까요? 아마도 로컬 state를 사용하는 것부터 시작할 것입니다. 트리 위쪽에서 사용할 수 있어야 하나요?

그런 다음 데이터를 위로 올려서 소품으로 다시 전달할 수 있습니다. 이보다 더 높은 수준 또는 훨씬 더 광범위한 규모로 필요할까요?

React 외부에 존재하며 애플리케이션에 전 세계적으로 배포하는 redux나 zustand와 같은 “전역 상태 관리자”로 옮길 수 있습니다.

그리고 앱에서 클릭하는 토글 버튼이든 네트워크를 통해 가져와야 하는 이슈 목록이나 프로필 데이터이든 모든 종류의 상태에 대해 이 작업을 수행해 왔습니다. 모든 상태를 똑같이 취급했습니다.

사고의 전환은 상태가 어디에 사용되는지가 아니라 어떤 종류의 상태인지에 따라 다르게 나눌 때 이루어집니다.

우리가 완전히 소유하고 있고 동기적으로 사용할 수 있는 상태(예: 다크 모드 토글 버튼을 클릭했을 때)는 이슈 목록처럼 원격으로 비동기적으로 사용할 수 있는 상태와는 완전히 다른 요구 사항을 가지고 있기 때문입니다.

비동기 상태 또는 “서버 상태”를 사용하면 스냅샷을 가져온 시점의 스냅샷만 볼 수 있습니다. 우리가 해당 상태의 유일한 소유자가 아니기 때문에 오래된 상태가 될 수 있습니다. 백엔드, 아마도 우리 데이터베이스가 소유하고 있을 것입니다. 우리는 방금 스냅샷을 표시하기 위해 이를 빌린 것입니다.

브라우저 탭을 30분 동안 열어 두었다가 다시 돌아와 보면 이런 현상이 나타날 수 있습니다. 최신의 정확한 데이터를 자동으로 볼 수 있다면 좋지 않을까요? 다른 사용자도 그 사이에 데이터를 변경할 수 있기 때문에 최신 상태로 유지해야 합니다. 또한 상태는 동기적으로 사용할 수 없으므로 로딩 및 오류 상태와 같은 해당 상태와 관련된 메타 정보도 관리해야 합니다.

따라서 데이터를 자동으로 최신 상태로 유지하고 비동기 수명 주기를 관리하는 것은 기존의 다목적 상태 관리자에게서 얻을 수 있거나 필요한 기능이 아닙니다. 하지만 비동기 상태에 특화된 도구가 있기 때문에 이 모든 작업을 수행할 수 있습니다. 작업에 적합한 도구를 사용하기만 하면 됩니다.

State Manager?

두 번째로 이해해야 할 부분은 “상태 관리자”가 무엇이며, 왜 React Query가 상태 관리자인지입니다. 상태 관리자는 일반적으로 앱에서 상태를 효율적으로 사용할 수 있도록 만드는 일을 합니다. 여기서 중요한 부분은 효율적으로, 다른 말로 표현하자면 ‘프레임워크’입니다:

업데이트를 원하지만 너무 많지는 않습니다.

너무 많은 업데이트가 문제가 되지 않는다면, 우리는 모두 React 컨텍스트에 상태를 고정시키면 됩니다. 하지만 이것은 실제 문제이며, 많은 라이브러리가 다양한 방법으로 이 문제를 해결하려고 노력합니다. 널리 사용되는 두 가지 상태 관리 솔루션인 Redux와 zustand는 모두 선택기 기반 API를 제공합니다:

이를 통해 컴포넌트는 관심 있는 상태의 일부만 구독하도록 할 수 있습니다. 스토어의 다른 부분이 업데이트되더라도 해당 컴포넌트는 상관하지 않습니다. 라이브러리를 통해 전 세계에서 사용할 수 있으므로 앱의 어느 곳에서나 해당 훅을 호출하여 해당 상태에 액세스할 수 있다는 것이 원칙입니다.

그리고 React Query를 사용하면 실제로 크게 다르지 않습니다. 구독하는 부분 또는 슬라이스가 QueryKey에 의해 정의된다는 점을 제외하면요.

이제 useIssues() 커스텀 훅을 호출할 때마다 쿼리 캐시의 이슈 조각에서 변경된 사항이 있으면 업데이트를 받게 됩니다. 이것으로 충분하지 않다면, ReactQuery에는 선택기도 있기 때문에 한 단계 더 나아갈 수 있습니다:

이제 우리는 컴포넌트가 저장된 것의 계산된 결과 또는 파생된 결과에만 관심을 갖는 “세분화된” 구독에 대해 이야기하고 있습니다. 하나의 이슈를 “열린”에서 “닫힘”으로 전환하면 길이가 변경되지 않았기 때문에 useIssueCount 훅을 사용하는 컴포넌트가 다시 렌더링되지 않습니다.

그리고 다른 상태 관리자와 마찬가지로 필요할 때마다 useQuery를 호출하여 해당 데이터에 액세스할 수 있으며, 그렇게 해야 할 가능성이 높습니다.

따라서 useEffect를 호출하여 React Query의 데이터를 다른 곳에서 동기화하거나 (이미 사용되지 않는) onSuccess 콜백에서 데이터를 로컬 상태로 설정하는 것과 같은 특정 작업을 시도하는 모든 솔루션은 안티패턴이 됩니다.

이 모든 것은 단일 진실 소스를 제거하는 상태 동기화의 한 형태이며, React Query가 이미 상태 관리자이므로 해당 상태를 다른 상태에 넣을 필요가 없으므로 불필요합니다.

이제 제가 이 작업을 하고 있고, 원할 때마다/필요할 때마다 useQuery를 호출하고 있다고 생각할 수 있습니다. 컴포넌트 3개, useIssues() 3개. 그러나 일부 컴포넌트가 조건부로 렌더링되는 경우(예: 대화 상자를 열 때 또는 종속 쿼리가 있기 때문에) 동일한 엔드포인트에 대한 많은 가져오기가 표시되기 시작할 수 있습니다.

방금 2초 전에 가져온 건데 왜 벌써 다시 가져오는 거지? 그래서 문서로 눈을 돌려보세요…

백엔드에 스팸을 많이 보내지 않기 위해 모든 것을 한꺼번에 끄기 시작합니다. 결국 데이터를 리덕스에 넣을 걸 그랬나 봐요…

잠시만 기다려주세요. 이 광기에는 논리가 있기 때문입니다. React Query가 왜 이 모든 요청을 하는 걸까요?

다시 비동기 상태의 필요성으로 돌아갑니다: 오래된 상태일 수 있으므로 특정 시점에 업데이트하고 싶을 수 있으며, React Query는 창 포커스, 컴포넌트 마운트, 네트워크 연결 재개 및 QueryKey 변경과 같은 특정 트리거를 통해 이를 수행합니다.

이러한 이벤트 중 하나가 발생할 때마다 React Query는 해당 쿼리를 자동으로 다시 가져옵니다.

하지만 이것이 전부는 아닙니다. 중요한 것은: React Query는 모든 쿼리에 대해 이 작업을 수행하지 않으며, 오래된 것으로 간주되는 쿼리에 대해서만 수행합니다. 이제 오늘의 두 번째 중요한 내용을 살펴보겠습니다:

staleTime is your Best Friend

React Query는 데이터 동기화 도구이기도 하지만, 그렇다고 해서 백그라운드에서 모든 쿼리를 맹목적으로 리페치하는 것은 아닙니다. 이 동작은 “데이터가 부실해질 때까지의 시간”을 정의하는 staleTime으로 조정할 수 있습니다. 오래된 것의 반대말은 신선한 것이므로, 다시 말해 데이터가 신선한 것으로 간주되는 한, 리프레시 없이 캐시에서만 데이터가 제공됩니다. 그렇지 않으면 캐시된 데이터와 함께 리프레시됩니다.

따라서 오래된 쿼리만 자동으로 업데이트되지만, 문제는 staleTime의 기본값이 0이라는 점입니다.

네, 0 밀리초는 0이므로 React Query는 모든 것을 즉시 오래된 것으로 표시합니다. 이는 확실히 공격적이며 오버페칭으로 이어질 수 있지만, 네트워크 요청을 최소화하는 측면에서 오류가 발생하는 대신 최신 상태를 유지하는 측면에서 React Query 오류가 발생합니다.

이제 staleTime을 정의하는 것은 사용자의 리소스와 필요에 따라 크게 달라집니다. 또한 staleTime에 대한 “올바른” 값은 없습니다.

서버가 다시 시작될 때만 변경되는 구성 설정을 쿼리하는 경우 staleTime: Infinity가 좋은 선택이 될 수 있습니다.

반면에 여러 사용자가 동시에 업데이트하는 고도로 협업적인 도구가 있다면 staleTime: 0으로 만족할 수 있습니다.

따라서 React 쿼리 작업에서 매우 중요한 부분은 staleTime을 정의하는 것입니다. 다시 말하지만, 정확한 값은 없으며, 전역적으로 기본값을 설정한 다음 필요할 때 잠재적으로 덮어쓰는 것을 선호합니다.

자, 비동기 상태의 필요성에 대해 다시 한 번 빠르게 돌아가 보겠습니다. 데이터가 오래된 것으로 간주되고 이러한 이벤트 중 하나가 발생하면 React Query가 캐시를 최신 상태로 유지한다는 것을 알고 있습니다.

그중에서도 가장 중요하고 제가 집중하고 싶은 이벤트는 바로 쿼리키 변경 이벤트입니다.

이러한 이벤트는 주로 언제 발생할까요? 이제 마지막 질문으로 넘어가겠습니다:

Treat Parameters as Dependencies

매개 변수를 종속성으로 취급해야 합니다.

문서에 이미 설명되어 있고 별도의 블로그 포스팅 을 작성했지만 이 부분에 대해 강조하고 싶습니다.

이 예제의 필터와 같이 queryFn 내부에서 요청에 사용하려는 매개변수가 있는 경우 해당 매개변수를 queryKey에 추가해야 합니다.

이것은 React Query를 작업하기 좋은 많은 것들을 보장합니다: 우선, 입력에 따라 항목이 별도로 캐시되므로 서로 다른 필터가 있는 경우 캐시에서 서로 다른 키 아래에 저장하여 경쟁 조건을 피할 수 있습니다.

또한 한 캐시 항목에서 다른 캐시 항목으로 이동하기 때문에 필터가 변경될 때 자동으로 다시 가져올 수 있습니다. 그리고 일반적으로 디버깅하기 매우 어려운 오래된 클로저 문제를 방지할 수 있습니다.

이 기능은 매우 중요하기 때문에 자체 eslint 플러그인을 출시했습니다. 이 플러그인은 queryFn 내부에서 무언가를 사용하고 있는지 확인하고 키에 추가하라고 알려줍니다. 또한 자동 수정이 가능하므로 사용을 적극 권장합니다.

원한다면, useEffect에 대한 의존성 배열처럼 queryKey를 생각할 수 있지만 참조 안정성에 대해 생각할 필요가 없기 때문에 단점이 없습니다.

여기에는 useMemouseCallback이 관여할 필요가 없으며, queryFn도 아니고 queryKey도 아닙니다.

마지막으로, 이제 앱의 모든 수준에서 필요한 곳에서 useQuery를 사용하고 있지만 이제 화면의 특정 부분에만 존재하는 쿼리에 대한 종속성이 생겼습니다: 사용이슈를 호출하고 싶을 때 필터에 액세스할 수 없는 경우 어떻게 해야 하나요? 어디에서 오는 건가요?

정답은 다시 말하지만 React Query는 상관없습니다. 이는 순수한 클라이언트 상태 관리 문제입니다. 적용된 필터는 클라이언트 상태이기 때문입니다. 그리고 이를 어떻게 관리할지는 여러분에게 달려 있습니다.

필요에 따라 로컬 상태 또는 전역 상태 관리자를 사용하는 것도 괜찮습니다. URL에 필터를 저장하는 것도 좋은 방법입니다.

예를 들어 zustand와 같은 상태 관리자에 필터를 넣었을 때 어떻게 보이는지 살펴보겠습니다:

변경한 유일한 사항은 커스텀 훅에 입력으로 filters를 전달하는 대신 스토어에서 직접 필터를 가져온다는 점입니다. 이것은 커스텀 훅을 작성할 때 구성의 힘을 보여줍니다.

그리고 useQuery로 관리되는 서버 상태와 이 경우 useStore로 관리되는 클라이언트 상태가 명확하게 분리되어 있는 것을 볼 수 있습니다. 스토어에서 필터를 업데이트할 때마다 - 위치에 관계없이 - 쿼리가 자동으로 실행되거나 사용 가능한 경우 캐시에서 최신 데이터를 읽습니다.

이 패턴을 사용하면 React Query를 진정한 비동기 상태 관리자로 사용할 수 있습니다.

Summary

요약하자면:

  • React Query는 data fetching 라이브러리가 아니라 비동기 상태 관리자입니다.
  • staleTime은 가장 좋은 친구이지만 필요에 맞게 설정해야 합니다.
  • 매개 변수를 종속성으로 취급하고 린트 규칙을 사용하여 이를 적용합니다.

신발 끈을 묶는 방법을 조금만 바꿔도 삶의 질이 크게 향상되는 것처럼, 이 세 가지 사항을 따르도록 생각을 바꾸면 React Query를 훨씬 더 즐겁게 사용할 수 있습니다.

여기까지입니다. 들어주셔서 감사합니다. 블루스카이에서 저를 팔로우하고 제 블로그를 구독해 주세요. React Query v5가 곧 출시될 예정이니 최신 소식을 받아보세요. 감사합니다!