React Query

Dodam·2024년 8월 27일
1
post-thumbnail

React Query

React Query는 서버에서 가져온 데이터를 웹 브라우저 앱에서 사용하기 쉽게 도와주는 기술로
데이터 가져오기(fetching), 캐싱(caching), 서버 데이터와의 동기화를 쉽게 만들어 주는 상태 관리 라이브러리이다.

복잡하고 장황한 코드없이 React Component 내부에서 간단하고 직관적으로 API를 사용할 수 있다.

Client 데이터와 Server 데이터 간의 분리

Client Data는 상태 관리 라이브러리가 관리하고, Server Data는 리액트 쿼리가 관리하는 구조를 통해
클라이언트 데이터와 서버 데이터를 온전히 분리할 수 있다.

데이터베이스에서 가져온 데이터를 클라이언트에서 보여주기 위해 ajax를 이용하는데,
이 때 서버에서 가져오는 데이터를 서버의 상태라고 이야기한다.

server state

서버는 특정 시점에 클라이언트의 요청에 대해 DB에서 유저 정보를 가져와 서버의 상태 값을 만들어낸다.
DB에 있는 값을 그대로 클라이언트에게 전달할 수도 있고, 요청에 담긴 특정 값을 이용해 정보를 가공해서 메모리에 들고 있는다. 그리고 이 정보를 클라이언트에게 전달해준다.

client state

client state는 크게 두 가지로 나눌 수 있다.

  • client에서 자체적으로 만드는 state (최초 데이터의 발생지가 클라이언트)
  • server에서 전달받은 값으로 만드는 state (최초 데이터의 발생지가 서버)

다양한 상태 관리 툴

  • external state: Redux, MobX, Zustand, Zotai, Recoil 등
  • internal state : Context API, useState 등

첫 번째로 클라이언트가 자체적으로 만드는 state는 대개 UI를 담당하는 부분으로
모달이 열리고 닫혔는지, 어떤 버튼이 클릭되었는지, 지금 창이 리사이징 되고 있는지에 대한 메타 정보를 담은 상태값이다.

두 번째로 server state를 client state로 가져오는 부분에 대한 내용을 살펴보자.
리액트는 서버의 상태 값을 받아올 때 컴포넌트의 생명 주기를 파악한 후 적절한 시점에 ajax 호출을 하고 서버에서 데이터를 받아온다.
그리고 useState를 사용할 경우, 데이터를 불러와 setState 호출을 통해 응답 당시의 server state를 component state로 wrapping 한다.

useEffect(() => {
	customFetch("...").then(setState)
}, []);

이렇게 클라이언트 앱은 server state와 client state에 대한 로직 처리를 나누어 선언해주어야 한다.

Data Fetching 단순화

기존의 방식은 Fetching 코드를 작성하고 데이터를 담아 둘 상태 생성, useEffect를 이용해 컴포넌트 Mount시 데이터를 Fetching 한 뒤 상태에 저장하였다. 이는 3가지 단계로 요약할 수 있다.

  1. Fetching 코드 작성
  2. 데이터를 담아 둘 상태(state) 생성
  3. useEffect를 이용해 컴포넌트 Mount시 데이터를 Fetching 한 뒤 상태에 저장

해당 예시를 코드로 작성하면

import { useEffect, useState } from 'react';

const getServerData = async () => {
	const data = await fetch(
    	'https://jsonplaceholder.typicode.com/posts'
    ).then((response) => response.json());
  return data;
};

export default function App() {
	const [state, setState] = useState<any[]>([]);
  
  	useEffect(() => {
    	getServerData()
      		.then((dataList) => setState(dataList))
      		.catch((e) => setState([]));
    }, []);
  
  	return <div>{JSON.stringify(state)}</div>;
}

위의 과정을 useQuery 한 줄로 처리할 수 있다.

import { useQuery } from '@tanstack/react-query';

const getServerData = async () => {
	const data = await fetch(
    	'https://jsonplaceholder.typicode.com/posts'
    ).then((response) => response.json());
  return data;
};

export default function App() {
	const { data } = useQuery(['data'], getServerData);
  
  	return <div>{JSON.stringify(data)}</div>;
}

이로 인한 장점으로

  1. 코드 수 감소로 인한 Side Effect 제거
  2. Data Fetching 방식 규격화
  3. enabled를 이용한 동기적 실행

등이 있다.

다음은 동기적 실행에 대한 예시 코드이다.

const [state1, setState1] = useState();
const [state2, setState2] = useState();

useEffect(() => {
	getServerData().then((dataList) => {
    	setState1(dataList[0]);
    })
}, []);

// state1의 데이터를 파라미터로 넣어 호출하는 API
useEffect(() => {
	if(state1) {
    	getAfterData(state1).then((dataList) => {
        setState2(dataList);
        })
    }
}, [state1]);

API 호출을 위한 조건을 추가하여 동기적으로 관리하며, useEffect의 순서가 다른 코드들과 섞이면 어떤 데이터가 언제 어떻게 호출되는지의 흐름 및 시점을 파악하기 어려워진다.

하지만 리액트 쿼리의 옵션은 enabled를 사용하면 아래와 같이 간단하게 동기적 실행이 가능하다.

const { data: data1 } = useQuery(['data1'], getServerData);
const { data: data2 } = useQuery(['data2', data1], getServerData, {
	enabled: !!data1
});

Data Caching

statetime, cachetime 옵션을 사용하면 React Query에서 자체적으로 제공하는 데이터 캐싱 기능을 이용할 수 있다. 캐싱을 활용하여 불필요한 API 호출을 줄임으로써 전체적인 애플리케이션 성능을 향상시킬 수 있다.

1) staletime (갱신 지연 시간)
staletime은 캐시된 데이터의 유효 기간을 나타내는 옵션이다. 기본적으로 0으로 설정되어 있어, 데이터가 한 번 캐시되면 즉시 만료되고 다시 요청된다.

이 값을 조정해서 데이터를 일정 시간동안 캐시로 사용하고, 그 이후에만 다시 요청하도록 할 수 있다.

const { data } = useQuery(['data', getServerData, {
	staletime: 10 * 60 * 1000;
})

자주 변경되지 않는 데이터의 경우, 캐시된 데이터를 사용함으로써 불필요한 네트워크 요청을 줄이고 애플리케이션의 성능을 향상시킬 수 있다.

2) cachetime (캐시 유지 시간)
cachetime은 캐시된 데이터가 얼마나 오랫동안 메모리에 유지될지를 나타내는 옵션이다. 이 값을 설정하면 캐시된 데이터가 일정 시간동안 메모리에 유지된 후 자동으로 삭제된다.

const { data } = useQuery(['data', getServerData], {
	cachetime: 30 * 60 * 1000, // 30분
})

Query & Mutation

React Query는 API의 요청을 Query, Mutation의 두 가지로 처리한다.

1) Query
Query 함수를 사용해서 데이터를 가져오고 캐싱할 수 있다. Query 함수는 데이터 패칭용으로 사용되며, 보통 GET으로 받아오는 대부분의 API에서 useQuery() 함수를 사용한다.

const { data } = useQuery(
	queryKey,
  	queryFunction,
  	options,
)

첫 번째는 Query Key로 응답 데이터의 Unique key 이다. 응답 데이터를 캐싱할 때 사용된다.

두 번째는 Query Function으로 Promise를 반환하는 함수이다. 이 쿼리 요청을 수행하기 위한 fetch, axios 등의 함수를 의미한다.

세 번째는 useQuery에 사용되는 옵션을 지정하는 객체이다.

useQuery를 실행하면 다양한 리턴 값을 받을 수 있는데, 그 중 자주 사용하는 것들에 대해 살펴보자.

  • data : 마지막으로 성공한 데이터 (Response)
  • error : 에러가 발생했을 때 반환되는 객체
  • isFetching, isLoading, isSuccess 등 : 현재 Query의 상태
  • refetch : 해당 Query를 refetch 하는 함수

아래 코드는 useQuery를 사용하여 user의 프로필 정보를 가져오는 예시이다.

import { useQuery } from 'react-query';

// 데이터 패칭 함수
const getUserData(userId) = () => {
	return fetch(`/api/user/${userId}`).then((response) => response.json());
}

const UserProfile = ({ userId }) => {
	// useQuery를 사용하여 데이터를 가져온다. ['user', userId]는 쿼리 키를 의미한다.
  const { data, isError, error, isLoading } = useQuery(['user', userId], () => fetchUserData(userId));
  
  if (isLoading) { // isLoading을 사용하여 데이터가 로딩중일 때 화면을 렌더링한다.
  	return <div>Loading...</div>;
  }
  
  if (isError) {
  	return <div>error: {error.message}</div>;
  }
  
  return (
  	<div>
    	<h1>User Profile</h1>
    	<p>Name: {data.name}</p>
		<p>Email: {data.email}</p>
    </div>
  )
}

2) Mutation
Mutation 함수를 사용하여 데이터와 캐시를 업데이트할 수 있다. Mutation 함수는 데이터 생성, 수정, 삭제용으로 사용되며 POST, PUT, DELETE 요청시에 사용한다.

가장 기본적인 형태의 useMutation 요청 형태는 다음과 같다.

const { mutate } = useMutation(
	mutationFn,
  	options,
);

첫 번째는 Mutation Function으로 Promise를 반환하는 함수이다. useQuery에서와 마찬가지로 fetch, axios 등의 함수를 의미한다.

두 번째는 useMutation에 사용되는 옵션을 지정하는 객체이다.

아래 코드는 useMutation을 사용하여 사용자의 정보를 업데이트하는 예시이다.

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

// 데이터 업데이트 함수
function updateUser(userId, updatedData) {
	return fetch(`/api/user/${userId}`, {
    	method: 'PUT',
      	body: JSON.stringify(updatedData),
    }).then((response) => response.json();
}

function UserProfileEditor({ userId }) {
	const queryClient = userQueryClient();
  
  	const { mutate } = useMutation((updatedData) => updateUser(userId, updatedData), {
    	onSuccess: () => {
        	// 데이터 업데이트 후 캐시를 재로드
          	queryClient.invalidateQueries(['user', userId]);
        },
    });
  
  	const handleSubmit = (updatedData) => {
  		mutate(updatedData); // mutate는 자동으로 실행되지 않으므로 submit 시에 mutate 실행
  	};
  
  	return (
  		<div>
    		<h2>Edit User Profile</h2>
    		<UserForm onSubmit={handleSubmit} />
    	</div>
  	);
}

profile
⏰ Good things take time

0개의 댓글