Group Study (2021-2022)/React(Deep)

[웹FE 심화 스터디] 5주차 - react-query / SWR

숙명권은지 2022. 6. 28. 11:29

React-query 란?

react-query는 리액트 애플리케이션에서 서버 상태 가져오기캐싱동기화 및 업데이트를 보다 쉽게 다룰 수 있도록 도와주며 클라이언트 상태와 서버 상태를 명확히 구분하기 위해서 만들어진 라이브러리

react-query 상태

✅ fresh : 새롭게 추가된 쿼리 & 만료되지 않은 쿼리 ➜ 컴포넌트가 마운트, 업데이트되어도 데이터 재요청 ❌

✅ fetching : 요청 중인 쿼리

✅ stale : 만료된 쿼리 ➜ 컴포넌트가 마운트, 업데이트되면 데이터 재요청 ⭕️

✅ inactive : 비활성화된 쿼리 ➜ 특정 시간이 지나면 가비지 컬렉터에 의해 제거

 

react-query 장점

  • 캐싱
  • get을 한 데이터에 대해 update를 하면 자동으로 get을 다시 수행한다. (예를 들면 게시판의 글을 가져왔을 때 게시판의 글을 생성하면 게시판 글을 get하는 api를 자동으로 실행 )
  • 데이터가 오래 되었다고 판단되면 다시 get (invalidateQueries)
  • 동일 데이터 여러번 요청하면 한번만 요청한다. (옵션에 따라 중복 호출 허용 시간 조절 가능)
  • 무한 스크롤 (Infinite Queries (opens new window))
  • 비동기 과정을 선언적으로 관리할 수 있다.
  • react hook과 사용하는 구조가 비슷하다.

 

react-query 사용 방법

npm i react-query // npm 사용
or
yarn add react-query // yarn 사용

App컴포넌트 최상단에 추가한다.

import { QueryClient, QueryClientProvider } from "react-query";

 

useQuery

GET요청과 같은 CREATE작업을 할 때 사용되는 훅

const requestData = useQuery(쿼리 키, 쿼리 함수, 옵션);

✅ 쿼리 키 : 문자열 or 배열, 캐싱 처리에 있어서 중요한 개념

✅ 쿼리 함수: Promise를 리턴하는 함수, ex) axios(), fetch()

✅ 옵션 : useQuery 기능을 제어

❗️중요❗️ 쿼리 키가 다르면 호출하는 API가 같더라도 캐싱을 별도로 관리한다.

  • return 값은 api의 성공, 실패여부, api return 값을 포함한 객체
  • useQuery는 비동기로 작동 즉, 한 컴포넌트에 여러 개의 useQuery가 있다면 하나가 끝나고 다음 useQuery가 실행되는 것이 아닌 두 개의 useQuery가 동시에 실행
function Todos() {
  const { status, data, error } = useQuery("todos", fetchTodoList);

  if (status === "loading") {
    return <span>Loading...</span>;
  }

  if (status === "error") {
    return <span>Error: {error.message}</span>;
  }

  return (
    <ul>
      {data.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

 

useQueries

useQuery를 비동기로 여러 개 실행할 경우 promise.all처럼 useQuery를 하나로 묶는 방법

const result = useQueries([
  {
    queryKey: ["getRune", riot.version],
    queryFn: () => api.getRunInfo(riot.version)
  },
  {
    queryKey: ["getSpell", riot.version],
    queryFn: () => api.getSpellInfo(riot.version)
  }
]);

useEffect(() => {
  console.log(result); // [{rune 정보, data: [], isSucces: true ...}, {spell 정보, data: [], isSucces: true ...}]
  const loadingFinishAll = result.some(result => result.isLoading);
  console.log(loadingFinishAll); // loadingFinishAll이 false이면 최종 완료
}, [result]);

 

QueryCache

쿼리에 대해 성공, 실패 전처리를 할 수 있음

const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error, query) => {
      console.log(error, query);
      if (query.state.data !== undefined) {
        toast.error(`에러가 났어요!!: ${error.message}`);
      },
    },
    onSuccess: data => {
      console.log(data)
    }
  })
});

 

useMutation

POST, PUT, DELETE와 같은 변경 및 수정 작업을 할 때 사용되는 훅

const requestData = useMutation(API 호출 함수, 콜백);

✅ API 호출 함수: 특정 endpoint로 요청을 보내고 Promise를 반환하는 함수

✅ 콜백 : 라이프사이클에 따라 로직 작성

  • useQuery와 같은 반환값을 받으며 mutate 메서드가 추가됨 mutate 메서드를 이용하면 API 요청 함수를 호출하여 요청이 이루어짐
  • 값을 바꿀때 사용하는 api
import { useState, useContext, useEffect } from "react";
import loginApi from "api";
import { useMutation } from "react-query";

const Index = () => {
  const [id, setId] = useState("");
  const [password, setPassword] = useState("");

  const loginMutation = useMutation(loginApi, {
    onMutate: variable => {
      console.log("onMutate", variable);
      // variable : {loginId: 'xxx', password; 'xxx'}
    },
    onError: (error, variable, context) => {
      // error
    },
    onSuccess: (data, variables, context) => {
      console.log("success", data, variables, context);
    },
    onSettled: () => {
      console.log("end");
    }
  });

  const handleSubmit = () => {
    loginMutation.mutate({ loginId: id, password });
  };

  return (
    <div>
      {loginMutation.isSuccess ? "success" : "pending"}
      {loginMutation.isError ? "error" : "pending"}
      <input type="text" value={id} onChange={e => setId(e.target.value)} />
      <input
        type="password"
        value={password}
        onChange={e => setPassword(e.target.value)}
      />
      <button onClick={handleSubmit}>로그인</button>
    </div>
  );
};

export default Index;

 

SWR

데이터를 가져오기 위한 React Hooks

Stale-While-Revalidate의 줄임말로 백그라운드에서 캐시를 재검증(revalidate)하는 동안에 기존의 캐시 데이터(stale)를 사용하여 화면을 그려줌

도중에 에러를 반환하더라도 캐시된 데이터를 활용할 수 있게 함으로써 불필요한 데이터 호출과 렌더링에 시간을 쓰지 않고 효율적으로 동작

SWR의 빌트인 캐싱과 중복제거는 불필요한 네트워크 요청을 줄여줌

 

SWR이 보장하는 것 3가지

  • no unnecessary requests: 불필요한 요청
  • no unnecessary re-renders: 불필요한 리렌더링
  • no unnecessary code imported: 불필요한 code imported

useSWR을 호출하는 여러 개의 컴포넌트가 있다고 가정하고 SWR로 데이터를 가져오는 useUser가 전역적으로 존재한다고 가정할 때

// App.tsx
function App() {
  return <>
    <Component />
    <Component />
    <Component />
    <Component />
  </>;
}
// Component.tsx
function Component() {
  const { data, error } = useUser()

  if (error) return <Error />if (!data) return <Spinner />return <img src={data.avatar_url} />}

모든 Component는 매번 useUser를 사용하는 것처럼 보이지만, 사실 네트워크 요청은 1번만 일어남

기본적으로 데이터 변경 사항을 비교하고, 변경이 없다면 다시 렌더링되지 않도록 함(Deep Comparison)

 

Dependency Collection

useSWR은 data, error, isValidating 3개의 stateful 값이 있습니다. 각각은 독립적으로 업데이트됨

사용은 아래와 같음

function App() {
  const { data, error, isValidating } = useSWR('/api/blah', fetcher);
  console.log(data, error, isValidating);
  return null
}

코드에서 만약 데이터가 처음에는 fail하고 두 번째에 successful이 되었다면, 콘솔에는 다음 4줄이 찍힘 즉 4번의 렌더링이 일어나는 것

undefined undefined true  // => start fetching
undefined Error false     // => end fetching, got an error
undefined Error true      // => start retrying
Data undefined false      // => end retrying, get the data

불필요한 리렌더링 방지를 위해서 만약 data가 변경되었을 때만 렌더링을 하고 싶다면 다음과 같이 dependency를 조정해주면 됨

function App() {
  const { data } = useSWR('/api/blah', fetcher);
  console.log(data);
  return null
}

이렇게 하면 data가 변경되었을 때만 렌더링이 일어나게 되어, 위에서 처럼 처음에 fail하고 두 번째에 successful이 된다면 콘솔에는 다음 2줄이 찍힘

undefined // => hydration / initial render
Data      // => end retrying, get the data

 

사용 방법

yarn add swr 을 이용하여 다운로드

 

useSWR의 기본 구성

const { data, error, isValidating, mutate } = useSWR(key, fetcher, options)

✅ key: unique한 key값으로 string 또는 function, array, null 등이 올 수 있음

✅ fetcher: data를 fetch하여 넘어온 Promise를 말함

✅ options: SWR hook의 옵션


✅ data : fetcher에 의해 반환된 data

✅ error: fetcher에서 던진 오류입니다. 성공 시에는 undefined가 들어옴

✅ isValidating : 만약 요청이 있거나 로딩 중인 경우에 반환 ( boolean)

✅ mutate(data? shouldRevalidate?) : 캐시된 데이터를 mutate하기 위한 함수

 

사용 순서

  1. fetcher를 만듬 여기서 url을 지정하고 앞으로 이 fetcher로 데이터를 불러오게 됨
    fetcher를 만들 때는 자신이 사용하는 그 어떤 방식이든(Fetch, Axios, GraphQL 등) 상관없음
    import axios from 'axios'; const fetcher = url => axios.get(url).then(res => res.data);
import axios from 'axios'; const fetcher = url => axios.get(url).then(res => res.data);

2. 데이터를 요청할 때 사용할 key값을 fetcher와 함께 넘겨줌
key에는 API 명세서에 적힌 url 끝 부분을 입력해줌

const { data, error, isValidating, mutate } = useSWR(key, fetcher, options)
import useSWR from 'swr'

function Profile () {
  const { data, error } = useSWR('/api/blah', fetcher)

  if (error) return <div>failed to load</div>if (!data) return <div>loading...</div>// render data
  return <div>hello {data.name}!</div>}

 

에러 및 재시도

SWR은 Exponential backoff 알고리즘으로 에러 시 request를 다시 보냄

이 알고리즘 덕분에 너무 잦은 재시도를 줄이고 리소스를 낭비하지 않을 수 있게 됨

useSWR('/api/user', fetcher, {
  onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
    // 404에서 재시도 안함
    if (error.status === 404) return

    // 특정 키에 대해 재시도 안함
    if (key === '/api/user') return

    // 10번까지만 재시도함
    if (retryCount >= 10) return

    // 5초 후에 재시도
    setTimeout(() => revalidate({ retryCount }), 5000)
  }
})

에러 처리에 대한 내용은 전역적으로 관리할 수 있음

 

전역 설정하기

SWRConfig를 통해 모든 SWR hook에 대한 전역 설정이 가능함

기본적인 형태

<SWRConfig value={options}>
  <Component/>
</SWRConfig>

SWRConfig의 value로 모든 SWR에 적용할 options를 넘겨주면 전역적으로 사용이 가능

공식문서의 예시

import useSWR, { SWRConfig } from 'swr'

function App() {
  const { data: events } = useSWR('/api/events')
  const { data: projects } = useSWR('/api/projects')
  const { data: user } = useSWR('/api/user', { refreshInterval: 0 }) // 오버라이드

  // ...
}

function App () {
  return (
    <SWRConfigvalue={{
        refreshInterval: 3000,
        fetcher: (resource, init) => fetch(resource, init).then(res => res.json())
      }}>
      <App />
    </SWRConfig>)
}

SWRConfig로 감싸진 하위 컴포넌트들에서 사용되는 SWR은 3초마다 데이터를 갱신하게 됨

 


 

과제 : useSWR로 유저의 정보를 받아오는 훅 만들어서 데이터를 받아와서 내려주는 형태가 아니라 각 컴포넌트들이 데이터를 받아올 수 있게 만들기(?) → fake api 사용

JSONPlaceholder