ziglog

    Search by

    swr vs react-query

    October 11, 2024 • ☕️ 6 min read

    리액트에서 서버 상태 관리


    전설적으로 리액트에서 서버 상태 관리는 어렵다.

    당연하다. 프론트엔드 라이브러리에서 어떻게 서버 상태 관리까지 하란 말이냐?

    하지만 서버 데이터 없는 프론트엔드는 그냥 팥 없는 붕어빵…

    어쩔 수 없이 프론트엔드 개발자가 고생해서 서버 데이터를 잘 받아와서 관리해야 한다.

    SWR 인트로


    Stale-While-Revalidate라는 뜻으로, 서치가 조금 까다롭다;;

    역시 책이든 아이돌 이름이든 라이브러리든 뭐든 이름으로 어그로를 끌어야 하는데.

    발음도 안 감기고 그닥인 것 같다.

    Next.js에서 만들었다.

    swr은 먼저 캐시에서 데이터를 반환한 다음, 서버에 데이터를 가져오는 요청을 보내고, 마지막으로 최신 데이터를 제공하는 전략을 사용한다.

    그리고, 데이터를 전역 상태로 관리한다는 특징이 있다.

    react-query 인트로


    나 취준할 때까지만 해도 그냥 react-query였는데… tanstack 아저씨가 유명해지고 싶었나. 언젠가부터 @tanstack/react-query로 이름이 바뀜.

    당연히 별로다. 이름이 좀 간지났으면 몰라도. 그런데 어쩌겠냐.

    유명해져라! 그러면 똥을 싸도 박수를 쳐줄 것이다…

    react-query는 서버 상태를 가져오고, 캐싱하고, 동기화하고, 업데이트하는 순서대로 서버 상태를 관리한다.

    swr과는 달리 애플리케이션 루트에서 Provider로 감싸 조금 더 디테일한, 커스텀한 캐싱 전략을 제공한다.

    → React Query는 보다 전반적인 서버 상태 관리에 중점을 두고 있고, SWR은 사용자 경험을 위해 빠르고 최신 상태의 데이터를 제공하는 데 중점을 둔다.

    이제… (내가 궁금했던 것들 위주로) 구체적으로 알아보자.

    캐시를 관리하는 방법


    swr은 전역 캐시를 자동으로 관리하며, 별도의 Provider 없이도 캐시를 전역적으로 사용할 수 있도록 설계되다. swr의 캐시는 전역적인 컨텍스트에 저장되며, 사용자가 별도로 설정할 필요가 없다. swr 훅을 사용하는 컴포넌트들끼리는 내부적으로 동일한 키를 공유하는 경우 동일한 데이터를 캐시에서 자동으로 가져온다.

    반면에 react-query는 더 복잡한 캐시 관리 기능을 제공한다. react-query에서는 캐시의 범위나 캐시 제공자 등을 더 세밀하게 제어할 수 있게 하기 위해 QueryClient와 같은 설정을 Provider로 전달할 수 있는 구조를 사용한다. 다양한 상황에서 전역 상태를 제어하거나, 서로 다른 QueryClient를 사용해야 하는 경우 유용하다.

    ⚠️ 그러나 실제로 여러 개의 서로 다른 QueryClient를 사용하는 방식은 권장되지 않는다. ^^

    두 라이브러리는 서버 데이터를 바라보는 시각이 조금 다른데,

    react-query는 상태를 서버 상태와 클라이언트 상태로 구분한다. 반면 swr은 서버에서 데이터를 받아와 데이터를 클라이언트에서 전역 상태로 업데이트하는 것으로 본다.

    데이터 로딩 상태

    react-query와 swr이 상태를 바라보는 시각이 조금 다르듯, 상태를 관리하는 법도 조금씩 다르다.

    react-query는 isLoading, isFetching을 통해 데이터의 상태를 보여준다.

    두 가지는 비슷해 보이는데, isLoading캐시된 데이터조차 없이, 처음 실행된 쿼리 일 때 데이터 로딩 여부를 보여준다는 점이 isFetching과 다르다.

    swr에도 isLoading과 비슷한 듯 다른 isValidating 값을 제공한다.

    swr의 isLoading은 데이터가 로드되지 않은 상태에서 현재 진행중인 요청이 있는지를 나타내는, 반면 isValidating은 데이터의 로드 여부 상관없이 현재 진행중인 요청이 있는지를 나타낸다.

    개인적으로 isValidating이라는 이름에서 드러나듯, swr은 stale한 데이터의 갱신 여부에 조금 더 초점이 맞춰져 있는 것 같다.

    mutation


    흔히들 react-query에서 mutation은 서버 데이터를 업데이트하는 요청으로 알고 있다.

    예를 들면 POSTDELETE 같은 요청들…

    기본 query와는 달리 서버 데이터에 변경이 있기 때문에 useQuery와는 조금 다른 기능들을 제공하는 useMutation을 통해 작성한다.

    Copy
    import { useMutation, useQueryClient } from '@tanstack/react-query';
    import axios from 'axios';
    
    function Profile() {
      const queryClient = useQueryClient();
    
      const mutation = useMutation(
        newProfile => axios.post('/api/user/update', newProfile),
        {
          onSuccess: () => {
            // 요청 성공 시 캐시 무효화
            queryClient.invalidateQueries('/api/user');
          },
        }
      );
    
      return (
        <div>
          <h1>Profile</h1>
          <button onClick={() => mutation.mutate({ name: 'New Name' })}>
            Update Profile
          </button>
        </div>
      );
    }

    완전 깔끔&쌈뽕… onSuccess, onSettled 등의 옵션도 제공하여 손쉽게 서버의 최신 데이터로 캐시를 업데이트까지 할 수 있다.

    그러다가 swr을 보면서 알게 된 충격적인 사실. swr은 mutation을 제공하지 않는다?

    swr은 기본적으로 GET 요청을 처리하는 데 최적화되어 있으며, mutation을 위한 별도의 훅이 제공되지 않(았었)다. 이 이유는 앞서 언급한 것과 같이, 데이터를 ‘받아오는 것’에 중점을 두고 있기 때문이기도 하다.

    따라서 데이터를 변경하는 작업(앞서 언급한 POST, DELETE 등)은 수동으로 처리해야 하며, 그 후 변경된 데이터를 다시 페칭하거나 캐시를 업데이트해야 했다.

    Copy
    import useSWR from 'swr'
     
    function Profile () {
      const { data, mutate } = useSWR('/api/user', fetcher)
     
      return (
        <div>
          <h1>My name is {data.name}.</h1>
          <button onClick={async () => {
            const newName = data.name.toUpperCase()
            await requestUpdateUsername(newName)
            // 👇 직접 서버 데이터 업데이트
            mutate({ ...data, name: newName })
          }}>Uppercase my name!</button>
        </div>
      )
    }

    여기서 mutate의 정확한 동작은, 서버 상태를 업데이트시키는 것이 아니라, 해당 key를 사용하는 useSWR의 캐시를 업데이트 시켜주는 것이다. 그래서 mutate로 캐시를 최신화했다 할지라도, 서버의 데이터를 최신화시키지 않았다면 여전히 캐시에는 업데이트 이전 데이터가 남아있을 수 있다. ;;

    전역으로 mutator를 사용하려면 useSWRConfig를 사용하는 방법도 있었다.

    Copy
    import { useSWRConfig } from "swr"
     
    function App() {
      const { mutate } = useSWRConfig()
      mutate(key, data, options)
    }

    그러나 이와 같은 코드 작성 방식에 수많은 개발자들의 봉기가 있었던 탓인지 useSWRMutation이라는 훅을 내놓긴 했다.

    Copy
    import useSWRMutation from 'swr/mutation'
     
    function Profile() {
      const { data, error } = useSWR('/api/user', fetcher);
      
      // useSWRMutation 훅으로 mutation 처리
      const { trigger, isMutating } = useSWRMutation('/api/user/update', updateProfile, {
        onSuccess: () => {
          // 성공 시 데이터 갱신
          mutate('/api/user');
        },
      });
    
      return (
        <div>
          <h1>{data?.name}</h1>
          <button 
            onClick={() => trigger({ name: 'New Name' })} 
            disabled={isMutating}>
            {isMutating ? 'Updating...' : 'Update Profile'}
          </button>
        </div>
      );
    }

    isMutating과 같은 상태값 및 onSuccess등의 후속 조치를 위한 옵션이 추가된 것 같다. 암튼 열심히 react-query의 useMutation을 따라한 것 같아… 왠지 짜쳐.

    성능 최적화


    swr은 내장 캐싱과 중복 제거 기능을 사용하여 불필요한 네트워크 요청을 생략한다.

    동일한 swr key를 가지는 컴포넌트가 여러번 렌더링된다면 단 한 번의 네트워크 요청만 발생한다.

    또 데이터의 깊은 비교를 통해 data가 변경되지 않았다면 리렌더링을 하지 않는다. (compare 옵션으로 커스터마이징할 수도 있다.)

    react-query 역시 기본적으로 swr과 같은 렌더링 최적화 방식을 내장하고 있다. 그리고 실제로 useQuery의 반환값이 사용되었을 때만 컴포넌트를 리렌더링한다는 점에서 조금 더 섬세한 최적화를 수행한다.

    여기에 추가적으로 react-query는 notifyOnChangeProps'tracked' 옵션 등을 통해 조금 더 섬세하게 데이터의 변경에 따른 리렌더링 여부를 커스텀할 수 있다.

    오랜 시간을 거쳐 진화하더니 좀… 변태스러워졌다 😨

    두 라이브러리 중 어떤 것이 월등히 성능 최적화가 더 낫다고 하긴 어렵지만,

    기본적인 렌더링 최적화 등은 두 라이브러리에서 모두 지원하므로 조금 더 디테일한 렌더링 시점을 조절하고 싶다면, 커스터마이징이 더 다양한 react-query를 쓰는 것도 좋을 듯 하다.

    그래서 뭘 써야 하는가?


    와 같은 질문은 나에게 너무 어려워…

    모든 기술 블로거들과 챗GPT가 말하듯,

    • 단순한 데이터 페칭 및 캐싱이 필요한 프로젝트 -> swr
    • 복잡한 비동기 상태 관리나 더 많은 최적화 기능이 필요하다면 -> react-query

    가 맞는 것 같다.

    하지만 또 다른 많은 기술 블로거들과 기업들에서 그러듯…

    swr을 선택하더라도 결국 시간이 지나 react-query로 넘어가게 되는 것 같다. 😇

    Refs



    Relative Posts:

    React 리렌더링 삐그덕 삐그덕

    August 4, 2024

    zigsong

    지그의 개발 블로그

    RotateLinkImg-iconRotateLinkImg-iconRotateLinkImg-icon