[React-Query] 상태관리 라이브러리 뭐쓰세요?

Sungho Kim·2023년 3월 12일
3

React

목록 보기
7/7
post-thumbnail

시작에 앞서,

제가 프론트엔드 면접에 받은 단골 질문은 상태관리 라이브러리를 어떤 걸 사용해보셨나요? 입니다. 아마 많은 분들이 하나만 사용하진 않았을거라 추측해봅니다. 스터디를 한참 할때도 스터디원이 react-query와 recoil을 사용하셨지만, 두개의 차이점에 대해 물었을때 명확한 해답을 얻을 순 없었습니다.

제가 써본 상태관리 라이브러리는 apollo와 recoil이 전부였지만, 한참 이직을 준비할 때는 apollo도 상태관리의 일종인지 잘 모르고 썼던 것 같습니다. 아마 이제는 상태관리 라이브러리 뭐 써보셨어요? 라고 물으신다면 server state으로 apollo를, client사이드는 recoil을 사용해봤습니다 라고 말할 것 같습니다. (apollo에 대해 잠깐 설명드리자면 react-query와 비슷한 성격의 라이브러리지만 graphQL에서 사용하는 라이브러리입니다.)

상태관리는 하나만 사용하면 되는거 아닌가?

아직 이러한 의문점을 가진 분들을 위해
1. 왜 상태관리는 라이브러리를 사용해서 하는지,
2. 왜 클라이언트 상태관리 라이브러리 이외에 서버상태관리 라이브러리가 필요한지에 대해한 글입니다.

상태관리란,

등장배경

원래 모든 데이터는 BE(Back-end) 서버에서 관리했습니다. 하지만 점차 웹이 진화함에 따라, 기존에 서버에서 해오던 역할을 줄이는 방향으로 발전해왔습니다. 초창기에는 BE에서 브라우저의 요청에 따라 웹페이지를 랜더링 해줬기 때문에, FE에서의 상태라는 개념이 없었는데, 점차 서버의 부하를 줄이고, 많은 사용자들에게 보다 나은 렌더링과 UI 작업을 처리하기 위해 추가로 FE 서버를 두기 시작했습니다.

상태란?

상태는 영어로는 state라고 합니다. State은 현 시점에서의 상태를 가리키는데요. State of emergency는 비상사태 같은 현 상황을 말하는 단어입니다. 하지만 한국말로 표현하면 직관적인 단어는 아니기 때문에, 가장 맞는 표현은 "데이터"라고 표현하는 게 가장 직관적으로 이해할 수 있습니다.

아래 예제를 보고 상태에 대해 조금 더 자세하게 이해해 봅시다.

type IphoneType = "max"|"pro"|"mini"

class Iphone{
  image: string
  color: string
  type: IphoneType
  constructor(image: string, color:string, type: CarType){
    this.image = image
  	this.color = color
    this.type= type
  }
}

위에서 보이는 아이폰의 이미지, 색상, 타입이 우리가 앞으로 다룰 상태라고 보시면 될 거 같습니다. 해당 상태를 기준으로 UI를 만들고, 유저에게 보여줄 컴포넌트를 제작할 수 있습니다.

아직 클래스에 익숙하지 않은 분들을 위해, 간단하게 제가 간단하게 만든 Movie web app을 기준으로 설명해 보겠습니다.

서버로부터 API요청을 보내고, 받은 데이터에는 많은 정보들이 보입니다

  • adult: boolean
  • backdrop_path: string
  • genre_ids: number[]
  • id: number
  • original_language: string
  • original_title: string
  • overview: string
  • popularity: number
  • poster_path: string
  • release_date: string
  • title: string
  • video: boolean
  • vote_average: number
  • vote_count: number

이런 상태(데이터)를 기반으로 UI를 드리는거죠.

위에 그림을 보시면 제목, 평점, 몇명이 투표를 했는지, 요약, 이미지 총 5개의 상태를 갖고 각각의 컴포넌트를 제작했습니다. 웹사이트에서는 이렇게 서버에서부터 전달받은 상태를 통해 웹사이트를 렌더링합니다. 화면을 구성하기 위해 필요한 모든 데이터를 프론트엔드에서는 상태라고 부릅니다.

상태관리란?

상태관리는 State Management를 한국어로 직역한 단어입니다. 위에서 우리는 상태라는 단어를 쉽게 데이터라고 부르기로 했으니까, 쉽게 표현하자면 데이터를 관리하는 방법을 상태관리라고 합니다.

상태관리는 크게 server state management와 client state management 두개로 나뉩니다. 이 포스팅은 react-query 즉, server state management에 대한 글이기 때문에, recoil은 다음 포스팅에서 자세하게 다뤄보도록 하겠습니다.

server state managment를 왜 사용해야 할까요?

1. 네트워크를 통해 서버로 전달되는 클라이언트의 요청에 따라 상태를 쉽게 관리할 수 있습니다.

웹사이트를 제작하면 사용자의 요청에 따라 서버에서 데이터를 보내거나 실패하게 되는데 해당 상태를 더욱 쉽게 관리함으로서, 유저에게 더 나은 경험을 제공할 수 있습니다. 요청 실패나 요청 중에 따른 상태를 서버 상태 관리 라이브러리를 통해 UI를 변경해서 사용자에게 결과에 따른 여러 가지 메시지를 보여줄 수 있기 때문입니다.

2. 캐시를 사용해서 동일 데이터의 요청을 줄이고, 서버의 부하를 줄일 수 있도록 도와줍니다.

예를 들어봅시다. 실시간으로 비트코인을 사고팔아야 하는 서비스(예:업비트)와, 블로그를 쓰거나 읽는 서비스(예:벨로그)가 있다고 했을 때 상대적으로 데이터를 더 빨리 봐야 하는 서비스는 무엇일까요? 아마도 비트코인 매매 서비스일 겁니다. 따라서 업비트 같은 서비스를 만들땐 캐시 타임을 0초에 수렴하게 만들어서, 실시간으로 계속해서 가격 정보를 요청하는 반면, 블로그는 최근 트렌딩에 조금 더 넉넉한 시간 (60초~300초) 정도의 최신화 시간을 줘도 유저가 사용하는 데에 있어서 크게 불편함을 주지 않죠.

서버 상태관리 라이브러리를 사용하면, 캐시 타임(staleTime, cacheTime)을 유동적으로 조정할 수 있을 뿐 아니라, 다시 브라우져를 켰을 때(refetchOnWindowFocus) refetch를 할지 말지 등을 쉽게 설정할 수 있습니다.

리액트 쿼리란

React Query is often described as the missing data-fetching library for React, but in more technical terms, it makes fetching, caching, synchronizing and updating server state in your React applications a breeze.
React-Query official site

리액트 쿼리는 스스로를 리액트에서 없어진 데이터 요청 라이브러리라고 칭할 정도로 리액트 상태관리에 대한 쉽고 편안한 기능들을 제공합니다. 기술적 언어로는 fetching, caching, synchronizing and updating server state에 특화되어 있는 라이브러리라고 할 수 있습니다.

라이프 사이클

리액트 쿼리의 상태는 fetching, fresh, stale, inactive, delete로 총 5가지 형태를 가지고 있습니다.

  • fetching - 요청중인 데이터
  • fresh - 신선한 데이터로, 아직 stale(만료)되지 않은 상태의 데이터로, 컴포넌트가 마운트, 업데이트 되어도 데이터를 다시 요청하지 않습니다.
  • stale - 만료된 데이터, 컴포넌트가 마운트, 업데이트되면 다시 요청을 진행합니다.
  • inactive - 사용하지 않는 데이터로 일정 시간이 지나면 가비지 컬렉터가 캐시에서 제거합니다.
  • delete - 가비지 컬렉터에 의해 캐시에서 제거된 쿼리

React-Query Default 값

앞서, 리액트쿼리는 캐시를 효과적으로 관리해서 프론트엔드 개발자가 하나하나 설정해야할 기능들을 미리 셋팅해줘서 보다 쉽게 캐시나 refetching 옵션등을 관리할 수 있다고 언급했습니다. 일단 default값들이 어떤 것들이 있는지 알아보고, 개인적으로 자주 쓰이거나 알아두면 좋을 옵션만 다뤄보겠습니다.

  • useQuery,useInfiniteQuery로 가져온 데이터는 기본적으로 즉시 stale 상태가 됩니다.
  • stale 상태의 데이터는 다음 조건이 만족했을 때, 백그라운드에서 refetch가 됩니다.
    • 새로운 인스턴스가 마운트되었을 때
    • 브라우저 창이 다시 포커스되었을 때
    • 네트워크가 다시 연결되었을 때
    • refetch interval 옵션을 수동으로 설정했을 때
  • inactive 데이터는 5분 후에 가비지 컬렉터에 의헤 제거됩니다.
  • 백그라운드에서 3회 이상 실패한 쿼리는 에러 처리가 됩니다.
    retry 옵션과 retryDelay옵션으로 쿼리 요청 횟수와 시간을 설정할 수 있습니다.
  • staleTime: 쿼리가 fresh에서 stale상태로 변하는 시간입니다. 디폴트 값은 즉시변하는 것이지만, 초기옵션에서 변경하거나, 쿼리마다 다르게 설정할 수 있습니다.
  • cacheTime: inactive데이터가 캐시에서 삭제되는 시간을 의미합니다. 디폴트값은 5분이며 이 역시 초기옵션이나 쿼리마다 설정 가능합니다.

리액트 쿼리 시작하기

react-query 설치

npm i react-query
# or
tarn add react-query

QueryClientProvider

  • 리액트 쿼리 사용을 위해 QueryClientprovider를 최상단에서 감싸주어야 합니다
  • 쿼리 인스턴스 생성 후, client={queryClient}를 작성해줍니다.

기본셋팅 (_app.tsx)

import BasicLayout from "@/components/atoms/layout/BasicLayout";
import styled from "@emotion/styled";
import { AppProps } from "next/dist/shared/lib/router/router";
import React from "react";
import { QueryClient, QueryClientProvider } from "react-query";

const Base = styled.div`
  width: calc(100vw - calc(100vw - 100%));
  height: calc(100vh - calc(100vh - 100%));
  display: flex;
  flex-direction: column;
  align-items: center;
  position: relative;
`;

export default function App({ Component, pageProps }: AppProps) {
  const queryClient = new QueryClient();
  return (
    <QueryClientProvider client={queryClient}>
      <Base>
        <BasicLayout>
          <Component {...pageProps}></Component>
        </BasicLayout>
      </Base>
    </QueryClientProvider>
  );
}

디폴트 옵션을 스스로 관리하는 셋팅(_app.tsx)

import BasicLayout from "@/components/atoms/layout/BasicLayout";
import styled from "@emotion/styled";
import { AppProps } from "next/dist/shared/lib/router/router";
import React from "react";
import { QueryClient, QueryClientProvider } from "react-query";

const Base = styled.div`
  width: calc(100vw - calc(100vw - 100%));
  height: calc(100vh - calc(100vh - 100%));
  display: flex;
  flex-direction: column;
  align-items: center;
  position: relative;
`;

export default function App({ Component, pageProps }: AppProps) {
    const queryClient = new QueryClient({
        defaultOptions: {
          queries: {
            refetchOnWindowFocus: false,
            refetchOnMount:true,
            refetchOnMount: false,
            retry: 3,
            staleTime: 2000,
          },
        },
      })
  );
  return (
    <QueryClientProvider client={queryClient}>
      <Base>
        <BasicLayout>
          <Component {...pageProps}></Component>
        </BasicLayout>
      </Base>
    </QueryClientProvider>
  );
}

위에 보이는 defaultOption안에 들어가는 값은 밑에 useQuery에서 직접 변경할 수 있습니다. useQuery에서 사용하는 것과 queryClient에서 사용하는 것에 차이는 전역 vs 지역이라고 생각하시면 편하게 이해하실 수 있을것 같습니다.

useQuery

import { useQuery } from "react-query";
// 주로 사용되는 3가지 return 값 외에도 더 많은 return 값들이 있다. 
const { 
    data, // 💡 data.pages를 갖고 있는 배열
    error, // 💡 error 객체
    isFetching, // 💡 첫 페이지 fetching 여부, Boolean, 잘 안쓰인다
    status, // 💡 loading, error, success 중 하나의 상태, string
    } = useQuery(queryKey, queryFn, options)

Query Key

  • queryKey로 데이터 캐싱을 관리합니다. unique한 값이여야 합니다.
  • 문자열 또는 배열로 지정할 수 있습니다.
// 문자열
useQuery('movies', ...)
// 배열
useQuery(['movies'], ...)
  • 쿼리가 변수에 의존하는 경우에는 QueryKey 에도 해당 변수를 추가해주어야합니다.
const { data, isLoading, error } = useQuery(movie, () => axios.get(`http://.../${id}`));

Query Function

  • useQuery의 두번쨰 인자에는 promise를 반환하는 함수를 넣어주어야만 합니다
 useQuery('movies', fetchMovies);
 useQuery(['movies', movieId], () => fetchMovieById(movieId));
 useQuery(['movies', movieId], async () => {
   const data = await fetchMovieById(movieId);
   return data
 });

Query Options

  • 리액트 쿼리 공식 문서에는 더 많은 옵션이 존재하지만, 자주 쓰이거나 자주 쓰일것 같은 옵션만 다뤄보겠습니다.
  • react-query-API Docs

enabled(boolean)

  • enabled는 쿼리가 자동으로 실행되지 않게 설정하는 옵션입니다.
  • 특정 값 예를들면 id가 존재할때만 쿼리를 요청할때 사용됩니다.
  const { isLoading, error, data } = useQuery("movie", getMovie, {
    enabled: !!id,
  });

retry (boolean | number | (failureCount: number, error: TError) => boolean)

  • retry는 기본적으로 쿼리를 재시도하는 옵션입니다.
  • 디폴트값은 3회입니다.
  • true로 설정할시 무한으로 재시도하고, false로 설정할 시 최초 한번 실행후에 재시도를 하지 않습니다.

staleTime (number | Infinity)

  • stale time은 쿼리가 fresh한 상태로 유지되는 시간입니다. 설정한 시간이 지나면 stale상태가 됩니다.
  • default stale time은 0입니다.
  • fresh 상태에서는 쿼리가 다시 mount 되어도 fetch가 되지 않습니다.

cacheTime (number | Infinity)

  • cacheTimeinactive 상태인 캐시 데이터가 메모리에 남아있는 시간입니다. 이 시간이 지나면 가비지 컬렉터에 의해 메모리가 제거됩니다.
  • 디폴트값은 300초입니다.

refetchOnMount (boolean | "always")

  • refetchOnMount는 쿼리가 stale 상태일 경우, 마운트시마다 refetch를 실행할지 말지 결정하는 옵션입니다.
  • 티폴트값은 true고, always로 설정하면 마운트시마다 매번 refetch가 됩니다.

refetchOnWindowFocus (boolean | "always")

  • refetchOnWindowFocus는 쿼리가 stale 상태일 경우, 윈도우 포커싱이 될때마다 refetch를 하는 옵션입니다.
  • 디폴트값은 true입니다

마무리

이미 많은 분들이 react-query를 사용해서 server-state 상태를 관리하고 있을거라 생각합니다. 하지만 리액트 쿼리를 단순하게 서버에 데이터를 요청하는 라이브러리로 끝낸다면, 리액트 쿼리가 갖고 있는 유용한 기술들을 놓칠 수 있다고 생각합니다.

특히, 위에 option들을 보면 리액트 쿼리가 caching과 re-fetching에 상당히 많은 옵션을 제공한다는 점을 알 수 있습니다. 사용자가 10명인 서비스에서는 api요청을 100번을 해도 10,000건이지만, 사용자가 100,000명인 서비스에서 사용자가 동시에 100번씩 api를 요청하면 10,000,000건으로 서버에서는 엄청난 부하가 걸릴 수 있습니다.

이러한 사실들을 잘 유의해서 상태관리 라이브러리를 사용한다면, 아마 우리의 서비스는 더 적은 비용으로, 더 많은 사람에게 좋은 경험을 줄거라 생각합니다.

긴 글 읽어주셔서 감사합니다

참고자료

profile
공유하고 나누는걸 좋아하는 개발자

0개의 댓글