React-Query, 리액트 쿼리에 대하여

서지수·2023년 1월 11일
0
post-thumbnail

우리 팀의 경우 Recoil을 이용해서 데이터를 관리하고 msw를 이용해 목서버를 만들어서 사용 중이었다, 하지만 Recoil이나 Redux 등의 라이브러리는 클라이언트의 데이터들을 관리하기에는 좋아도 서버의 데이터들을 관리하기에는 적합하지 않다는 이야기를 접했고 서버에서 주는 데이터들을 좀 더 적합하고 효과적인 구현을 위해 리액트쿼리를 공부하게 되었다.

리액트쿼리란 무엇인가?

리액트쿼리란 데이터 Fetching, 캐싱, 동기화, 서버 쪽 데이터 업데이트 등을 쉽게 만들어 주는 React 라이브러리이다.

리액트쿼리를 사용하는 이유

  • 데이터뿐만 아니라 isIdle, isLoading, isFetching, isSuccess, isError등과 같은 여러 가지 부가적인 상태 값들이 제공된다.
  • query key를 통하여 데이터 캐싱과 각 쿼리 간에 디펜던시 조작도 손쉽게 할 수 있다.(우리 팀의 경우에도 unique key를 이용해서 변경사항이 바로바로 반영되도록 했다.)
  • useEffect로 처리했던 여러 가지 상황들을 refetchOnMount, refetchOnReconnect, refetchOnWindowFocus와 같은 옵션으로 쉽게 처리할 수 있다.
  • 데이터 캐싱
  • get을 한 데이터에 대해 update를 하면 자동으로 get을 다시 수행한다. (우리 팀의 경우에도 댓글을 post하면 바로 get해서 보여주고 싶어서 리액트 쿼리를 채택했다. )
  • 데이터가 오래 되었다고 판단되면 다시 get해준다. (invalidateQueries)
  • 동일 데이터 여러번 요청하면 한번만 요청한다. (중복 호출 허용 시간 조절이 가능하다. )
  • 무한 스크롤 (우리 팀의 경우 미래를 생각하면 정말 몇 천개씩의 데이터를 보여주어야하기 때문에 무한 스크롤을 구현했다.)

구현

데이터를 get해올 때는 useQuery를, poat, patch, delete할 때는 useMutation을 사용한다.

get

데이터를 get해오는 최종코드는 아래와 같다.

userComment.tsx

const { data } = useQuery(["beatId",beatId], ()=>getComment(beatId)
  , {
    refetchOnWindowFocus: false, 
    retry: 0, 
    onSuccess: data => {
      if (data?.status === 200) {
        // console.log(data);
        // console.log("성공");
        setComments(data?.data.data.commentList)
      }    
    },
    onError: error => {
      console.log("실패");
    }
  });

trackPost.ts

export async function getComment(props:number) {
  const state=props
  try {
    const data = await axios.get(`${process.env.REACT_APP_BASE_URL}/tracks/comments/8?page=1&limit=20`, 
    {
      headers: {
        Authorization: `Bearer ${`${process.env.REACT_APP_PRODUCER_ACCESSTOKEN}`}`,
      },
    });
    // data && console.log(data);
    return data;
  } catch (e) {
    console.log(e);
  }
}

refetchOnWindowFocus는 window가 다른 곳을 왔다가 돌아왔을 때의 재실행 여부를, retry는 실패 시 재호출 여부를 뜻한다.

const { data } = useQuery(()=>getComment(beatId)

사실 이렇게 한 줄만 작성해도 데이터는 잘 받아와진다, 최종코드가 위와 같이 길어진 이유에는 두 가지 이유가 있다.

첫째, 데이터가 바로바로 반영되지 않았다. 우리 서비스에는 음악카테고리를 클릭하면 클릭한 카테고리에 따라 해당하는 트랙을 바로바로 보여줘야하는데, 위의 한 줄 코드로는 데이터가 바로바로 반영되지 않았다. 음악 카테고리를 클릭한 후 한참 뒤에야 데이터가 반영되는 것이 아닌가! 그래서 리액트쿼리의 unique key 를 이용했다.

const { data } = useQuery(["beatId",beatId], ()=>getComment(beatId)

이렇게 "beatId"라는 unique key를 부여해주었다. ["beatId", beatId] 에서 첫번째 파라미터인 "beatId"는 해당 쿼리를 의미하는 unique key이고, 두번째 파라미터인 beatId는 비동기함수이다. 즉, 두번째 파라미터인 beatId가 변경될 때마다 data를 불러오는 쿼리가 재실행된다!

둘째, 데이터를 가져오는데에 성공하거나 실패했을 때의 액션을 표현하기에 위의 한 줄은 부족했다. 원래는 그냥 .then이나 await을 이용해서 처리를 해주었었는데, 이번에는 리액트쿼리에 내장되어있는 onSuccess, onError를 이용해서 액션을 구현해주었다.

post

데이터를 Post하는 최종코드는 아래와 같다.

useComment.tsx

원클릭 이벤트 함수

function uploadComment(uploadData:UploadDataType){
    setIsCompleted(true);
    console.log("1")
  } 

  useEffect(()=>{
    if(content&&wavFile){      
	if(uploadData.content&&uploadData.wavFile){      
      console.log("2")

      let formData = new FormData();
      formData.append("wavFile", wavFile);
      formData.append("content", content);

      mutate(formData)
    }
  },[isCompleted])

mutate

const queryClient = useQueryClient();

const {mutate} = useMutation(postComment, {
    onSuccess: () => {
      console.log("3")
      queryClient.invalidateQueries("beatId");
      setUploadData({
        content: "",
        wavFile: null,
      });
      setContent("")
      setWavFile(null)
      setIsCompleted(false)
      setIsEnd(false)
    }
  });

여기서 queryClient.invalidateQueries("beatId"); 는 post에 성공하면 "beatId"라는 unique key를 가진 쿼리를 재실행한다는 뜻이다. post에 성공하면 자동적으로 get을 실행하도록 한다.

trackPost.ts

export async function postComment(formData:any) {
  try {
     const data=await axios.post(`${process.env.REACT_APP_BASE_URL}/tracks/8`, formData,
    {
      headers: {
        'Content-Type': 'amultipart/form-data',
        Authorization: `Bearer ${`${process.env.REACT_APP_VOCAL_ACCESSTOKEN}`}`,
      },
    });
    data && console.log(data);
  } catch (e) {
    console.log(e);
  }
}

사실 post에서 고생을 좀 많이 했다. 버튼을 클릭하면 원클릭 함수가 발생하고, 그 때 useMutation이 돌아야하는데, 원클릭 함수 안에 useMutation을 집어넣으면 상당한 에러가 뜨기 때문. useMutation을 따로 쓴 함수를 작성하고, 원클릭했을 때 mutate(formData)와 같은 방식으로 불러와주니 해결되었다. 또한, 콘솔에 찍으면 잘 나오던 데이터들이 막상 post하니까 자꾸 null값으로 찍혔는데, 최종적으로 post할 데이터들을 recoil로 저장해준 뒤 post하니 해결되었다.

배운점

사실 그동안 아주 기본적인 라이브러리로도 웬만한 구현이 가능했기에 리액트쿼리를 적용할 생각을 하지 않았다. 하지만, 우리 팀의 서비스는 카테고리를 클릭하면 클릭한대로 데이터를 필터링해주어야하고, 댓글을 작성하면 바로바로 get이 되어야하는 서비스였다. 리액트쿼리가 없으면 안되는 상황에서 억지로 리액트쿼리를 공부하게 되었는데 데이터에 변화를 주면 바로 반영이 되고, post를 하면 바로 get이 되는 등 리액트 쿼리의 장점을 알게 되니 이전으로 돌아갈 수 없을 것만 같다.

참고자료

react-query overview
리액트쿼리 개념 및 정리
리액트쿼리 캐싱 이슈 해결하기
리액트쿼리 사용법 및 사용이유
리액트쿼리로 파일 다운로드 구현하기
useMutation 사용하기
yunsoho 깃허브
useMutation 알아보기
리액트쿼리의 라이프사이클
리액트쿼리 장점

profile
이룰 때까지 도전하는 사람

1개의 댓글

comment-user-thumbnail
2024년 4월 19일

안녕하세요. 포스트보다가 궁금한게 있어서 댓글드립니다.
코드에 보면 JWT 토큰전달하려고 헤더에 아래 코드를 반복적으로 넣고 있는데요.

Authorization: Bearer ${${process.env.REACT_APP_VOCAL_ACCESSTOKEN}},

이부분을 공통으로 사용할수도있나요?

답글 달기