[11월] 원티드 프리온보딩 과제 (feat. TanStack Query)

devAnderson·2024년 11월 7일
0

TIL

목록 보기
106/106

✅ 1. 각자 Tanstack Query 공부해서 적용해보기

오랜만에 기술블로그에서 react-query라고 검색을 해보았다. 2022년 당시 작성했던 글이 두개가 나를 반겨주었다.
그 중 하나를 다시 읽어보니, 그 당시의 나의 열정이 편린을 다시한번 느끼게 되는 순간이었다. "아, 이 당시의 나를 다시 본받는 시간을 갖자"

XMLHttpRequest 객체로 요청을 날려서 응답을 콜백으로 받아서 처리하던 걸 공부하던 시절이 있었고,
axios 라이브러리를 처음 접하고 이렇게 편한 기술이 있는가 하면서 좋아하던 시절이 있었다.

그리고 useSWRreact-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
  }
});

✅ 2. 공식 문서 & tkdodo의 블로그 글을 기반으로 UI / Server State 분리하는 이유에 대해 생각해보기

react-query에 대해서 정리했었던 글 중 첫번째 글에서 굉장히 그 당시의 나는 야무지게 정리를 해놨던 것 같다. ( 다시금 과거의 나에게 박수를 보내며, 또 다시 글을 읽으며 공부를 해본다. )

  • 상태는 크게 Client State와 Server State로 나눌 수 있다
  • Client State은 브라우저에 의해 나타내지는 최신 데이터로, ajax 요청에 의해 서버로부터 받아온 그 당시의 최신 snapshot을 나타낸다
  • Server State은 서버에서 관리되고 있는 데이터로, 항상 외부 요청으로 인해 변동이 되는 값들이다.
  • 이런 두 간극으로 인해, 클라이언트는 항상 서버의 최신 데이터를 보여준다고 장담할 수가 없다 ( 언제든 바뀌어있을 것이기 떄문에 )
  • 따라서, 서버에서 변경된 내용을 지속적으로 보여주기 위해서는 필요한 비동기 요청의 절차가 필요하다.
  • 이런 요청들을 해결하기 위해 만들어진 것이 react-query이다

여기서 다시금 지금의 내가 다시 과제에서 주어졌던 레퍼런스 블로그를 읽어보며 느끼는 새로운 생각은 아래와 같다.

  • 관심사적인 관점에서 보았을 때, UI 상태와 서버 상태는 성격이 다르다.
  • UI 상태는 모달의 켜짐과 닫힘등과 같은 즉각적이며 예측 가능한 상태이고 이는 클라이언트의 환경에서 종속되며 컴포넌트별로 귀속되기 쉬운 좁은 의미의 상태이다.
  • 하지만 일반적으로 서버 상태는 외부 환경에 저장되어 있는 데이터로 네트워크의 상태에 따라서 그 요청의 결과가 유동적으로 처리될 수 있는 예측하기 어려운 상태이다. 또한 전역적으로 저장, 유지, 관리되어 Single source of truth로서 모든 컴포넌트에서 활용할 수 있어야 하는 공통적 상태이다.
  • 따라서 두 상태의 성격이 다르다면 서로의 관심사가 다른 것이므로 이는 분리되어 관리될 필요가 있다. 이의 혼용은 자칫 예측하기 어려운 사이드 이펙트를 유발하기 쉽다.

더욱이 결국 서버에서 받아오는 상태는 snapshot의 성격과 유사하다. 서버에서 받아오는 데이터는 캐싱을 통해 유지되지만 서버의 데이터는 변경되어있을 수 있다. 따라서 특정 상황에 따라서는 이 상태의 유효성을 검증하여 유효하지 않을 경우 새로운 데이터를 받아와 동기화를 해주어야 하는 작업이 추가로 필요해진다.

되도록 UI는 data binding을 통해 순수함수로서 예측할 수 있는 결과물을 리턴하는 것이 좋다고 생각한다.

서버 상태와 UI 상태를 분리하면, UI 상태는 컴포넌트 내부에서 단순하게 관리하고, 서버 상태와 동기화, 캐싱 등은 React Query를 이용해 처리하면 코드 복잡도를 크게 감소시킬 수 있다.

✅ 3. Cache, stale, stale-while-revalidate(http) 공부하기

이건 시작하기 전 사담인데, 기술 블로그를 꾸준하게 쓰는 이유중 하나이기도 한다.

지금 이 주제를 보자마자 'ㅋ 캐싱 분명히 공부 해서 달달 외웠는데 기억안남 그게 뭐였쬬' 하고있는 내 머리를 발견하게 된다.
역시 사람의 기억력은 믿을게 못된다. 그래서 블로그를 자시 찾아보니 아니나 다를까 정리한 게 있었다.

네트워크의 요청이 성공하게 되면, 브라우저는 이 결과값을 캐싱하여 저장해놓게 된다.

image

비싼 네트워크 요청을 다시 하지 않도록, 일정 시간 내의 요청에 대해서는 이 캐싱값을 계속 사용하고 이 캐싱 시간이 지나게 되면(stale) 서버에 요청하여 캐싱값과 서버 상태가 다르지 않을 경우 캐싱값을 그대로 사용하여 네트워크 비용을 줄이고, 다를 경우라면 새로운 요청을 받아 캐싱하는 구조로 되어있다.

여기서 캐싱된 데이터의 유효성 처리에 따른 추가작업의 성격에 따라 크게 두가지 방법이 존재하는데

  1. stale-while-revalidate = 캐싱 시간이 지난 후 새 요청이 들어오면 즉각 캐싱데이터를 주고 백그라운드에서 네트워크 요청 새로함
  2. if-modified-since = 캐싱 시간이 지난 후 새 요청이 들어오면 서버에 요청하여 응답에 따라 304일 경우는 캐싱 업데이트 및 그걸 쓰고 아니라면 서버에서 받아온 응답값으로 최신화.

표로 나타내면 아래와 같다.

항목stale-while-revalidateIf-Modified-Since
설정 위치서버의 Cache-Control 응답 헤더클라이언트의 요청 헤더
갱신 방식백그라운드에서 데이터 갱신, 즉시 캐시 사용변경 시에만 서버에서 새 데이터를 요청
사용자 응답 속도즉시 응답 가능 (캐시 제공)서버 확인 후 응답
사용 시점빠른 응답이 필요한 동적 데이터리소스가 자주 업데이트되지 않는 정적 콘텐츠
서버 트래픽 절감최신 데이터 요청을 백그라운드에서 처리, 캐시 사용 우선리소스 변경 시에만 데이터를 전송

✅ 5. 적용 이전 대비 어떤 효용이 있는지 Before / After 작성해보기

a. Before

  • 로컬 캐시의 효율적인 관리 로직의 부재로 불필요한 요청이 반복되어 서버에 부담을 주었다.
  • 네트워크 패칭에 대한 결과 상태 관리가 제대로 이루어지지 않고, 복잡한 코드로 관리되어 유지보수의 난도가 늘어났었다.

b. After

  • 네트워크 요청 결과에 대한 캐싱 기법을 이용하여 동일 데이터에 대한 네트워크 요청을 최소화하여 서버와 클라이언트의 네트워크 커뮤니케이션 코스트를 줄일 수 있었다.
  • UI 표출을 위한 상태값을 추상화시켜 코드를 간소화하고 유지보수를 용이하게 만들 수 있었다.
  • 외래 데이터 소스인 서버 상태의 캐싱과 수명주기를 클라이언트 사이드에서 컨트롤 할 수 있는 자유도를 얻을 수 있었다.

✅ 6. Redux 코드 분석하고 직접 최소 구현 작성해보기 (선택)

codepen

profile
자라나라 프론트엔드 개발새싹!

0개의 댓글