🖊️

SWR

Tags
React
hook
state manage
Date
2021/04/13
간만에 재미난 장난감을 발견한 기분이 들게하는 라이브러리가 생겨서 일단 자세한 내용보다는 간단하게 살펴본 정도로 정리를 할까 한다.

개요

Next.js를 개발한 zeit 그룹에서 사용하는 라이브러리
SWR - React hook library for data fetching
The name “SWR” is derived from stale-while-revalidate, a HTTP cache invalidation strategy popularized by HTTP RFC 5861.
사용하면 아래의 항목들을 얻을 수 있다고 함.
JAM stack oriented
Fastlightweight and reusable data fetching
Built-in cache and request deduplication
Real-time experience
Transport and protocol agnostic
TypeScript ready
React Native
Fast page navigation
Polling on interval
Data dependency
Revalidation on focus
Revalidation on network recovery
Local mutation (Optimistic UI)
Smart error retry
Pagination and scroll position recovery
React Suspense
...

About SWR

import useSWR from 'swr' function Profile() { const { data, error } = useSWR('/api/user', fetcher) if (error) return <div>failed to load</div> if (!data) return <div>loading...</div> return <div>hello {data.name}!</div> }
JavaScript

Traditionally

이전까지는 일반적으로 parent 컴포넌트에서 data fetch 를 실행한 후 child 컴포넌트에 props 로 전달하는 형태로 로직이 구성된다 (만약 전역상태를 관리한다면 fetch 만 top level 에서 하고 child 컴포넌트에서는 주로 사용하는 형태.)
하지만 SWR 에서는 사용하는 곳에서 바로 data fetch 를 실행함과 동시에 로컬 스테이트를 정의할 수 있다.
function useUser (id) { const { data, error } = useSWR(`/api/user/${id}`, fetcher) return { user: data, isLoading: !error && !data, isError: error } } 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} /> }
JavaScript
결과적으로 데이터와 해당 데이터를 필요로하는 컴포넌트는 결합되고 각 컴포넌트는 더 독립적으로 된다. top level 컴포넌트(그리고 parent 도)는 데이터에 대해서, 그리고 해당 데이터의 state 에 대해서 알 필요가 없어진다.
그리고 코드도 훨씬 깔끔해보이기까지... 너무 좋다.
그리고 무엇보다 좋은 것은 위 코드에서 단 한번의 API request 만 있을 것이라는 것이다.

어떻게 한번만 request 한다는거지?

useSWR 의 첫번째 인자인 key 값을 기준으로 중복 요청을 제거하고, 캐시를 하며, 해당 값을 마치 전역 상태관리처럼 share 까지 해준다!

data fetching

useSWR 의 두번째 인자인 fetcher 은 SWR 내부적으로 구현되어있는 것이 아니라 사용자가 직접 정의하는 것이다. 때문에 graphql, axios 던 문제 없이 사용 가능하다. (심지어 로컬 변수도?)
examples

Revalidate

캐싱된 값이 아니라 새로 request 를 하는 시기.
manually
mutate 함수를 사용면 된다.
examples
automatic (react-native 는 어떻게 동작할까?)
해당 페이지를 re-focus 할 때 (비활성화도 가능)
refetch interval
useSWR('/api/todos', fetcher, { refreshInterval: 1000 })
JavaScript
reconnect 인터넷이 offline ⇒ online 으로 바뀌었을 때, 혹은 컴퓨터가 잠금에서 해제되었을 때 등.

SWR 이 해결하는 문제

Front-end 의 반복적인 작업

원격 서버의 상태를 UI로 표현하는 일. 이 일을 위해서 로컬 스토어의 상태를 정의하고, 각 화면에서 상태에 따라 어떻게 반응(react)할지 정의한다.
그리고 적절한 시점에 data fetch 를 해서 로컬 스토어 상태를 초기화하고, 사용자 액션을 통해서 원격 데이터 상태를 추가, 수정 삭제를 한다. 그리고 원격 상태가 변경됨에 따라서 로컬 상태도 변경해야한다.
데이터의 추가/수정/삭제가 발생할 때마다 원격의 상태와 로컬의 상태를 동기화시키는 일.
SWR이 해결하고자 하는 문제의 핵심은 원격상태와 로컬상태를 하나로 통합하는데 있다.

데이터 갱신(create, update) 후에는?

mutate 함수를 사용
import useSWR from 'swr' function UserInfo(){ const {data, error, mutate} = useSWR('/api/users', url => { return fetch(url).then(res => res.json()) }) const handleChange = async (user) => { await updateUser(user) mutate() } return <div>~생략~</div> }
JavaScript
mutate 함수가 호출되면 해당 상태를 즉시 다시 fetch 한 후 데이터를 갱신함.

비효율적이지 않나?

서버의 상태 갱신 후 해당 데이터 정보를 다시 fetch 하는 것이 비효율적으로 느껴질 수 있다. (어차피 로컬에서 어떻게 갱신되었는지 아니까). 그럴 땐 mutate 함수를 사용할 때 fetch 하지 않고 로컬의 캐시 상태를 갱신하는 것도 가능하다.
const handleChange = async (user) => { await updateUser(user) mutate(user, false) // 첫번재 인자로 갱신할 데이터, 두번째 인자로 데이터 fetch 여부를 인자로 받습니다. }
JavaScript

with Typescript

쌉가능
import fetch from '/libs/fetch' ... const { data } = useSWR<{ forks_count: number stargazers_count: number watchers: number }>('/api/data?id=' + id, fetch);
JavaScript

Performance

Deduplication

에서 설명했듯이 key 를 기준으로 중복된 요청을 하지 않도록한다. dedupingInterval 옵션도 제공한다. (default 로 2초가 설정되어 있다.)

Deep Comparison

SWR 에서는 기본적으로 deep comparison 을 통해서 데이터가 변경되었는지 확인한다. 만약 비교해서 변경이 없다면 re-render 는 trigger 되지 않는다.
compare option 도 커스터마이즈 가능

Dependency Collection

useSWR 은 3가지 stateful 한 값들을 반환한다. (data, error, isValidating) 이 세 값들은 각각 독립적으로 update 됨.
function App () { const { data, error, isValidating } = useSWR('/api', fetcher) console.log(data, error, isValidating) return null }
JavaScript
만약 위와 같이 구현되어 있다면 최악의 경우 5번의 랜더링이 실행된다.
// 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
JavaScript
만약 위 코드를 data 만 사용하는 것으로 바꾼다면
function App () { const { data } = useSWR('/api', fetcher) console.log(data) return null }
JavaScript
// console.log(data) undefined // => hydration / initial render Data // => end retrying, get the data
JavaScript
mobx 처럼 컴포넌트 내에서 사용되는 변수가 변경되면 re-rendering 이 일어나는 것 같다.

상태 관리?

SWR 이 상태관리 툴로 사용이 가능한 것은 바로 컴포넌트간 전역 상태 공유가 가능하다는 특성 때문.

기존 상태 관리 절차

1.
로컬의 상태를 정의한다
2.
컴포넌트에서는 스토어 상태에 따른 렌더링을 정의
3.
적절한 시점에 데이터를 fetch하여 로컬 스토어를 초기화
그럼 자연스럽게 해당 데이터들이 컴포넌트에서 정의한대로 화면에 뿌려짐

SWR

위 3가지 과정을 하나로 통합할 수 있음
// usePoints.js import useSWR from 'swr' export default () => { const {data, error} = useSWR('/api/points', url => { return fetch(url).then(res => res.json()) }) return {data, error} }
JavaScript

without data fetch

import React from "react"; import useSWR from "swr"; function useCounter() { const { data, mutate } = useSWR("state", () => window.count); // 위에서 data 를 사용하지 않는다고 지우면 데이터가 실시간으로 UI 에 반영이 안됨. return { data: window.count || 0, mutate: (count) => { window.count = count; mutate(); } }; } export default function Counter() { const { data, mutate } = useCounter(); const handleInc = () => mutate(data + 1); const handleDec = () => mutate(data - 1); return ( <div> <span>count: {data}</span> <button onClick={handleInc}>inc</button> <button onClick={handleDec}>dec</button> </div> ); }
JavaScript

결론

편한가?

개꿀이다. 일반적으로 요청에 따라오는 부가적인 코드 (로딩, 에러 핸들링 등...)를 더 편하고 깔끔하게 사용할 수 있다.
redux 를 사용하지 않고 mobx 를 사용하는 큰 이유도 바로 redux 에 딸린 그 많은 과정들이 너무 부담스럽게 느껴져서였는데, 마찬가지로 data fetching 에 딸린 일련의 과정을 쉽고 이해하기 쉽게 해결할 수 있을 것 같아서 매우 좋은 것 같다.
만약 상태 관리 툴로써 사용한다면 mobx 를 사용하면서 들었던 약간의 죄악감 (pure 하지 않은 상태관리랄까)도 씻어낼 수 있지 않을까....

상태관리툴로써 적합한가

사실 만들어진 목적이 애초에 상태관리로써의 목적은 아니기 때문에 적합한지는 모르겠으나 간단하게 상태관리가 가능하다는 측면에서는 매력적이다. 개인적으로는 만약 도입한다면 전면적으로 도입해보는 것도 나쁘지 않은 선택이라고 생각이 든다.

힛플 팀에서 사용할 수 있는가?

일단 최근 hook 을 사용하기 시작한 시점에 맞춰 적합한 솔루션으로 기대된다. 그러나 legacy 인 Mobx-state-tree 를 대체하는데 공수가 꽤 들 것으로 생각이 된다. 그리고 아직 SWR 를 제대로 사용해보지 않아서 러닝 커브 및 실제 사용했을 때 문제가 있긴 하겠지만 충분히 매력적으로 다가온다.

그럼에도 무서운 점은

아무런 설정 없이 사용하면 무서운 일이 벌어질 것 같다. 만약 한 페이지 안에서 많은 request 를 포함한 swr 을 사용하고 있고, 갑자기 page focus 를 통해서 한꺼번에 해당 요청이 실행된다거나... 뭐 그런 일이 일어나면 어떻게 해야하는가. 괜찮을까? 싶은 느낌. 그러나 역시 써봐야 아는 일이다. 일단 한번만 request 를 한다고 나와있으니 뭐.... 괜찮지 않을까?

후기

이 글을 쓰고 몇 달이 지나서 되돌아본다. 결국에 SWR 을 사용하지 않고 현재 Apollo Client 를 사용하고 있다. 두 라이브러리 모두 비슷한 점이 많기도 하고 현재 Apollo graphql 을 사용하고 있는 현재 구성으로서 더 적합하다고 판단했기 때문이다.
상대적으로 Apollo client 가 러닝 커브가 더 높게 느껴지기는 했다. (문서를 보지 않으면 이해가 되지 않는 동작들이 좀 있다.) 하지만 어느정도 러닝 커브를 극복하고 나니까 편하게 사용하고 있다. 굳이 비교하자면 SWR 은 코드를 실행하는 곳에서 더 선언적으로 코드를 작성하고 Apollo Client 는 config 를 통해서 미리 정의된 동작을 하도록 한달까?
무튼 써보니 좋다.

참고