[React] SWR tutorial

soryeongk·2021년 6월 28일
6

JavaScript

목록 보기
4/7
post-thumbnail

아직 미흡하지만, 혼자서 공부한 튜토리얼을 이곳에 정리해봅니다.

SWR: React Hooks for Remote Data Fetching

The name “SWR” is derived from stale-while-revalidate, a HTTP cache invalidation strategy popularized by HTTP RFC 5861. SWR is a strategy to first return the data from cache (stale), then send the fetch request (revalidate), and finally come with the up-to-date data.

SWR 이라는 이름은 Stale-While-Revalidate의 줄임말로 HTTP 캐시 무효화 전략이며 HTTP RFC 5861(HTTP의 캐시 제어 확장)에 의해 동작합니다. SWR은 캐시로부터 데이터(stale)를 일단 반환하고, fetch 요청(revalidate)을 보낸 후, 최신 데이터를 업데이트합니다.

즉, 백그라운드에서 캐시를 재검증(revalidate)하는 동안에 기존에 캐시된 데이터(stale; 탁한, 지루한)를 사용하게 한다는 것입니다. 이는 에러를 반환하더라도 캐시된 데이터를 활용할 수 있게 함으로써 데이터를 계속 호출하는데 시간을 쓰지 않고, 캐시된 데이터를 이용해 효율적으로 동작합니다.

공신 문서의 부제로 적힌 React Hooks for Remote Data Fetching처럼 SWR은 Fetching에 특화된 Hook입니다. 그렇다고 Post가 불가한 것은 아니지만, 큰 이점도 없다고 합니다. 웹 개발을 공부하면서 처음 배운 axios는 필요할 때마다 api를 호출해야 데이터가 갱신되었습니다. 이와 달리 SWR은 포커싱을 다른 곳으로 옮겼다가 돌아오기만해도 자동으로 재검증을 통해 업데이트를 해준다는 장점이 있습니다. 물론, 설정에 따라 원하는 순간과 주기로 업데이트(revalidate)하는 것도 가능합니다.

또한 useSWR 자체로 useEffect처럼 동작하기 때문에 useEffect 하위에서는 사용할 수 없습니다. 아래 두 코드는 동일하게 작동합니다.

// useEffect
import React from 'react';
import { useState, useEffect } from 'react';
import { getData } from './lib/api';

const App = () => {
  const [data, setData] = useState(null);
  useEffect(() => {
    const data = getData();
    setData(data);
  });
  
  return <div>{data.name}</div>;
}

export default App;
//useSWR
import useSWR from 'swr';
import { fetch } from './lib/api';

const App = () => {
  const { data } = useSWR('api/data/', fetch);
  
  return <div>{data.name}</div>;
}

export default App;

기본 사용

본격적인 튜토리얼은 가장 마지막에 있습니다.
일단 SWR의 기본 구성은 다음과 같습니다.
다음 내용은 대부분 공식 홈페이지 내용을 번역 및 재구성하고, 일부 내용은 TwentyFiveSeven.log를 참조했습니다.


For normal RESTful APIs with JSON data, first you need to create a fetcher function, which is just a wrapper of the native fetch:

JSON 데이터가 포함된 일반 RESTfull API의 경우, 일단 기본 fetch wrapper인 fetcher함수를 만들어두어야합니다.

const fetcher = (...args) => fetch(...args).then(res => res.json());

이제 어느 컴포넌트에서든 useSWR을 호출하여 사용할 수 있습니다.

const { data, error } = useSWR({key}, fetcher);

useSWR을 사용하기 위해서는 keyfetcher가 필요합니다.
fetcher는 데이터를 fetch하는 도구입니다.
위에서 정의한 것처럼 native fetch를 사용해도 좋고, 비동기함수나 axios와 같은 도구를 사용할 수도 있습니다.
key는 데이터의 고유 식별자이며 fetcher로 전달될 인자로 일반적으로 API URL을 사용합니다.

useSWRdataerror를 반환합니다.
반환된 값에 이상이 없다면 그 내용을 던져 data에 담고, 그렇지 않다면, hook에 의해 error가 반환된다. error는 fetch 프로미스가 거부(rejected)될 때 정의된다.

Sometimes we want an API to return an error object alongside the status code. Both of them are useful for the client.
We can customize our fetcher to return more information. If the status code is not 2xx, we consider it an error even if it can be parsed as JSON.

API가 error를 반환할 때 상태코드와 오류 객체를 함께 반환되게 설정하고 싶을 수도 있습니다. 이를 위해 더 많은 정보를 반환하도록 fetcher를 커스터마이징할 수 있는데, 상태코드가 2xx(성공)가 아니면 JSON으로 구문 분석이 가능하더라도 오류로 간주하게 할 수 있습니다.

이상의 fetcher 코드를 다음과 같이 수정할 수 있습니다.

const fetcher = async url => {
  const res = await fetch(url)
  
  // 상태코드가 2xx가 아니라면 parse와 throw를 계속 시도합니다.
  if (!res.ok) {
    const error = new Error("[FAIL] fetch data");
    //error.info에는 오류 객체를 담습니다.
    error.info = await res.json();
    //error.status에는 상태코드를 담습니다.
    error.status = res.status
    throw error
  }
  
  return res.json();
}

SWR uses the exponential backoff algorithm to retry the request on error. The algorithm allows the app to recover from errors quickly, but not waste resources retrying too often.

오류가 발생하면 SWR은 exponential backoff algorithm을 사용해 다시 request를 보냅니다. 이 알고리즘은 앱에서 오류를 빠르게 복구할 수 있지만, 너무 자주 재시도하면 리소스가 낭비되오니 주의해야 합니다. 다음과 같이 onErrorRetry 옵션을 통해 오류 재시도를 제어할 수도 있습니다. 이상의useSWR 코드를 다음과 같이 수정하면 됩니다.

const { data, error } = useSWR('/api/user', fetcher, {
  onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
    // 상태코드 404 not Found에서는 더이상 시도하지 않습니다.
    if (error.status === 404) return

    // 특정 key에서 재시도를 하지 않습니다.
    if (key === '/api/user') return

    // 최대 10번까지만 시도합니다.
    if (retryCount >= 10) return

    // 5초의 간격을 두고 재시도합니다.
    setTimeout(() => revalidate({ retryCount }), 5000)
  }
})

This callback gives you the flexibility to retry based on various conditions. You can also disable it by setting shouldRetryOnError: false.
이 코드를 통해 조금 더 유연하게 재시도를 제어할 수 있고, shouldRetryOnError: false를 통해 재시도를 비활성화하는 것도 가능합니다.

튜토리얼

SWR 공식 문서Examples > Basic Usage의 내용을 재구성하였습니다.

폴더 구성

create-react-app {folder-name}으로 리액트 폴더를 생성합니다.
폴더의 구성은 다음과 같이 정리했습니다.

📦public
 ┗ 📜index.html
📦src
 ┣ 📂assets
 ┣ 📂components
 ┣ 📂libs
 ┃ ┣ 📜api.js
 ┃ ┗ 📜fetch.js
 ┣ 📂pages
 ┃ ┣ 📜Detail.js
 ┃ ┗ 📜Main.js
 ┃ ┗ 📜index.js
 ┣ 📜App.js
 ┗ 📜index.js

본 튜토리얼에서는 main만 구성하고, asset과 component가 존재하지 않지만, 추후 활용을 위해 폴더만 생성했습니다.

페이지 전환과 화면 출력 확인하기

일단 App.js와 pages의 내용들을 정리해 기본 화면을 구성합니다.


pages/Main.js

import React from "react";

const Main = () => {
  return <div>Main</div>;
};

export default Main;

detail.js도 같은 방식으로 작성합니다.

pages/index.js

export { default as Main } from "./Main";
export { default as Detail } from "./Detail";

pages의 index.js를 이렇게 정의해두면 pages의 내용들을 App.js에서 한 번에 호출할 수 있습니다.

App.js

page의 전환을 위해 터미널에 yarn add react-router-dom을 입력하여 react-router-dom을 설치합니다.

import React from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
//pages/index.js에서 한 번에 export했기 때문에
// import { Main } from "./pages";
// import { Detail } from "./pages";
// 로 하나씩 불러오지 않고 아래와 같이 호출할 수 있습니다.
import { Main, Detail } from "./pages";

const App = () => {
  return (
    <Router>
      <Switch>
        <Route exact path="/" component={() => <Main />} />
        <Route exact path="/detail" component={() => <Detail />} />
      </Switch>
    </Router>
  );
};

export default App;

yarn start로 앱을 실행하면 다음과 같은 화면이 나옵니다.

페이지 전환이 되는 것을 확인했다면, api를 작성합니다.

SWR을 사용한 api 작성하기

fetch.jsfetcher를 정의합니다.
그리고 api.js에서 useSWR을 사용한 GetData함수를 정의합니다. 이 때, fetch.js에서 fetcher를 불러와 데이터를 반환합니다.
이때 useSWR의 key값은 공식 문서에서 사용한 API URL을 사용했습니다.

코드 작성 전, 터미널에 yarn add swr를 입력합니다.

코드는 기본 구성에서 작성한 것과 동일합니다.


lib/fetch.js

export const fetcher = async (url) => {
  const res = await fetch(url);

  // 상태코드가 2xx가 아니라면 parse와 throw를 계속 시도합니다.
  if (!res.ok) {
    const error = new Error("[FAIL] fetch data");
    //error.info에는 오류 객체를 담습니다.
    error.info = await res.json();
    //error.status에는 상태코드를 담습니다.
    error.status = res.status;
    throw error;
  }

  return res.json();
};

lib/api.js

import useSWR from "swr";
import { fetcher } from "./fetch";

export const GetData = () => {
  const { data, error } = useSWR(
    "https://api.github.com/repos/vercel/swr",
    fetcher,
    {
      onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
        // 상태코드가 404라면 더이상 시도하지 않습니다.
        if (error.status === 404) return;
        // 최대 10번까지만 시도합니다.
        if (retryCount >= 10) return;
        // 5초에 한 번 재검증합니다.
        setTimeout(() => revalidate({ retryCount }), 5000);
      },
    }
  );

  if (error) return "[FAIL] get data";
  if (!data) return "[FAIL] no data";
  return data;
};

아직 api를 호출한 곳이 없기 때문에 아무런 변화가 없습니다.

두근두근 api 호출하여 data 확인하기

pages/Main.js에서 api를 불러와 확인해보겠습니다.


pages/Main.js

import React from "react";
import { GetData } from "../lib/api";

const Main = () => {
  const data = GetData();
  console.log(data);
  return <div>Main</div>;
};

export default Main;

lib/api.js에서 정의한 GetData 함수를 불러와 data에 담고, 그 내용을 일단 콘솔로 찍어봅니다.

{id: 218115303, node_id: "MDEwOlJlcG9zaXRvcnkyMTgxMTUzMDM=", name: "swr", full_name: "vercel/swr", private: false,}
archive_url: "https://api.github.com/repos/vercel/swr/{archive_format}{/ref}"
archived: false
assignees_url: "https://api.github.com/repos/vercel/swr/assignees{/user}"
...
deployments_url: "https://api.github.com/repos/vercel/swr/deployments"
description: "React Hooks library for remote data fetching"
disabled: false
downloads_url: "https://api.github.com/repos/vercel/swr/downloads"
events_url: "https://api.github.com/repos/vercel/swr/events"
fork: false
forks: 611
forks_count: 611
forks_url: "https://api.github.com/repos/vercel/swr/forks"
full_name: "vercel/swr"
...
stargazers_count: 17654
stargazers_url: "https://api.github.com/repos/vercel/swr/stargazers"
statuses_url: "https://api.github.com/repos/vercel/swr/statuses/{sha}"
subscribers_count: 141
subscribers_url: "https://api.github.com/repos/vercel/swr/subscribers"
subscription_url: "https://api.github.com/repos/vercel/swr/subscription"
...}

이 중에서 몇 가지만 불러와 화면에 표시합니다.


pages/Main.js

import React from "react";
import { GetData } from "../lib/api";

const Main = () => {
  const data = GetData();
  // console.log(data);
  return (
    <div>
      <h5>id : {data.id}</h5>
      <p>{data.description}</p>
      <strong>👀 {data.subscribers_count}</strong>{" "}
      <strong>{data.stargazers_count}</strong>{" "}
      <strong>🍴 {data.forks_count}</strong>
    </div>
  );
};

export default Main;

최종 화면 확인하기

데이터가 잘 불러와졌다면 다음과 같은 화면이 나타납니다.

얏호!
SWR로 포스팅하는 것도 시간나면 공부해야게쑵니다.

참조

profile
웹 프론트엔드 개발자 령이의 어쩌구 저쩌구

1개의 댓글

comment-user-thumbnail
2021년 9월 3일

굿굿입니다!

답글 달기