Posts SWR
Post
Cancel

SWR

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은 keyfetcher 함수를 받는다.
    • key는 고유한 식별자이며, fetcher로 전달된다.
    • fetcher는 데이터를 반환하는 어떠한 비동기 함수도 될 수 있다.
  • useSWR은 dataerror, isLoading을 반환한다.

SWR이 해결하는 문제

전통적으로 리액트에서 비동기적인 데이터 로딩을 처리하는 방식은 다음과 같다.

  1. 최상위 레벨 컴포넌트에서 useEffect를 사용해 데이터를 받아온다.
  2. 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에 초기값으로 프리패칭된 데이터를 넘겨주기 위해 SWRConfigfallback 옵션을 사용할 수 있다.

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

useSWRarrayfunction 타입을 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 변경으로 데이터를 로딩하고 있을 때 true
  • isValidading : 데이터를 로딩하거나, revalidaing 할 때 true
This post is licensed under CC BY 4.0 by the author.

next.js Compiler

module federation

Comments powered by Disqus.