SWR은 vercel에서 제작한 React Hooks로, 먼저 캐시(stale)로부터 데이터를 반환한 후, fetch 요청(revalidate)을 하고, 최종적으로 최신화된 데이터를 가져오는 전략이다. 이름은 HTTP 캐시 무효 전략인 stale-while-revalidate
에서 유래되었다.
전체적인 기능은 React Query와 비슷하며 React Query 쪽의 커뮤니티가 더 크다. 따라서 사용하기 전에 장단을 잘 비교해보고 둘 중 하나를 선택하는 것이 좋을 것 같다.
예시
1
2
3
4
5
6
7
8
9
import useSWR from 'swr'
function Profile() {
const { data, error, isLoading } = useSWR('/api/user', fetcher)
if (error) return <div>failed to load</div>
if (isLoading) return <div>loading...</div>
return <div>hello {data.name}!</div>
}
- useSWR hook은
key
와fetcher
함수를 받는다.key
는 고유한 식별자이며,fetcher
로 전달된다.fetcher
는 데이터를 반환하는 어떠한 비동기 함수도 될 수 있다.
- useSWR은
data
와error
,isLoading
을 반환한다.
SWR이 해결하는 문제
전통적으로 리액트에서 비동기적인 데이터 로딩을 처리하는 방식은 다음과 같다.
- 최상위 레벨 컴포넌트에서
useEffect
를 사용해 데이터를 받아온다. - props를 통해 자식 컴포넌트에 전달한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 페이지 컴포넌트
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로 추가해야한다. 페이지에 더 많은 데이터 의존성을 추가한다면 코드는 점점 더 유지보수가 어려워진다.
React Context나 global state를 사용하여 props 전달을 막을 수 있지만, 동적콘텐츠 문제가 여전히 존재한다. 페이지 콘텐츠 내 컴포넌트들은 동적일 수 있으며 최상위 레벨 컴포넌트는 자식 컴포넌트가 필요로 하는 데이터가 뭔지 정확히 알 수 없다.
SWR은 이 문제를 해결한다. SWR에서 위 코드는 다음과 같이 리팩토링 할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function useUser () {
const { data, error, isLoading } = useSWR(`/api/user`, fetcher)
return {
user: data,
isLoading,
isError: error
}
}
// 페이지 컴포넌트
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} />
}
데이터는 이제 데이터가 필요한 컴포넌트로 범위가 제한되었으며 모든 컴포넌트는 서로에게 독립적이다. 모든 부모 컴포넌트들은 데이터나 데이터 전달에 관련된 것들을 알 필요가 없다. 그냥 렌더링만 담당한다.
또한 동일한 SWR 키를 사용한다면, 요청은 중복 제거, 캐시, 공유되므로 단 한번의 요청만 API로 전송되어 네트워크 비용의 낭비도 없다.
또한 애플리케이션은 이제 사용자 포커스나 네트워크 재연결 시에 데이터를 갱신할 수 있다. 브라우저 탭을 전환할 때 자동으로 데이터가 갱신된다는 것을 의미한다.
기능
SWR의 기능에는 다음과 같은 것이 있다고 한다. 다 다루기에는 너무 많기에 이 글에서는 개인적으로 흥미있는 것들만 정리하였다.
- 빠르고, 가볍고, 재사용 가능한 데이터 가져오기
- 내장된 캐시 및 요청 중복 제거
- 실시간 경험
- 전송 및 프로토콜에 구애받지 않음
- SSR / ISR / SSG support
- TypeScript 준비
React Native
- 빠른 페이지 네비게이션
- 인터벌 폴링
- 데이터 의존성
- 포커스시 재검증
- 네트워크 회복시 재검증
- 로컬 뮤테이션(Optimistic UI)
- 스마트한 에러 재시도
- 페이지 및 스크롤 위치 복구
- React Suspense
프리패칭
최상위 레벨 페이지 데이터
` rel=”preload”를 권장함. 아래와 같이 html 코드를 작성 후,
<head>`에 넣기만 하면 됨.
1
<link rel="preload" href="/api/data" as="fetch" crossorigin="anonymous">
rel="preload"
는 페이지 요청 시 해당 소스 자원을 우선적으로 로드하라는 뜻이고, 따라서 JS가 다운로드 되기 전에 데이러를 프리패칭한다. 동일한 URL로 fetch 요청 결과는 재사용된다.
프로그래밍 방식으로 프리패치
SWR은 preload
API를 제공하여 자원을 프리패치하고 캐시 안에 저장할 수 있게 해준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function App({ userId }) {
const [show, setShow] = useState(false)
// preload in effects
useEffect(() => {
preload('/api/user?id=' + userId, fetcher)
}, [userId])
return (
<div>
<button
onClick={() => setShow(true)}
{/* preload in event callbacks */}
onHover={() => preload('/api/user?id=' + userId, fetcher)}
>
Show User
</button>
{show ? <User /> : null}
</div>
)
}
next.js 페이지 프리패칭과 함께 다음 페이지와 데이터 모두를 즉시 로드할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import useSWR, { preload } from 'swr'
// should call before rendering
preload('/api/user', fetcher);
preload('/api/movies', fetcher);
const Page = () => {
// useSWR hook이 렌더링을 지연시키지만, "/api/user", "/api/movies"는 이미 프리로딩을 시작하고 있고,
// 따라서 waterfall 문제는 발생하지 않는다.
const { data: user } = useSWR('/api/user', fetcher, { suspense: true });
const { data: movies } = useSWR('/api/movies', fetcher, { suspense: true });
return (
<div>
<User user={user} />
<Movies movies={movies} />
</div>
);
}
데이터 프리필
이미 존재하는 데이터를 SWR 캐시에 미리 채우길 원한다면, fallbackData
옵션을 사용할 수 있다.
1
useSWR('/api/data', fetcher, { fallbackData: prefetchedData })
SWR가 데이터를 아직 가져오지 않았다면, 폴백으로 prefetchedData
를 반환할 것이다.
<SWRConfig>
및 fallback
옵션을 사용하여 모든 SWR hooks 및 다중 키에 대해서도 이것을 구성할 수 있다.
Next.js SSG 및 SSR
기본값으로 프리렌더링하기
페이지가 프리렌더링 되어야하면, Next.js는 2가지 형태의 프리렌더링을 지원한다.
- 정적생성 (SSG)
- 서버 사이드 렌더링 (SSR)
SWR와 함께 SEO를 위해 페이지를 프리렌더링할 수 있고, 캐싱, 재검증, 포커스 추적, 클라이언트 사이드에서 간격을 두고 다시 가져오기와 같은 기능도 있다.
모든 SWR hooks에 초기값으로 프리패칭된 데이터를 넘겨주기 위해 SWRConfig의 fallback
옵션을 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export async function getStaticProps () {
// `getStaticProps`는 서버 사이드에서 실행됩니다.
const article = await getArticleFromAPI()
return {
props: {
fallback: {
'/api/article': article
}
}
}
}
function Article() {
// `data`는 `fallback`에 있기 때문에 항상 사용할 수 있습니다.
const { data } = useSWR('/api/article', fetcher)
return <h1>{data.title}</h1>
}
export default function Page({ fallback }) {
// `SWRConfig` 경계 내부에 있는 SWR hooks는 해당 값들을 사용합니다.
return (
<SWRConfig value=>
<Article />
</SWRConfig>
)
}
Complex Keys
useSWR
은 array
나 function
타입을 key로 사용할 수 있다. 이 타입의 키를 이용해 미리 패치된 데이터를 사용하기 위해선 fallback
key들을 unstable_serialize
와 함께 직렬화 해야한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import useSWR, { unstable_serialize } from 'swr'
export async function getStaticProps () {
const article = await getArticleFromAPI(1)
return {
props: {
fallback: {
// unstable_serialize()에 배열 스타일의 키
[unstable_serialize(['api', 'article', 1])]: article,
}
}
}
}
function Article() {
// 배열 스타일의 키 사용
const { data } = useSWR(['api', 'article', 1], fetcher)
return <h1>{data.title}</h1>
}
export default function Page({ fallback }) {
return (
<SWRConfig value=>
<Article />
</SWRConfig>
)
}
Suspense
리액트의 Suspense
API와 함께 사용할 수도 있다. suspense
옵션을 활성화하면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Suspense } from 'react'
import useSWR from 'swr'
function Profile () {
const { data } = useSWR('/api/user', fetcher, { suspense: true })
return <div>hello, {data.name}</div>
}
function App () {
return (
<Suspense fallback={<div>loading...</div>}>
<Profile/>
</Suspense>
)
}
서스펜스 모드에서 data
는 항상 응답을 가져와서 undefined
를 검사할 필요가 없다. 하지만 에러가 발생할 경우 ErrorBoundary를 사용해 캐치해야한다.
conditional fetch와 함께 사용하기
일반적으로 suspense
를 활성화하면 렌더링 시에 data는 undefined
를 갖지 않는다.
하지만 conditional fetch나 의존적 fetch와 함께 사용하면 data는 요청이 일시 중단된 경우 undefined
가 된다.
1
2
3
4
5
6
function Profile () {
const { data } = useSWR(isReady ? '/api/user' : null, fetcher, { suspense: true })
// `isReady`가 false이면 `data`는 `undefined`입니다
// ...
}
Server-Side Rendering
suspense
를 SSR에서 사용하려면 fallbackData나 fallback을 통해 초기 데이터를 반드시 넣어줘야한다. 이것은 <Suspense>
를 서버사이드에서 데이터를 가져오는데 사용할 수 없고, 클라이언트사이드나 getStaticProps
등 프레임워크에서 제공하는 data fetching 메소드에서만 가져올 수 있다는 말이다.
Middleware
SWR hook의 전후에 로직을 실행할 수 있도록 해줌. 여러 미들웨어가 존재하면 각 미들웨어는 다음 미들웨어를 감쌈. 마지막 미들웨어가 원본 SWR hook useSWR
을 받는다.
API
1
2
3
4
5
6
7
8
9
10
11
function myMiddleware (useSWRNext) {
return (key, fetcher, config) => {
// hook을 실행하기 전...
// 다음 미들웨어를 처리하거나, 마지막인 경우 `useSWR` hook을 처리합니다.
const swr = useSWRNext(key, fetcher, config)
// hook을 실행한 후...
return swr
}
}
1
2
3
4
5
<SWRConfig value=>
// 또는...
useSWR(key, fetcher, { use: [myMiddleware] })
확장하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Bar () {
useSWR(key, fetcher, { use: [c] })
// ...
}
// useSWR(key, fetcher, { use: [a, b, c] }) 와 같음.
function Foo() {
return (
<SWRConfig value=>
<SWRConfig value=>
<Bar/>
</SWRConfig>
</SWRConfig>
)
}
사용 예제 : 이전 결과 유지하기
useSWR
의 키가 변경되었더라도 새로운 데이터가 로드되기 전까지는 여전히 이전 결과를 반환받길 원할 수 있다.
이는 useRef
와 함께 사용하여 지연 미들웨어로 구축할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { useRef, useEffect, useCallback } from 'react'
// 키가 변경되더라도 데이터를 유지하기 위한 SWR 미들웨어입니다.
function laggy(useSWRNext) {
return (key, fetcher, config) => {
// 이전에 반환된 데이터를 저장하기 위해 ref를 사용합니다.
const laggyDataRef = useRef()
// 실제 SWR hook.
const swr = useSWRNext(key, fetcher, config)
useEffect(() => {
// 데이터가 undefined가 아니면 ref를 업데이트합니다.
if (swr.data !== undefined) {
laggyDataRef.current = swr.data
}
}, [swr.data])
// 지연 데이터가 존재할 경우 이를 제거하기 위한 메서드를 노출합니다.
const resetLaggy = useCallback(() => {
laggyDataRef.current = undefined
}, [])
// 현재 데이터가 undefined인 경우에 이전 데이터로 폴백
const dataOrLaggyData = swr.data === undefined ? laggyDataRef.current : swr.data
// 이전 데이터를 보여주고 있나요?
const isLagging = swr.data === undefined && laggyDataRef.current !== undefined
// `isLagging` 필드 또한 SWR에 추가합니다.
return Object.assign({}, swr, {
data: dataOrLaggyData,
isLagging,
resetLaggy,
})
}
}
데이터 상태
useSWR
이 반환하는 데이터 중 두가지가 현재 데이터의 상태를 알려줌
isLoading
: 처음 또는 key 변경으로 데이터를 로딩하고 있을 때 trueisValidading
: 데이터를 로딩하거나, revalidaing 할 때 true