본문 바로가기
개발적인

프론트엔드의 뜨거운 감자, 리액트 쿼리

by klm hyeon woo 2023. 10. 30.

목차

· 리액트 쿼리에 들어가면서

· 리액트 쿼리의 캐싱 처리

· StaleTime과 CacheTime

· useQuery와 useMutation

· 코어 프로젝트에 실제 적용해보기

· 레퍼런스


리액트 쿼리에 들어가면서

요즘 프론트엔드 개발을 진행하면서 리액트 쿼리에 대해서 많은 이야기들을 들었다. 회사 프로젝트 및 개인 프로젝트에서 많이 사용되는 라이브러리인데 눈팅만 슥 하자니 배워보고싶은 마음에 무작정 프로젝트에 적용을 하면서 배움의 기회를 얻게 되었다. 리액트 쿼리는 간단하게 말하자면 데이터 페칭, 캐싱, 서버 데이터와의 동기화를 지원해주는 라이브러리이다. 리액트 훅과 같이 쉽게 사용이 가능하며 비동기 로직을 보다 간편하게 작성하는데 도움을 준다. 그 밖에 서버 값을 클라이언트에서 사용할 때 자동으로 업데이트 해주고, 에러 처리 및 중복 방지를 자동으로 도와준다. 

 

한국에서의 대표적인 K기업에서도 리액트 쿼리를 원활하게 쓰고 있으며, 기술 스택으로 핫한 T기업에서도 리액트 쿼리를 도입중이다. K기업에서는 다음과 같은 이유로 리액트 쿼리를 선택하였고, 조직에서 이러한 이유로 리액트 쿼리를 사용한다고 언급을 하였다.

  • 리액트 쿼리는 리액트 어플리케이션에서 서버 상태를 불러오고, 캐싱하며 지속적으로 동기화하고 업데이트하는 작업을 도와주는 라이브러리이다.
  • 복잡하고 장황한 코드가 필요한 다른 데이터 페칭 방식과 달리 리액트 컴포넌트 내부에서 간단하고 직관적으로 API를 사용할 수 있다.
  • 더 나아가 리액트 쿼리에서 제공하는 캐싱, 윈도우 포커스 페칭 등 다양한 기능을 활용하여 API 요청과 관련된 번잡한 작업 없이 핵심 로직에 집중할 수 있다.

리액트 쿼리는 프론트엔드에서 비동기 데이터를 불러오는 과정 중 발생하는 문제점들을 해결해주는데, 과연 어떤 문제들을 해결해줄까?

리액트 쿼리의 캐싱 처리

리액트 쿼리의 장점 중 하나는 데이터의 캐싱을 용이하게 해준다는 점이다.

(캐싱은 특정 데이터의 복사본을 저장하여 이후 동일한 데이터의 접근 속도를 높이는 것을 뜻한다)

리액트 쿼리는 캐싱을 통해 동일한 데이터에 대한 반복적인 비동기 데이터 호출을 방지하고, 이는 불필요한 API 콜을 줄여 서버에 대한 부하를 줄이는 좋은 결과를 가져온다.

 

만약 서버 데이터를 불러와 캐싱한 후, 실제 서버 데이터를 확인했을 때 서버 상에서 데이터의 상태가 변경되어있다면 사용자는 실제 데이터가 아닌 변경 전의 데이터를 바라볼 수 밖에 없게 된다. 이럴 경우에 사용자에게 잘못된 정보를 보여주는 문제점이 발생한다. 이럴 경우에는 어떻게 대처를 할 수 있을까?

 

에러를 발생시키지 않는 좋은 캐싱 기능을 제공한다는 것은 결국 필요한 상황에 적절하게 데이터를 갱신해줄 수 있다는 말과 같다. 이러한 상황은 다음과 같은 예시들로 설명을 할 수 있다.

  • 사용자가 화면을 라이브로 보고 있을 때
  • 페이지의 전환이 일어났을 때
  • 페이지 전환 없이 어떠한 이벤트가 발생하여 데이터를 요청할 때

이를 위해 리액트 쿼리에서는 기본적인 옵션들을 제공을 하고 있다.

· refetchOnWindowFocus (default : true)
· refetchOnMount (default : true)
· refetchOnReconnect (default : true)
· staleTime (default : 0)
· cacheTime (default : 5m .. 60 * 5 * 1000)

위의 옵션들을 통해 리액트 쿼리가 어떤 시점에 데이터를 리페칭하는지 알 수 있다.

  • 브라우저에 포커스가 들어온 경우 (refetchOnWindowFocus)
  • 새로운 컴포넌트에 마운트가 발생한 경우 (refetchOnMount)
  • 네트워크 재연결이 발생한 경우 (refetchOnReconnect)

Stale Time과 Cache Time

· Stale Time

1) staleTime은 데이터가 fresh ➠ stale 상태로 변경되는데 걸리는 시간을 의미한다.

2) fresh 상태일 때는 위의 3가지 경우와 같은 리페칭 트리거가 발생해도 리페치가 일어나지 않는다.

3) 기본 값이 0이므로 따로 설정해주지 않는다면 리페치 트리거가 발생했을 때 무조건 리페치가 발생한다.

 

· Cache Time

1) cacheTime은 데이터가 inactive한 상태일 때 캐싱된 상태로 남아있는 시간이다.

2) 특정 컴포넌트가 unmount(페이지 전환 등으로 화면에서 사라질 때) 되면 사용된 데이터는 inactive 상태로 바뀌고 이때 데이터는 cacheTime 만큼 유지가 되어진다.

3) cacheTime 이후 데이터는 가비지 콜렉터로 수집되어 메모리에서 해제된다.

4) 만일 cacheTime이 지나지 않았는데 해당 데이터를 사용하는 컴포넌트가 다시 mount 된다면, 새로운 데이터를 페치하는 동안 캐싱된 데이터를 보여준다.

5) 즉, 캐싱된 데이터를 계속 보여주는게 아니라 페치하는 동안 임시로 보여준다는 것이다.

 

이외에도 사용자가 특정 이벤트가 발생했을 때 리페칭을 하도록 설정을 해줄 수 있다. 리액트 쿼리의 이러한 기능들을 통해 사용자는 언제나 최적화된 데이터를 제공받게 되는 것이다.

useQuery와 useMutation

리액트 쿼리에서 데이터 페칭을 위해 제공하는 대표적인 기능들이 있다. 기본적으로 GET에는 useQuery를, 서버에 데이터를 보낼 때는 useMutation이 사용이 되어진다.

 

· useQuery

1) 첫 번째 파라미터로 unique key를 포함한 배열이 들어간다. 이후 동일한 쿼리를 불러올 때 유용하게 사용이 된다.

2) 첫 번째 파라미터에 들어가는 배열의 첫 요소는 unique key로 사용되고, 두 번째 요소부터는 query 함수 내부의 파리미터로 값들이 전달된다.

3) 두 번째 파라미터로 실제 호출하고자 하는 비동기 함수가 들어간다. 이때 함수는 Promise를 반환하는 형태여야한다.

4) 최종 반환 값은 API의 성공, 실패 여부, 반환 값을 포함한 객체이다.

import {
  QueryClient,
  QueryClientProvider,
  useQuery,
} from '@tanstack/react-query'

const queryClient = new QueryClient()

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  )
}

function Example() {
  const { isLoading, error, data } = useQuery({
    queryKey: ['repoData'],
    queryFn: () =>
      fetch('https://api.github.com/repos/tannerlinsley/react-query').then(
        (res) => res.json(),
      ),
  })

  if (isLoading) return 'Loading...'

  if (error) return 'An error has occurred: ' + error.message

  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.description}</p>
      <strong>👀 {data.subscribers_count}</strong>{' '}
      <strong>✨ {data.stargazers_count}</strong>{' '}
      <strong>🍴 {data.forks_count}</strong>
    </div>
  )
}

useQuery 함수가 반환하는 객체를 보면 isLoading을 통해 로딩 여부를, error를 통해 에러 발생 여부를, data를 통해 성공 시 데이터를 반환할 수 있다. isLoading과 error를 이용하여 각 상황 별 분기 처리를 쉽게 진행할 수 있다.

· useQuery의 동기적 실행

1) useQuery에서 enable 옵션을 사용하면 비동기 함수인 useQuery를 동기적으로 사용 가능하다.

2) useQuery의 세 번째 인자로 다양한 옵션 값들이 들어가는데, 여기서 enabled에 값을 대입하면 해당 값이 true 일 때 useQuery를 동기적으로 실행한다.

const { data: todoList, error, isFetching } = useQuery("todos", fetchTodoList);
const { data: nextTodo, error, isFetching } = useQuery(
	"nextTodos",
    fetchNextTodoList,
    {
    	enabled: !!todoList // true가 되면 fetchNextTodoList를 실행
    }
);

· useQueries

여러 개의 useQuery를 한 번에 실행하고자 하는 경우, 기존의 Promise.all()처럼 묶어서 실행할 수 있도록 도와준다.

const results = useQueries({
	queries: [
    	{ queryKey: ['post', 1], queryFn: fetchPost, staleTime: Infinity },
        { queryKey: ['post', 2], queryFn: fetchPost, staleTime: Infinity },        
    ]
})

// 두 query에 대한 반환 값이 배열로 묶여 반횐이 되어진다.

· useMutation

서버에 데이터를 보낼 때 사용되는 리액트 쿼리의 API이다.

function App() {
  const mutation = useMutation({
    mutationFn: (newTodo) => {
      return axios.post('/todos', newTodo)
    },
  })

  return (
    <div>
      {mutation.isLoading ? (
        'Adding todo...'
      ) : (
        <>
          {mutation.isError ? (
            <div>An error occurred: {mutation.error.message}</div>
          ) : null}

          {mutation.isSuccess ? <div>Todo added!</div> : null}

          <button
            onClick={() => {
              mutation.mutate({ id: new Date(), title: 'Do Laundry' })
            }}
          >
            Create Todo
          </button>
        </>
      )}
    </div>
  )
}

코어 프로젝트에 실제 적용해보기

코어 프로젝트에 멋쟁이사자처럼 구성원들의 소식을 담을 수 있는 피드 페이지를 개발을 진행했다. 블로그의 RSS를 이용해 파이썬으로 스크랩핑을 진행하고, 이를 Git Action으로 CI 자동화를 진행했다. Next 프레임워크로 개발을 진행했기 때문에 ISR을 통해 페칭된 데이터 값을 페이지로 보여줘도 되지만, 리액트 쿼리를 이용한 캐싱 데이터를 사용해보고 싶었기 때문에 이번 프로젝트에서는 리액트 쿼리를 추가로 사용을 하였다. 

// 리액트 쿼리를 적용해 피트 데이터 가져오기

const CACHE_TIME = 1000 * 60 * 60; // 60m
const STALE_TIME = 1000 * 60 * 60; // 60m

  const { isLoading, error, data } = useQuery({
    queryKey: ["feedData"],
    queryFn: () =>
      axios
        .get(
          "https://raw.githubusercontent.com/klmhyeonwoo/klmhyeonwoo/main/data/feed.json"
        )
        .then((res) => {
          let feed: feedProps[] = [];

          Object.keys(res.data).forEach((i) => {
            feed.push({
              date: res.data[i].date,
              title: res.data[i].title,
              link: res.data[i].link,
              writer: res.data[i].writer,
            });
          });

          return feed;
        }),
    staleTime: STALE_TIME,
    cacheTime: CACHE_TIME,
  });
// 리액트 쿼리로 가져온 데이터를 다음과 같이 사용을 하고 있다.

{data &&
    data.length >= 1 &&
    data.map((item: feedProps) => {
      return (
        <>
          <Item
            title={item.title}
            link={item.link}
            date={item.date}
            writer={item.writer}
            key={item.link}
          />
        </>
  	);
})}

초기에는 피드 페이지에 접속을 할 때마다 데이터 페칭을 진행했지만 블로그를 계속해서 채팅처럼 실시간으로 업데이트를 진행하지는 않기 때문에, 그리고 Git Action 또한 크론 타임을 하루로 잡아놓았기 때문에 페이지에 접속할 때마다 데이터를 가져올 필요성을 느끼지 못했다. 그래서 사용자에게는 처음 페치한 데이터를 계속 보여주고 일정 시간에만 업데이트를 한 데이터를 제공해주면 된다.

위와 같이 데이터 페칭이 성공적으로 이루어졌고, 다시 피드 페이지에 접속을 하면 캐싱된 데이터를 이용해 데이터를 보여주는 것을 확인할 수 있다. 리액트 쿼리는 앞으로도 많이 공부하고 싶고, 많이 사용하고 싶은 흥미로운 라이브러리였다. 다양한 프로젝트를 진행하고 있기 때문에 리액트 쿼리의 장점을 활용할 수 있는 프로젝트라면 적극 도입을 하여 사용을 해보려고 한다.


레퍼런스

 

React-Query를 Next.js와 함께 사용해보자 Part 1.

React-query를 보다 더 잘 사용하기 위한 스터디 결과

blog.kmong.com

 

[React-Query] React-Query 개념잡기

React-Query

velog.io

 

React Query와 Axios로 서버통신 쉽게하기 🌐

서버통신한다고 fetch() 쓰고... .json() 파싱하고... 에러핸들링하고... useEffect로 성공 실패 분기처리하고...? 서버통신만 회피해오던 프론트엔드 개발자가 세상 편하게 서버연결하는 이야기 (feat. Axi

velog.io

 

댓글