이전에 작성한 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.
🎉 SWR provides critical functionality in all kinds of web apps, so performance is a top priority.
SWR은 모든 종류의 앱에서 중요한 기능을 제공하고 있으므로 성능은 가장 중요한 요소 중 하나입니다. useSWR hook의 성능은 여전히 중요합니다. 복잡한 앱의 경우 useSWR이 한 페이지에서 수백번 호출될 수도 있기 때문이쥬.
SWR의 빌트인 캐싱과 중복제거는 불필요한 네트워크 요청을 줄여줍니다. SWR이 보장하는 것 3가지는 아래와 같습니다.
또한, 아래처럼 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으로 커스텀할 수 있으니 혹 필요하다면 알아두시길!
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
설치부터해볼까요오~
참고로 SWR 패키지는 핵심 useSWR API만 가져오는 경우 useSWRInfinite와 같이 사용되지 않는 API는 번들에 포함되어있지 않는 Tree Shaking이 됩니다 호호
여러가지로 똑똑한 칭구입니다.
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하기 위한 함수입니다.
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>
)
}
사용 순서는 다음과 같습니다.
fetcher를 만듭니다. 여기서 url을 지정하고 앞으로 이 fetcher로 데이터를 불러오게 됩니다. SWR은 캐싱을 사용할 수 있도록 도와주는 도구라고 생각하면 됩니다.
fetcher를 만들 때는 본인이 사용하는 그 어떤 방식이든(Fetch, Axios, GraphQL 등) 상관없습니다. 개인적으로 주로 사용하는 axios를 예시로 들겠습니다.
import axios from 'axios';
const fetcher = url => axios.get(url).then(res => res.data);
데이터를 요청할 때 사용할 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>
}
주로 갱신 시점이나 방식에 대한 이야기가 있는데, 우리 서비스에서 필요한 옵션들을 적절하게 사용하면 좋을 듯합니다!
공식문서 에러 처리 - 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)
}
})
에러 처리에 대한 내용은 전역적으로 관리할 수 있습니다.
서비스 내에서 같은 데이터를 fetch하는 일이 많을 수도 있는데, 그 때마다 같은 작업을 반복하는 것은 좋아보이지 않습니다. 중복을 제거하기 위해서 hook을 만들어 사용하는 방법을 보겠습니다.
🎉 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 deduped, cached and shared automatically.
가장 아름다운 것은 이들이 동일한 SWR 키를 사용하며 그 요청이 자동으로 중복 제거, 캐시, 공유되므로, 단 한 번의 요청만 API로 전송된다는 것입니다.
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초마다 데이터를 갱신하게 됩니다.
공식문서가 잘 나와있으니 꼭 함께 확인하기!
가장 중요한 것은 서비스의 특징과 상황에 맞추어 잘 켜고 끄고 하기!
공식문서 : 자동 갱신 - SWR
캐시된 데이터가 아닌 새로운 request로 새로운 데이터를 불러오는 것을 말합니다. 원하는 시점에 맞추어 주기적으로 갱신할 수도 있습니다.
revalidateOnFocus
: 페이지에 다시 포커스했을 때 갱신refreshInterval
: 다중 기기, 다중 사용자로 인해 탭이 변경될 때 최종적으로 동일한 데이터를 렌더링하게 해줍니다.refreshWhenHidden
, refreshWhenOffline
: 웹 페이지가 화면상에 있지 않거나 네트워크 연결이 없어도 데이터를 가져오게 합니다. 기본적으로는 비활성화되어 있습니다.revalidateOnReconnect
: 용자가 컴퓨터를 잠금 해제하고 동시에 인터넷이 아직 연결되지 않았을 때, 데이터를 항상 최신으로 보장하기 위해 네트워크가 회복될 때 자동으로 갱신하게 합니다. 기본적으로 비활성화되어 있습니다.revalidateIfStale
: 컴포넌트 마운트 시, stale data(이미 캐시된 데이터)가 존재할 경우 SWR이 갱신을 할지 말지를 제어할 수 있습니다. (boolean) 리소스가 불변하다면 이 부분을 false로 하면 됩니다.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>
)
}
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 })
기본적으로 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)
나중에 필요하면 찾아볼 수 있도록 모아두기!
있는지도 몰랐지만, useSWRPages는 더이상 사용하지 않는다고 합니다. 혹 검색하다가 나와도 사용은 지양해주세여