[React] Todo App에 React Query 적용하기

wonyu·2022년 8월 19일
0
post-thumbnail

UI / Server State를 분리하는 이유

  • 서버 상태를 클라이언트 상태로서 사용하기 위해서는 상태 관리 라이브러리 선택 및 보일러 플레이트 코드 작성, 사용자의 행동에 따른 재요청, 중복 요청 처리(나는 해 본 적이 없지만ㅎㅎ..), 요청 실패 시 재요청 혹은 에러 처리, 로딩 처리 등 다양한 작업이 필요하다. 그리고 많은 경우에 앞의 작업들을 리액트 컴포넌트 파일에서 처리해야 하기 때문에 관심사를 깔끔히 분리하기가 어렵다.
  • 그런데 이런 복잡한 처리의 목적은 대부분 최신 데이터를 사용하기 위함이다. 그런데 결국 클라이언트에서는 서버의 상태를 받아와서(빌려와서) 복사해서 사용한다. 그렇다면 "정말 최신 데이터가 맞나요?"라는 질문에 YES라고 대답하기는 어렵다.
  • 그렇다면 클라이언트에서는 사용자의 입력처럼 UI 영역에서 사용되는 상태만을 갖고, 서버 상태는 서버 상태대로 받아와서 사용할 수 있다면 "최신 데이터"를 사용할 수 있고 관심사도 적절히 분리될 수 있을 것이다.

stale-while-revalidate

stale-while-revalidate는 확장 Cache-Control 디렉티브이다. (Cache-Control은 HTTP 요청과 응답에서 사용되는 헤더의 일종으로, 캐싱 메커니즘을 위해 사용된다.)
캐시된 컨텐츠를 즉시 로드하는 즉시성과 업데이트 된 캐시 컨텐츠가 향후에 사용되도록 하는 최신성을 보장하는 데 도움을 준다. stale-while-revalidate는 비동기적으로 컨텐츠를 재검증하는 동안 클라이언트가 stale한(=최신이 아닌) 응답을 받을 것임을 나타낸다.

Cache-Control: max-age=1, stale-while-revalidate=59
  1. HTTP 요청 시 캐싱되어 있는 응답이 max-age(1초) 이내일 경우 캐싱된 응답 반환
  2. HTTP 요청 시 캐싱되어 있는 응답이 max-age ~ swr(1~60초) 이내일 경우 우선 캐싱된 응답을 반환하고 재검증 요청(데이터 요청)
  3. HTTP 요청 시 캐싱되어 있는 응답이 swr(60초) 이후일 경우 요청을 보내고 응답 반환

React Query는 이러한 stale-while-revalidate 전략을 채택한 비동기 상태 관리자이다. 즉, React Query는 데이터를 캐싱하고 요청 시 데이터가 stale하더라도 데이터를 반환한다.

React Query 적용-Before/After

1. GET 요청 -> useQuery

  • 기존

    • 컴포넌트 안에서 fetcher 함수를 호출하는 함수를 만듦
    • 토큰이 유효하지 않을 경우에 대해 api 요청을 보내기 전에 처리
    • 응답으로 받은 값을 state에 저장해서 사용
    • 서버와 클라이언트의 데이터가 밀접한 상태
  • 수정 후

    • useQuery를 사용하여 생성한 커스텀 훅 사용
    • useQuery 훅에서 반환한 status를 이용하여 로딩 처리

    • 컴포넌트에서 options 작성(커스텀 훅에 options props를 넘김)

2. POST 요청 -> useMutation

  • 기존
    • GET의 경우와 동일
    • 생성/수정/삭제 후 state를 업데이트하도록 다시 api 요청을 보냄

  • 수정 후
    • GET의 경우와 동일
    • 생성/수정/삭제 후 state 업데이트를 위해 invalidateQueries 메서드 사용

3. 전역 에러 처리

  • App.tsx에서 QueryClient를 이용하여 전역 에러 처리

React Query 적용-트러블 슈팅

useMutation

API 요청은 문제 없이 동작하는데 invalidateQueries가 동작하지 않는 문제가 있었다. 코드는 아래와 같았다.

응답으로 받아온 data에 id가 제대로 들어오지 않는 건가? 라는 생각에 onSuccess 콜백 함수를 async/await로 수정해보았다. 하지만 문제가 해결되지 않았다.
Query Key가 잘못된 게 아니라면 queryClient가 문젠가 싶어서 다른 사람들의 코드와 공식 문서 예시를 찾아보았다. 찾아보니 new QueryClient()가 아니라 useQueryClient()를 사용해야 함을 발견했다.

React Query는 동일한 QueryClientProvider에 있는 경우 동일한 데이터를 가져오고, 동시에 발생하는 요청의 중복을 제거하므로 위의 시나리오에서는 두 컴포넌트가 동일한 데이터를 요청하더라도 네트워크 요청은 하나만 있습니다.

[번역] #10: 리액트 쿼리는 상태 관리자다

queryClient에 대해 잘 알지 못해서 원인을 찾는 데에 시간이 걸렸다. 동일한 queryClient가 아니었기 때문에 invalidate할 해당 쿼리가 존재하지 않아서 생긴 문제인 것 같다. 좀 더 알아봐야겠다..!

전역 에러 처리

App.tsx에서 아래와 같이 queryClient에서 QueryCache를 만들어 OnError 옵션에 대해 콜백 함수를 작성했다. 그런데 몇 몇 API 요청 시 에러가 발생해도 toast가 보이지 않는 문제가 있었다.

모든 API 요청에서 발생한 문제가 아니라 로그인, todo 수정&삭제에서 발생한 문제였다. 코드를 확인해보니 해당 동작의 커스텀 훅을 사용할 때 다른 옵션을 주었기 때문에 옵션이 덮어씌워져서 동작하지 않는 게 아닐까?라고 생각했다.

그래서 옵션을 지우고 다시 확인해보았으나 문제는 해결되지 않았다. 그래서 처음에 전역 에러 처리를 할 때 참고했던 TkDodo의 글에서 'overwrite'를 검색해서 아래와 같은 댓글을 발견했다.

그런데 전역 에러 처리를 하려면 QueryCache나 MutationCache를 사용하라고 적혀있는 것을 보고 MutationCache를 작성하지 않은 게 문제였음을 깨달았다. 그리고 코드를 아래와 같이 수정했고, 문제 없이 잘 동작하는 것을 확인했다!

느낀 점

지난 번에 개인 토이 프로젝트를 할 때에도 React Query를 사용해보려고 했으나, 급한 마음으로 슥 훑어보고는 '어렵다.. 다음에 해 봐야지' 하고 생각했었다. 그런데 어려워도 일단 써봐야 어떻게 하는 건지 조금이라도 감이 오고 결국 사용할 수 있게 되는 것 같다.

공식문서와 여러 포스팅을 읽고 React Query를 사용해보면서 그동안 API 요청에 대해 꼼꼼히 모든 경우를 처리한 적이 없음을 깨달았다. 그리고 관련 코드를 작성할 때 필요한 부분이라는 이유로 코드 품질을 떨어뜨리고 있지는 않았는지 반성하게 되었다. 부족한 나를 도와줄 수 있는 알맞는 도구를 선택하는 것도 중요하겠다는 생각을 했다.

내가 느낀 React Query의 장점은 서버 데이터 관리 로직을 React Query에서 처리함으로써 코드가 간결해지고 서버 데이터를 관리하는 게 편리해졌다는 것이다. 그래서 서버에서 데이터를 가져오는 작업이 많은 앱일 경우 React Query를 사용하는 게 훨씬 좋을 듯하다. React Query에 대한 글을 읽으면서 SWR이 같이 언급되는 경우가 종종 있었는데 둘의 차이점은 무엇인지도 한 번 알아봐야겠다.


참고한 글

0개의 댓글