오랜만에 기술블로그에서 react-query라고 검색을 해보았다. 2022년 당시 작성했던 글이 두개가 나를 반겨주었다.
그 중 하나를 다시 읽어보니, 그 당시의 나의 열정이 편린을 다시한번 느끼게 되는 순간이었다. "아, 이 당시의 나를 다시 본받는 시간을 갖자"
XMLHttpRequest
객체로 요청을 날려서 응답을 콜백으로 받아서 처리하던 걸 공부하던 시절이 있었고,
axios
라이브러리를 처음 접하고 이렇게 편한 기술이 있는가 하면서 좋아하던 시절이 있었다.
그리고 useSWR
와 react-query
의 도입을 두고 서로 무엇이 더 나은지 npm trend를 들여가며 격렬한 논쟁을 벌였던 때도 기억난다.
시간은 순식간에 흘러가고 react-query는 이제 대중적인 비동기 처리 라이브러리의 정석처럼 받아들여지고 있는 것 같다.
앞으로도 또 다른 좋은 라이브러리가 나올 수 있겠지만, 아마 이만큼 네트워크 패칭에 대한 전체적인 플로우를 추상화를 잘 해놓은 라이브러리가 있을까.. 싶기도 하다. 기존의 try~catch로 범벅이 되어 매번 react 상태를 두고 업데이트시킨 뒤 이를 활용해서 UI로 표출하는 번거로움을 한번에 없애준 고마운 친구고, 종종 소스코드 공부하러 가는 공부거리인 친구기도 하다.
여튼 현재 팀에서는 기존 react-query의 useQuery에 빠져버린 callback 처리부를 확장하여 사용중인데, 개인적으론 편리하고 불필요한 useEffect를 작성하지 않고 선언적으로 콜백을 처리할 수 있어서 만족하는 중이다.
// libs/react-query/useQuery
'use client';
import { useEffect } from 'react';
import {
DefaultError,
QueryKey,
useQuery as useQueryOrigin,
UseQueryOptions,
UseQueryResult,
DefinedInitialDataOptions,
UndefinedInitialDataOptions,
DefinedUseQueryResult,
QueryClient,
} from '@tanstack/react-queryOrigin';
import { QueryCallbacks } from '@/@types/global/meta';
type ExtendedUseQueryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = UseQueryOptions<TQueryFnData, TError, TData, TQueryKey> & QueryCallbacks<TData, TError>;
type ExtendedDefinedInitialDataOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey> &
QueryCallbacks<TData, TError>;
...
export function useQuery<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: ExtendedUseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
queryClient?: QueryClient,
): UseQueryResult<TData, TError>;
export function useQuery<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(options: any, queryClient?: QueryClient): UseQueryResult<TData, TError> {
const queryInstance = useQueryOrigin<TQueryFnData, TError, TData, TQueryKey>(
options,
queryClient,
);
useEffect(() => {
try {
if (queryInstance.data) options.onSuccess?.(queryInstance.data);
if (queryInstance.error) options.onError?.(queryInstance.error);
...
} finally {
options.onSettled?.();
}
}, [queryInstance.data, queryInstance.error]);
return queryInstance;
}
-------------------------------------------------------------------------------------------------------------
// 실제 사용하는 쪽에는 각 api별로 훅으로 분리하여서 컴포넌트에서 모듈처럼 불러와 사용중이다.
// useQueryManagerListAPI.ts
import { useQuery } from '@tanstack/react-query';
/**
* @공간정보_관리자리스트(빌딩 유일 유무 판단하는) 조회 API
*/
export interface Manager {
buildingId: string;
authorityId: number;
userId: string;
userName: string;
userEmail: string;
userCompany: string;
userPhoneNumber: string;
}
type UseQueryManagerListParams = APIParams<
Partial<{
buildingId: string;
authorityId: number;
}>,
UseQueryCustomOptions<ResponseSuccess<{ managerList: Manager[] }>>
>;
export const useQueryManagerListAPI = (useQueryManagerListParams?: UseQueryManagerListParams) => {
const { buildingId, authorityId, ...useQueryOptions } = useQueryManagerListParams ?? {};
const queryKey = getRequestURL('/manager-list', { buildingId, authorityId });
const queryFn = () => axiosInstance.get(queryKey).then(data => data?.data);
return useQuery({ queryKey: [queryKey], queryFn, ...useQueryOptions });
};
---------------------------------------------------------------------------------------------------
// 사용처
import { useQueryManagerListAPI } from '@/app/_api/space-info/useQueryManagerList';
...
const totalManagerListByAuthorityId = useQueryManagerListAPI({
buildingId,
authorityId: useEditManagerFormProps.authorityValue,
enabled: useEditManagerFormProps.open,
onSuccess(data){
... treat the success situation callback
}
});
react-query에 대해서 정리했었던 글 중 첫번째 글에서 굉장히 그 당시의 나는 야무지게 정리를 해놨던 것 같다. ( 다시금 과거의 나에게 박수를 보내며, 또 다시 글을 읽으며 공부를 해본다. )
여기서 다시금 지금의 내가 다시 과제에서 주어졌던 레퍼런스 블로그를 읽어보며 느끼는 새로운 생각은 아래와 같다.
더욱이 결국 서버에서 받아오는 상태는 snapshot
의 성격과 유사하다. 서버에서 받아오는 데이터는 캐싱을 통해 유지되지만 서버의 데이터는 변경되어있을 수 있다. 따라서 특정 상황에 따라서는 이 상태의 유효성을 검증하여 유효하지 않을 경우 새로운 데이터를 받아와 동기화를 해주어야 하는 작업이 추가로 필요해진다.
되도록 UI는 data binding을 통해 순수함수로서 예측할 수 있는 결과물을 리턴하는 것이 좋다고 생각한다.
서버 상태와 UI 상태를 분리하면, UI 상태는 컴포넌트 내부에서 단순하게 관리하고, 서버 상태와 동기화, 캐싱 등은 React Query를 이용해 처리하면 코드 복잡도를 크게 감소시킬 수 있다.
이건 시작하기 전 사담인데, 기술 블로그를 꾸준하게 쓰는 이유중 하나이기도 한다.
지금 이 주제를 보자마자 'ㅋ 캐싱 분명히 공부 해서 달달 외웠는데 기억안남 그게 뭐였쬬' 하고있는 내 머리를 발견하게 된다.
역시 사람의 기억력은 믿을게 못된다. 그래서 블로그를 자시 찾아보니 아니나 다를까 정리한 게 있었다.
네트워크의 요청이 성공하게 되면, 브라우저는 이 결과값을 캐싱하여 저장해놓게 된다.
비싼 네트워크 요청을 다시 하지 않도록, 일정 시간 내의 요청에 대해서는 이 캐싱값을 계속 사용하고 이 캐싱 시간이 지나게 되면(stale) 서버에 요청하여 캐싱값과 서버 상태가 다르지 않을 경우 캐싱값을 그대로 사용하여 네트워크 비용을 줄이고, 다를 경우라면 새로운 요청을 받아 캐싱하는 구조로 되어있다.
여기서 캐싱된 데이터의 유효성 처리에 따른 추가작업의 성격에 따라 크게 두가지 방법이 존재하는데
stale-while-revalidate
= 캐싱 시간이 지난 후 새 요청이 들어오면 즉각 캐싱데이터를 주고 백그라운드에서 네트워크 요청 새로함if-modified-since
= 캐싱 시간이 지난 후 새 요청이 들어오면 서버에 요청하여 응답에 따라 304일 경우는 캐싱 업데이트 및 그걸 쓰고 아니라면 서버에서 받아온 응답값으로 최신화.표로 나타내면 아래와 같다.
항목 | stale-while-revalidate | If-Modified-Since |
---|---|---|
설정 위치 | 서버의 Cache-Control 응답 헤더 | 클라이언트의 요청 헤더 |
갱신 방식 | 백그라운드에서 데이터 갱신, 즉시 캐시 사용 | 변경 시에만 서버에서 새 데이터를 요청 |
사용자 응답 속도 | 즉시 응답 가능 (캐시 제공) | 서버 확인 후 응답 |
사용 시점 | 빠른 응답이 필요한 동적 데이터 | 리소스가 자주 업데이트되지 않는 정적 콘텐츠 |
서버 트래픽 절감 | 최신 데이터 요청을 백그라운드에서 처리, 캐시 사용 우선 | 리소스 변경 시에만 데이터를 전송 |
a. Before
b. After