대부분의 SPA(Single Page Application)는 화면을 그리는 로직들이 모두 클라이언트에 존재하고, 화면을 그리는데 필요한 데이터만 서버에서 요청하여 가져오는 방식으로 구현하고 있습니다. 이렇게 할 경우 MPA(Multiple Page Application)보다 자연스러운 사용자 경험(UX)나 필요한 리소스만 부분적으로 로딩하기에 성능적으로 유리한 장점이 있습니다. 그러나 SPA에서도 “뒤로가기”, “앞으로가기” 등의 동작을 수행 할 경우 이미 한 번 불러왔던 데이터를 다시 불러오는 불필요한 로딩이 발생하게 되는데, 이 때 API Cache를 사용하면 불필요한 API 통신을 막아 사용자에게 더 좋은 사용자 경험을 제공할 수 있습니다.
평소에 비동기 통신을 Axios 라이브러리를 사용하여 구현하고 있는데, Axios에서는 어댑터(Adapter)를 통해 요청 핸들링을 커스텀 할 수 있는 방법을 제공하고 있습니다.
Axios Extensions에서 제공하는 cacheAdapterEnhancer
를 활용하면 API 요청을 캐싱하기 위한 어댑터를 매우 쉽게 생성 할 수 있습니다.
import axios from 'axios';
import { cacheAdaperEnhancer } from 'axios-extensions';
const instance = axios.create({
baseUR: '/',
Accept: 'application/json',
headers: { 'Cache-Control': 'no-cache' }
adapter: cacheAdapterEnhancer(axios.defaults.adapter),
})
Cache-Control
헤더는 서버와 브라우저 사이의 캐시 정책을 명시합니다. 이 헤더값에 따라 브라우저가 해당 파일을 캐시해야하는지 언제 다시 서버에 요청하는지 결정하게 됩니다.
별다른 설정을 하지 않을 경우 cache will be enabled
되며, 커스텀한 캐싱을 원할 경우 no-cache
로 설정을 해줘야 합니다. no-cache
는 브라우저가 서버의 응답을 캐시할 지 개발자 스스로 결정 할 수 있습니다.
cacheAdapterEnhancer
에 axios의 기본 adapter를 넘겨줍니다.
내부로직을 확인해보면 buildSortedURL
를 사용하여 요청들을 모두 index화 하여 caching합니다. 매 호출 시 마다 cache.get
를 통해 기존에 호출 했던 적이 있다면 기존의 data를 내려주는 방식으로 구현되어 있습니다.
instance.get('/users'); // 1
instance.get('/users'); // 2
instance.get('/users', { cache: false }); // 3
/users
요청users에 대한 요청이 처음 발생했기 때문에 실제 네트워크 요청이 발생한다.
/users
요청이미 users에 대한 요청이 1에서 캐싱 되었으므로 실제 요청을 하지 않고 1에 캐싱된 데이터를 반환한다.
/users
요청기본적으로 cacheAdapterEnhancetr
의 기본 설정은 모든 요청을 캐싱한다. 다만 처음 캐싱이 일어난 후 새로운 데이터를 가져오고 싶다면, cache: false
옵션을 전달하면 된다. 해당 옵션을 포함한 요청은 캐싱 여부와 상관없이 실제 네트워크 요청이 발생 된다.
위와 같은 방식으로 캐시 어댑터를 적용하게 되면 모든 네트워크 요청이 캐싱되므로, 사용자가 데이터를 갱신하고 싶은 상황에 문제가 발생한다. 따라서 뒤로가기 또는 앞으로 가기 동작 수행 시에만 캐싱된 데이터를 사용하도록 하고 새로운 데이터를 필요로 하는 상황에서는 캐싱된 데이터가 아닌 새로운 데이터를 받아오는 것이 가장 좋은 방법이라 생각된다.
axios instance 생성 및 fetch method
import axios from 'axios';
import { cacheAdaperEnhancer } from 'axios-extensions';
const instance = axios.create({
baseUR: '/',
Accept: 'application/json',
headers: { 'Cache-Control': 'no-cache' }
adapter: cacheAdapterEnhancer(
axios.defaults.adapter,
{ enabledByDefault: false }
),
});
export const getUser = async (forceUpdate: boolean = false) => {
const response = await instance.get(
'/',
{
forceUpdate,
cache: true,
},
);
return response.data;
};
먼저 enabledByDefault
옵션을 false
로 설정하여 모든 네트워크 요청에 대해 캐싱된 데이터를 사용하지 않도록 합니다. 만약 새로운 데이터를 필요로 한다면 forceUpdate
에 true
값을 넘겨 줍니다.forceUpdate
는 데이터를 통신하여 최신화 합니다.
React
import React, { useEffect, useState } from 'react';
import getUser from '@/apis/user';
const User = ({ history }) => {
const [userInfo, setUserInfo] = useState(null);
const fetchUser = async (forceUpdate) => {
const data = await getUser(forceUpdate);
setUserInfo(data);
}
useEffect(() => {
fetchUser(history.action === 'PUSH');
}, [history.action]);
return (
<article>
{
userInfo && (
<figure>
<img src={userInfo.image} alt="유저 이미지"/>
<figcaption>{userInfo.name}</figcaption>
</figure>
)
}
<button onClick={() => { fetchUser(true); }}>새로고침</button>
</article>
)
};
기본적으로 새로고침 버튼을 통해 업데이트 할 경우에는 forceUpdate를 강제한다.
그 외에는 history.action
을 통해 사용자가 링크 클릭을 한 것인지, 뒤로가기 또는 앞으로가기 동작을 수행한 것인지 판단하여 캐싱된 데이터를 사용하도록 할 것인지 여부를 알 수 있다.
(링크를 클릭하는 경우 history.action
은 PUSH
이고, 뒤로가기 또는 앞으로가기 동작 수행시엔 POP
이 발생한다.)
🧐 위 코드는 좀 더 간소화 할 수 있을 듯 하다..
현재 제일 최신 버전은 0.19.2 이기에 새롭게 프로젝트 세팅을 하는 분들은 큰 문제가 없으나, 작년까지는 0.19.0이 최신 버전이었고 커스텀 설정 옵션과 관련된 버그가 있어 forceUpdate
옵션을 사용할 수 없습니다. 해당 옵션을 사용하시려면 0.19.1
이상의 버전을 사용하시거나, 0.18.1
버전을 사용해야 합니다.
😢 동작이 안되어서 한참 헤맴..ㅠ
위의 과정처럼 axios 내 cacheAdapterEnhancer 를 사용한다면 React 환경의 SPA에서도 매우 간단하게 사용자 경험을 개선 할 수 있습니다. (통신의 비효율을 줄여주기에 성능에도 이득!)