SWR

swr (stale-while-revalidate)

  • 원격 데이터를 가져오기 위한 React Hooks 라이브러리
  • SWR은 캐시 데이터를 반환 한 다음 가져오기 요청을 전송하고 최신 데이터를 다시 제공
  • revalidateOnFocus, revalidateOnReconnect, refershInterval 등의 기능을 지원하여 프론트와, DB 데이터 간의 차이를 없애주는 다양 한 옵션들이 제공된다.

기본 사용법

const { data, error, isValidating, mutate } = useSWR(key, fetcher, options)
  • parameter
    • key: 요청에 대한 unique 한 값
    • fetcher: fetch 함수, promise를 return 하는 함수.
    • options : swr hook의 옵션 설정
  • return values
    • data
    • error
    • isValidating
    • mutate(data?, shouldRevalidate?) : 캐시 데이터 변경을 위한 함수

Global configuration

import useSWR, { SWRConfig } from 'swr' function Dashboard () { const { data: events } = useSWR('/api/events') const { data: projects } = useSWR('/api/projects') const { data: user } = useSWR('/api/user', { refreshInterval: 0 }) // override // ... } function App () { return ( <SWRConfig value={{ refreshInterval: 3000, fetcher: (resource, init) => fetch(resource, init).then(res => res.json()) }} > <Dashboard /> </SWRConfig> ) }

<SWRConfig>를 사용해서 global configuration을 할 수 있다. graphql을 사용할 경우 endpoint가 하나이기 때문에 fetcher를 global로 미리 등록을 해주어도 좋을 것 같다.

Error handling

fetcher 안에서 에러가 발생되면 hook에 의해서 error가 return 된다.

fetcher 안에서 status code에 따라서 에러를 custom 해서 throw 할 수도 있다.참고

onErrorRetry 옵션을 통해서 에러가 발생했을 때 retry를 조절할 수 있다.참고

Conditional fetching

useSWR의 첫번째 인자인 key에 falsy한 value가 주어지면 SWR이 request를 보내지 않는다. 함수나 조건부 삼항 연산자를 이용해서 fetching을 할 지 말지 결정할 수 있다.

또한 한 요청의 결과를 이용해 다음 요청을 보낸다면 serial fetching이 된다.

function MyProjects () { const { data: user } = useSWR('/api/user') const { data: projects } = useSWR(() => '/api/projects?uid=' + user.id) // When passing a function, SWR will use the return // value as `key`. If the function throws or returns // falsy, SWR will know that some dependencies are not // ready. In this case `user.id` throws when `user` // isn't loaded. if (!projects) return 'loading...' return 'You have ' + projects.length + ' projects' }

pattern

function useUser (id) {
  const { data, error } = useSWR(`/api/user/${id}`, fetcher)
  return {
    user: data,
    isLoading: !error && !data,
    isError: error
  }
}

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

SWR을 사용했을 때와 사용하지 않았을 때의 차이

사용하지 않았을 때

// page component
function Page () {
  const [user, setUser] = useState(null)
  // fetch data
  useEffect(() => {
    fetch('/api/user')
      .then(res => res.json())
      .then(data => setUser(data))
  }, [])
  // global loading state
  if (!user) return <Spinner/>
  return <div>
    <Navbar user={user} />
    <Content user={user} />
  </div>
}
// child components
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} />
}

사용했을 때

// page component
function Page () {
  return <div>
    <Navbar />
    <Content />
  </div>
}
// child components
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} />
}

부모 컴포넌트에서 자식 컴포넌트로 어떤 데이터를 전달해야하는지 알 필요가 없다. 또한 자동으로 캐시를 하고 공유하기 때문에 request를 한 번만 보낸다.

GraphQL

  • graphql-request 와 같은 libs와 같이 사용한다
import { request } from 'graphql-request'

const fetcher = query => request('/api/graphql', query)
function App () {
  const { data, error } = useSWR(
    `{
      Movie(title: "Inception") {
        releaseDate
        actors {
          name
        }
      }
    }`,
    fetcher
  )
  // ...
}

여러가지 인수를 넣는 방법

  • 배열로 여러가지 매개변수를 사용할 수 있다.
const {data} = useSWR(['/api/orders', user, ]fetcher);

object passing

만약 user 정보를 가지고 데이터를 fetching 해야하는 경우가 있다고 하면,

const { data: user } = useSWR(['/api/user', token], fetchWithToken) // ...and pass it as an argument to another query const { data: orders } = useSWR(user ? ['/api/orders', user] : null, fetchWithUser)

위와 같이 코드가 작성되는데, request의 key가 두 값의 combination이 된다. SWR은 render할 때마다 arguments를 얕은 비교를 하고 바뀌었으면 revalidation을 한다. 따라서 redering을 할 때마다 object를 다시 만드는 것은 좋지 않다.

// Don’t do this! Deps will be changed on every render. useSWR(['/api/user', { id }], query) // Instead, you should only pass “stable” values. useSWR(['/api/user', id], (url, id) => query(url, { id }))

Mutation

같은 key를 가진 SWR들에게 revalidation message를 mutate(key)를 통해서 revalidation 할 수 있다.

대부분의 경우에 mutate를 사용해 local data를 업데이트 한 다음에 revalidation을 하는 것이 더 속도가 빠르다.

import useSWR, { mutate } from 'swr' function Profile () { const { data } = useSWR('/api/user', fetcher) return ( <div> <h1>My name is {data.name}.</h1> <button onClick={async () => { const newName = data.name.toUpperCase() // update the local data immediately, but disable the revalidation mutate('/api/user', { ...data, name: newName }, false) // send a request to the API to update the source await requestUpdateUsername(newName) // trigger a revalidation (refetch) to make sure our local data is correct mutate('/api/user') }}>Uppercase my name!</button> </div> ) }

만약 api가 업데이트 된 결과를 반환하면 다음과 같이 코드를 줄일 수 있다.

mutate('/api/user', newUser, false) // use `false` to mutate without revalidation mutate('/api/user', updateUser(newUser)) // `updateUser` is a Promise of the request, // which returns the updated document

현재 데이터를 기반으로 mutate

mutate의 두 번째 인자로 async 함수를 넣으면 매개변수로 현재 캐시된 값을 받는다.참고

자동 재 검증

  • Revalidate on Focus : 현재 페이지에 focus 되면 데이터를 다시 받아온다
  • Revalidate on Interval : 정해진 시간 간격마다 데이터를 다시 받아온다.
    • refreshInterval 옵션을 사용해서 interval을 지정할 수 있다.
  • Revalidate on Reconnect : 다시 연결되었을 때 데이터를 다시 받아온다.

데이터 관리

  • mutate를 사용하여 로컬 데이터를 최신으로 업데이트 하는 동시에 유효성을 검사하고, 마지막으로 최신 데이터로 바꿀 수 있다.

  • mutate를 swr로 불러서 사용했을 경우, key 값을 넣어줘야한다

import {mutate} from 'swr';
...
mutate(key,{...data, name: newName})
  • useSWR에서 반환된 mutate에는 바인딩되어있어 key값이 필요하지 않다.
const {data, mutate} = useSWR(key, fetcher);
mutate({...date, name : newName}, true);

두번째 인자로 true 값을 주었을 때 먼저 newName를 캐시에 추가하고, DB와 비교해서 최신 값을 보여준다. false 로 주었을 때, revalidation을 하지 않는다.

data prefetch

  • serverSideProps로 데이터를 미리 받아와 props로 내려줬을 때, initalData로 설정할 수 있다
const { data: article, mutate: articleMutate } = useSWR( id ? `${URL}/${id}` : null, fetcher, { refreshInterval: 0, revalidateOnFocus: false, revalidateOnReconnect: false, initialData: JSON.parse(initalPostData), } );

성능

SWR은 캐싱과 deduplication을 통해서 불필요한 네트워크 비용을 줄인다.

deduplication

같은 키를 가진 useSWR hook을 여러 곳에서 쓰는 경우가 많다. 그런 경우 하나의 네트워크 요청만 가도록 한다.

data comparison

SWR은 data 변화를 깊은 비교를 한다. data가 변하지 않으면 리렌더링이 되지 않는다. 만약 비교 함수를 customize 하고 싶으면 compare 옵션을 사용할 수 있다.

dependency collection

useSWR은 3개의 state를 나타내는 값을 가진다. 그리고 이 세가지는 독립적으로 업데이트가 된다. 따라서 다음과 같은 코드가 있을 때 data, error, isValidating의 변화는 5단계가 될 수 있다.

코드

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

결과

// console.log(data, error, isValidating)
undefined undefined false // => hydration / initial render
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

그러면 component는 5번 리렌더링이 된다. 이 문제를 고치기 위해서 data만을 컴포넌트가 사용하도록 할 수 있다.

코드

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

결과

// console.log(data)
undefined // => hydration / initial render
Data      // => end retrying, get the data

참고 자료

재남님 useSWR 무한스크롤 blog
SWR 공식 문서
SWR use Hook

개인적 의견

  • SWR을 사용해보지는 않아 러닝커브가 있을수도 있지만, 데이터를 revalidation 해주고 상태관리의 많은 부분을 swr이 해준다고 느꼈다. apollo를 둘다 사용해봤으면 apollo를 사용하는 것이 더 빠르게 프로젝트를 진행할 수 있다고 생각했지만, 둘중 한명이라도 안사용해봤기 때문에 swr을 사용해 보는것도 좋을 것 같다.
tags: tech sharing