Skip to content

상태관리 (서버편)

1. 왜 라이브러리를 써야될까

서버 상태 관리 라이브러리를 사용하는 이유는 크게 두 가지다.

  1. 캐싱
  • 첫 번째는 캐싱이다. 동일한 데이터를 반복적으로 요청하지 않고 메모리에 저장해서 불필요한 네트워크 요청을 줄일 수 있다.
  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

  1. _app.tsx에 QueryClient 인스턴스를 생성하여
  2. 전체 앱을 provider로 아래와 같이 감싸준다.
tsx
// QueryClient 인스턴스 생성 (앱 전체에서 하나만)  
const queryClient = new QueryClient({  
  defaultOptions: {  
    queries: {  
      retry: 1,                    // 실패시 1번만 재시도  
      refetchOnWindowFocus: false, // 창 포커스시 자동 refetch 비활성화  
    },  
  },  
});

// 전체 앱을 Provider로 감싸기  
<QueryClientProvider client={queryClient}>  
  {/* 모든 하위 컴포넌트에서 React Query 사용 가능 */}  
</QueryClientProvider>
  1. 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 속성을 적용해야됨

적용 결과

Pasted image 20250620192819