react-query

성석민·2022년 1월 28일
170

프론트엔드

목록 보기
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일

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

답글 달기