제가 프론트엔드 면접에 받은 단골 질문은 상태관리 라이브러리를 어떤 걸 사용해보셨나요? 입니다. 아마 많은 분들이 하나만 사용하진 않았을거라 추측해봅니다. 스터디를 한참 할때도 스터디원이 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요청을 보내고, 받은 데이터에는 많은 정보들이 보입니다
이런 상태(데이터)를 기반으로 UI를 드리는거죠.
위에 그림을 보시면 제목, 평점, 몇명이 투표를 했는지, 요약, 이미지 총 5개의 상태
를 갖고 각각의 컴포넌트를 제작했습니다. 웹사이트에서는 이렇게 서버에서부터 전달받은 상태를 통해 웹사이트를 렌더링합니다. 화면을 구성하기 위해 필요한 모든 데이터를 프론트엔드에서는 상태
라고 부릅니다.
상태관리는 State Management를 한국어로 직역한 단어입니다. 위에서 우리는 상태
라는 단어를 쉽게 데이터
라고 부르기로 했으니까, 쉽게 표현하자면 데이터를 관리하는 방법을 상태관리
라고 합니다.
상태관리는 크게 server state management와 client state management 두개로 나뉩니다. 이 포스팅은 react-query 즉, server state management에 대한 글이기 때문에, recoil은 다음 포스팅에서 자세하게 다뤄보도록 하겠습니다.
server state managment를 왜 사용해야 할까요?
웹사이트를 제작하면 사용자의 요청에 따라 서버에서 데이터를 보내거나 실패하게 되는데 해당 상태를 더욱 쉽게 관리함으로서, 유저에게 더 나은 경험을 제공할 수 있습니다. 요청 실패나 요청 중에 따른 상태를 서버 상태 관리 라이브러리를 통해 UI를 변경해서 사용자에게 결과에 따른 여러 가지 메시지를 보여줄 수 있기 때문입니다.
예를 들어봅시다. 실시간으로 비트코인을 사고팔아야 하는 서비스(예:업비트)와, 블로그를 쓰거나 읽는 서비스(예:벨로그)가 있다고 했을 때 상대적으로 데이터를 더 빨리 봐야 하는 서비스는 무엇일까요? 아마도 비트코인 매매 서비스일 겁니다. 따라서 업비트 같은 서비스를 만들땐 캐시 타임을 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가지 형태를 가지고 있습니다.
앞서, 리액트쿼리는 캐시를 효과적으로 관리해서 프론트엔드 개발자가 하나하나 설정해야할 기능들을 미리 셋팅해줘서 보다 쉽게 캐시나 refetching 옵션등을 관리할 수 있다고 언급했습니다. 일단 default값들이 어떤 것들이 있는지 알아보고, 개인적으로 자주 쓰이거나 알아두면 좋을 옵션만 다뤄보겠습니다.
useQuery
,useInfiniteQuery
로 가져온 데이터는 기본적으로 즉시 stale
상태가 됩니다.staleTime
: 쿼리가 fresh
에서 stale
상태로 변하는 시간입니다. 디폴트 값은 즉시변하는 것이지만, 초기옵션에서 변경하거나, 쿼리마다 다르게 설정할 수 있습니다.cacheTime
: inactive
데이터가 캐시에서 삭제되는 시간을 의미합니다. 디폴트값은 5분이며 이 역시 초기옵션이나 쿼리마다 설정 가능합니다.npm i react-query
# or
tarn add react-query
QueryClientprovider
를 최상단에서 감싸주어야 합니다client={queryClient}
를 작성해줍니다.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>
);
}
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 지역이라고 생각하시면 편하게 이해하실 수 있을것 같습니다.
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)
queryKey
로 데이터 캐싱을 관리합니다. unique한 값이여야 합니다.// 문자열
useQuery('movies', ...)
// 배열
useQuery(['movies'], ...)
const { data, isLoading, error } = useQuery(movie, () => axios.get(`http://.../${id}`));
useQuery('movies', fetchMovies);
useQuery(['movies', movieId], () => fetchMovieById(movieId));
useQuery(['movies', movieId], async () => {
const data = await fetchMovieById(movieId);
return data
});
enabled
는 쿼리가 자동으로 실행되지 않게 설정하는 옵션입니다. const { isLoading, error, data } = useQuery("movie", getMovie, {
enabled: !!id,
});
retry
는 기본적으로 쿼리를 재시도하는 옵션입니다.stale time
은 쿼리가 fresh
한 상태로 유지되는 시간입니다. 설정한 시간이 지나면 stale
상태가 됩니다.cacheTime
은 inactive
상태인 캐시 데이터가 메모리에 남아있는 시간입니다. 이 시간이 지나면 가비지 컬렉터에 의해 메모리가 제거됩니다.refetchOnMount
는 쿼리가 stale 상태일 경우, 마운트시마다 refetch를 실행할지 말지 결정하는 옵션입니다.true
고, always
로 설정하면 마운트시마다 매번 refetch가 됩니다.refetchOnWindowFocus
는 쿼리가 stale 상태일 경우, 윈도우 포커싱이 될때마다 refetch를 하는 옵션입니다.이미 많은 분들이 react-query를 사용해서 server-state 상태를 관리하고 있을거라 생각합니다. 하지만 리액트 쿼리를 단순하게 서버에 데이터를 요청하는 라이브러리로 끝낸다면, 리액트 쿼리가 갖고 있는 유용한 기술들을 놓칠 수 있다고 생각합니다.
특히, 위에 option들을 보면 리액트 쿼리가 caching과 re-fetching에 상당히 많은 옵션을 제공한다는 점을 알 수 있습니다. 사용자가 10명인 서비스에서는 api요청을 100번을 해도 10,000건이지만, 사용자가 100,000명인 서비스에서 사용자가 동시에 100번씩 api를 요청하면 10,000,000건으로 서버에서는 엄청난 부하가 걸릴 수 있습니다.
이러한 사실들을 잘 유의해서 상태관리 라이브러리를 사용한다면, 아마 우리의 서비스는 더 적은 비용으로, 더 많은 사람에게 좋은 경험을 줄거라 생각합니다.
긴 글 읽어주셔서 감사합니다