나만의 react-query 구현기 (WIP)

November 02, 2022

프로젝트를 시작하며 react-query를 직접 구현해보기로 했습니다. react-query가 최근 인기있는 라이브러리지만 작동 원리를 정확히 모르는 상태에서 사용하는 것은 복잡한 기능 추가나 에러 발생 시 병목 요인이 될 수 있기에 기술 부채가 될 수 있다고 생각했습니다.

왜 react-query인가

서버-클라이언트 아키텍쳐를 사용하는 웹 어플리케이션에는 서버 상태클라이언트 상태가 존재합니다.

  • 서버 상태: 서버에 저장되어 있는 데이터로, 비동기 요청을 통해 클라이언트에 캐싱됩니다. 클라이언트에 캐싱된 데이터와 서버 상태가 동일한지 확신할 수 없습니다.
  • 클라이언트 상태: 서버로부터 독립적인 클라이언트의 데이터입니다. 클라이언트에서만 사용되는 UI 로직 등에 사용됩니다. 컴포넌트 레벨로 격리시키면 컴포넌트의 재사용성을 높입니다. 전역 상태는 Context API로 전달하거나 상태 관리 라이브러리를 사용하는 편입니다.

react-query는 클라이언트에서 서버 상태를 최소한의 코드로 관리할 수 있게 하는 라이브러리입니다. 그 과정에서 비동기 요청으로 서버 상태를 캐싱하고 비동기 요청에 대한 상태를 자동 관리합니다. 아래는 체크메이트 서비스에 react-query를 도입하고 싶게 만들었던 몇가지 특징입니다.

  • react hook으로 구현되어 있습니다.
  • stale-while-revalidate 전략으로 캐싱된 데이터를 즉시 반환하는 동시에 백그라운드 요청을 통해 재검증합니다.
  • 페칭 상태에 대한 추가 로직을 선언적으로 작성할 수 있습니다.
  • 자체적으로 DevTools를 지원하여 개발 시 디버그가 편리합니다.

react-query 분석

Query Function과 Mutation Function

react-query에서는 비동기 요청을 querymutation으로 구분합니다. query는 서버 상태를 가져오기 위한 요청, mutation은 데이터를 생성/수정/삭제하거나 서버 사이드 이펙트를 수행하기 위한 요청입니다.

react-query는 특정 라이브러리에 의존성이 없습니다. Promise 객체를 반환하는 어떤 비동기 함수라도 query function과 mutation function으로 사용할 수 있습니다. Promise 객체를 반환해야만 하는 이유는 react-query가 에러 감지를 할 수 있게 하기 위함입니다.

useQuery

query function으로 서버 데이터를 요청하고 캐싱, 재요청, 자동 상태관리를 하는 훅입니다. 다음과 같은 형태로 사용합니다.

// react-query v3 기준
const result = useQuery(["todos"], fetchAllTodos)

// tanstack-query v4 기준
const = result = useQuery({ queryKey: ["todos"], queryFn: fetchAllTodos })

배열 형태의 query keys와 query function, query options를 매개변수로 전달받습니다. v4에서 기존 매개변수 형태에 QueryOptions 객체 형태로도 받을 수 있도록 오버로딩이 추가되었으나 이번 프로젝트에서는 v3의 형태로만 사용했습니다.

Query Keys

react-query가 쿼리를 구분하는 고유한 키값의 배열입니다. react-query 내부에서 리페칭, 캐싱, 다른 곳에 쿼리 공유에 사용합니다.

유용하다고 느꼈던 부분은 비동기 함수 내에서 사용하는 값이 동적으로 업데이트되어야 하는 경우입니다. query keys는 query function의 인자로 전달되어 함수 내부에서 사용할 수 있습니다. 비동기 함수를 커링으로 감싸 동적으로 변하는 값을 인자로 전달하지 않아도 자동으로 주입되어 편리했습니다.

Query Options

옵션 객체를 전달해 react-query의 다양한 기능을 제어합니다. 요청 가능 여부, 요청 재시도 최대 횟수, 마운트/렌더링 시 리페치 여부, stale time, cache time 등의 설정값과 페칭 상태에 대한 콜백 함수로 구성되어 있습니다.

반환값

데이터, 에러, 페칭 상태, 페칭 상태에서 파생된 flag들을 반환합니다.

  • 쿼리 상태의 경우 status라는 문자열로 관리합니다.
  • status에 따라 is~ 형태의 flag 변수도 파생하여 반환하기 때문에 컴포넌트에서 각각의 상태에 따라 조건문을 편리하게 작성할 수 있도록 한 것 같습니다.
  • 쿼리 상태가 success인 경우 페칭 데이터를 data 프로퍼티로 접근할 수 있습니다.
  • 쿼리 상태가 error인 경우 throw(혹은 Promise에서 rejected)된 에러 객체를 error 프로퍼티로 접근할 수 있습니다.

useMutation

useQuery와 비슷하지만 query keys를 사용하지 않습니다. 데이터, 에러, 상태와 함께 요청 트리거에 사용할 수 있는 mutate 함수를 반환합니다.

QueryClient

query, mutationm, cache과 함께 다양한 상태를 저장하는 객체이며 해당 값들을 구독, 조작하기 위한 다양한 기능을 제공합니다. query client는 query, mutation, cache 조작을 위한 다양한 기능을 제공하지만 훅으로 충분하다고 생각하여 캐싱과 invalidation에만 주목했습니다.


우정민

웹 개발, 프론트엔드