React Query와 Custom Hooks를 이용한 친구 관리 시스템 구현하기

미키오·2024년 7월 3일
0

DizzyCode

목록 보기
2/5
post-thumbnail

0. 들어가며..

이번에 채팅 서비스를 만드는 프로젝트를 진행하며 친구 관리 시스템 부분을 담당하게 되었다. 서버로부터 정보를 받아오고, 요청을 보내는 작업을 리액트 쿼리로 서버 상태를 관리하게 되었는데 Custom Hook과 조합해서 직접 사용해보니 친구 관리 시스템을 구축하는 데에 엄청난 장점들이 있다는 것을 깨달았다.

1. React Query

React Query는 서버 상태를 관리하고 비동기 데이터 페칭, 캐싱, 자동 재요청, 동기화 등을 쉽게 처리할 수 있도록 도와주는 라이브러리이다. 특히, 서버와 클라이언트 간의 데이터 동기화가 필요한 경우 매우 유용하다.

리액트 쿼리 설치 및 기본 개념은 예전 글 참고..

https://velog.io/@mikio/React-Query-시작하기-feat.-useEffect로-데이터-패칭하는-것의-단점

새롭게 느낀 리액트 쿼리의 장점들

이론으로만 접했던 저번 글과는 달리 실제로 리액트 쿼리를 써보며 피부로 느낀 장점들에는 성공/실패 핸들링, 쿼리 무효화 및 재갱신, 의존성 관리 등이 있다.

1. 성공 / 실패 핸들링

useMutation 과 같은 훅을 사용하면 데이터 변경 작업이 성공했을 때와 실패했을 때의 후속 작업을 쉽게 정의할 수 있다.

const { mutate: sendFriendRequestByIdMutation } = useMutation({
  mutationFn: async ({ friendId }) => {
    await sendFriendRequestById(userId, friendId);
  },
  onSuccess: () => {
    toast({ title: '친구 요청 성공', description: '친구 요청이 성공적으로 전송되었습니다.', status: 'success' });
    queryClient.invalidateQueries({ queryKey: ['friends'] });
  },
  onError: () => {
    toast({ title: '친구 요청 실패', description: '다시 시도해주세요.', status: 'error' });
  },
});

예를 들어, 친구 요청이 성공하면 성공 메시지를 보여주고, 실패하면 오류 메시지를 보여주는 작업을 간편하게 처리할 수 있다.

2. 쿼리 무효화 및 재갱신

특정 쿼리를 무효화(invalidate)하여 필요한 데이터를 다시 가져오도록 할 수 있다. 이는 친구 목록이 변경될 때마다 최신 상태를 유지하는 데 용이하다.

queryClient.invalidateQueries({ queryKey: ['friends'] });
  },`

위의 sendFriendRequestByIdMutation 에서 친구 요청이 성공적으로 완료된 후에 쿼리를 무효화하여 해당 쿼리가 다시 데이터를 가져오게 하여 데이터 변경 시 캐시에 저장된 데이터를 최신 상태로 유지하는 데 도움을 준다.

3. 의존성 관리

React Query는 의존성에 따라 데이터를 자동으로 다시 가져오므로, 특정 데이터가 변경될 때마다 연관된 데이터를 자동으로 업데이트할 수 있다. 이 역시 쿼리 무효화 과정처럼 데이터의 일관성 유지와 최신 상태 유지에 도움이 된다.

const useGetFriendsListQuery = () => {
  const { user } = useAuthStore();
  const userId = Number(user!.id);
  
  return useQuery<IFriendRequest[]>({
    queryKey: ['friends', userId],
    queryFn: () => getFriendsList(userId),
  });
};

useGetFriendsListQuery는 현재 사용자의 친구 목록을 가져오는 쿼리이다. queryKey['friends', userId]를 사용하여 사용자 ID에 따라 데이터를 캐싱하고 관리할 수 있다.

이러한 장점들을 지원하는 핵심 기능인 useMutation , useQueryClient , useQuery 에 대해 더 상세히 알아보자.

useMutation, useQueryClient, useQuery

1. useMutation

useMutation은 서버에 데이터를 전송하거나 변경하는 작업을 수행할 때 사용된다. 이전의 코드를 다시 한번 살펴보자.

const { mutate: sendFriendRequestByIdMutation } = useMutation({
  mutationFn: async ({ friendId }) => {
    await sendFriendRequestById(userId, friendId);
  },
  onSuccess: () => {
    toast({
      title: '친구 요청 성공',
      description: '친구 요청이 성공적으로 전송되었습니다.',
      status: 'success',
    });
    queryClient.invalidateQueries({ queryKey: ['friends'] });
  },
  onError: () => {
    toast({
      title: '친구 요청 실패',
      description: '다시 시도해주세요.',
      status: 'error',
    });
  },
});

useMutation을 사용하면, 비동기 함수와 함께 onSuccess, onError와 같은 콜백을 설정하여 성공 또는 실패 시의 처리를 할 수 있다.

주요 속성

  • mutationFn: 비동기 작업을 수행하는 함수. 예를 들어, 데이터베이스에 새로운 항목을 추가하는 함수가 될 수 있다.
  • onSuccess: 비동기 작업이 성공적으로 완료되었을 때 호출되는 콜백 함수
  • onError: 비동기 작업이 실패했을 때 호출되는 콜백 함수

2. useQueryClient

useQueryClient는 React Query의 클라이언트 인스턴스를 가져오는 훅이다. 이를 통해 전역적으로 관리되는 캐시 데이터를 조작할 수 있다. 예를 들어, 데이터를 무효화하거나, 특정 쿼리를 다시 불러오게 할 수 있다.

주요 역할

  • invalidateQueries: 특정 쿼리를 무효화하여, 해당 쿼리가 다시 데이터를 가져오도록 한다.
  • setQueryData: 특정 쿼리 키에 대한 캐시 데이터를 수동으로 설정한다.
  • getQueryData: 특정 쿼리 키에 대한 캐시 데이터를 가져온다.
const queryClient = useQueryClient();

queryClient.invalidateQueries({ queryKey: ['friends'] });

위 예시는 ['friends'] 키를 가진 쿼리를 무효화하여, 다음에 해당 쿼리가 사용될 때 데이터를 다시 가져오게 한다.

3. useQuery

useQuery는 서버에서 데이터를 가져오고 이를 캐시에 저장하는 작업을 처리한다. 주로 데이터를 조회(read)할 때 사용되며, 컴포넌트가 처음 렌더링될 때와 지정된 의존성이 변경될 때 데이터를 가져온다.

주요 속성

  • queryKey: 쿼리를 식별하는 고유 키. 이 키를 사용하여 캐시된 데이터를 식별하고 관리한다.
  • queryFn: 데이터를 가져오는 비동기 함수. 예를 들어, API 요청을 통해 데이터를 가져올 수 있다.
  • staleTime: 데이터가 신선하게 유지되는 시간이다. 이 시간이 지나면 데이터를 다시 가져오게 된다.
  • cacheTime: 데이터가 캐시에 유지되는 시간이다. 이 시간이 지나면 데이터가 캐시에서 삭제된다.

예시

import { useQuery } from '@tanstack/react-query';
import { getFriendsList } from '../api/friendApi';

const useGetFriendsListQuery = (userId) => {
  return useQuery({
    queryKey: ['friends', userId],
    queryFn: () => getFriendsList(userId),
    staleTime: 1000 * 60 * 5, // 5분
    cacheTime: 1000 * 60 * 10, // 10분
  });
};

export default useGetFriendsListQuery;

queryKey: ['friends', userId] 키를 통해 특정 사용자의 친구 목록을 식별하고 queryFn: () => getFriendsList(userId)로 비동기적으로 친구 목록을 가져온다.

staleTime: 1000 * 60 * 5 으로 5분 동안은 데이터를 다시 가져오지 않고 캐시된 데이터를 사용한다. 5분이 지나면 데이터가 신선하지 않다고 간주되고, 다음 요청 시 데이터를 다시 가져온다.

cacheTime: 1000 * 60 * 10는 데이터가 캐시에 유지되는 시간이다. 10분 동안은 캐시된 데이터를 유지하고, 그 이후에는 캐시에서 데이터를 삭제한다.

2. Custom Hook

Custom Hook은 React의 Hook 기능을 확장하여 로직을 재사용 가능하게 만드는 기능이다. React Hook (예: useState, useEffect, useContext)을 사용하여 컴포넌트 내부에 로직을 작성할 수 있지만, Custom Hook을 사용하면 이 로직을 분리하여 여러 컴포넌트에서 재사용할 수 있다.

import { useState, useCallback } from 'react';

const useInput = (initialValue) => {
  const [value, setValue] = useState(initialValue);
  
  const handleChange = useCallback((e) => {
    setValue(e.target.value);
  }, []);
  
  return [value, handleChange, setValue];
};

export default useInput;

처음 리액트를 배울 때 최초로 만들어본 커스텀 훅이 바로 useInput 이다.

리액트에서 input창을 사용할때마다 useState를 활용한 상태관리, 이벤트 핸들러 설정을 중복적으로 처리해야해서 번거로웠는데 useInput을 만들어 두고 필요할때마다 재사용하니 코드 중복을 줄이고 코드가 간결해져서 유지보수에도 좋았다.

이러한 장점들을 가지고 친구 관리 시스템의 다양한 기능들을 모아두기 위해서 useHandleFriend 라는 Custom Hook을 만들었다.

useHandleFriend

이 Hook은 친구 요청을 보내고, 요청을 수락하거나 거절하며, 친구 목록을 가져오고, 친구를 삭제하는 등의 기능을 포함하여 React Query를 활용하면서 서버 상태를 관리하고, 상태 변경 시 이를 반영하여 UI를 업데이트하는 역할을 한다.

위에서 정리한 내용으로 이제 본격적으로 코드를 작성해보았다.

3. 코드

  • 친구 요청 보내기 (이름으로)
    const { mutate: sendFriendRequestByNameMutation } = useMutation({
      mutationFn: async ({ friendName }) => {
        await sendFriendRequestByName(userId, friendName);
      },
      onSuccess: () => {
        toast({ title: '친구 요청 성공', description: '친구 요청이 성공적으로 전송되었습니다.', status: 'success' });
        queryClient.invalidateQueries({ queryKey: ['friends'] });
      },
      onError: () => {
        toast({ title: '친구 요청 실패', description: '다시 시도해주세요.', status: 'error' });
      },
    });
    
  • 친구 요청 거절하기
    const { mutate: rejectFriendRequestMutation } = useMutation({
      mutationFn: async ({ member2Id }) => {
        await rejectFriendRequest(userId, member2Id);
      },
      onSuccess: () => {
        toast({ title: '친구 요청 거절 성공', status: 'success' });
        queryClient.invalidateQueries({ queryKey: ['friends'] });
      },
      onError: () => {
        toast({ title: '친구 요청 거절 실패', description: '다시 시도해주세요.', status: 'error' });
      },
    });
    
    • sendFriendRequestByNameMutation을 호출하여 이름으로 친구 요청을 보낸다.
    • rejectFriendRequestMutation을 호출하여 친구 요청을 거절한다.
    • 성공 시, 성공 메시지를 보여주고 친구 목록을 무효화하여 최신 데이터를 가져온다.
    • 실패 시, 오류 메시지를 보여준다.

  • 친구 요청 수락하기

    const { mutate: acceptFriendRequestMutation } = useMutation({
      mutationFn: async ({ member2Id }) => {
        await acceptFriendRequest(userId, member2Id);
      },
      onSuccess: () => {
        toast({ title: '친구 요청 수락 성공', status: 'success' });
        queryClient.invalidateQueries({ queryKey: ['friends'] });
      },
      onError: () => {
        toast({ title: '친구 요청 수락 실패', description: '다시 시도해주세요.', status: 'error' });
      },
    });
    
  • 친구 삭제하기

    const { mutate: deleteFriendRequestMutation } = useMutation({
      mutationFn: async ({ member2Id }) => {
        await deleteFriendRequest(userId, member2Id);
      },
      onSuccess: () => {
        toast({ title: '친구 삭제 성공', description: '친구가 성공적으로 삭제되었습니다.', status: 'success' });
        queryClient.invalidateQueries({ queryKey: ['friends'] });
      },
      onError: () => {
        toast({ title: '친구 삭제 실패', description: '다시 시도해주세요.', status: 'error' });
      },
    });
    
    • acceptFriendRequestMutation을 호출하여 친구 요청을 수락한다.
    • deleteFriendRequestMutation을 호출하여 친구를 삭제한다.
    • 성공 시, 성공 메시지를 보여주고 친구 목록을 무효화하여 최신 데이터를 가져온다.
    • 실패 시, 오류 메시지를 노출.
  • 친구 목록 가져오기

    const useGetFriendsListQuery = () =>
      useQuery<IFriendRequest[]>({
        queryKey: ['friends', userId],
        queryFn: () => getFriendsList(userId),
      });
    
  • 펜딩 친구 요청 목록 가져오기

    
    const useGetPendingFriendRequestsQuery = () =>
      useQuery<IFriendRequest[]>({
        queryKey: ['pendingFriends', userId],
        queryFn: () => getPendingFriendRequests(userId),
      });
    
    • useQuery를 사용하여 친구 목록 데이터를 가져온다.
    • queryKey는 캐시된 데이터를 식별하는 데 사용된다.
    • queryFn은 데이터를 가져오는 비동기 함수이다.

4. 느낀 점, 결론

여기저기서 React Query 좋다는 이야기는 많이 들었지만 직접 써보며 기능의 강력함을 느낀 것은 처음이었던 것 같다. 특히나 useMutation을 사용하여 데이터 변경 작업을 처리하고, onSuccessonError 콜백을 통해 성공 및 실패 후속 작업을 간편하게 처리한 것이 좋았다. 더불어 useQueryClient를 사용하여 별도의 코드 작성 없이 전역적으로 캐시 데이터를 조작하고, 필요한 시점에 데이터를 다시 가져오도록 설정할 수 있었다.

또한 다양한 Custom Hook을 작성하며 반복되는 로직을 한 곳에 모아 관리하며 이전에 useInput 을 만들어서 사용했을 때의 감동을 useHandleFriend 를 구현해나가며 다시 느꼈던 것 같다. useInput, useGetFriendsListQuery, useHandleFriend와 같이 각각의 Hook이 특정한 책임을 가지도록 인터페이스를 분리하여 ISP (인터페이스 분리 원칙)에 입각한 구현이 가능했다.

추가적으로 리액트 쿼리와 커스텀 훅을 조합하며 SRP (단일 책임 원칙)에 기반한 설계가 가능했던 것 같다. 예를 들어, useGetFriendsListQuery는 친구 목록을 가져오는 책임만, useInput은 입력 필드의 상태를 관리하는 책임만 가지는 식으로 작성하여 React Query와 Custom Hook으로 각 Hook이 하나의 책임만 갖도록 설계할 수 있었다.

최근에 추구하는 SOLID 원칙에 입각한 프로그래밍에 한 걸음 가까워진 느낌이 든 기능들이었다!

📚 Bibliography

profile
교육 전공 개발자 💻

2개의 댓글

comment-user-thumbnail
2024년 7월 5일

useQuery에서 select를 쓰면 데이터 가공도 할 수 있어요~!

1개의 답글