본문 바로가기

Frontend

React Query와 상태관리

이 글은 우아한테크세미나를 듣고 내용 및 제 생각을 정리한 글입니다. 

State

  • 상태란, 주어진 시간에 대해 시스템을 나타내는 것으로 언제든지 변경될 수 있음
  • 즉, 문자열, 배열, 객체등의 형태로 응용 프로그램에 저장된 데이터
  • 개발자 입장에서는 관리해야할 데이터들

UI/UX의 중요성과 함께 프로덕트 규모가 커지고 FE에서 수행하는 역할이 늘어났습니다. 모던 FE에서는 웹사이트가 정말 많은 상태를 가지고 있고, 상태는 시간에 따라서 변화하기 때문에 이를 관리해주는 Redux, Mobx같은 라이브러리가 등장합니다. (React에서는 단방향 바인딩이므로 Props Drilling 이슈도 존재)

 

그 중 가장 많이 사용하는 redux를 잠깐 들여다 보겠습니다. 흔히 리덕스에서는 store라는 전역 상태 저장 공간이 있습니다. 하지만 리덕스 코드를 살펴보면 상태관리 보다는 API 통신 코드를 많이 작성하고 있는 것을 볼 수 있습니다. 

 

배달의 민족 앱의 주문하는 로직이 들어있는 store 내부의 코드를 들여다 봅시다. 

 

export const FETCH_ORDER_REQUEST = 'FETCH_ORDER_REQUEST';
export const FETCH_ORDER_SUCCESS = 'FETCH_ORDER_SUCCESS';

export const fetchOrderRequest = createAction<number, string>(types.FETCH_ORDER_REQUEST, (page, string): number => {
    const parsed = Number.parseInt(page, 10);
    
    return Number.isNaN(parsed) ? 1 : parsed;
});

export const fetchOrderSuccess = createAction(types.FETCH_ORDER_SUCCESS, (order: OrderPayload) => order);

export const initialState: OrderState = {
    order: undefined,
    isFetching: false,
    ...
}

export default handleActions<OrderState, OrderPayload>({
    [FETCH_ORDER_REQUEST]: (state): OrderState => ({
        ...state,
        isFetching: true,
    }),
    [FETCHING_ORDER_SUCCESS]: (state, action:Action<OrderPayload> : OrderState => ({
    	...state,
        order: {
        	...action.payload
        },
        isFetching: false
    })
    ...
    
	},
    initialState
)


// redux-saga를 활용한 비동기 통신 예시
export function* fetchOrder$() {
	try {
    const params = yield select(paramsSelector);
    cosnt {data} = yield call(api.fetchOrder.params);
    yield put(fetchOrderSuccess(data))
  } catch (error) {
    ...
  }
}

export function* fetchOrderSuccess(action: Action<OrderPayload>) {
  try {
    yield callbackify(saveHistorys, action.payload)
  } catch (error) {
    ...
  }
}

export default function* order$() {
  yield all([
    takeLatest(FETCH_ORDER_REQUEST, fetchOrder$),
    takeLatest(FETCH_ORDER_SUCCESS, fetchOrderSuccess),
  ])
}

 

 

Store는 전역 상태가 저장되고 관리되는 공간인데 API 통신 코드로 도배가 된 것을 볼 수 있습니다.  서버에서 받아야 하는 상태들은 다음과 같은 특성을 지닙니다.

 

  • Client에서 제어하거나 소유되지 않은 원격의 공간에서 관리되고 유지됨
  • Fetching이나 Updating에 비동기 API가 필요함
  • 다른 사람들과 공유되는 것으로 사용자가 모르는 사이에 변경될 수 있음
  • 신경쓰지 않는다면 잠재적으로 "out of date"가 될 가능성을 지님
  • 사실상 FE에서 이 값들이 저장되어있는 state들은 일종의 캐시

위와 같은 특성상 이 상태들은 client에서 관리하는 상태들과는 특성이 다릅니다. 즉 Client State와는 다른 Server State입니다. 이 두가지 상태를 비교해 보겠습니다.

Client State

  • Client에서 소유하며 온전히 제어가능함
  • 초기값 설정이나 조작에 제약사항이 없음
  • 다른 사람들과 공유되지 않으며 Client내에서 UI/UX흐름이나 사용자 인터렉션에 따라 변할 수 있음
  • 항상 Client 내에서 최신 상태로 관리됨
  • Ownership이 Client에

Server State

  • Client에서 제어하거나 소유되지 않은 원격의 공간에서 관리되고 유지됨
  • Fetching/Updating에 비동기 API가 필요함
  • 다른 사람들과 공유되는 것으로 사용자가 모르는 사이에 변경될 수 있음
  • 신경 쓰지 않는다면 잠재적으로 "out of date"가 될 가능성을 지님
  • Ownership이 Server에

이 Server State를 관리하기 위해 배달의 민족 주문팀에서는 React-Query를 선택하게 되었다고 합니다. 

React-Query

  • Performant and powerful data synchronization for React
  • Fetch, cache and update data in your React and React Native applications all without touching any "global state"
  • 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 is hands down one of the best libraries for managing server state. It works amazingly well out-of-the-box, with zero-config, and can be customized to your liking as your application grows.

React-Query에는 3가지 core 컨셉이 존재합니다.

 

Queries (CRUD 중 R)

  • A query is a declarative dependency on an asynchronous source of data that is tied to a unique key.
  • A query can be used with any Promise based method(including GET and POST methods) to fetch data from a server.
  • If your method modifies data on the server, we recommend using Mutations instead.
  • Queries는 데이터 fetching용
  • Query Key에 따라 query caching을 관리
  • Query Function은 Promise를 반환하는 함수 -> 데이터 resolve 하거나 error를 throw

useQuery('fetchOrder', () = fetchOrder(orderNo), options)

export const fetchOrder = (orderNo: string): Promisze<ServerResponse<FetchORderResponse>> => 
	orderHistoryApiRequester
    	.get<ServerResponse<FetchOrderResponse>>(`example/${orderNo}`)
        .then((response: AxiosResponse<ServerResponse<FetchOrderResponse>>) => response.data);

 

useQuery가 반환하는것은 다음과 같습니다.

 

 

useQuery의 옵션도 매우 많습니다. 

 

 

 

 

onSuccess에서 action을 dispatch 하지만..

서버 상태는 서버 상태 자체로 관리. 클라이언트 상태와 디펜던시가 생긴다는 것 자체가 좋지않음.

 

Mutations

데이터 updating시 사용하는 아이

 

  • Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects. For this purpose, React Query exports a useMutation hook.
  • useQuery보다 더 심플하게 Promise를 반환하는 함수만 있어도 된다. (단 Query key를 넣어주면 devtools에서 볼 수 있다.)

 

const mutation = useMutation(newTodo => {return axios.post('/todos', newTodo)})

 

 

mutation은 Optimistic UI를 구현하기 좋다. 

Optimistic update라는 것은 페이스북에 좋아요를 누를 때와 같이 좋아요를 누르면 좋아요가 반영되고 API통신이 성공할 것으로 보고 UI를 먼저 반영하고 API 성공하면 update, 실패하면 rollback 한다.

 

stale-while-revalidate.

(stale response) 신선하지 않은 response 

stale data가 있으면 api를 찔러야한다. 위 예제에서 600초가 지나면 이 데이터는 낡은 것이다. 600초가 지나면 일단 낡은 데이터를 보여주는데 나는 백그라운드에서 refetch하고 있겠다!!

 

이 컨셉을 메모리 캐시에도 적용해보자!

  • cacheTime: 메모리에 얼마만큼 있을 건지 (해당 시간 이후 GC에 의해 처리, default 5분)
  • staleTime: 얼마의 시간이 흐른 후에 데이터를 stale 취급 할 것인지 (default 0)
  • refetchOnMount / refetchOnWindowFocus / refetchOnReconnect true이면 Mount / window focus / reconnect 시점에 data가 stale이라고 판단되면 모두 refetch (모두 default true)

react-query default config 

  • staleTime : default 0, quries에서 cached data는 언제나 stale 취급
  • refetchOnMount/refetchOnWindowFocus/refetchOnReconnect -> default 값 true. 각 시점에서 data가 stale이라면 항상 refetch 발생
  • cacheTime -> default 값 60 * 5 * 1000. inActive Query들은 5분 뒤 GC에 의해 처리
  • retry -> default 3, retryDelay -> default 값 exponential backoff function, Query 실패시 3번까지 retry.

전역 상태처럼 관리되는 데이터들 (feat. Context API)

마무리

react query를 사용하며 좋아진 점은 다음과 같다고 한다.

  • 서버 상태 관리 용이하며 (Redux, MobX)사용할 때보다 직관적인 API 호출 코드
  • API 처리에 관한 각종 인터페이스 및 옵션 제공
  • Client Store가 FE에서 정말로 필요한 전역 상태만 남아 Store 답게 사용됨 (Boilderplate code 감소)
  • devtool 제공으로 원활한 디버깅
  • Cache 전략 필요할 때 아주 좋음

 

 

'Frontend' 카테고리의 다른 글

Next.js 환경 변수  (0) 2022.08.11
PWA (점진적 웹 앱)  (0) 2022.06.16
React Server Components  (0) 2022.06.09
em vs rem  (0) 2022.06.02
HTTP와 HTTPS  (0) 2022.03.06