상태 관리란 ?
먼저 상태 관리 ( Statement Management )
의 정의를 짚고 넘어가고 싶습니다..
상태 관리는 애플리케이션의 여러 영역에서 접근이 가능하고 일관되며 간단하게 업데이트할 수 있도록 데이터를 구성하고 보존하는 작업
입니다.
❓ 상태관리 종류
- 로컬 상태 관리: 각 컴포넌트 내에서 상태를 관리합니다. React의 useState나 Vue의 data 옵션을 사용할 수 있습니다.
- 글로벌 상태 관리: 전체 애플리케이션에서 공유되는 상태를 관리합니다. Redux, MobX, Context API 등이 이에 해당합니다.
- 서버 상태 관리: 서버로부터 받은 데이터를 관리합니다. 데이터 페칭, 캐싱, 동기화 등을 다룹니다. SWR, React Query 같은 라이브러리가 이에 해당합 니다.
SWR이 뭔가요 ?
SWR이라는 이름은 HTTP 캐시 무효 전략인
stale-while-revalidate
에서 유래되었습니다. SWR은 먼저 캐시(stale)로부터 데이터를 반환한 후, fetch 요청(revalidate)을 하고, 최종적으로 최신화된 데이터를 가져오는 전략입니다.
❓ 캐시는 어디에 저장이 되는건가요?
캐시된 데이터는 기본적으로 JavaScript의 메모리 내에 저장됩니다. 이는 클라이언트 측에서의 상태 관리에 해당하며, 브라우저의 메모리 내에 있는 JavaScript의 실행 컨텍스트에 데이터가 유지됩니다.
특징
서버 상태 관리 라이브러리인 SWR를 사용하는 이유는 여러가지가 있지만, 대표적인 예를 들어보려고 합니다.
추상화
유저의 정보를 가져온다고 가정을 하고 axios
를 통해서 가져오고, 로딩과 에러 상태를 또한 표시하기 위해 상태를 추가한 코드입니다.
import React, { useState, useEffect } from "react";
import axios from "axios";
function Profile() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
try {
const response = await axios.get("/api/user");
setData(response.data);
setError(null);
} catch (error) {
setError(error);
setData(null);
} finally {
setIsLoading(false);
}
};
fetchData();
}, []);
if (error) return <div>failed to load</div>;
if (isLoading) return <div>loading...</div>;
return <div>hello {data?.name}!</div>;
}
useSWR로 마이그레이션 한 코드입니다.
import useSWR from "swr";
function Profile() {
const { data, error, isLoading, isValidating } = useSWR("/api/user", fetcher);
if (error) return <div>failed to load</div>;
if (isLoading) return <div>loading...</div>;
return <div>hello {data.name}!</div>;
}
useSWR
훅을 사용하면 아래와 같이 코드가 짧아지는 것을 볼 수 있습니다.
이는 useSWR안에서 필요한 것에 대해서 추상화를 해주었기 때문입니다.
// useSWR 코드 내부
// 페칭을 시작할때. `isValidating` 상태를 변경하고, 캐시를 업데이트합니다.
const initialState: State<Data, Error> = { isValidating: true }
// 캐시된 데이터가 없으면 `isLoading` 상태입니다.
// 이것은 폴백 데이터와 지연된 데이터를 우회합니다.
if (isUndefined(getCache().data)) {
initialState.isLoading = true
}
try {
if (shouldStartNewRequest) {
setCache(initialState)
// 현재 렌더링되고 있는 캐시가 없으면(빈 페이지를 보여줍니다),
// 로딩이 느리게 되는 이벤트를 트리거합니다.
if (config.loadingTimeout && isUndefined(getCache().data)) {
setTimeout(() => {
if (loading && callbackSafeguard()) {
getConfig().onLoadingSlow(key, config)
}
}, config.loadingTimeout)
// ...code
isLoading vs isValidating ❓
둘이 처음에 보면 헷갈릴 수 있지만 isLoading은 초기 데이터 로딩 상태에 관한 것이고, isValidating은 초기 가져오기든 이후 업데이트든 상관없이 데이터가 새로고침되거나 재검증되는 상태에 관한 것입니다. 두 상태 모두 데이터 가져오기 상태에 따라 로딩 스피너나 새로고침 표시기와 같은 UI 요소를 관리하는 데 유용합니다.
캐싱을 통한 성능
function useUser() {
return useSWR("/api/user", fetcher);
}
function Avatar() {
const { data, error } = useUser();
if (error) return <DefaultImage />;
if (!data) return <Spinner />;
return <img src={data.avatar_url} />;
}
function App() {
return (
<>
<Avatar />
<Avatar />
<Avatar />
<Avatar />
<Avatar />
</>
);
}
<Avatar> 컴포넌트를 위와 같이 5번 렌더를 하지만 네트워크탭 내에서 확인하면 1번의 Request만 발생한것을 확인 할 수 있습니다.
같은 키를 사용하기 때문이죠. useSWR
을 자세히 들여다보면 페칭이 성공적으로 끝나면 아래와 같이 응답을 캐시에 저장하게 된다.
// useSWR 코드 내부
// 추가적인 리렌더링을 피하기 위해 최신 상태와 깊은 비교를 합니다.
// 로컬 상태의 경우, 비교하고 할당합니다.
const cacheData = getCache().data;
finalState.data = compare(cacheData, newData) ? cacheData : newD;
이때, 캐시 데이터를 key:value와 같이 Map() 형태로 저장을 하게 되고, 같은 키 값이 이미 캐싱되어 있다면 다시 서버로 요청을 보내지 않고 캐시데이터를 가져와서 보여주게 된다.
import useSWR, { SWRConfig } from "swr";
function App() {
return (
<SWRConfig value={{ provider: () => new Map() }}>
<Page />
</SWRConfig>
);****
}
// console.log(cache)
// Map(2) {
// 'key' => {
// // The value is an object with the following structure
// data: {
// // ...responseData
// },
// error: undefined, // No error in fetching this data
// isLoading: false, // Loading is complete
// isValidating: false, // No ongoing validation
// _k: ['key'] // Internal key structure
// },
// }
React-query와 SWR
일단 리액트 쿼리 측에서 비교해 놓은 자료를 확인하고 본인이 필요에 따라 선택해서 사용하면 괜찮을 것 같다. 보통 SWR에서 지원하는 기능은 리액트 쿼리에 서 모두 지원하지만 반대로 SWR에서 지원안하는 경우가 있다. 이는 SWR이 리액트 쿼리보다 기능이 적고, 필수적으로 필요한 기능에 집중하였기 때문에 번들사이즈가 거의 3배 차이가 난다.
내가 보기엔 제일 크게 차이나는 두가지 차이점은 아래와 같다.
자동 가비지 콜렉션
- React-query: 기본으로 5분동안 사용되지 않으면 해당 쿼리에 대한 가비지 콜렉션을 진행한다.
- SWR: 따로 built-in으로 지원하는 것이 없기때문에 직접 조작해서 지워줘야 한다.
쿼리 취소
- 리액트 쿼리 : 각 쿼리에서 AbortSignal 인스턴스를 제공합니다.
- SWR: 따로 built-in으로 지원하지 않지만 Fetcher에 AbortController를 구현하여 해당 동작을 구현 가능합니다.