react-query

성석민·2022년 1월 28일

프론트엔드

목록 보기
4/7
post-thumbnail

유튜브 Codevolution 채널의 react-query 강의를 바탕으로 작성한 글입니다.

Github에 아래 내용을 추가해뒀습니다.

  • staleTime vs cacheTime
  • isLoading vs isFetching
  • custom hooks를 이용한 useQuery 호출
  • useIsFetching을 이용한 로딩처리

react-query는 리액트 애플리케이션에서 서버 상태 가져오기, 캐싱, 동기화 및 업데이트를 보다 쉽게 다룰 수 있도록 도와주며 클라이언트 상태와 서버 상태를 명확히 구분하기 위해서 만들어진 라이브러리이다.

react-query에서 기존 상태 관리 라이브러리(redux, mobX)는 클라이언트 상태 작업에 적합하지만 비동기 또는 서버 상태 작업에는 그다지 좋지 않다고 말하고 있다.

클라이언트 상태(Client State) 와 서버 상태(Server State)는 완전히 다르며
클라이언트 상태는 컴포넌트에서 관리하는 각각의 input 값으로 예를 들 수 있고
서버 상태는 database에 저장되어있는 데이터로 예를 들 수 있다.

react-query 상태

상태

✅ fresh : 새롭게 추가된 쿼리 & 만료되지 않은 쿼리 ➜ 컴포넌트가 마운트, 업데이트되어도 데이터 재요청 ❌

✅ fetching : 요청 중인 쿼리

✅ stale : 만료된 쿼리 ➜ 컴포넌트가 마운트, 업데이트되면 데이터 재요청 ⭕️

✅ inactive : 비활성화된 쿼리 ➜ 특정 시간이 지나면 가비지 컬렉터에 의해 제거

react-query 사용방법

설치 및 초기 설정

npm i react-query // npm 사용
or
yarn add react-query // yarn 사용

캐시를 관리하기 위해 QueryClient 인스턴스를 생성한 후 QueryClientProvider를 통해 컴포넌트가 QueryClient 인스턴스에 접근할 수 있도록 App컴포넌트 최상단에 추가한다.

기존 요청 방식 VS react-query 요청 방식

  • 기존 요청 방식 : isLoading과 data를 state로 가지며 서버 데이터를 불러온 후 상태 update
  • react-query : useQuery훅을 이용해 반환받은 isLoading과 data 사용

👍 코드의 길이는 물론 가독성까지 좋아보인다 👍

데이터 가져오는 방법 with useQuery

GET요청과 같은 CREAT작업을 할때 사용되는 훅이다.

const requestData = useQuery(쿼리 키, 쿼리 함수, 옵션);

✅ 쿼리 키 : 문자열 or 배열, 캐싱 처리에 있어서 중요한 개념
✅ 쿼리 함수: Promise를 리턴하는 함수, ex) axios(), fetch()
✅ 옵션 : useQuery 기능을 제어

❗️중요❗️ 쿼리 키가 다르면 호출하는 API가 같더라도 캐싱을 별도로 관리한다.

데이터 요청

✅ data : 서버 요청에 대한 데이터
✅ isLoading : 캐시가 없는 상태에서의 데이터 요청 중인 상태 (true / false)
✅ isFetching : 캐시의 유무 상관없이 데이터 요청 중인 상태 (true / false)
✅ isError : 서버 요청 실패에 대한 상태 (true / false)
✅ error : 서버 요청 실패 (object)

const fetchSuperHeroes = () => {
  return axios.get('http://localhost:4000/superheroes');
};

// 방법1. 구조분해 X
const responseData = useQuery('super-heroes', fetchSuperHeroes);
// 방법2. 구조분해 O
const { data, isLoading, isFetching, isError, error } = useQuery('super-heroes', fetchSuperHeroes);

if(isLoading) {
  return <h2>Loading...</h2>
}

if(isError) {
  return  <h2>{error.message}</h2> // Request failed with status code 404
}

console.log(responseData) // 아래 이미지 참조

옵션

✅ cacheTime : 언마운트된 후 어느 시점까지 메모리에 데이터를 저장하여 캐싱할 것인지를 결정

  • 기본값 : 30000 -> 5분
// cacheTime을 3초로 설정
const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes, {
  cacheTime: 3000,
});

✅ staleTime : 쿼리가 fresh 상태에서 stale 상태로 전환되는 시간

  • 기본값 : 0
  • fresh 상태에서는 컴포넌트가 마운트, 업데이트가 되어도 재요청을 보내지 않으므로 API 요청 횟수를 줄일 수 있다.
  • 보통 쉽게 변하지 않는 컴포넌트에 한해서 staleTime을 지정한다.
// staleTime을 3초로 설정
const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes, {
  staleTime: 3000,
});

✅ refetchOnMount : 컴포넌트 마운트시 새로운 데이터 패칭

  • 기본값 : true
  • false로 설정할 경우 마운트시 새로운 데이터를 가지고 오지 않는다.
const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes, {
  refetchOnMount: true, // or false
});

✅ refetchOnWindowFocus : 브라우저 클릭 시 새로운 데이터 패칭

  • 기본값 : true
  • flase로 설정할 경우 브라우저가 포커스 되어도 데이터를 가지고 오지 않는다.
const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes, {
  refetchOnWindowFocus: true, // or false
});

✅ refetchInterval : 지정한 시간 간격만큼 데이터 패칭

  • 기본값 : 0
  • 브라우저에 포커스가 없을 때 실행되지 않는다.
// 2초 간격으로 데이터 패칭
const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes, {
  refetchInterval: 2000,
});

✅ refetchIntervalInBackground : 브라우저에 포커스가 없어도 refetchInterval에서 지정한 시간 간격만큼 데이터 패칭

  • 기본값 : false
// 2초 간격으로 데이터 패칭
const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes, {
  refetchInterval: 2000,
  refetchIntervalInBackground: true,
});

✅ enabled : 컴포넌트가 마운트 되어도 데이터 패칭 ❌

  • 기본값 : true
  • useQuery의 반환값 중 refetch를 활용하여 데이터 패칭을 할 수 있다.
const { data, isLoading, refetch } = useQuery('super-heroes', fetchSuperHeroes, {
  enabled: false,
});

return (
  <button onClick={ refetch }>Fetch Button</button>
)

✅ onSuccess : 데이터 패칭 성공

const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes, {
  onSuccess: (data) => {
  	console.log('데이터 요청 성공', data)
  }
});

✅ onError : 데이터 패칭 실패

const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes, {
  onError: (error) => {
  	console.log('데이터 요청 실패', error)
  }
});

✅ select : 데이터 패칭 성공 시 원하는 데이터 형식으로 변환 가능

const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes);

console.log(data.data) 
/*
[
  {id: 1, name: 'batman'},
  {id: 2, name: 'superman'},
  {id: 3, name: 'wonder woman'},
]
*/
const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes, {
  select: (data) => {
    return data.data.map(hero => hero.name)
  }
});

console.log(data) // ['batman', 'superman', 'wonder woman']

병렬 처리

데이터 패칭이 여러개 실행되어야 한다면 useQuery를 병렬로 선언하면 된다.

import { useQuery } from 'react-query';
import axios from 'axios';

const fetchSuperHeroes = () => {
  return axios.get('http://localhost:4000/superheroes');
};
const fetchFriends = () => {
  return axios.get('http://localhost:4000/friends');
};

const ParallelQueries = () => {
  const heroes = useQuery('super-heroes', fetchSuperHeroes);
  const friends = useQuery('freinds', fetchFriends);

  return (
  	<div>
    	{heroes.data?.data.map(hero => (
     	    <div key={hero.id}>{hero.name}</div>
     	)}

    	{friends.data?.data.map(friend => (
     	    <div key={friend.id}>{friend.name}</div>
     	)}
    </div>
  );
};

export default ParallelQueries;

하지만 쿼리의 수가 많아지면 많아질수록 변수를 다 기억해야 하는 단점이 생기고 모든 쿼리에 대한 로딩, 성공, 실패 처리를 다 해줘야 하므로 불편함을 겪을 수 있다. 그럴때는 useQueries를 사용하면 된다.

const results = useQueries([
  {
    queryKey: ["super-hero"],
    queryFn: () => fetchSuperHeroes()
  },
  {
    queryKey: ["freinds"],
    queryFn: () => fetchFriends()
  }
]);

console.log(results) // 아래 이미지 참조

동기적 실행

어느 순간이든 코드가 동기적으로 수행되어야 하는 일이 발생한다. 그럴 때는 react-query의 어떤 방식을 이용해야 할까? 위에서 봤던 enabled 속성을 이용하면 된다.

useQuery는 enabled 속성의 값이 true일때 실행된다.

const fetchUserByEmail = (email) => {
  return axios.get(`http://localhost:4000/users/${email}`);
};

const fetchCoursesByChannelId = (channelId) => {
  return axios.get(`http://localhost:4000/channels/${channelId}`);
};

const DependentQueries = ({ email }) => {
  const { data: user } = useQuery(['user', email], () => fetchUserByEmail(email));
  const channelId = user?.data.channelId;

  // 집중❗️ 이중 부정을 통해서 channelId이 true -> useQuery 실행, false -> 실행 X
  useQuery(['courses', channelId], () => fetchCoursesByChannelId(channelId), {
    enabled: !!channelId,
  });
  return <div>DependentQueries</div>;
};

export default DependentQueries;

데이터 변경 및 삭제 방법 with useMutation

POST, PUT, DELETE와 같은 변경 및 수정작업을 할때 사용되는 훅이다.

const requestData = useMutation(API 호출 함수, 콜백);

✅ API 호출 함수: 특정 endpoint로 요청을 보내고 Promise를 반환하는 함수
✅ 콜백 : 라이프사이클에 따라 로직 작성

useQuery와 같은 반환값을 받으며 mutate 메서드가 추가된다.
mutate 메서드를 이용하면 API 요청 함수를 호출하여 요청이 이루어진다.

import { useMutation } from 'react-query';

const AddSuperHero = () => {
  const addSuperHero = (hero) => {
    return axios.post('http://localhost:4000/superheroes', hero);
  };
  
  const { mutate: addHero, isLoading, isError, error } = useMutation(addSuperHero);

  const handleAddHeroClick = () => {
    const hero = { 이름, 성별 };
    addHero(hero);
  };

  if (isLoading) {
    return <h2>Loading...</h2>;
  }

  if (isError) {
    return <h2>{error.message}</h2>;
  }
}

하지만 ADD HERO을 클릭 후 수동적으로 Fetch를 해줘야 화면에 보여진다는 불편함이 있다.

이 문제점을 해결하기 위해서는 쿼리 무효화(Invalidation)를 시켜줘야 한다.
이 전에 캐싱된 쿼리를 직접 무효화 시킨 후 데이터를 새로 패칭하도록 해야 한다.

import { useMutation, useQueryClient } from 'react-query';

const AddSuperHero = () => {
  ✅ const queryClient = useQueryClient();
  
  const addSuperHero = (hero) => {
    return axios.post('http://localhost:4000/superheroes', hero);
  };
  
  const { mutate: addHero, isLoading, isError, error } = useMutation(addSuperHero, {
    onSuccess: () => {
    // 캐시가 있는 모든 쿼리 무효화
    ✅ queryClient.invalidateQueries();
    
    // queryKey가 'super-heroes'로 시작하는 모든 쿼리 무효화
    ✅ queryClient.invalidateQueries('super-heroes');
    }
  });

  const handleAddHeroClick = () => {
    const hero = { 이름, 성별 };
    addHero(hero);
  };

  if (isLoading) {
    return <h2>Loading...</h2>;
  }

  if (isError) {
    return <h2>{error.message}</h2>;
  }
}

mutate 함수가 실행되기 전, 성공 여부, 끝과 같이 라이프사이클에 따라 콜백함수를 작성할 수 있다.

useMutation(addSuperHero, {
   onMutate: (variables) => {
     // mutate 함수가 실행되기 전에 실행
     console.log(variables) // addSuperHero에 들어가는 인자
   },
   onSuccess: (data, variables) => {
     // 성공
   },
   onError: (error, variables) => {
     // 에러 발생
   },
   onSettled: (data, error, variables, context) => {
     // 성공 or 실패 상관 없이 실행
   },
 })

틀린 부분이 있거나 보충해야 할 내용이 있다면 댓글이나 DM(sungstonemin)으로 알려주시면 감사하겠습니다😄

profile
기록하는 개발자

17개의 댓글

comment-user-thumbnail
2022년 2월 1일

와~ 내용 정리 깔끔하게 잘하셨네요~ 잘 보고갑니다 :)

1개의 답글
comment-user-thumbnail
2022년 2월 3일

속성명이 기능을 잘 나타내주어 대충 예측만하고 있었는데, 직접 실행해 정리해주시니 보기가 좋네요.
정성글 감사합니다!
동기적 실행 방식은 리렌더를 활용한 방법 같아 보이는데 리렌더를 하지 않고 동기적 실행 방식을 할 수 있는 방법을 생각해보면 복잡해질 것 같네요 ㅎㅎ

1개의 답글
comment-user-thumbnail
2022년 2월 4일

지난달에 업무에서 react-query를 처음 도입하고 캐싱이 상당히 매력적이라고 느꼈어요.
생각보다 내용이 많아서 어떻게 정리를 해야하지 고민했는데 정리를 굉장히 잘하셨네요~!!!

1개의 답글
comment-user-thumbnail
2022년 2월 10일

너무 깔끔한 정리 감사합니다 !

1개의 답글
comment-user-thumbnail
2022년 4월 15일

제가 본 리액트 쿼리 정리글 중에 최고네요

1개의 답글
comment-user-thumbnail
2022년 5월 31일

깔끔하고 알찬 글 감사합니다 :)

1개의 답글
comment-user-thumbnail
2022년 8월 4일

좋은 글 잘봤습니다 감사합니다 ~!

1개의 답글
comment-user-thumbnail
2023년 1월 18일

이해하는데 큰 도움됬습니다. 감사합니다~

1개의 답글
comment-user-thumbnail
2023년 11월 9일

큰 도움 받고 갑니다. 감사합니다!!

답글 달기