스토어를 스토어 답게, React Query

김채은·2022년 5월 24일
1

리액트 쿼리를 시작하기 전 공식문서를 읽어보았다. 누군가가 정리해둔 블로그 글을 읽는 것보다 공식문서를 읽었을 때 누군가의 해석을 거치지 않고 나의 언어로 기술을 이해할 수 있는 것 같다. 하지만 영어의 벽은 아직도 너무나 높다.

🐇 Overview

리액트 쿼리

  • 리액트 데이터 패칭 라이브러리이다.
  • 서버 상태(Server state)를 패칭, 캐싱, 동기화, 업데이트 해준다.

서버 상태

  • Client가 제어하지 않는 위치에 원격으로 유지돼있다.
  • 패칭, 업데이트 시 비동기 API 요청이 필요하다.
  • 다른 사용자가 이 데이터를 변경할 수 있다.
  • 주의하지 않으면 잠재적으로 out of date 상태가 될 수 있다.

서버 상태랑 쉽게 말하자면 서버에서 관리되는 데이터들, API 통신을 통해 CRUD하는 데이터들을 말한다. 서버와 DB에서 관리되는 데이터를 다른 사용자가 변경했을 때, client가 최초 데이터 패칭 후 신경을 쓰지 않는다면 이러한 변경이 화면에 반영되지 않는다. 따라서 화면에 나타나는 데이터는 유효기간이 지난, out of date 상태가 된다.

🐇 왜 리액트 쿼리인가?

스토어를 스토어 답게

스토어를 사용하는 이유는 전역 상태 관리를 위해서이다. 또한 상태 관리 도구가 필요한 이유는 상속 관계 간 프로퍼티를 공유할 때 부모에서 자식으로, 자식에서 부모로 공유되는 과정이 복잡하기 때문이다. 다음 그림처럼 말이다.

그런데 실제로 스토어의 상당 부분이 비동기 통신을 위해 쓰이고 있다. isFetching, isError와 같이 API Response에 대한 상태가 이러한 전역 스토어에 포함될 때가 많다. 이렇게 생각하면 혼란이 온다. 스토어는 상태를 관리하는 코드일까? API 통신을 담당하는 코드일까?

배민근 님이 우아한형제들 기술블로그에 작성해주신 문장이 와닿았다.

어쩌면 기존에 우리가 FE에서 사용하고 있는 전역 상태 관리 방법들은 API 통신과 서버 상태 관리에 적정기술이 아닐 수도 있겠다.

서버 상태 관리에 적합한 기술

리액트 쿼리의 주 기능은 서버 상태(Server state)를 패칭, 캐싱, 동기화, 업데이트 해주는 것이다.

예를 들어 리액트 쿼리를 사용하면 데이터가 로딩 중인지, 패칭 중인지 isLoading, isFetching 상태를 통해 알 수 있다. 또한 통신에 실패했을 때 기본적으로 3번 다시 요청을 보내고, 역시 실패한다면 에러 상태를 리턴한다.

이처럼 그동안 스토어에서 하고 있던 서버 상태 관리를 리액트 쿼리에서 처리할 수 있다. 스토어는 client 측 전역 상태 관리를 위해서만 사용한다.

🐇 프로젝트에 사용해보기

세팅하는 방법을 알려주는 글들은 많이 때문에 실제로 내가 프로미서 프로젝트에 리액트 쿼리를 어떻게 적용했는지 적어보려고 한다.

useQuery

데이터를 조회할 때 쓰면 유용한 훅이다. 예를 들어 사용자의 그룹 리스트를 모두 조회하려고 하면 다음과 같이 사용할 수 있다.

get 으로 가져온 데이터를 리턴하면 data로 결과 값이 입력된다. data를 groupListData라는 이름으로 사용할 수 있다.

const { data: groupListData } = useQuery("groupList", async () => {
    const { data } = await axios.get("/groups");
    return data;
});

데이터 외에도 다음과 같은 상태를 가져올 수 있다.

const { isLoading, isFetching, error } = useQuery("key", func);
  • isLoading : 쿼리에 데이터가 없고 현재 fetch 중일 때 true 반환
  • isFetching : 어떤 상태에서든 쿼리가 fetch 중이면 true 반환
  • error : 쿼리를 fetch하며 발생한 에러 데이터

useMutation

특정한 이벤트가 발생했을 때 서버 상태에 접근하고 싶은 경우, 또는 서버 데이터를 생성, 수정, 삭제 할 때 사용할 수 있다.

첫 번째 케이스는 사용자 검색 기능을 구현할 때 사용했다. 검색 Form이 제출됐을 때 사용자를 보여주려면 useQuery를 함수 안에 넣었어야 했고, 이 경우 Invalid Hook Call 에러가 발생한다. 다음과 같이 useMutation을 사용하면 필요할 때 mutate 함수를 호출해서 데이터를 가져올 수 있다.

버튼이 클릭됐을 때 getFriend를 호출하도록 했다.

const { mutate: getFriend } = useMutation(async () => {
    await axios
      .get("/friends", {
        params: { findEmail: inputValue },
      })
      .then((res) => {
        setSearchData(res.data);
      })
      .catch((err) => {
        alert(err.response.data.message);
      });
  });

두 번째 케이스는 친구 추가 기능을 구현할 때 사용했다. 사용자를 검색한 후 친구로 등록할 때, post 한 후 바로 친구 리스트 조회 API를 실행시킬 수 있다. 새로고침을 하지 않아도 변경된 데이터가 화면에 반영되기 때문에 사용자 경험이 좋아진다.

onSuccess에 실행할 함수를 넣어줬다. post 가 성공하면 queryClient에 등록한 friendList 키를 통해 친구 조회 API를 호출한다.

const { mutate: addFriend } = useMutation(
    async () => {
      return await axios.post(`/friends/${searchData.id}`).catch((err) => {
        alert(err.response.data.message);
      });
    },
    {
      onSuccess: () => {
        queryClient.invalidateQueries("friendList");
      },
    }
  );

onSuccess와 비슷하게 다음과 같은 기능도 있다.

  • onError : 에러가 발생했을 때 실행된다.
  • onSettled : 요청이 성공하든 실패하든 실행된다.

이번 포스트에서 다룬 훅들은 프로젝트에서 적용해본 부분이며 공식문서에는 더 다양한 사용법이 안내되어있다.

profile
배워서 남주는 개발자 김채은입니다 ( •̀ .̫ •́ )✧

2개의 댓글

comment-user-thumbnail
2022년 6월 15일

혹시 react-query가 선택적 비동기인가요? 훅스를 사용할때 async await 선언해주는 이유가 있을까요? 비동기 선언을 안해주면 동기적 처리가 되는지 궁금합니다.

1개의 답글