상태관리 (서버편)
1. 왜 라이브러리를 써야될까
서버 상태 관리 라이브러리를 사용하는 이유는 크게 두 가지다.
- 캐싱
- 첫 번째는 캐싱이다. 동일한 데이터를 반복적으로 요청하지 않고 메모리에 저장해서 불필요한 네트워크 요청을 줄일 수 있다.
- 백그라운드 업데이트
- 두 번째는 백그라운드 업데이트인데, 사용자가 인지하지 못하는 사이에 데이터를 최신 상태로 유지해주는 기능이다.
2. 라이브러리 종류엔 뭐가 있는가
SWR
- 번들 크기가 가벼움
- 네트워크 재연결시 자동으로 데이터 갱신
- 복잡한 서버 상태 로직 처리에는 한계가 있음
Tanstack query
- Mutation 관리 기능
- Mutation이란, 서버의 데이터를 "변경"하는 작업을 의미함. (POST, PUT, DELETE 등...)
- 무한 스크롤 및 페이지네이션 지원
- 상대적으로 SWR보다 번들 크기가 큼
- 오버엔지니어링 가능성 有
Apollo Client
- GraphQL과의 궁합이 좋음
- GraphQL이란, REST API와 달리 하나의 요청으로 필요한 모든 데이터를 갖고오는 것.
- 서버와 클라이언트 상태를 하나로 관리
- REST API와의 궁합은 별로 안좋음
3. 그래서, 이 프로젝트에선 라이브러리를 쓰는게 좋은가?
- RealWorld의 주기능은 Create, Read, Update, Delete
- 부가기능으로 팔로우, 게시글 좋아요도 있음
- Article과 Comment CRUD는 SWR로도 충분하다고 생각함.
- 그러나 user follow와 Article Favorite는 useState를 사용하는 것 보다 Tanstack Query를 사용해보는 것이 좋을 것 같음.
- 물론 현재는 Article Preview 컴포넌트를 Profile과 공통적으로 사용하고 있어서 좋아요의 상태가 공통으로 관리되기에 지금 당장은 사용하지 않아도 괜찮음.
- Follow도 버튼 컴포넌트 하나로 전역에서 관리되기에 괜찮음.
- 하지만 만약 좋아요 기능을 다른 컴포넌트에서도 사용한다면? 팔로우 기능도 다른 곳에서 사용한다면?
- 학습 목적에서 두 가지 서비스에만 Tanstack Query를 적용해보고 비교해보자. 또한 전역에서 관리한 게 아까우니까 헤더의 사용자 이름 옆에, 프로필의 배너에 팔로워 수를 추가적으로 띄워보자. 이러면 Tanstack Query를 사용하는게 의미가 있으니까.
4. 설정 방법
tanstack query
_app.tsx
에 QueryClient 인스턴스를 생성하여- 전체 앱을 provider로 아래와 같이 감싸준다.
tsx
// QueryClient 인스턴스 생성 (앱 전체에서 하나만)
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1, // 실패시 1번만 재시도
refetchOnWindowFocus: false, // 창 포커스시 자동 refetch 비활성화
},
},
});
// 전체 앱을 Provider로 감싸기
<QueryClientProvider client={queryClient}>
{/* 모든 하위 컴포넌트에서 React Query 사용 가능 */}
</QueryClientProvider>
- hook 라이브러리에 (각자의 디렉토리 구조에 맞게 설정하면 된다) mutation이나 query 관련 코드를 추가한다. follow 상태 코드를 예로 들자면 아래와 같다.
tsx
type FollowMutationParams = {
username: string;
isFollowing: boolean;
};
export const useFollowersCount = (username: string) => {
return useQuery({
queryKey: ['followers', username],
queryFn: () => UserAPI.getFollowersCount(username),
enabled: !!username,
staleTime: 1000 * 60 * 5, // 5분간 캐시 유지
});
};
- follower count를 세는 부분에서 useQuery를 사용한 이유는 팔로워 수를 "조회"하는 것이기 때문이다.
- useQuery는 서버에서 데이터를 가져와서 캐싱하고, 컴포넌트가 마운트될 때 자동으로 실행된다.
- 또한 enabled 옵션으로 username이 있을 때만 실행되도록 조건을 걸어두었다.
tsx
export const useFollowMutation = () => {
const queryClient = useQueryClient();
return useMutation<any, Error, FollowMutationParams, MutationContext>({
mutationFn: async ({ username, isFollowing }: FollowMutationParams) => {
if (isFollowing) {
return await UserAPI.unfollow(username);
} else {
return await UserAPI.follow(username);
}
},
onMutate: async ({
username,
isFollowing,
}: FollowMutationParams): Promise<MutationContext> => {
await queryClient.cancelQueries({ queryKey: ['profile', username] });
const previousProfile = queryClient.getQueryData(['profile', username]);
queryClient.setQueryData(['profile', username], (old: any) => ({
...old,
data: {
...old?.data,
profile: {
...old?.data?.profile,
following: !isFollowing,
},
},
}));
return { previousProfile };
},
onError: (err, variables, context) => {
if (context?.previousProfile) {
queryClient.setQueryData(
['profile', variables.username],
context.previousProfile,
);
}
},
onSettled: (data, error, variables) => {
queryClient.invalidateQueries({
queryKey: ['profile', variables.username],
});
},
});
};
- 반면 follow/unfollow엔 useMutation을 사용한 이유는 이 행동이 서버의 데이터를 "변경"하는 작업이기 때문이다.
- useMutation은 자동으로 실행되지 않고, 사용자가 버튼을 클릭했을 때처럼 특정 이벤트가 발생했을 때만 실행된다.
- 그리고 onMutate에서 관련된 쿼리들을 무효화시켜서 최신 데이터로 업데이트 되도록 했다.
만약 swr을 썼다면?
- 아래와 같이 follow가 사용되는 컴포넌트에 수동으로 mutation을 하나하나 설정해줘야 했을 것이다.
tsx
// profile/[pid].tsx
const Profile = () => {
const { data: profile, mutate: mutateProfile } = useSWR(['profile', username], fetcher);
const { data: followersCount, mutate: mutateFollowers } = useSWR(['followers', user_id], fetcher);
// FollowUserButton에서 팔로우했을 때 수동으로 업데이트해야 함
const handleFollowUpdate = () => {
mutateProfile(); // 프로필 데이터 갱신
mutateFollowers(); // 팔로워 수 갱신
};
};
// Navbar.tsx
const Navbar = () => {
const { data: followersCount, mutate: mutateFollowers } = useSWR(['followers', currentUser?.id], fetcher);
// profile처럼 수동으로 mutate를 호출하거나 페이지를 새로고침해야 변경사항이 적용됨
};
// FollowUserButton.tsx (SWR만 사용했다면)
const FollowUserButton = ({ username, following, onFollowUpdate }) => {
const handleClick = async () => {
try {
await UserAPI.follow(username);
onFollowUpdate(); // 부모 컴포넌트에게 알려줘야 함
// 하지만 Navbar를 위에처럼 적었다면 navbar는 여전히 변경사항을 모름
} catch (error) {
console.error(error);
}
};
};
5. 문제점 및 해결방안
- 현재 React 16 버전을 사용하고 있기 때문에 tanstack은 4@ 버전을 설치하여 사용함
- 안 그러면
Module not found: Can't resolve 'react/jsx-runtime
jsx-runtime을 찾을 수 없다는 오류가 발생함 - v5 이상부턴 ispending 속성, v4에선 isLoading 속성을 적용해야됨
적용 결과
