유튜브 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컴포넌트 최상단에 추가한다.
👍 코드의 길이는 물론 가독성까지 좋아보인다 👍
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 : 언마운트된 후 어느 시점까지 메모리에 데이터를 저장하여 캐싱할 것인지를 결정
// cacheTime을 3초로 설정
const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes, {
cacheTime: 3000,
});
✅ staleTime : 쿼리가 fresh 상태에서 stale 상태로 전환되는 시간
// staleTime을 3초로 설정
const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes, {
staleTime: 3000,
});
✅ refetchOnMount : 컴포넌트 마운트시 새로운 데이터 패칭
const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes, {
refetchOnMount: true, // or false
});
✅ refetchOnWindowFocus : 브라우저 클릭 시 새로운 데이터 패칭
const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes, {
refetchOnWindowFocus: true, // or false
});
✅ refetchInterval : 지정한 시간 간격만큼 데이터 패칭
// 2초 간격으로 데이터 패칭
const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes, {
refetchInterval: 2000,
});
✅ refetchIntervalInBackground : 브라우저에 포커스가 없어도 refetchInterval에서 지정한 시간 간격만큼 데이터 패칭
// 2초 간격으로 데이터 패칭
const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes, {
refetchInterval: 2000,
refetchIntervalInBackground: true,
});
✅ enabled : 컴포넌트가 마운트 되어도 데이터 패칭 ❌
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)으로 알려주시면 감사하겠습니다😄
와~ 내용 정리 깔끔하게 잘하셨네요~ 잘 보고갑니다 :)