본문으로 건너뛰기

"React" 태그로 연결된 3개 게시물개의 게시물이 있습니다.

모든 태그 보기

· 약 11분
최민석

서론

  • 리액트는 현재 웹 애플리케이션 개발에 널리 사용되는 프론트엔드 라이브러리입니다. 그 특징 중 하나는 상태 관리의 유연성이며, 이를 위해 자주 사용되는 도구 중 하나가 ContextAPI입니다. 그러나 많은 개발자들이 오해하는 것이, ContextAPI는 사실상 상태 관리 도구가 아니라 DI, 즉 의존성 주입(Dependency Injection) 도구라는 사실입니다.

  • 그렇다면 이러한 의존성 주입을 리액트에서는 어떻게 활용할 수 있을까요? 이를 위해 우리는 props와 ContextAPI를 통한 의존성 주입 방법에 대해 살펴볼 것입니다. 이를 통해 리액트에서 의존성 주입이 왜 필요하며, 어떻게 사용하는지에 대한 이해를 높일 수 있을 것입니다.

의존성 주입(Dependency Injection)

의존성 주입이란, 객체 지향 프로그래밍에서 흔히 볼 수 있는 디자인 패턴 중 하나로, 클래스 간의 결합도를 낮추고 코드의 재사용성과 테스트 용이성을 높이는 방법입니다. 이는 특정 객체가 다른 객체에 의존하는 경우, 해당 객체를 직접 생성하는 것이 아니라 외부에서 생성된 객체를 주입받아 사용하는 것을 의미합니다.

의존성 주입을 알았으니 React에서는 어떻게 의존성 주입 하는 지와 알아보겠습니다.

1. props를 통한 의존성 주입

  • React에서는 props를 통해 의존성 주입을 할 수 있습니다.
  • 아래 코드는 App 컴포넌트에서 Counter 컴포넌트로 count 상태와 setCount 함수를 props로 전달하여 의존성 주입을 하고 있습니다.
  • Counter 컴포넌트에서는 props로 전달받은 count 상태와 setCount 함수를 사용하여 의존성을 주입받아 사용하고 있습니다.
//App.tsx
const App = () => {
const [count, setCount] = useState(0);
return (
<div>
<Counter count={count} setCount={setCount} />
</div>
);
};

//Counter.tsx
const Counter = ({ count, setCount }) => {
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
};

2. ContextAPI를 통한 의존성 주입

  • 다른 방법으로는 ContextAPI를 통해 의존성 주입을 할 수 있습니다.
  • 아래 코드는 App 컴포넌트에서 Counter 컴포넌트로 count 상태와 setCount 함수를 ContextAPI를 통해 의존성 주입을 하고 있습니다.
  • Counter 컴포넌트에서는 useContext hook을 통해서 의존성을 주입받아 사용하고 있습니다.
//App.tsx
const CountContext = createContext({ count: 0, setCount: () => {} });
const App = () => {
const [count, setCount] = useState(0);
return (
<CountContext.Provider value={{ count, setCount }}>
<Counter />
</CountContext.Provider>
);
};

//Counter.tsx
const Counter = () => {
const { count, setCount } = useContext(CountContext);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
};

리액트에서 의존성 주입을 하는 이유

  • 기본적으로 의존성 주입은 확장 가능한지, 재사용 가능한지에 대해 고민하면서 작업하게 되면서 내 코드가 변경에 유연한지를 고민하게 됩니다.

즉, 리액트에서는 의존성 주입을 통해 UI 컴포넌트 변경이 유연한지 고민하고 재사용성이 있는지에 대한 고민을 해결할 수 있는 과정입니다.

다만 의존성을 잘못 사용할 때의 두 객체 사이의 결합도가 높아지는 문제가 발생하면서 변경에 유연하지 않도록 되기 때문에 잘 사용해야 합니다.

이를 만족할 수 있는 사항들을 알아보겠습니다.

1. 의존성은 교체 가능해야 한다.

  • 의존성 주입을 사용하면 의존하는 객체를 교체할 수 있습니다. 따라서, 코드의 특정 부분을 수정하지 않고도 다른 객체로 교체할 수 있어야 합니다. 이는 유지보수성을 높이고 기능 확장이 용이하게 만들어줍니다.

2. 테스트에 용이해야 한다.

  • 의존성 주입을 통해 의존하는 객체를 모의(mock) 객체로 대체할 수 있습니다. 이를 통해 단위 테스트를 수행할 때 의존하는 객체의 동작을 제어하고 검증할 수 있습니다. 따라서, 테스트 용이성이 높아집니다.

3. 컴포넌트를 통합할 수 있어야 한다.

  • 의존성 주입을 사용하면 여러 컴포넌트 간에 의존 관계를 쉽게 통합할 수 있습니다. 이를 통해 컴포넌트 간의 결합도를 낮출 수 있고, 재사용 가능한 컴포넌트를 만들 수 있습니다.

위와 같은 사항들을 만족할 수 있도록 의존성 주입을 사용해야 합니다.

만약 의존성 주입을 사용하지 않는다면?

의존성 주입을 사용하지 않으면 어떤 문제가 발생할까요? 의존성 주입을 사용하지 않으면 어떤 문제가 발생할 수 있는지 알아보겠습니다.

결합도 증가

  • 컴포넌트가 직접 의존하는 외부 리소스를 생성하거나 관리하는 경우, 컴포넌트와 의존성 사이에 강한 결합이 형성됩니다. 이는 코드의 유연성을 저하시키고 재사용성을 감소시킵니다. 예를 들어, 특정 컴포넌트가 API 호출을 직접 처리한다면, 해당 컴포넌트는 다른 API로의 교체가 어렵고, 테스트하기도 어려워집니다.

테스트의 어려움

  • 의존성 주입이 없으면 특정 컴포넌트의 의존성을 모의(mock) 객체로 대체하여 테스트하기가 어렵습니다. 외부 리소스에 직접 의존하는 경우, 해당 리소스를 테스트 환경에서 재현하기가 어려우며, 테스트 결과의 일관성과 신뢰성이 떨어질 수 있습니다.

코드 중복

  • 의존성 주입을 사용하지 않으면 여러 컴포넌트에서 동일한 의존성을 생성하거나 관리해야 할 수 있습니다. 이는 코드 중복을 증가시키고 유지보수를 어렵게 만듭니다. 의존성 주입을 통해 의존성을 중앙에서 관리하면 중복을 피하고 의존성 관리를 효율적으로 할 수 있습니다.

확장의 어려움

  • 의존성 주입이 없으면 애플리케이션의 확장이 어려워집니다. 새로운 기능이나 외부 리소스에 의존하는 컴포넌트를 추가할 때, 기존 컴포넌트들이 직접 수정되어야 할 수 있습니다. 의존성 주입을 사용하면 새로운 컴포넌트를 추가하거나 기존 컴포넌트를 변경하지 않고도 의존성을 주입함으로써 애플리케이션의 확장성을 향상시킬 수 있습니다.
const UserGreeting = () => {
const user = {
name: "John",
};

return <Greeting name={user.name} />;
};

const GuestGreeting = () => {
return <Greeting name="Guest" />;
};

const Greeting = ({ name }) => {
return <h1>Hello, {name}</h1>;
};

결론

리액트에서 의존성 주입은 코드의 재사용성과 유지보수성을 향상시키기 위해 사용되는 중요한 개념입니다.

의존성 주입을 통해 컴포넌트 간의 결합도를 낮추고 코드를 더 유연하게 만들 수 있습니다. 의존성 주입은 컴포넌트의 의존성을 명확하게 정의하고, 필요한 의존성을 주입함으로써 더욱 확장 가능하고 테스트 용이한 애플리케이션을 개발하는 데 도움을 줍니다.

이를 통해 좀 더 변화에 유연한 코드를 작성할 수 있습니다.

Refs.

· 약 12분
최민석

서론

  • Google Map 기반의 프로젝트를 진행하면서 Google에서 제공해주는 JavaScript Map API는 React에서 바로 사용하기에 어려운 환경을 가지고 있습니다.
  • DOM을 생성하는 시점에 Map을 생성하고 Map을 사용하는 시점에 DOM이 생성되어 있어야 하는데, React에서는 DOM을 생성하는 시점과 Map을 사용하는 시점이 다르기 때문에 문제가 발생합니다.
  • 이러한 점을 고려해 React 라이프사이클 환경에서 Map을 사용하는 방법을 알아보고자 합니다.

Google Map JavaScript API

React 환경에서 Map을 구현하기 전에 Google에서 제공해주는 JavaScript API를 살펴보겠습니다.
Google Map Document를 보면 Map을 생성하는 방법은 아래와 같습니다.

let map: google.maps.Map;
async function initMap(): Promise<void> {
//@ts-ignore
const { Map } = await google.maps.importLibrary("maps");
map = new Map(document.getElementById("map") as HTMLElement, {
center: { lat: -34.397, lng: 150.644 },
zoom: 8,
});
}

initMap();
export {};

위 코드를 통해 Map을 생성함을 알 수 있고 생성된 Map을 사용하기 위해서는 map 변수를 통해 접근할 수 있습니다.
이 코드를 React 환경에서 쓰기 위해서는 어떤 방법으로 접근해야 할 지 알아보겠습니다.

React에서 Map을 사용하기 위한 방법

리액트의 라이프사이클은 크게 mount, update, unmount로 나눌 수 있습니다.
위에서 살펴본 Google Map API를 사용하기 위해서는 mount 시점에 Map을 생성하며 update 시점에서의 최적화를 고려해야 합니다.

또한, Map API를 호출하는 시점 이전에는 로딩 처리를 하며 Map API를 호출하는 시점 이후에는 Map의 기능들을 사용할 수 있도록 구현해야 합니다.

Map API 호출 이전 시점

리액트 DOM이 생성되고 난 이후에 Map을 생성해야 합니다.

제공되는 map의 생성 방식은 DOM에 의존적이여서 DOM이 생성되기 전에 Map API를 호출하여 생성하면 에러가 발생하게 되고, 이에 대한 로딩 처리를 해주어야 합니다.

// Map.tsx
const [map, setMap] = useState<google.maps.Map | null>(null);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
async function initMap(): Promise<void> {
//@ts-ignore
const { Map } = await google.maps.importLibrary("maps");
setMap(
new Map(document.getElementById("map") as HTMLElement, {
center: { lat: -34.397, lng: 150.644 },
zoom: 8,
})
);
setLoading(false);
}
initMap();
}, []);

if (loading) return <div>loading...</div>;
return <div id="map" />;

위 코드를 통해 Map을 생성하고, Map이 생성되기 전에는 로딩을 표시하도록 구현할 수 있습니다. 다만 이렇게 했을 때 단순하게 Map을 생성만 하게 됩니다. 이를 위해 Map이 생성된 이후에 기능들을 사용할 수 있도록 구현해야 합니다.

Map API 호출이후 시점

Map이 생성된 이후에는 Map의 기능들을 사용할 수 있습니다. 이를 위해 Map이 생성된 이후에 기능들을 사용할 수 있도록 구현해야 합니다.

여러 방법이 있겠지만 여러 Map 라이브러리를 살펴보면 Wrapper를 만들고 Map API 호출이후로 다른 기능들을 render를 하는 방식으로 구현되어 있습니다.

저는 google maps에서 제공해주는 @googlemaps/js-api-loader를 활용하여 JavaScript Map API를 호출하여 검증하는 Wrapper를 만들어보았습니다.

해당 라이브러리를 사용한 이유는 싱글톤 패턴으로 구현되어 있어 여러번 호출해도 한번만 호출되고, Map API 호출 이후 다른 기능들을 render할 수 있도록 구현되어 있어 효율적으로 사용할 수 있기 때문입니다.

// GoogleMapWrapper.tsx
const GoogleMapWrapper = ({ children }) => {
const [status, setStatus] = useState<google.maps.Status>("SUCCESS");
useEffect(() => {
const googleMapResponse = new Loader(options);
const setStatusAndExecuteCallback = (status: Status) => {
if (callback) callback(status, googleMapResponse);
setStatus(status);
};
setStatusAndExecuteCallback(Status.LOADING);
googleMapResponse.load().then(
() => setStatusAndExecuteCallback(Status.SUCCESS),
() => setStatusAndExecuteCallback(Status.FAILURE)
);
}, []);

if (status === Status.SUCCESS && children) return <>{children}</>;
return <></>;
};

// MapComponent.tsx
const MapComponent = () => {
const ref = useRef(); // Map을 생성할 DOM을 참조하기 위한 ref
useEffect(() => {
new window.google.maps.Map(ref.current, {
center,
zoom,
});
});
return <div ref={ref} id="map" />;
};

// Map.tsx
const Map = () => {
return (
<GoogleMapWrapper>
<MapComponent />
</GoogleMapWrapper>
);
};

위 코드의 흐름을 살펴보면 아래와 같습니다.

  1. GoogleMapWrapper에서 Google Map API를 호출하게 된다.
  2. Google Map API 호출이 성공하면 MapComponent를 render한다.
  3. MapComponent에서 Map을 생성한다.

이를 통해 Google Map API 호출 이후 시점에 Map을 생성하여 사용할 수 있게 되었습니다.
더 나아가서 Google Map에서 제공하고 있는 Marker, InfoWindow 등 기능들을 사용할 수 있도록 구현하고 최적화를 고려해보겠습니다.

MapComponent의 기능 확장

MapComponent에서 Map을 생성하고, Marker, InfoWindow 등의 기능을 사용할 수 있도록 추상화하여 구현해보겠습니다.

// MapComponent.tsx
export const MapComponent = ({ children, ...options }) => {
const map = useState<google.maps.Map | null>(null);
const mapRef = useRef();

useEffect(() => {
if (!mapRef.current) return;
const initialMap = new window.google.maps.Map(mapRef.current);
setMap(initialMap);
}, [map]);

return (
<>
<div ref={mapRef} style={style} />
{Children.map(children, (child) => {
// children으로 받은 Map 기능들을 cloneElement를 통해 map을 전달하여 사용할 수 있도록 구현.
if (isValidElement(child)) {
return cloneElement(child as ReactElement<google.maps.MapOptions>, {
map,
});
}
return null;
})}
</>
);
};

// Map.tsx
const Map = () => {
return (
<GoogleMapWrapper>
<MapComponent>
<Clusters />
{/* 조건문을 통해 분기도 가능하다. */}
{isVisibleMarker && <VehicleMarkers />}
<VehicleInfoWindows />
<PolyLines />
{/* 맵 기능들을 활용할 수 있다. */}
</MapComponent>
</GoogleMapWrapper>
);
};

위와 같이 MapComponent에서 Map을 생성하고, Children으로 받은 Map 기능들을 cloneElement를 통해 map을 전달하여 사용할 수 있도록 구현하였습니다. 더 나아가서 실시간으로 업데이트되는 Marker 기능을 구현하기 위해 onIdle 함수를 통해 Map의 상태를 변경하는 방법을 살펴보겠습니다.

onIdle 함수를 통한 Map 상태 변경

google.maps.Map에서 제공하는 onIdle 함수를 통해 Map의 상태를 변경할 수 있습니다.
이를 리액트 환경에서 사용하기 위해 useEffect를 통해 onIdle 함수를 등록하고, onIdle 함수에서 Map의 상태를 변경하는 함수를 호출하도록 구현하였습니다.

// useControlledStateMap.ts
const [map, setMap] = useState<google.maps.Map | null>(null);
const [mapCenter, setMapCenter] = useState<LatLngLiteral | null>(null);
const [mapZoom, setMapZoom] = useState<number | null>(null);
const [mapBounds, setMapBounds] = useState<LatLngBoundsLiteral | null>(null);
const onIdle = () => {
// Map의 상태를 변경하는 함수를 호출한다.
const _zoom = map.getZoom();
const _center = map.getCenter();
const _bounds = map.getBounds();
if (_center) {
const { lat, lng } = _center.toJSON();
setMapCenter({
lat,
lng,
});
}
if (_zoom) {
setMapZoom(_zoom);
}
if (_bounds) {
const { east, north, south, west } = _bounds.toJSON();
setMapBounds({
east,
north,
south,
west,
});
}
};

useEffect(() => {
if (!map) return;
// onIdle 함수를 Map Event Listener에 등록한다.
map.addListener("idle", onIdle);
}, [map]);

이 외로 Marker 컴포넌트와 같이 맵의 구성 요소가 바뀌면서 onIdle 함수가 계속 실행되는 사이클을 가지게 되는데 이를 해결하기 위해 useDeepCompareEffect 개념을 활용하여 최적화를 고려해보겠습니다.

useDeepCompareEffect 개념을 활용한 최적화

useDeepCompareEffect란?

useEffect와 동일한 기능을 가지고 있지만, useEffectdeps에 전달된 값이 변경되면 useEffect가 실행되지만, useDeepCompareEffect는 깊은 비교를 통해 값이 변경되었는지 확인하고 변경되었을 때에만 useEffect가 실행되도록 구현되어 있습니다.

  • 실제로 발생했던 이슈 중 하나로, 맵에서 제공하는 Center값과 Zoom값이 변경되면서 onIdle 함수가 계속 실행되는 사이클이 발생했고, onIdle에서 실행되는 Map 상태 변경 함수가 계속 실행되어 성능에 문제가 발생했습니다.
  • 이를 해결하기 위해 useDeepCompareEffect를 활용하여 맵의 zoom, center, bounds가 변경되어도 onIdle 함수가 실행되지 않도록 구현하였습니다.
// useDeepCompareEffect.ts
import { createCustomEqual } from "fast-equals";
const deepCompareEqualsForMaps = createCustomEqual((deepEqual) => (a, b) => {
if (
isLatLngLiteral(a) ||
a instanceof google.maps.LatLng ||
isLatLngLiteral(b) ||
b instanceof google.maps.LatLng
) {
return new google.maps.LatLng(a).equals(new google.maps.LatLng(b));
}
return deepEqual(a, b);
});

const useDeepCompareMemoize = (value) => {
const ref = React.useRef();
// LatLngLiteral과 같은 객체를 비교하기 위해 deepCompareEqualsForMaps를 사용합니다.
if (!deepCompareEqualsForMaps(value, ref.current)) {
ref.current = value;
}

return ref.current;
};

export const useDeepCompareEffectForMaps = (
callback: React.EffectCallback,
dependencies: any[]
) => {
React.useEffect(callback, dependencies.map(useDeepCompareMemoize));
};

// useControlledStateMap.ts
useDeepCompareEffectForMaps(() => {
if (!map) return;
map.setOptions({
theme: mapTheme,
// ... 기타 옵션들
});
}, [map]);

결과적으로 Map의 Lat, Lng가 변경되어도(Center, Bounds 등) onIdle 함수가 실행되지 않도록 구현하였고 효율적인 렌더링을 할 수 있게 되었습니다.

결론

  • 대부분의 제공하는 라이브러리 형태는 Vanila JavaScript 환경에서 사용할 수 있도록 예시가 구현되어 있습니다.
  • 사용할 수 있는 JavaScript Map API가 제공되어도 이를 React 환경에서 사용하기 위해서는 어떤 방식으로 접근해야 할 지 고민해야 합니다.
  • 시중에 제공되는 라이브러리를 살펴보면서 어떤 방식으로 접근해야 할 지 고민해보고, 이를 어떻게 추상화하여 사용할 수 있을 지 고민해보는 것이 중요합니다.

reference

· 약 10분
최민석

서론

  • 프로젝트를 진행하면서 Google Map API를 활용해 위도, 경도를 주소로 변환하는 로직을 필요로 했습니다.
  • 제공하는 API는 매 건당 6.6원의 비용이 발생했었는데 서비스 사용자가 많을 수록 굉장히 많은 호출이 이루어지다보니 비용적인 부담이 있었습니다.
  • 이러한 비용을 최적화하기 위해서는 최대한 효율적으로 API 호출을 해야 했고 해당 과정을 포스팅하게 되었습니다. 이번 포스팅에서는 API를 비용을 최적화하기 위한 방법을 알아보겠습니다.

image

문제 상황

  • 서비스에서 비용이 드는 API를 호출하는 경우가 있습니다. 사용자가 많은 서비스의 경우는 비용이 기하급수적으로 늘어날 수 있을 뿐만 아니라 페이지 첫 접속때 비용이 드는 데이터 패칭이 존재하게 될 경우 비용 남비가 더 심할 수 있는 문제가 발생합니다. 단순히 새로고침을 통해 한 명의 사용자가 여러 번 API를 호출하는 경우도 있고 악의(?)를 갖고 무한 새로고침을 하게 된다면 100번 이상의 API 호출을 할 수 있습니다. 즉, 비용이 많이 드는 API를 호출하는 경우에는 최대한 적은 API 호출을 하도록 최적화를 해야 합니다. 현재 문제 상황을 정리하면 다음과 같습니다.

    • 비용이 많이 드는 API를 호출하는 경우
    • 사용자가 많은 서비스의 경우 비용이 기하급수적으로 늘어날 수 있음.
    • 페이지 첫 접속때 비용이 드는 데이터 패칭이 존재하게 될 경우 비용 남비가 더 심할 수 있음.
    • 새로고침을 통해 한 명의 사용자가 여러 번 API를 호출하는 경우 (악의를 가지고 무한 새로고침을 하게 된다면 짧은 시간에 100번 이상의 API 호출을 할 수 있음)
    • 비용이 많이 드는 API를 호출하는 경우에는 최대한 적은 API 호출을 하도록 최적화를 해야 함.

문제 해결을 위해 API 비용을 최소화하기 위한 방법을 고민했고 API 호출 최소화를 위한 데이터를 캐싱으로 방향성을 잡고 작업했습니다.

queryKey를 통한 데이터 캐싱 방법

  • React 환경에서 데이터 패칭 도구로 react-query를 활용했습니다. useQuery 메소드에 지원하는 queryKey를 활용하여 데이터 캐싱을 유용하게 관리할 수 있는 기능을 활용하려고 했습니다.
    useQuery(['minsgy', { status, page }], ...)
useQuery(['minsgy', { page, status }], ...)
useQuery(['minsgy', { page, status, other: undefined }], ...)

위와 같은 방식을 통해 페이지 접속 시 API 호출은 최소화할 수 있었고 불필요한 API 호출은 막을 수 있었습니다.

그러나 queryKey로만 원하는 방식의 데이터 캐싱 관리는 불가능했습니다. 새로고침을 통하면 클라이언트 상태가 초기화되어 새로운 API 호출이 이루어지는 문제가 발생했습니다.

Storage를 통한 데이터 캐싱 방법

클라이언트 상태를 유지해야 했고 Fetching한 데이터를 저장하는 방법을 고민해 추가 방안은 다음과 같았습니다.

  • Refresh를 통해 클라이언트 상태를 유지하여 추가적인 API 호출을 막는다.
  • Fetching한 유효한 데이터를 저장하고 추가적인 API 호출을 막는다.
  • +) 5분 이상 지난 데이터는 삭제 후, 새로운 API 호출을 한다.

페이지 단에서 Refresh가 발생해도 데이터를 유지할 수 있는 방법으로 web storage와 indexed DB를 고민했지만 100줄 이하의 작은 데이터를 저장하는 것이 목적이여서 WebStorage를 활용했습니다.

queryKey + timestamp + fetching response data

유효한 데이터인지 확인하기 위한 timestamp, react-query와 함께 사용하기 위한 queryKey, 데이터 패칭한 response를 저장하기 위한 data로 묶어서 관리하는 방식으로 구현했습니다.

  • 아래와 같은 useCustomCacheQuery 커스텀 훅을 만들어서 timestamp를 통해 유효한 데이터인지 확인하고 유효한 데이터가 아닌 경우 API 호출을 하도록 구현했습니다.
  • 추가적으로 initialData react-query option를 통해 캐싱된 데이터가 있으면 캐싱된 데이터를 사용하도록 구현했습니다.
 // useCustomCacheQuery.ts
const storedData = JSON.parse(sessionStorage.getItem(queryKey));
const timestamp = storedData ? storedData.timestamp : Date.now();
const queryKey = storedData ? storedData.queryKey : customQueryKey;

// 저장된 데이터가 있고, timestamp가 5분 이내인 경우
if (storedData && timestamp && Date.now() - timestamp < 5 * 60 * 1000) {
...
}

// 저장된 데이터가 있지만, timestamp가 5분 이상인 경우
if (storedData && timestamp && Date.now() - timestamp >= 5 * 60 * 1000) {
...
}

// 저장된 데이터가 없거나, timestamp가 5분 이상인 경우
if (!storedData || !timestamp || Date.now() - timestamp >= 5 * 60 * 1000) {
...
}

return useQuery(queryKey, fetchFunction(), {
initialData: storedData ? storedData.data : undefined, // 캐싱된 데이터가 있으면 캐싱된 데이터를 사용
...
})

나올 수 있는 문제점과 고민 해야 할 점

이러한 캐싱 데이터를 사용할 때 나올 수 있는 문제점은 다음과 같았습니다.

  • 사용자가 확인하고 있는 데이터가 신뢰할 수 있는 데이터인지 확인하기 어렵다.
  • 캐싱된 데이터가 실제 데이터와 다를 수 있다.

해당 로직을 사용하기 위해서는 서비스 특징을 고민하고 적절한 방법을 선택해야 합니다.

  • 캐싱 된 데이터를 제공할 때 문제가 될 수 있는가?
  • 데이터 정합성이 중요한가? (데이터가 실시간으로 변경되는 경우)
  • 데이터 정합성이 중요하지 않은가? (데이터가 실시간으로 변경되지 않는 경우)

에 대한 서비스 특징을 고려하여 적절한 방법을 선택해야 한다고 생각합니다.

결론

API 비용을 최소화하기 위해 데이터 캐싱을 사용하는 방법에 대해 고민해보았습니다.

  • queryKey를 통한 데이터 캐싱 방법
  • Storage를 통해 유효한 데이터 캐싱을 위한 방법

위의 두 가지 방법을 통해 API 비용을 최소화할 수 있었습니다.

하지만 queryKey로만 원하는 방식의 데이터 캐싱 관리는 불가능했고 Storage를 통해 유효한 데이터 캐싱을 위한 방법을 사용했습니다.

또한, 캐싱된 데이터가 실제 데이터와 다를 수 있다. 라는 문제점이 있었습니다.

react-query에서 제공하는 persistance 기능

  • react-query에서는 persistQueryClient 기능을 제공합니다.
  • 직접 useCustomCacheQuery 커스텀 훅을 만들지 않고도 캐싱된 데이터를 사용할 수 있습니다.
  • 아직 react-query v4에서는 persistQueryClient 기능이 stable 버전이 아니기 때문에 사용에 주의가 필요합니다.