본문 바로가기

프론트엔드

react query로 Get요청하기 - 캐시처리, 동적쿼리, 요청 취소, 비활성화

🧐 Tanstack Query란?

http 요청을 전송하고 프론트엔드 UI를 백엔드 데이터와 동기화된 상태로 유지하는데 이용하는 라이브러리

 

Tanstack Query가 반드시 필요한건 아니고, useEffect와 fetch로 충분히 구현할 수 있다.
하지만 Tanstack Query를 사용하면 훨씬 깔끔하고 수월하게 개발할 수 있다.
또한, 캐싱처리나 refetch트리거 등 고급 기능을 이용할 수 있다.

🔧 설치, 사용법, 이점

설치

npm install @tanstack/react-query

사용법

- tanstack query는 HTTP요청을 전송하는 로직이 내장되어 있지 않고, 요청을 관리하는 로직을 제공: 요청 데이터와 발생 가능한 오류를 추적하는 역할 등
- 모든 get요청에서 query key를 이용해 요청으로 생성된 데이터를 캐시처리한다.-> 나중에 동일한 요청을 할 경우, 이전 요청의 응답을 재사용할 수 있다.

const { data, isPending, isError, error, refetch } = useQuery({
    queryKey: ['events'],
      queryFn: fetchEvents,
});
export async function fetchEvents() {
    const response = await fetch('~~');

      if(!response.ok) {
        const error = new Error('An error occurred while fetching the events');
          error.code = response.status;
          error.info = await response.json();
          throw error;
    }

      const { events } = await response.json();

      return events;
}

전달 프로퍼티

  1. queryKey: 배열
  • 값은 배열이고, 이 배열을 리액트 쿼리는 내부적으로 저장한다.
  • 유사한 값으로 이루어진 배열을 사용할 때마다 배열을 확인하고 기존 데이터를 재사용한다.
  • 배열 내 데이터 유형은 문자열로 제한되지 않는다.
  1. queryFn: 쿼리함수로 실제 요청을 전송할 때 실행할 코드 정의로 프로미스를 반환하는 함수

반환 객체

  • data, isPending, isError, error, refetch

프로바이더 래핑

const queryClient = new QueryCient();

function App(){
    return (
      <QueryClientProvider client={queryClient}>
          <RouterProvider router={router} />
      </QueryClientProvider>
    );
}

이점

위 처럼 기본적인 구성으로만 사용해도, 페이지를 나갔다가 들어오면 refetch가 되서 최신 항상 최신 데이터를 보여줄 수 있다.

💰 쿼리 동작 이해 및 구성 -캐시 및 만료된 데이터

캐시 처리

  • useEffet로 데이터를 가져올 때는 이미 응답된 데이터를 페이지 이동 등으로 다시 요청할 경우에 전체 데이터를 새로 받아왔지만, react query를 사용하면 캐시처리를 통해 데이터를 즉각적으로 가져온다.
  • react query는 요청을 통해 얻은 응답 데이터를 캐시 처리하고 나중에 동일한 쿼리키를 가진 다른 useQuery가 실행되면 이 데이터를 재사용한다.
  • 사용자가 페이지에 재진입하여 컴포넌트 함수가 다시 실행되면 리액트 쿼리는 이 쿼리 키가 이전에 이미 사용되었고, 이 키의 데이터를 캐시 처리한 것을 확인하여 데이터를 즉시 제공할 수 있다.
  • 이와 동시에 내부적으로 해당 요청을 다시 전송하여 업데이트된 데이터가 있는지 확인하여 업데이트된 데이터로 이 데이터를 자체적으로 교체한다.

 

캐시 처리 실행 여부 동작 제어

staleTime

  • staleTime: 캐시에 데이터가 있을 때 업데이트된 데이터를 가져오기 위한 요청을 자체적으로 전송하기 전에 기다릴 시간 설정
  • 0(default)일 경우 항상전송
  • 5000일 경우 5000ms동안 기다린 후 추가요청: 5초가 지나기 전에 재요청하면 자체적 전송을 하지 않고 캐시 데이터를 사용하지만, 5초가 지난 후 재요청하면 자체적 전송

gcTime

  • Garbage Collection Time(가비지 수집 시간)
  • 데이터와 캐시를 얼마나 오랫동안 보관할지 제어
  • 5분(default)
  • 이 시간이 지나면 캐시가 날라가므로, 컴포넌트를 재렌더링 했을 때 데이터를 가져오기 위한 새로운 요청을 전송
const { data, isPending, isError, error } = useQuery({
    queryKey: ['events'],
      queryFn: fetchEvents,
      staleTime: 5000,
      // gcTime: 30000,
});

데이터를 보관하는 기간과 새 요청을 전송하는 시기를 제어할 수 있다!

🗝️ 동적 쿼리 함수 및 쿼리 키

쿼리함수가 파라미터를 받아 동적으로 구성될 경우,

 

쿼리 함수

export async function fetchEvents(searchTerm) {
      let url = 'http://localhost:3000/events';
      if(searchTerm) {
        url += '?search=' + seatchTerm
    }

      const response = await fetch(url);

      if(!response.ok) {
        const error = new Error('An error occurred while fetching the events');
          error.code = response.status;
          error.info = await response.json();
          throw error;
    }

      const { events } = await response.json();

      return events;
}

 

useQuery

  • 이 쿼리는 모든 이벤트를 가져오는게 아니라 검색된 결과만 가져오는 쿼리이므로, 전체를 반환하는 쿼리키와는 다른 쿼리키를 가져야 한다.
  • 쿼리 키를 동적으로 구성함으로써 리액트 쿼리는 동일한 쿼리를 기반으로 서로 다른 키에 대해 서로 다른 데이터를 캐시하고 재사용할 수 있다.
  • 아래 쿼리는 사용자가 입력한 값에 따라 결과가 달라지므로 동적인 쿼리키를 가져야한다.
    const { data, isPending, isError, error } = useQuery({
      queryKey: ['events', { search: searchElement.current.value }],
        queryFn: () => fetchEvents(searchElement.current.value),
    });
    ❗️ 하지만 ref값을 fetchEvent와 key에 모두 이용하는 것은 바람직하지 않다. state와 달리 ref는 이 컴포넌트가 다시 실행되도록 할 수 없기 때문.

👇

const searchElement = useRef();
const [searchTerm, setSearchTerm] = useState('');

const { data, isPending, isError, error } = useQuery({
    queryKey: ['events', { search: searchTerm }],
      queryFn: () => fetchEvents(searchTerm),
});

function handleSubmit(event) {
    event.preventDefault();
      setSearchTerm(searchElement.current.value);
}

let content;

if (isPending) {
    content = <LoadingIndicator />
}

if (isError) {
    content = <ErrorBlock message={error.info?.message || '오류가 발생했습니다.' }/>
}

if (data) {
    content = <ul>
        { data.map(event => 
              <li key={event.id}>
                <EventItem event={event} />
            </li>          
        )}
      </ul>
}
...

return (
      ...
    <form onSubmit={handleSubmit}>
      <input type="search" ref={searchElement} />
      <button>Search</button>
    </form>
    { content }
)

❌ 쿼리 구성 객체 및 요청 취소

  • react query는 쿼리함수에 기본 데이터를 전달한다.
  • 콘솔로 찍어보면, 코드상에서 어디에서도 전달한 적 없는 객체가 찍혀져 나온다.
  • react query가 쿼리함수에 기본적으로 전달하는 데이터는 쿼리키와 신호(signal)에 대한 정보를 제공하는 객체이다.
  • signal은 요청을 취소할 때 필요하다. (ex. 요청이 완료되기 전에 사용자가 페이지를 이탈하는 경우)
  • 리액트는 자동으로 요청을 취소할 수 있는데 그 용도로 signal을 사용한다.
    👉🏽 이러한 signal을 제공하고 데이터를 가져오는 함수에 필요한 쿼리키를 제공하기 위해 리액트 쿼리는 쿼리 함수에 이 객체를 전달한다.

그래서 파라미터 유무에따라 동적으로 url을 구성하고 싶을 경우, 아래와 같이 쿼리 함수를 작성하면 searchTerm으로 기본 데이터가 들어가게 되므로 원하는대로 동작하지 않게된다.

export async function fetchEvents(searchTerm) {
      let url = 'http://localhost:3000/events';
      if(searchTerm) {
        url += '?search=' + seatchTerm
    }

      const response = await fetch(url);

      if(!response.ok) {
        const error = new Error('An error occurred while fetching the events');
          error.code = response.status;
          error.info = await response.json();
          throw error;
    }

      const { events } = await response.json();

      return events;
}

 

수정

 

searchTerm 쓰는 곳

const { data, isPending, isError, error } = useQuery({
	queryKey: ['events', { search: searchTerm }],
  	queryFn: ({signal}) => fetchEvents({signal, searchTerm}), 
	// 프로퍼티명을 쿼리 함수와 맞춰준다.
});

 

searchTerm 안쓰는 곳 (전체 데이터 받아올 때)

export async function fetchEvents({ signal, searchTerm }) 
// 요청이 취소된 것을 파악하기 위해 signal 받기
{
  	let url = 'http://localhost:3000/events';
  	if(searchTerm) {
    	url += '?search=' + seatchTerm
    }
	// fetch함수로 signal전달하여 취소할 수 있게 하기
  	// 👉🏽 브라우저 내부에서 signal을 받아 요청을 취소할 수 있다.
  	const response = await fetch(url, {signal: signal});
  
  	if(!response.ok) {
    	const error = new Error('An error occurred while fetching the events');
      	error.code = response.status;
      	error.info = await response.json();
      	throw error;
    }
  
  	const { events } = await response.json();
  
  	return events;
}

 

쿼리 함수

🎛️ 쿼리 활성화 및 비활성화

검색어를 입력하기 전에는 결과 가져오지 않게 하기 (특정 조건에서 비활성화 처리): enabled: false

 

const [searchTerm, setSearchTerm] = useState();

const { data, isLoading, isError, error } = useQuery({
	queryKey: ['events', { search: searchTerm }],
  	queryFn: ({signal}) => fetchEvents({signal, searchTerm}), 
  	enabled: searchTerm !== undefined
});

 

- isLoading vs isPending
- isPending은 쿼리가 시도가 완료되지 않은 경우 (데이터 요청을 하지 않아도 enabled가 false이면 true가 된다.)
- isLoading은 데이터를 로드하고 있을 때 true가 된다. (=== isFetching && isPending)