React Query와 상태관리
를 주제로 이루어진 2월 우아한 테크 세미나에 참여하고 배운 점을 바탕으로 작성되었습니다. (아래 링크는 이번 세미나에서 발표해주신 분이 작성한 기술블로그입니다.)
우아한 형제들 기술 블로그 - Store에서 비동기 통신 분리하기 (feat. React Query)
세미나 발표자님이 "상태 관리란 무엇일까요?"라고 물었을 때, 나는 Redux
와 같은 상태 관리 라이브러리를 떠올랐다😅 아무래도 상태 관리를 위해 라이브러리를 많이 쓰니까 그런 것 같다...(많이 반성했다.)
상태 관리를 정의하기 위해서는 먼저 상태가 무엇인지 알아야 한다.
Plain Javascript object holds information that influences the output of render
React 공식문서에서 state
를 컴포넌트를 렌더링하는데 있어 영향을 주는 정보를 지닌 객체라고 정의했다.
더 넓은 의미의 상태는 다양한 데이터 타입의 형태로 응용 프로그램에 저장된 데이터 (개발자 입장에선 관리해야 하는 데이터)라고 볼 수 있다. 중요한 것은 상태는 시간에 따라 변화한다는 것이다.
UI/UX의 중요성과 함께 프로덕트의 규모가 커지고 FE에서 수행하는 역할이 늘면서 관리하는 상태가 많아졌다. 여러 컴포넌트에서 같은 상태를 공통적으로 접근하고 공유해야 하는 경우가 빈번해지면서 효율적인 상태 관리의 필요성이 높아졌다. 그래서 전역 상태를 관리해줄 여러 상태 관리 라이브러리가 존재한다.
Redux
를 써보셨던 분들이라면 아래에 있는 코드가 익숙할 것이다.
Redux Middleware
를 이용하여 비동기 API 통신하는 코드이고, 로딩 상태와 가져온 데이터를 Store
에 저장한다.
const GET_POST = "GET_POST";
const GET_POST_SUCCESS = "GET_POST_SUCCESS";
const GET_POST_FAILURE = "GET_POST_FAILURE";
export const getPost = createAction(GET_POST, (id) => id);
// state 의 초기값 설정
const initialState = {
isLoading: false,
post: null
};
const post = handleActions(
{
[GET_POST]: state => ({
...state,
isLoading: true
})
[GET_POST_SUCCESS]: (state, action) => ({
...state,
post: action.payload,
}),
},
initialState
);
export default leader;
//** Saga **//
export function* leaderSaga() {
yield takeLatest(GET_POST, getPostSaga);
}
function* getPostSaga(action) {
try {
const post = yield call(api.getPost, action.payload);
yield put({
type: GET_POST_SUCCESS,
payload: post.data,
});
} catch (e) {
yield put({
type: GET_POST_FAILURE,
payload: e,
error: true,
});
}
}
만약 서버에서 가져와야 하는 데이터가 많아지면, IsLoading
, IsError
등 API 관련 상태가 중복되고, 비슷한 구조의 API 통신 코드가 반복될 것이다. 이는 개발자 입장에서 매우 비효율적이다.
Redux
상태 관리 라이브러리는 컴포넌트끼리 공유해야 하는 전역 상태를 효율적으로 관리하기 위해 사용되어야 하는데 비동기 통신을 위해 상태 관리 라이브러리를 사용하는 것이 아닐까 하는 생각이 들었다. 하지만 비동기 API 통신을 통해 얻은 데이터를 여러 컴포넌트에서 전역적으로 사용할 때도 있어, 섣불리 Redux
에서 비동기 API 통신을 분리하기에는 어려움이 있다.
정리를 하자면,
Redux
에서 비동기 API 통신을 없애고 온전히 Client Side
전역 상태를 Store
에 저장하고 관리하기Store
밖에서 API 요청을 하고, 받은 데이터를 전역 상태처럼 사용위와 같은 요구사항을 충족시켜줄 만한 적정 기술이 바로 React Query
이다!
React Query
의 첫인상 😀아래는 공식 문서에서 보여주는 코드이고, 이 코드를 보고 다음과 같은 느낌이 들었다.
useQuery
를 사용하여 간단하게 비동기 API 통신 상태와 데이터를 받을 수 있다.QueryClientProvider
로 컴포넌트를 감싸는 것을 보면 내부적으로 Context
를 사용하는 것 같아서 useQuery
로 받아온 데이터를 전역적으로 사용할 수 있을 것 같다.import { QueryClient, QueryClientProvider, useQuery } from 'react-query'
const queryClient = new QueryClient()
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
)
}
function Example() {
const { isLoading, error, data } = useQuery('repoData', () =>
fetch('https://api.github.com/repos/tannerlinsley/react-query').then(res =>
res.json()
)
)
if (isLoading) return 'Loading...'
if (error) return 'An error has occurred: ' + error.message
return (
<div>
<h1>{data.name}</h1>
<p>{data.description}</p>
<strong>👀 {data.subscribers_count}</strong>{' '}
<strong>✨ {data.stargazers_count}</strong>{' '}
<strong>🍴 {data.forks_count}</strong>
</div>
)
}
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 - Overview-
공식문서에서 state
를 client state
와 sever state
를 구분한다. 세미나에서 아래와 같이 client state와 server state를 구분하여 설명해주었다.
Key point
는 ownership
이다. client state의 ownership은 client
에 있고 sever state의 ownership은 server
에 있다.
While most traditional state management libraries are great for working with client state, they are not so great at working with async or server state. This is because server state is totally different. ... React Query is hands down one of the best libraries for managing server state.
three core concepts of React Query: Queries, Mutations, Query Invalidation
보통 GET
으로 받아올 대부분의 API에 사용하고, 데이터 Fetching
용이라고 생각하면 된다. (CRUD
중 Read
에 사용됨.)
import { useQuery } from "react-query";
function App(){
const info = useQuery('todos', fetchTodoList);
}
// 'todos' : Query Key
// fetchTodoList: Query Function
Query Key
에 따라 query caching
을 관리한다.Promise
를 반환하는 함수resolve
하거나 error를 throw
✔ If your query function depends on a variable, include it in your query key. Since query keys uniquely describe the data they are fetching, they should include any variables you use in your query function that change.
중요한 것만 정리하자면,
data
: 마지막에 성공적으로 resolved된 데이터error
: 에러가 발생했을 때 반환되는 객체isFetching
: Request가 in-flight 중일 때 truestate
, isLoading
, isSuccess
, isLoading
등등 : 현재 query 상태refetch
: 해당 query를 refetch하는 함수remove
: 해당 query cache에서 지우는 함수option도 정말 많다.... config
를 커스텀할 수 있다는 것도 하나의 장점이다.
onSuccess
, onError
, onSettled
: query fetching 성공/실패/완료 시 실행할 Side Effect 정의enabled
: 자동으로 query를 실행시킬지 말지 여부 설정retry
: query 동작 실패 시, 자동으로 retry할지 결정하는 옵션select
: 성공 시 가져온 data를 가공해서 전달keepPreviousData
: 새롭게 fetching 시 이전 데이터 유지 여부refetchInterval
: 주기적으로 refetch할지 결정하는 옵션자세한 내용은 아래 링크에 있습니다😉
🌸 React Query API Reference
이거까지 설명하면 내용이 길어질 거 같아 아래 링크를 참고해주세요😉
👉 공식문서에서 설명하는 Parallel Queries
✔ 이외에도 Paginated Queries
, Infinite Queries
등 정말 다양한 기능이 있습니다!
데이터 생성/수정/삭제시 사용된다. (CRUD
중 Create
/Update
/Delete
에 사용됨)
useMutation
은 Promise 반환 함수만 있어도 된다.
const mutations = useMutation(newTodo => {
return axios.post('/todos', newTodo);
});
mutate
: mutations을 실행하는 함수mutateAsync
: mutate와 비슷하지만 Promise 반환reset
: mutation 내부 상태 cleanonMutate
: 본격적인 Mutation 동작 전에 동작하는 함수, Optimistic update
적용할 때 유용!! // Invalidate every query in the cache
queryClient.invalidateQuries();
// Invalidate every query with a key that starts with 'todos'
queryClient.invalidateQueries('todos');
해당 key를 가진 query는 stale
취급되고, 백그라운드에서 refetch
된다.
const mutation = useMutation(addTodo, {
onSuccess: () => {
queryClient.invalidateQueries('todos')
},
})
mutation
성공 이후 특정 key
의 쿼리를 refetch
하고 싶을 때 Query Invalidation
이 주로 사용된다. Query Invalidation
을 이용하여 todos를 refetch할 수 있다.useQuery option 중에서
cacheTime
: 메모리에 얼마 만큼 있을 건지 설정 (해당 시간 이후 GC에 의해 처리, default 5분)staleTime
: 얼마의 시간이 흐른 후에 데이터를 stale 취급할 것인지 (default 0)refetchOnMount
,refetchOnWindowFocus
, refetchOnReconnet
: true이면 mount/window focus/reconnect 시점에 data가 stale
이라고 판단되면 모두 refetch
(모두 default true)fetching
fresh
: staleTime이 만료되기 전까지stale
: staleTime 만료, 스크린에서 사용되는 동안 staleinactive
: 스크린에서 사용 안함, cacheTime이 만료되기 전까지 inactivedeleted
: cacheTime이 0이 되면 GC에 의해 처리됨.QueryClient 내부적으로 Context
를 사용한다!
앞서 QueryClientProvider
로 컴포넌트를 감싸는 것을 보고 내부적으로 Context
를 사용하는 것 같아서 useQuery
로 받아온 데이터를 전역적으로 사용할 수 있을 것 같다라고 했는데 정답이다!
import { useQueryClient } from 'react-query'
const queryClient = useQueryClient();
const data = queryClient.getQueryData(queryKey);
앞서 Query Key
에 따라 query caching
을 관리한다고 했는데 위 코드처럼 Query Key
를 이용하여 캐시된 데이터를 바로 가져올 수 있다.
❗ React Query는 server state를 전역 상태처럼 관리한다!
이번에 처음으로 우아한 테크 세미나에 참여했는데 너무 좋은 경험이었다.👍 발표자님이 쓰신 블로그를 먼저 읽고 참여를 해서 내용이 많이 겹치지 않을까 걱정했는데 블로그에 작성한 내용 이외에도
react-query
에 대해 더 자세하게 설명해주셨다. 이번 세미나를 통해Redux
와 같은 상태 관리 라이브러리 사용의 본래 목적에 대해 다시 한 번 더 생각해보게 되었고,state
를client state
와server state
로 구분하는 것을 새롭게 알게 되었다.react query
라는 새로운 라이브러리를 알게 되어 좋은 경험이었고,react query
가 제공하는 다양한 기능을smart
하게 사용해보도록 하겠다😎
State 상태 흐름 중, stale과 inactive의 간극에 대한 의문이 들었는데 명료하게 해결되었습니다. 감사합니다.