SWR과 React Query와 같은 서버 상태 관리 및 Data fetching 라이브러리와 그 필요성에 대해 서술하기 전에 먼저, React에서 사용하는 State(상태)를 구분할 필요가 있습니다.
Local State(지역 상태)
: React 컴포넌트 내에서만 사용되는 상태Global State(전역 상태)
: Global Store에 정의되어 프로젝트 어디에서나 접근할 수 있는 상태Server State(서버 상태)
: 서버로부터 받아오는 상태기존에는 Redux와 같은 전역 상태 관리 라이브러리에 비동기 로직을 처리하는 미들웨어(redux-thunk, redux-saga)를 통해 전역 상태(Global state)와 서버 상태(Server state)를 함께 관리할 수 있었습니다. 하지만 이로 인해 스토어(중앙 상태 저장소)가 관리하는 영역이 점점 커지게 되고, 코드도 장황하고 복잡해지게 됩니다.
따라서 서버 상태 관리 및 Data fetching 등의 비동기 로직을 React Query나 SWR과 같은 라이브러리를 이용하여 분리함으로써 관심사를 분리할 수 있습니다. 서버 상태 관리 라이브러리는 주로 데이터 fetching, caching, invalidation 등에 중점을 둔 라이브러리로써 서버와의 통신 데이터를 효율적으로 관리할 수 있도록 합니다.
SWR(Stale-While-Revalidate)
: Next.js를 만든 Vercel 팀에서 만든 라이브러리로 가벼운 사이즈로 API 데이터 요청 처리에서 캐싱 문제를 해결하기 위한 최소한의 API를 제공 간단하게 데이터를 가져오기에 좋습니다.React Query(TanStack Query)
: TanStack 팀에서 만든 라이브러리로 데이터 캐싱 및 기능적으로 더 많은 제어가 필요한 경우에는 React Query가 좋다고 할 수 있습니다. (기존의 React Query라는 이름에서 React뿐만 아니라 다양한 프레임워크에서도 사용이 가능해지면서 네이밍을 Tanstack Query로 변경하였습니다.)React Query의 경우 초기 설정이 필요합니다. (SWR은 별도의 Provider 없이 컴포넌트에서 바로 사용 가능합니다.)
QueryClient
는 애플리케이션에서 데이터를 관리하는 핵심 객체로 캐시된 데이터를 저장하고, 데이터를 요청하고 관리하며, 쿼리의 상태를 추적합니다.
QueryClient를 앱 전역에서 사용 가능하게 하고, 앱의 모든 컴포넌트에서 useQuery 및 useMutation 등의 훅을 사용하여 데이터를 관리하기 위해, Root 파일(최상위 컴포넌트)에서 QueryClientProvider
하위 컴포넌트를 감싸서 적용할 수 있습니다.
아래는 React에서의 최상단 컴포넌트에서의 초기 설정 방법입니다.
// index.js
import { QueryClient, QueryClientProvider } from 'react-query'
const queryClient = new QueryClient();
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
)
const { data, error, isLoading, isValidating, mutate } = useSWR(key, fetcher, options);
key
fetcher
함수options
반환 값 (Promise 함수 Status Return)
data
: 데이터 로드 전에는 undefined, 로드된 후에는 fetcher가 이행한 데이터 저장error
: fetcher가 던진 에러(또는 undefined)isLoading
: 진행 중인 요청이 있고 "로드된 데이터"가 없는 경우. 폴백 데이터와 이전 데이터는 "로드된 데이터"로 간주하지 않습니다.isValidating
: 요청이나 갱신 로딩의 여부mutate(data?, options?)
: 캐시 된 데이터를 뮤테이트(변경)하기 위한 함수예시 코드
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then((res) => res.json());
function Profile() {
const { data, error, isLoading } = useSWR('/api/user', fetcher, options)
if (error) return <div>failed to load</div>
if (isLoading) return <div>loading...</div>
return <div>hello {data.name}!</div>
}
export default Profile;
const { isLoading, isError, data, error } = useQuery(key, fetcher, options);
key
fetcher
함수options
반환 값(Promise 함수 Status Return)
Promise의 함수 status (data, isLoading, isFetching, error 등)
data
: 데이터 로드 전에는 undefined(기본값), 로드된 후에는 fetcher가 이행한 데이터 저장isLoading
: 초기 데이터 로딩 여부isFetching
: query함수(fetcher 함수)가 실행될 때 마다 true이며, 초기 데이터(the first fetch)를 포함하여 refetch의 로딩 여부 포함 error
: 기본값 null, 에러 발생 시 해당 쿼리에 대한 에러 객체 저장const { status, data, error } = useQuery(key, fetcher, options);
예시 코드
import { useQuery } from '@tanstack/react-query';
const fetcher = (url) => fetch(url).then((res) => res.json());
const useUser = (id) => {
const result = useQuery(`/api/user/${id}`, fetcher, options);
return result;
}
function Profile() {
const { data, error, isLoading } = useUser('777')
if (error) return <div>failed to load</div>
if (isLoading) return <div>loading...</div>
return <div>hello {data.name}!</div>
}
export default Profile;
// 1. key(shouldFetch)값이 false인 경우 fetcher 함수 실행되지 않음
const { data } = useSWR(shouldFetch ? '/api/data' : null, fetcher)
// 2. key(shouldFetch)함수가 false를 반환하면 fetcher 함수 실행되지 않음
const { data } = useSWR(() => shouldFetch ? '/api/data' : null, fetcher)
// 3. user.id가 정의되지 않았다면 에러 발생하고 fetcher 함수 실행되지 않음
const { data } = useSWR(() => '/api/data?uid=' + user.id, fetcher)
위와 같이 null을 사용하거나 함수를 key로 전달하여 데이터를 조건부로 가져옵니다. 함수가 falsy 값을 던지거나 반환하면 SWR은 요청을 시작하지 않습니다.
// 사용자 정보 가져오기
const { data: user } = useQuery(['user', email], getUserByEmail);
// 사용자의 ID 가져오기
const userId = user?.id;
// 그 후 사용자의 프로젝트 가져오기
const { isIdle, data: projects } = useQuery(
['projects', userId],
getProjectsByUser,
{
// userId가 존재할 때까지 쿼리를 실행하지 않음
enabled: !!userId,
}
);
enabled
옵션은 쿼리를 활성화 또는 비활성화하는 역할을 합니다. enabled
가 true
일 때만 쿼리가 활성화됩니다. false
일 때는 해당 데이터를 가져오지 않습니다. 이것은 쿼리를 필요할 때만 실행하도록 하여 성능을 최적화하거나 불필요한 데이터 fetching을 방지하는 데 사용될 수 있습니다.
cf. isIdle
은 enabled
가 true
이고 쿼리가 데이터를 가져오기 시작할 때까지 true
입니다.
그 이후에 데이터를 가져오는 중일 때 isLoading
단계로 전환되고, 데이터를 가져오게 되면 isSuccess
단계로 전환됩니다.
mutate
함수를 통해 데이터를 다시 가져오거나, 다른 이벤트가 발생했을 때 데이터를 갱신할 수 있습니다.
mutate 함수에 데이터 없이 mutate() 혹은 mutate(key)를 호출하면, 리소스에 대한 재검증(Revalidation: 데이터를 만료된 것으로 표시하고 refetch를 트리거) 합니다.
import { useSWRConfig } from 'swr';
import { updateProfile } from './api';
const Profile = () => {
const { mutate } = useSWRConfig();
const [profile, setProfile] = useState({ nickName: 'jiyaho' });
const userId = 'yourUserId'; // userId 값을 적절히 설정
const handleEditProfile = async () => {
// 프로필 업데이트 로직 (예: 서버 API 호출 또는 상태 업데이트)
await updateProfile(profile);
// 수정된 프로필을 적용한 후 해당 유저의 데이터를 다시 가져오기
mutate(`/user/${userId}`);
};
return (
<div>
{/* 프로필 수정 UI */}
<input
type="text"
value={profile.nickName}
onChange={(e) => setProfile({ nickName: e.target.value })}
/>
<button onClick={handleEditProfile}>Edit Profile</button>
{/* 여기에서 유저 정보를 출력하거나 UI 표시 가능 */}
</div>
);
};
이 코드에서 handleEditProfile 함수는 프로필을 수정한 후 mutate 함수를 사용하여 해당 유저의 데이터를 다시 가져오고 있습니다. mutate 함수의 인자로 수정된 유저의 데이터를 다시 가져오는 URL(/user/${userId})을 전달하면, SWR은 해당 URL에 대한 새로운 데이터를 다시 가져오게 됩니다.
queryClient
에는 cache와 직접적으로 소통하기위해 사용되며 몇가지 method를 사용할 수 있는데, 그 중에 Refetch와 관련된 method 두 개를 비교하자면 아래와 같습니다.
invalidatingQueries
: 특정 query들을 무효화(invalidate)하여 data를 stale상태로 만들고, data refetching을 일으키고 싶은 경우 사용할 수 있습니다.refetchingQueries
: 특정 조건을 기준으로 쿼리를 refetch하고 싶은 경우 사용할 수 있습니다.import { useState } from 'react';
import { useQuery, useQueryClient } from 'react-query';
import { updateProfile } from './api';
const Profile = () => {
const queryClient = useQueryClient();
const [profile, setProfile] = useState({ nickName: 'jiyaho' });
const userId = 'yourUserId'; // userId 값을 적절히 설정
const handleEditProfile = async () => {
// 프로필 업데이트 로직 (예: 서버 API 호출 또는 상태 업데이트)
await updateProfile(profile);
// 해당 유저의 데이터를 무효화(invalidate) 시키고 다시 가져오기(refetch)
queryClient.invalidateQueries(`/user/${userId}`);
};
return (
<div>
{/* 프로필 수정 UI */}
<input
type="text"
value={profile.nickName}
onChange={(e) => setProfile({ nickName: e.target.value })}
/>
<button onClick={handleEditProfile}>Edit Profile</button>
{/* 여기에서 유저 정보를 출력하거나 UI 표시 가능 */}
</div>
);
};
export default Profile;
사용자가 클라이언트에서 데이터를 수정할 때 업데이트가 목적인 경우, refetch하여 불필요한 네트워크 요청을 추가로 할 필요 없이 refetch를 하지 않고 바로 즉각 업데이트를 할 수 있다.
mutate(key, data, shouldRevalidate)
key
: 타겟이 되는 데이터의 쿼리 키. 문자열 또는 문자열로 이루어진 배열 형태.data
: 업데이트에 사용될 새로운 데이터 또는 함수shouldRevalidate
: 옵션으로, 새로운 데이터를 서저에서 가져와서 캐시를 업데이트할지 여부를 결정. 기본값은 true로 설정되어 있어, 캐시를 업데이트한 후에 서버에 새로운 데이터를 요청합니다. false로 설정하면, 캐시만 업데이트하고 서버에 요청하지 않습니다.import { useSWRConfig } from 'swr';
import { updateProfile } from './api';
const Profile = () => {
const { mutate } = useSWRConfig();
const [profile, setProfile] = useState({ nickName: 'jiyaho' });
const userId = 'yourUserId'; // userId 값을 적절히 설정
const handleEditProfile = async () => {
// 프로필 업데이트 로직 (예: 서버 API 호출 또는 상태 업데이트)
await updateProfile(profile);
// 수정된 프로필을 refetch 안 하고 즉시 업데이트
mutate(`/user/${userId}`, profile, false);
};
return (
<div>
{/* 프로필 수정 UI */}
</div>
);
};
queryClient
의 setQueryData
method: 캐시된 쿼리의 데이터를 즉시 업데이트하기 위해 사용할 수 있는 동기화 기능입니다. setQueryData는 기존의 queryKey에 매핑되어있는 데이터를 새롭게 정의합니다. 만약 쿼리가 없으면 생성됩니다.
import { useState } from 'react';
import { useQuery, useQueryClient } from 'react-query';
import { updateProfile } from './api';
const Profile = () => {
const queryClient = useQueryClient();
const [profile, setProfile] = useState({ nickName: 'jiyaho' });
const userId = 'yourUserId'; // userId 값을 적절히 설정
const handleEditProfile = async () => {
// 프로필 업데이트 로직 (예: 서버 API 호출 또는 상태 업데이트)
await updateProfile(profile);
// 해당 유저의 프로필 업데이트
queryClient.setQueryData(`/user/${userId}`, profile);
};
return (
<div>
{/* 프로필 수정 UI */}
</div>
);
};
export default Profile;
useSWRMutation
은 SWR 2.0에서부터 도입된 훅으로, 데이터를 수정 또는 업데이트(Create, Update, Delete)할 때 사용됩니다. 이 훅은 useSWR처럼 자동으로 요청을 시작하지 않으며, 명시적으로 trigger 메서드를 사용하여 mutation을 수동으로 실행 할 수 있도록 합니다. 또한, useSWRMutation 훅은 상태를 공유하지 않으므로 여러 개의 독립적인 mutate 작업을 수행할 수 있습니다.
const { trigger } = useSWRMutation(key, fetcher, options);
key
fetcher(key, { arg })
options
반환 값
trigger(arg, options)
: remote mutation을 트리거 하는 함수isMutating
: remote mutation이 진행 중인 상태data
: fetcher 에서 반환된 지정된 키에 대한 데이터error
: fetcher에서 발생한 오류 (또는 undefined)reset
: 상태(data, error, isMutating)를 초기화하는 함수예시 코드
import useSWRMutation from 'swr/mutation'
// Fetcher 구현.
// 추가 인수는 두 번째 매개변수의 `arg` 속성을 통해 전달됩니다.
async function sendRequest(url, { arg }: { arg: { username: string } }) {
return fetch(url, {
method: 'POST',
body: JSON.stringify(arg)
}).then(res => res.json())
}
function App() {
// useSWR + mutate 같은 API를 사용하지만, 자동으로 요청을 시작하지는 않습니다.
const { trigger, isMutating } = useSWRMutation('/api/user', sendRequest, /* options */)
return (
<button
disabled={isMutating}
onClick={async () => {
// 특정 인수를 사용하여 `sendRequest`를 트리거 합니다.
try {
const result = await trigger({ username: 'johndoe' }, /* options */)
} catch (e) {
// 에러 핸들링
}
}}
>
Create User
</button>
)
}
위 코드는 SWR 공식 문서에 있는 예시 코드로 useSWRMutation을 사용하여 sendRequest라는 mutate 작업을 정의합니다.
trigger 메서드를 이용하여 명시적으로 sendRequest 작업을 트리거합니다. useSWRMutation은 useSWR과 유사한 API를 제공하지만, 자동으로 요청을 시작하지 않아 사용자가 버튼 클릭하는 액션을 통해 수동으로 트리거해야 합니다.
useMutation
은 데이터를 수정 또는 업데이트(Create, Update, Delete)할 때 사용됩니다. 주로 비동기 작업(예: API 호출)을 수행하고, 해당 작업의 결과에 따라 데이터를 갱신하거나 UI를 업데이트하는 데 사용됩니다.
const { mutate, data, error, isLoading } = useMutation(mutationFn, options);
mutationFn
options
반환 값
mutate, mutateAsync, status, data, isIdle, isSuccess 등이 있습니다. 아래에서는 mutate와 mutateAsync만 언급합니다.
mutate(variables, { onError, onSettled, onSuccess });
mutate
: mutate는 useMutation을 이용해 작성한 내용들이 실행될 수 있도록 도와주는 trigger 역할을 합니다. 따라서 useMutation을 정의하고 이벤트가 발생되었을 때 mutate를 사용하여 mutation을 실행시킬 수 있습니다. variables는 mutationFunction에 전달하는 객체입니다. mutateAsync(variables, { onError, onSettled, onSuccess });
mutateAsync
: mutate와 차이점은 promise를 반환한다는 점입니다.예시 코드
import { useMutation } from 'react-query';
const MyComponent = () => {
const { mutate } = useMutation(
async (newData) => {
const response = await api.updateData(newData);
return response.data;
},
{
onSuccess: (data, variables, context) => {
// 성공 시 로직
},
onError: (error, variables, context) => {
// 실패 시 로직
},
onSettled: (data, error, variables, context) => {
// 성공, 실패에 상관없이 실행할 로직
}
}
);
return (
<button onClick={() => mutate(newData)}>Update Data</button>
);
};
위 코드는 API 호출 및 버튼 클릭과 같은 사용자 액션을 처리하고, 성공/실패 등에 대한 로직을 수행합니다.
옵션들은 앱 전체에서 전역적으로 동일하게 동작하도록, 최상위 컴포넌트에서 SWR 과 React Query 모두 공통 config를 설정할 수 있습니다.
SWR에서 fetcher 함수를 전역적으로 사용하려면, SWR 훅을 호출할 때 마다 fetcher 함수를 재사용하도록 설정해야 합니다. 이를 위해 SWR의 SWRConfig를 사용할 수 있습니다. SWRConfig를 사용하면 애플리케이션의 모든 곳에서 동일한 fetcher 함수를 사용할 수 있습니다.
기본 옵션 또한 SWRConfig의 value 객체에 전달하여 설정할 수 있습니다.
개발자 도구
SWR의 경우 SWRDevTools
라는 개발자용 도구가 있으나 Vercel의 공식 프로젝트는 아니라고 합니다. 브라우저별 익스텐션을 설치하여 사용할 수 있습니다. (자세한 내용은 아래 웹사이트 및 레파지토리를 참고)
// index.js 또는 최상위 컴포넌트
import React from 'react';
import ReactDOM from 'react-dom';
import { SWRConfig } from 'swr';
import App from './App';
// SWRConfig를 사용하여 전역적으로 설정
const swrConfig = {
// 전역적으로 사용할 fetcher 함수
fetcher: (url) => fetch(url).then((res) => res.json()),
// 예시: 기본 옵션 설정
revalidateOnFocus: false,
// 기타 다른 기본 옵션 설정 가능
};
ReactDOM.render(
<React.StrictMode>
<SWRConfig value={swrConfig}>
<App />
</SWRConfig>
</React.StrictMode>,
document.getElementById('root')
);
QueryClient의 생성자에는 여러 옵션을 적용할 수 있는데, 예를 들어 defaultOptions을 통해 해당 QueryClient를 사용하는 모든 queries나 mutations에 기본 옵션을 적용할 수 있습니다. 이를 통해 캐시 설정, 재시도 설정 등을 전역으로 제어할 수 있습니다.
개발자 도구
리액트 쿼리에서 제공하는 Devtools를 사용하고 싶은 경우 아래와 같이 모듈을 불러와 ReactQueryDevtools 컴포넌트를 추가해주면 됩니다.
// index.js 또는 최상위 컴포넌트
import { QueryClient, QueryClientProvider } from 'react-query'
import { ReactQueryDevtools } from 'react-query/devtools'
import App from './App';
import React from 'react';
import ReactDOM from 'react-dom';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false, // 예시: 기본 옵션 설정
// 기타 다른 기본 옵션 설정 가능
},
},
});
ReactDOM.render(
<React.StrictMode>
<QueryClientProvider>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</React.StrictMode>,
document.getElementById('root')
);
React Query의 경우 2023년 말에 version 5를 정식 출시했습니다. v4에 비해 번들 크기를 20% 가량 줄이고 제공하는 API를 간소화하는데 중점을 두었다고 합니다. 자세한 내용은 공식 사이트의 v5 마이그레이션 관련 페이지 링크에서 확인하실 수 있습니다. (SWR 2.0의 경우 2022년 말에 발표)
이처럼 SWR과 React Query는 데이터 fetching 및 caching을 편리하게 다룰 수 있도록 도와주는 라이브러리로써, 두 라이브러리의 기본적인 사용법에 대해 비교해 보았습니다.
다음번에는 이를 통해 무한스크롤(Infinite Scroll), 페이지네이션(Pagination), 낙관적 업데이트(Optimistic Update)등과 같은 기능들에도 사용해볼 예정입니다.
Reference.
많은 도움이 되었습니다!