
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 내부 상태 clean
onMutate: 본격적인 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)fetchingfresh : 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의 간극에 대한 의문이 들었는데 명료하게 해결되었습니다. 감사합니다.