SWR 더 깊이 톺아보기

soryeongk·2022년 1월 26일
21

이전에 작성한 SWR 튜토리얼(클릭은 작동 원리를 보고 가볍게 경험해보는 정도였습니다. 이번에는 그보다는 조금 더 깊이 톺아보려합니다.

1. SWR 니 뭔데

데이터를 가져오기 위한 React Hooks입니다. Next.js를 개발한 zeit 그룹에서 사용하는 라이브러리입니다. SWR은 Stale-While-Revalidate의 줄임말로 백그라운드에서 캐시를 재검증(revalidate)하는 동안에 기존의 캐시 데이터(stale)를 사용하여 화면을 그려줍니다. 도중에 에러를 반환하더라도 캐시된 데이터를 활용할 수 있게 함으로써 불필요한 데이터 호출과 렌더링에 시간을 쓰지 않고 효율적으로 동작합니다.

❓ TypeScript에서 사용이 가능한가요?
넵 공식문서에서 자랑스럽게 말하고 있습니당
SWR is friendly for apps written in TypeScript, with type safety out of the box.

1-1. 성능에 대하여

🎉 SWR provides critical functionality in all kinds of web apps, so performance is a top priority.
SWR은 모든 종류의 앱에서 중요한 기능을 제공하고 있으므로 성능은 가장 중요한 요소 중 하나입니다. useSWR hook의 성능은 여전히 중요합니다. 복잡한 앱의 경우 useSWR이 한 페이지에서 수백번 호출될 수도 있기 때문이쥬.

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)

깊은 비교 부분도 compare option으로 커스텀할 수 있으니 혹 필요하다면 알아두시길!

1-2. 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

2. 기본 사용

설치부터해볼까요오~

참고로 SWR 패키지는 핵심 useSWR API만 가져오는 경우 useSWRInfinite와 같이 사용되지 않는 API는 번들에 포함되어있지 않는 Tree Shaking이 됩니다 호호

여러가지로 똑똑한 칭구입니다.

yarn add swr

2-1. 구성

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하기 위한 함수입니다.

mutate?
동일한 key를 사용하여 다른 SWR hook에게 갱신 메시지를 전역으로 브로드캐스팅할 수 있게 해줍니다. 예를 들어, 유저가 로그아웃 버튼을 클릭할 때 로그인 정보를 자동으로 갱신하기 위해서는 다음과 같이 사용할 수 있습니다. 참고로 useSWRConfig는 아래에서 설명드리겠지만, SWR hook을 전역적으로 사용할 수 있도록 해줍니다.

import useSWR, { useSWRConfig } from 'swr'

function App () {
  const { mutate } = useSWRConfig()
  
  return (
    <div>
      <Profile />
      <button onClick={() => {
        // 쿠키를 만료된 것으로 설정
        document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'
        // 이 키로 모든 SWR에게 갱신하도록 요청
        mutate('/api/user')
      }}>
        Logout
      </button>
    </div>
  )
}

사용 순서는 다음과 같습니다.

  1. fetcher를 만듭니다. 여기서 url을 지정하고 앞으로 이 fetcher로 데이터를 불러오게 됩니다. SWR은 캐싱을 사용할 수 있도록 도와주는 도구라고 생각하면 됩니다.

    fetcher를 만들 때는 본인이 사용하는 그 어떤 방식이든(Fetch, Axios, GraphQL 등) 상관없습니다. 개인적으로 주로 사용하는 axios를 예시로 들겠습니다.

    import axios from 'axios';
    
    const fetcher = url => axios.get(url).then(res => res.data);
  2. 데이터를 요청할 때 사용할 key값을 fetcher와 함께 넘겨줍니다.

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

    key에는 API 명세서에 적힌 url 끝부분을 입력해주시면 됩니다. 가령 아래와 같은 형태가 될 수 있습니다.

    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>
    }

2-2. 옵션(짱많음)

API 옵션 - SWR

주로 갱신 시점이나 방식에 대한 이야기가 있는데, 우리 서비스에서 필요한 옵션들을 적절하게 사용하면 좋을 듯합니다!

2-3. 에러 및 재시도

공식문서 에러 처리 - SWR

useSWR에서 error를 받아 처리하는 것을 살펴보겠습니다.

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)
  }
})

에러 처리에 대한 내용은 전역적으로 관리할 수 있습니다.

3. 재사용 가능하게 바꾸기

서비스 내에서 같은 데이터를 fetch하는 일이 많을 수도 있는데, 그 때마다 같은 작업을 반복하는 것은 좋아보이지 않습니다. 중복을 제거하기 위해서 hook을 만들어 사용하는 방법을 보겠습니다.

3-1. useSWR로 훅 만들어서 사용하기

🎉 It is incredibly easy to create reusable data hooks on top of SWR:

유저의 정보를 받아오는 훅을 만들어보겠습니다.

// hook 정의
function useUser (id) {
  const { data, error } = useSWR(`/api/user/${id}`, fetcher)

  return {
    user: data,
    isLoading: !error && !data,
    isError: error
  }
}
// 사용법
function UserProfile({ id }) {
  const { user, isLoading, isError } = useUser(id)

  if (isLoading) return <Spinner />
  if (isError) return <Error />
  return <img src={user.avatar} />
}

뭐가 좋은데?

보통 우리가 사용하는 방식은 다음과 같습니다.

// 페이지 컴포넌트
// useEffect로 첫 렌더링에 정보를 가져와서
// 아직 정보가 없다면 Spinner라는 로딩 페이지를 보여주고
// 정보가 들어오면 그에 맞는 자식 컴포넌트들을 보여줍니다.

function Page () {
  const [user, setUser] = useState(null)

  // 데이터 가져오기
  useEffect(() => {
    fetch('/api/user')
      .then(res => res.json())
      .then(data => setUser(data))
  }, [])

  // 전역 로딩 상태
  if (!user) return <Spinner/>

  return <div>
    <Navbar user={user} />
    <Content user={user} />
  </div>
}

// 자식 컴포넌트
// 페이지 컴포넌트에서 받아온 내용들을 보여줍니다.

function Navbar ({ user }) {
  return <div>
    ...
    <Avatar user={user} />
  </div>
}

function Content ({ user }) {
  return <h1>Welcome back, {user.name}</h1>
}

function Avatar ({ user }) {
  return <img src={user.avatar} alt={user.name} />
}

최상위 부모 컴포넌트에서 가져온 데이터를 유지하고 트리 아래의 자식 컴포넌트들을 props로 넘겨줘야합니다. context를 사용해서 props 드릴링을 피할 수 있지만, 동적 콘텐츠 문제가 있을 수 있습니다. 페이지 콘텐츠 내 컴포넌트들은 동적일 수 있으며, 최상위 레벨 컴포넌트는 그 자식 컴포넌트가 필요로하는 데이터가 무엇인지 알 수 없을 수도 있습니다.

알아보기 힘듭니다. 그래서 SWR로 리팩토링을 해봅시다.

// 페이지 컴포넌트

function Page () {
  return <div>
    <Navbar />
    <Content />
  </div>
}

// 자식 컴포넌트

function Navbar () {
  return <div>
    ...
    <Avatar />
  </div>
}

function Content () {
  const { user, isLoading } = useUser()
  if (isLoading) return <Spinner />
  return <h1>Welcome back, {user.name}</h1>
}

function Avatar () {
  const { user, isLoading } = useUser()
  if (isLoading) return <Spinner />
  return <img src={user.avatar} alt={user.name} />
}

페이지에서 데이터를 받아와서 내려주는 형태가 아니라 각 컴포넌트들이 데이터를 받아올 수 있게 되었습니다. 이제 모든 부모 컴포넌트들은 데이터 전달과 관련해서 신경쓰지 않아도 됩니다. 그저 렌더링할 뿐입니다. 코드를 유지하기에 더 간단하고 쉽습니답

✨ The most beautiful thing is that there will be only 1 request sent to the API, because they use the same SWR key and the request is dedupedcached and shared automatically.
가장 아름다운 것은 이들이 동일한 SWR 키를 사용하며 그 요청이 자동으로 중복 제거캐시공유되므로, 단 한 번의 요청만 API로 전송된다는 것입니다.

3-2. 전역 설정하기

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 (
    <SWRConfig 
      value={{
        refreshInterval: 3000,
        fetcher: (resource, init) => fetch(resource, init).then(res => res.json())
      }}
    >
      <App />
    </SWRConfig>
  )
}

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

4. 자동 갱신

공식문서가 잘 나와있으니 꼭 함께 확인하기!

가장 중요한 것은 서비스의 특징과 상황에 맞추어 잘 켜고 끄고 하기!

공식문서 : 자동 갱신 - SWR

4-1. revalidate

캐시된 데이터가 아닌 새로운 request로 새로운 데이터를 불러오는 것을 말합니다. 원하는 시점에 맞추어 주기적으로 갱신할 수도 있습니다.

  • revalidateOnFocus: 페이지에 다시 포커스했을 때 갱신
  • refreshInterval: 다중 기기, 다중 사용자로 인해 탭이 변경될 때 최종적으로 동일한 데이터를 렌더링하게 해줍니다.
  • refreshWhenHiddenrefreshWhenOffline: 웹 페이지가 화면상에 있지 않거나 네트워크 연결이 없어도 데이터를 가져오게 합니다. 기본적으로는 비활성화되어 있습니다.
  • revalidateOnReconnect: 용자가 컴퓨터를 잠금 해제하고 동시에 인터넷이 아직 연결되지 않았을 때, 데이터를 항상 최신으로 보장하기 위해 네트워크가 회복될 때 자동으로 갱신하게 합니다. 기본적으로 비활성화되어 있습니다.
  • revalidateIfStale: 컴포넌트 마운트 시, stale data(이미 캐시된 데이터)가 존재할 경우 SWR이 갱신을 할지 말지를 제어할 수 있습니다. (boolean) 리소스가 불변하다면 이 부분을 false로 하면 됩니다.

4-2. mutation

useSWRConfig로부터 mutate 함수를 얻을 수 있으며, 이때의 mutate(key)를 호출해 동일한 키를 사용한 다른 SWR hook에게 갱신 메시지를 전역으로 브로드캐스팅할 수 있습니다.

대부분의 경우에 데이터에 로컬 뮤테이션을 적용하는 것은 변경을 더욱 빠르게 느낄 수 있게 해줍니다.

import useSWR, { useSWRConfig } from 'swr'

function App () {
  const { mutate } = useSWRConfig()

  return (
    <div>
      <Profile />
      <button onClick={() => {
        // 쿠키를 만료된 것으로 설정
        document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'

        // 이 키로 모든 SWR에게 갱신하도록 요청
        mutate('/api/user')
      }}>
        Logout
      </button>
    </div>
  )
}

미리 bound된 mutate를 통해 데이터를 갱신하는 경우, pre-bound된 mutate에서는 key값을 다시 전달주지 않아도 자동으로 이전 key값으로 데이터를 갱신해줍니다.

import useSWR from 'swr';

function Profile () {
  const { data, mutate } = useSWR('/api/user', fetcher);
  return (
    <div>
      <h1>My name is {data.name}.</h1>
      <button onClick={async () => {
        const newName = data.name.toUpperCase();
        // send a request to the API to update the data
        await requestUpdateUsername(newName);
        // update the local data immediately and revalidate (refetch)
        // NOTE: key is not required when using useSWR's mutate as it's pre-bound
        mutate({ ...data, name: newName });
      }}>Uppercase my name!</button>
    </div>
  )
}

5. pre-fetching

SWR을 위한 데이터 프리패칭 방법은 다양합니다. 최상위 요청에 대해서는 [rel="preload"](https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content)를 적극적으로 권장합니다. 아래의 코드를 HTML head 안에 넣기만 하면 되고, 쉽고 빠릅니다.

<link rel="preload" href="/api/data" as="fetch" crossorigin="anonymous">

prefetch 함수를 따로 정의해서 사용할 수도 있습니다. 예를 들어, 사용자가 어떤 링크를 호버링할 때 데이터를 프리로딩하고 싶다면 가장 직관적인 방법은 전역 mutate로 캐시를 다시 가져오고 설정하는 함수를 두는 것입니다.

import { mutate } from 'swr'

function prefetch () {
  mutate('/api/data', fetch('/api/data').then(res => res.json()))
  // 두 번째 파라미터는 Promise입니다
  // 프로미스가 이행될 때 SWR은 그 결과를 사용합니다
}

이미 존재하는 데이터를 SWR 캐시에 채우고 싶다면 아래와 같은 방법으로 pre-fill을 할 수도 있습니다. SWR가 데이터를 아직 가져오지 않았다면, 이 hook은 폴백으로 prefetchedData 를 반환할 것입니다. 아직 데이터가 도착하지 않았을 때 보여줄 내용을 미리 넣어두는 것입니다.

useSWR('/api/data', fetcher, { fallbackData: prefetchedData })

6. TS

기본적으로 SWR은 키에서 fetcher의 인수 유형도 유추하므로 기본 유형을 자동으로 가질 수 있습니다.

// `key` is inferred to be `string`
useSWR('/api/user', key => {})
useSWR(() => '/api/user', key => {})

// `key` will be inferred as { a: string; b: { c: string; d: number } }
useSWR({ a: '1', b: { c: '3', d: 2 } }, key => {})
useSWR(() => { a: '1', b: { c: '3', d: 2 } }, key => {})

// `arg0` will be inferred as string.  `arg1` will be inferred as number
useSWR(['user', 8], (arg0, arg1) => {})
useSWR(() => ['user', 8], (arg0, arg1) => {})

아래와 같이 명시적으로 지정하는 것도 가능합니다.

import useSWR, { Key, Fetcher } from 'swr'

const uid: Key = '<user_id>'
const fetcher: Fetcher<string, User> = (id) => getUserById(id)

const { data } = useSWR(uid, fetcher)
// `data` will be `User | undefined`.

데이터 유형을 지정하는 것은 쉽습니다. 기본적으로 fetcher의 반환 유형(준비되지 않은 상태에 대해서는 undefined)을 데이터 유형으로 사용하지만 매개변수로 전달할 수도 있습니다.

// 🔹 A. Use a typed fetcher:
// `getUser` is `(endpoint: string) => User`.
const { data } = useSWR('/api/user', getUser)

// 🔹 B. Specify the data type:
// `fetcher` is generally returning `any`.
const { data } = useSWR<User>('/api/user', fetcher)

옵션에 대해서 타입을 지정할 때는 아래와 같이 SWRConfiguration을 사용하면 됩니다.

import { useSWR } from 'swr'
import type { SWRConfiguration } from 'swr'

const config: SWRConfiguration = {
  fallbackData: "fallback",
  revalidateOnMount: false
  // ...
}

const { data } = useSWR<string[]>('/api/data', fetcher, config)

7. 고오급?까진 아니고

나중에 필요하면 찾아볼 수 있도록 모아두기!

조건부 가져오기 - SWR

인자 - SWR

페이지네이션 - SWR

있는지도 몰랐지만, useSWRPages는 더이상 사용하지 않는다고 합니다. 혹 검색하다가 나와도 사용은 지양해주세여

서스펜스 - SWR

캐시 - SWR


참고

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

SWR

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

0개의 댓글