FrontEnd/React.js

[React.js] SWR(stale-while-revalidate)에 대하여

푸고배 2022. 2. 16. 18:24

SWR이란 무엇인가?

"SWR"이라는 이름은 HTTP RFC 5861에 의해 알려진 HTTP 캐시 무효 전략인 stale-while-revalidate에서 유래되었다. SWR은 먼저 캐시(stale)로부터 데이터를 반환한 후, fetch 요청(revalidate)을 하고, 최종적으로 최신화된 데이터를 가져오는 전략이다.

 

왜 사용하는 가?

최상위 레벨 컴포넌트에서 useEffect를 사용해 데이터를 가져오고, props를 이용해 자식 컴포넌트에 전달하는 방법으로 서버의 데이터를 로컬 상태변수로 사용이 가능하다.

// 페이지 컴포넌트
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} />
}

하지만, 보통 최상위 레벨 컴포넌트에서 가져온 모든 데이터를 유지하고, 데이터의 전달을 위해서 하위 컴포넌트에 값을 전달하는 과정에서 서버로부터 가져오는 데이터의 수가 많아진다면 코드는 점점 유지하기가 힘들어진다.

 

context나 여러 상태관리 라이브러리를 이용해 전역 상태를 이용한다면 props 전달을 피할 수 있지만 API 호출을 처리하는 비동기 호출의 문제가 여전히 존재한다. 즉, 페이지 콘텐츠 내 컴포넌트 들은 동적일 수 있으며, 최상위 레벨 컴포넌트는 그 자식 컴포넌트가 필요로 하는 데이터가 무엇인지 알 수 없는 경우가 발생한다.

 

SWR는 위 문제를 해결하기 위해 등장했으며, SWR hook를 사용해 위의 코드를 아래와 같이 리팩토링할 수 있다.

// 페이지 컴포넌트
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을 사용하면, 로컬 상태변수를 원격 상태와 연결된 데이터 스트림으로써 바라볼 수 있도록 데이터 fetching 단계를 추상화한다. 어떻게 원격 서버의 상태를 실시간 데이터 스트림으로 얻을 수 있을까?

 

SWR이 내부적으로 지속적인 데이터 폴링을 하고 있기 때문이다.

이러한 SWR의 특징 덕분에 아래와 같은 기능을 간편하게 이용할 수 있다.

1.  내장 캐싱과 중복 제거 기능

아래의 코드는 버튼을 클릭할 때마다, 어떤 한 API /path/getData로 부터 새로운 데이터를 받아와 data라는 state 변수에 저장한다.

const App = () => {
  const [data, setData] = useState(null);
  const onClickHandler = async () => {
    const response = await axios.get('/path/getData');
    if (response.status === 200) {
      setData(response.data);
    }
  };
  return <button onClick={() => onClickHandler}/>
}

 

이 /path/getData가 자주 업데이트 되지 않는 값이라면? 또 누군가가 button을 여러번 클릭한다면?

굳이 data를 업데이트하지 않아도 될 상황에서 업데이트가 발생하고, 하위 컴포넌트가 리렌더링될 것이다.

(물론 useMemo를 사용해 데이터가 변경 여부를 판단하여 렌더링 최적화를 해줄 수 있다.)

 

또한, 위의 코드에서는 로컬 상태변수(data)에 원격 상태(response.data)를 업데이트 해주는 과정이 필요하다. 

 

function useUser () {
  return useSWR('/api/user', fetcher)
}

function Avatar () {
  const { data, error } = useUser()

  if (error) return <Error />
  if (!data) return <Spinner />

  return <img src={data.avatar_url} />
}

function App () {
  return <>
    <Avatar />
    <Avatar />
    <Avatar />
    <Avatar />
    <Avatar />
  </>
}

위의 코드에서는 네트워크 요청은 단 한번만 발생한다. useSWR은 첫 번째 인자로 key(보통은 요청 url을 key 값으로 가짐)를 받는데, 동일 키의 중복 호출 시 중복을 제거하는 과정을 처리하기 때문이다. 

 

2. 포커스 시에 갱신 & 네트워크 재연결 시에 자동 데이터 갱신

탭 전환으로 포커싱 시, 16:21 =&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt; 16:22 기준으로 데이터 자동 업데이트

 

페이지에 다시 포커스하거나 탭을 전환할 때, SWR은 자동으로 데이터를 갱신한다.

최신 상태로 즉시 동기화 시킬 수 있어서 유용하며, 오래된 모바일 탭 또는 슬립 모드 상태인 노트북과 같은 상태에서 데이터를 새로 고치는데 유용하다. revalidateOnFocus 옵션을 통해 비활성화 할 수 있으며, 기본값은 활성화(true) 상태이다.

 

또한, 네트워크가 오프라인에서 온라인으로 돌아올 때 데이터 갱신이 가능하다. 사용자가 컴퓨터를 해제하고 인터넷이 아직 연결되지 않았을 때를 예로 들 수 있다. 데이터를 항상 최신으로 보장하기 위해 네트워크가 회복될 때 SWR는 자동으로 갱신된다. 이 기능은 기본적으로 활성화되어 있으며, revalidateOnReconnect 옵션을 이용해 비활성화 할 수 있다.

 

3. 실시간 페이지 업데이트

refreshInterval을 이용한 실시간 버스 위치 update

SWR은 데이터를 다시 가져오는 폴링을 옵션으로 제공해, 페이지 실시간 업데이트를 구현하기 편하다.

단, hook과 관련된 컴포넌트가 화면 상에 있을 때만 다시 가져옴으로써 불필요한 호출(fetch)이 발생하지 않게 한다.

refreshInterval 옵션을 통해 활성화 할 수 있다. (값 ms[밀리세컨즈]마다 새로 갱신)

useSWR('/api/todos', fetcher, {refreshInterval:1000}); // 1초마다

 

SWR로 상태관리가 가능할까?

SWR은 상태관리용 라이브러리가 아닌 데이터 가져오기(fetch)용 라이브러리이다.

하지만, useSWR을 모듈화 하여 독립된 함수로 처리하면, 여러 컴포넌트들에서 필요한 상태를 가져와 사용할 수가 있으며,

이러한 컴포넌트간 전역 상태를 공유할 수 있다는 특성 덕분에 기존 상태관리 라이브러리를 대체할 수 있다.

 

setState와 같이 변경된 데이터를 업데이트하기 위해서는, mutate 함수를 이용하면 된다.

mutate함수가 호출되면 해당 상태를 즉시 fetch하고 데이터를 갱신한다.

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)
    return mutate() // mutate() 는 프라미스!
  }  

  return <div>~생략~</div>
}

 

mutate함수를 사용할 때 데이터 fetch 없이 로컬에 캐시되어있는 상태만 제어하기 위해서는 두 번째 인자로 데이터 fetch 여부를 전달해주면된다.(즉, 두 번째 인자로 false 전달)

 

SWR API Option

onst { data, error, isValidating, mutate } = useSWR(key, fetcher, options)

Parameter

  • key: 요청을 위한 고유한 키 문자열(또는 함수 / 배열 / null) (고급 사용법)
  • fetcher: (옵션) 데이터를 가져오기 위한 함수를 반환하는 Promise (상세내용)
  • options: (옵션) SWR hook을 위한 옵션 객체

return value

  • data: fetcher가 이행한 주어진 키에 대한 데이터(로드되지 않았다면 undefined)
  • error: fetcher가 던진 에러(또는 undefined)
  • isValidating: 요청이나 갱신 로딩의 여부
  • mutate(data?, shouldRevalidate?): 캐시 된 데이터를 뮤테이트하기 위한 함수 (상세내용)

Options

  • suspense = false: React Suspense 모드를 활성화 (상세내용)
  • fetcher(args): fetcher 함수
  • revalidateIfStale = true: stale 데이터가 존재할 경우 마운트시에 자동으로 갱신 (상세내용)
  • revalidateOnMount: 컴포넌트가 마운트되었을 때 자동 갱신 활성화 또는 비활성화
  • revalidateOnFocus = true: 창이 포커싱되었을 때 자동 갱신 (상세내용)
  • revalidateOnReconnect = true: 브라우저가 네트워크 연결을 다시 얻었을 때 자동으로 갱신(navigator.onLine을 통해) (상세내용)
  • refreshInterval (상세내용):
    • 기본적으로는 비활성화: refreshInterval = 0
    • 숫자 설정 시, 인터벌 폴링
    • 함수 설정 시, 함수는 최신 데이터를 수신하고 간격을 밀리초 단위로 반환
  • refreshWhenHidden = false: 창이 보이지 않을 때 폴링(refreshInterval이 활성화된 경우)
  • refreshWhenOffline = false: 브라우저가 오프라인일 때 폴링(navigator.onLine에 의해 결정됨)
  • shouldRetryOnError = true: fetcher에 에러가 있을 때 재시도
  • dedupingInterval = 2000: 이 시간 범위내에 동일 키를 사용하는 요청 중복 제거
  • focusThrottleInterval = 5000: 이 시간 범위 동안 단 한 번만 갱신
  • loadingTimeout = 3000: onLoadingSlow 이벤트를 트리거 하기 위한 타임아웃
  • errorRetryInterval = 5000: 에러 재시도 인터벌
  • errorRetryCount: 최대 에러 재시도 수
  • fallback: 다중 폴백 데이터의 키-값 객체 (예시)
  • fallbackData: 반환될 초기 데이터(노트: hook 별로 존재)
  • onLoadingSlow(key, config): 요청을 로드하는 데 너무 오래 걸리는 경우의 콜백 함수(loadingTimeout을 보세요)
  • onSuccess(data, key, config): 요청이 성공적으로 종료되었을 경우의 콜백 함수
  • onError(err, key, config): 요청이 에러를 반환했을 경우의 콜백 함수
  • onErrorRetry(err, key, config, revalidate, revalidateOps): 에러 재시도 핸들러
  • compare(a, b): 비논리적인 리렌더러를 회피하기 위해 반환된 데이터가 변경되었는지를 감지하는데 사용하는 비교 함수. 기본적으로 dequal을 사용
  • isPaused(): 갱신의 중지 여부를 감지하는 함수. true가 반환될 경우 가져온 데이터와 에러는 무시합니다. 기본적으로는 false를 반환
  • use: 미들웨어 함수의 배열 (상세내용) 
  • 느린 네트워크(2G, <= 70Kbps)에서는, 기본적으로 errorRetryInterval이 10초이며, loadingTimeout은 5초

 


Reference

 

Redux 를 넘어 SWR 로(2)

앞서 첫번째 글에서는 Redux 를 사용할 때 일반적으로 경험할 수 있는 몇가지 문제들을 언급하였습니다. 이번 글에서는 SWR 은 무엇이고 SWR 이 어떻게 Redux 를 대체할 수 있는 지에 대해 설명해 보

min9nim.vercel.app

 

데이터 가져오기를 위한 React Hooks – SWR

데이터 가져오기를 위한 React Hooks Suspense Pagination

swr.vercel.app

 

반응형