티스토리 뷰
최근 들어 서버 데이터를 프론트 state로 변형해 관리하여 처리하는 경우가 종종 있었습니다.
모든 서버에 API 응답값이 페이지 내에서 구현해야 하는 비즈니스 로직에 알맞으면 좋겠지만 그러지 못한 경우가 대다수일 것 입니다.
당시에는 서버 응답값을 기반으로 파생 상태를 만들어 로직을 전개했지만 파생상태를 정의하는 과정이 매끄럽지 못한것을 인지하고 있었습니다. 지금 돌이켜서 어떻게 하면 더 좋은 방법으로 서버 데이터로부터 프론트 파생 상태를 정의할 수 있는지 고민해보았습니다.
여러 방법들이 눈에 띄었지만 다음 두가지 방법이 적용과 이해가 쉬웠습니다.
1. TanStack Query의 select 옵션 사용
import { useQuery } from '@tanstack/react-query';
const useProcessedData = () => {
return useQuery(['dataKey'], fetchData, {
select: (data) => {
// 여기서 데이터를 가공합니다
return processData(data);
},
});
};
// 사용 예
const { data: processedData } = useProcessedData();
위 예제처럼 적용한다면 다음과 같은 장점을 얻을 수 있습니다.
장점:
- React Query 내부에서 최적화되어 있어 성능상 이점이 있습니다.
- 코드가 간결하고 React Query의 패러다임에 잘 맞습니다.
- 캐시된 데이터가 변경될 때만 실행되므로 불필요한 재계산을 방지합니다.
하지만, 모든 방법과 기술이 장점만 있지는 않습니다. 단점으로는 다음과 같습니다.
단점:
- 복잡한 연산의 경우 디버깅이 어려울 수 있습니다.
- select 함수 내부에서 외부 상태에 접근하기 어렵습니다.
추가 예시
단점 중 'select 함수 내부에서 외부 상태에 접근하기 어렵습니다.' 를 보완하기 위해 다음과 같이 useQuery 훅에서 select와 같은 옵션을 받게 설정할 수 있습니다.
import { UseQueryOptions, useQuery } from '@tanstack/react-query';
// fetchData의 반환 타입을 RawDataType이라고 가정합니다.
type RawDataType = ReturnType<typeof fetchData>;
export const useProcessedData = <TData = RawDataType>(
options?: Omit<UseQueryOptions<RawDataType, Error, TData>, 'queryKey' | 'queryFn'>
) => {
return useQuery<RawDataType, Error, TData>(['dataKey'], fetchData, {
select: (data) => {
// 여기서 데이터를 가공합니다
return (options?.select ? options.select(data) : processData(data)) as TData;
},
...options,
});
};
// 사용 예시
// 1. 기본 사용 (processData의 반환 타입을 사용)
const { data: processedData } = useProcessedData();
// 2. 커스텀 select 함수를 사용하여 다른 타입으로 변환
const { data: customProcessedData } = useProcessedData<CustomDataType>({
select: (data) => customProcessData(data),
});
위 예시처럼 select 옵션을 사용하는쪽에서 정의하고 타입을 부여하면 select 함수 내에서 외부 상태에 접근할 수 있으나 함수의 순수성을 해칠 수 있고 오류를 파악할때 복잡성이 증가할 수 있습니다.
2. 메모이제이션 활용 (useMemo)
import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
const useProcessedData = () => {
const { data: rawData, ...rest } = useQuery(['dataKey'], fetchData);
const processedData = useMemo(() => {
if (!rawData) return null;
return processData(rawData);
}, [rawData]);
return { ...rest, data: processedData };
};
위 예시 코드의 장점은 다음과 같습니다.
장점:
- 컴포넌트의 다른 상태나 props에 의존하는 복잡한 계산에 유용합니다.
- 디버깅이 비교적 쉽습니다.
- 외부 상태나 함수를 활용한 계산에 유연합니다.
반대로 단점으로는 다음과 같습니다.
단점:
- React의 렌더링 사이클에 종속되어 있어, 불필요한 재렌더링이 발생할 수 있습니다.
- 의존성 배열 관리에 주의를 기울여야 합니다.
select 옵션 vs 메모이제이션
그렇다면 실제 실무에 적용할때는 어떤 방법을 선택해야 할까요?
각 방법의 장단점을 고려하여 다음과 같은 기준을 세울 수 있다고 생각합니다.
선택 기준:
1. 단순한 데이터 변환의 경우:
select 옵션을 사용하는 것이 더 적합합니다. 코드가 간결하고 React Query의 최적화를 직접 활용할 수 있습니다.
const useProcessedData = () => {
return useQuery(['dataKey'], fetchData, {
select: (data) => processData(data),
});
};
2. 복잡한 계산이나 외부 상태에 의존하는 경우:
useMemo를 사용하는 것이 더 적합할 수 있습니다. 특히 다른 상태나 props에 의존하는 계산의 경우에 유용합니다.
const useProcessedData = (someProps) => {
const { data } = useQuery(['dataKey'], fetchData);
const processedData = useMemo(() => {
if (!data) return null;
return complexProcessing(data, someProps);
}, [data, someProps]);
return { data: processedData };
};
3. 성능이 중요한 경우:
대부분의 경우 select 옵션이 성능상 이점이 있습니다. React Query가 내부적으로 최적화를 제공하기 때문입니다.
4. 재사용성과 테스트 용이성:
select 옵션 내의 함수를 별도로 분리하여 재사용하고 테스트하기 쉽게 만들 수 있습니다.
위 경우로 모든 경우가 대처되면 상관이 없지만 다음과 같은 경우에는 어떻게 해야할까?
- 서버로부터 데이터를 가져옴
- 프론트에서 데이터를 재가공함
- 재가공한 데이터를 사용자 동작에 따라 업데이트하여 UI로 보여주어야함
예시의 경우에는 결국 TanStack Query의 데이터 외에 useState라는 훅을 사용해 state와 setState를 사용해야 합니다.
TanStack Query와 useState 함께 사용하기
import { useState, useEffect, useCallback } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
const useProcessedData = () => {
const queryClient = useQueryClient();
const [localData, setLocalData] = useState(null);
// 서버에서 데이터 fetching
const { data: serverData, isLoading, error } = useQuery(['dataKey'], fetchData);
// 서버 데이터를 가공하여 로컬 상태로 설정
useEffect(() => {
if (serverData) {
const processedData = processServerData(serverData);
setLocalData(processedData);
}
}, [serverData]);
// 사용자 액션에 따른 로컬 데이터 업데이트 함수
const updateLocalData = useCallback((updateFn) => {
setLocalData(prev => {
const updated = updateFn(prev);
// React Query 캐시 업데이트 (선택적)
queryClient.setQueryData(['dataKey'], oldData => ({
...oldData,
// 업데이트된 데이터를 서버 데이터 형식에 맞게 변환
...reverseProcessServerData(updated)
}));
return updated;
});
}, [queryClient]);
return {
data: localData,
isLoading,
error,
updateLocalData
};
};
// 컴포넌트에서 사용
const MyComponent = () => {
const { data, isLoading, error, updateLocalData } = useProcessedData();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
const handleUserAction = () => {
updateLocalData(prevData => ({
...prevData,
// 사용자 액션에 따른 데이터 업데이트 로직
}));
};
return (
<div>
{/* 데이터 표시 및 사용자 인터랙션 UI */}
</div>
);
};
위처럼 사용하게 되면 다음과 같은 이점을 얻을 수 있습니다.
- 서버 데이터와 로컬 상태를 분리하여 관리할 수 있습니다.
- React Query의 장점(캐싱, 자동 리프레시 등)을 그대로 활용할 수 있습니다.
- 사용자 액션에 따른 즉각적인 UI 업데이트가 가능합니다.
- 필요한 경우 로컬 상태 변경을 React Query 캐시에 반영할 수 있습니다.
대신 다음 사항에 대한 주의가 필요합니다.
- 서버 데이터와 로컬 상태의 동기화를 주의깊게 관리해야 합니다.
- 성능 최적화를 위해 불필요한 리렌더링을 방지하는 것이 중요합니다. 필요한 경우 useMemo나 useCallback을 활용을 고려할 수 있습니다.
위 경우까지 처리를 한다면 아직까지 제 경험상 더 이상 고려해야할 서버 데이터의 파생상태 처리는 없을것으로 보입니다.
여러분들은 어떤 생각을 가지고 계신가요?
혹시 비슷한 문제를 고민하고 있던 개발자분이 계시면 공유부탁드립니다. 큰 도움이 될 것 같습니다!
'개발' 카테고리의 다른 글
Flutter에서 갤럭시 폴드 5(안드로이드) 카카오톡 열기 이슈 및 해결 (window.open & intent) (1) | 2024.01.24 |
---|---|
Flutter로 개발하고 Xcode에서 앱 띄울때 스플래쉬에 로고가 안나오는 이슈 원인과 해결 (1) | 2024.01.20 |
Module federation에서 겪은 tailwind css 이슈 그리고 해결방법 (2) | 2024.01.15 |
Cypress 적용 및 github actions CI 붙히기 (2) | 2024.01.13 |
아카이빙 : 프론트엔드 아티클 모으기 (0) | 2023.12.27 |
- Total
- Today
- Yesterday
- test
- Style
- frontend
- error handle
- design system
- subrouting
- yarn-berry
- CI
- aws
- 서버상태관리
- defineProps
- vue
- Micro Frontend Architecture
- DevOps
- TanStackQuery
- pnpm
- deploy
- 독서
- zero install
- MFA
- 프론트엔드최적화
- Flutter
- vue3
- Module Federation
- 상태관리전략
- 프론트엔드아키텍처
- 당신은 결국 무엇이든 해내는 사람
- Infra
- node module
- 독후감
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |