본문 바로가기

Web

[웹심화] SWR : 데이터를 가져오기 위한 React Hooks

Front-End에서 서버와 소통할 때 axios라는 HTTP 통신 라이브러리를 이용한다.

하지만 axios는 한번 데이터를 가져오면 다시 호출하지 않는 이상 이전의 데이터를 그대로 유지하기 때문에, setInterval함수 등을 통해

주기적으로 데이터를 업데이트 해줘야 한다.
여기서 api콜과 관련된 데이터들의 상태관리의 필요성이 생겼고, SWR / React-Query / apollo 등의 라이브러리가 등장하게 되었다.

 

1. SWR (SWR이란 데이터를 가져오기 위한 React Hooks)

SWR이 뭐냐..? 피클에서 리개선생님이 소개해줘서 사용했지만 아직까지도 정확한 동작원리를 설명할 수 없다는게 말이 안된다.

그래서 이번 기회에 SWR을 샅샅히 파헤쳐 보고 싶어서 이 주제를 선택하게 되었다~

 

SWR은 캐시로부터 데이터를 일단 반환하고, fetch요청을 보낸 후, 최신 데이터를 업데이트한다.

즉, 백그라운드에서 캐시를 revalidate하는 동안 기존에 캐시된 데이터를 사용하게 할 수 있는 라이브러리이다.
--> 백그라운드 요청에서 에러를 반환하더라도, 캐시된 데이터를 활용할 수 있기 때문에 데이터를 계속 호출하는데 시간을 들이지 않을 수 있고, UI를 더욱 빠르고 반응적이게 한다.

그리고 캐시된 데이터는 자동적으로 revalidate하여 지속적으로 업데이트를  할 수 있다.

 

SWR은 데이터 Fetching 즉 데이터를 가져오는데 특화되어있는 React Hooks라고 한다.

Post가 불가능하진 않지만, get에 더 특화되어있어 get이 아닌 동작에 있어서는 이점이 없다고 한다.

 

2. SWR의 장점 (공식문서)

  • 빠르고, 가볍고, 재사용 가능한 데이터 가져오기
  • 내장된 캐시 및 요청 중복 제거
  • 실시간 경험
  • 전송 및 프로토콜에 구애받지 않음
  • SSR / ISR / SSG support
  • TypeScript 사용가능

3. SWR의 단점

  • 데이터를 get하는 데에 특화되어있기 때문에 완벽하게 axios를 대체할 수 는 없다.
  • useSRW 자체가 useEffect 처럼 동작하기 때문에 useEffect 하위에서는 사용할 수 없다.

4. 시작하기

설치

yarn add swr
npm install swr

시작 - JSON데이터를 사용하는 일반적인 RESTful API라면 먼저 fetcher 함수를 생성한다.
네이티브 fetchf라면 fetcher 함수를, axios같은 라이브러리를 사용한다면 그냥 get하는 함수를 생성.

const fetcher = (...args) => fetch(...args).then(res => res.json())

//piickle
GET_SWR(path: string) {
    return real.get(path);
  }

사용 - useSWR을 임포트하고 함수 컴포넌트 내에서 사용하여 시작하면 된다. 

import useSWR from 'swr'

function Profile () {
  const { data, error } = useSWR('/api/user/123', fetcher)

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>

  // 데이터 렌더링
  return <div>hello {data.name}!</div>
}

이 예시에서, useSWR hook은 key문자열과 fetcher 함수를 받는다.

  • key : 데이터의 고유한 식별자 (일반적으로 API URL)
  • fetcher : 데이터를 반환하는 어떤한 비동기함수도 될 수 있다. key가 fetcher 함수로 전달된다.

hook은 요청의 상태에 기반한 data와 error 두개의 값을 반환한다. 반환값에 이상이 없다면 data를  반환하고, 거부되면 error을 반환.

재사용 - 위의 데이터 fetching함수를 재사용 가능하게 생성할 수 있다.

//생성
function useUser (id) {
  const { data, error } = useSWR(`/api/user/${id}`, fetcher)

  return {
    user: data,
    isLoading: !error && !data,
    isError: error
  }
}

//사용
function Avatar ({ id }) {
  const { user, isLoading, isError } = useUser(id)

  if (isLoading) return <Spinner />
  if (isError) return <Error />
  return <img src={user.avatar} />
}

//piickle

//생성
export default function useUserBookmarks() {
  const { data, error } = useSWR<PiickleSWRResponse<MyPiickle[]>>(`${PATH.USERS_}/bookmarks`, realReq.GET_SWR);

  return {
    userBookmarks: data?.data,
    isLoading: !error && !data,
  };
}


//사용
const { userBookmarks } = useUserBookmarks();
 {userBookmarks &&
          userBookmarks.data.map((myPiickle, idx: number) => (
            <MyPiickleItem key={myPiickle.cardId} cardId={myPiickle.cardId} content={myPiickle.content} idx={idx} />
          ))
  }

이제 어떤 컴포넌트에서든 useSWR을 호출하여 사용할 수 있다.

컴포넌트에서는 사용되는 데이터가 무엇인지 명시만 하면 되기  때문에, 코드가 더욱 선언적이 된다.

 

6. 옵션 (공식문서)

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

파라미터

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

반환 값

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

옵션

  • suspense = false: React Suspense 모드를 활성화
  • fetcher(args): fetcher 함수
  • revalidateIfStale = true: automatically revalidate even if there is stale data
  • revalidateOnMount: 컴포넌트가 마운트되었을 때 자동 갱신 활성화 또는 비활성화
  • revalidateOnFocus = true: 창이 포커싱되었을 때 자동 갱신
  • revalidateOnReconnect = true: 브라우저가 네트워크 연결을 다시 얻었을 때 자동으로 갱신(navigator.onLine을 통해)
  • refreshInterval:
    • 기본적으로는 비활성화: refreshInterval = 0
    • If set to a number, 인터벌 폴링
    • If set to a function, the function will receive the latest data and should return the interval in milliseconds
  • 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): 비논리적인 리렌더러를 회피하기 위해 반환된 데이터가 변경되었는지를 감지하는데 사용하는 비교 함수. 기본적으로 stable-hash을 사용합니다.
  • isPaused(): 갱신의 중지 여부를 감지하는 함수. true가 반환될 경우 가져온 데이터와 에러는 무시합니다. 기본적으로는 false를 반환합니다.
  • use: 미들웨어 함수의 배열

5. 데이터 가져오기

const { data, error } = useSWR(key, fetcher)

const fetcher = url => axios.get(url).then(res => res.data)

function App () {
  const { data, error } = useSWR('/api/data', fetcher)
  // ...
}

여기의 fetcher 은 SWR의 key를 받고 데이터를 반환하는 비동기 함수이다. 반환된 값은 data로 전달되고, 만약 throws라면 error로 잡힌다.

 

6. 에러처리

fetch 프로미스가 거부되면 error가 반환된다. 때로는 API상태코드와 함께 에러를 반환할 수도 있다.

const fetcher = async url => {
  const res = await fetch(url)

  if (!res.ok) {
    const error = new Error('An error occurred while fetching the data.')
    // 에러 객체에 부가 정보를 추가할 수 있다.
    error.info = await res.json()
    error.status = res.status
    throw error
  }

  return res.json()
}

7. 에러 재시도

SWR은 지수 백오프 알고리즘을 이용해 에러 시 요청을 재시도한다. 
(지수 백오프 알고리즘이란, 간단하게 지연 시간의 크기를 결정해주는 알고리즘이다. 지연 시간의 범위를 case마다 다르게 정해줌으로써 반복되게 충돌이 일어날 기회를 줄여준다.)

이 알고리즘을 사용하면 에러로부터 앱을 빠르게 복구할 수 있으며 너무 자주 재시도하여 리소스를 낭비하지도 않는다.

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)
  }
})

위 예시에서는 onErrorRetry라는 옵션을 사용해 에러 재시도 동작을 수행했다.

 

**조건부 가져오기

SWR은 null을 사용하거나 함수를 key로 전달하여 데이터를 조건부로 가져올 수 있다.

함수가 falsy 값을 던지거나 반환하면 SWR은 요청을 시작하지 않는다.

// 조건부 가져오기
const { data } = useSWR(shouldFetch ? '/api/data' : null, fetcher)

// ...또는 falsy 값 반환
const { data } = useSWR(() => shouldFetch ? '/api/data' : null, fetcher)

// ...또는 user.id가 정의되지 않았을 때 에러 throw
const { data } = useSWR(() => '/api/data?uid=' + user.id, fetcher)

 

8. 자동 갱신

  • 포커스 시에 갱신하기
    페이지에 다시 포커스하거나 탭을 전환할 때 SWR은 자동으로 데이터를 갱신할 수 있다.
    이 기능을 기본적으로 활성화되어 있고, revalidateOnFucus 옵션을 통해 비활성화할 수 있다.
    https://raw.githubusercontent.com/vercel/swr-site/master/.github/videos/focus-revalidate.mp4
  • 인터벌 시에 갱신하기
    SWR은 자동으로 데이터를 다시 fetching 하는 옵션을 제공한다.
    hook과 관련된 컴포넌트가 화면상에 있을 때만 다시 가져오기가 발생하기 때문에 화면을 보고 있지 않을 때 불필요하게 데이터가 갱신되는 일도 없다!
    refreshInterval 값을 설정하여 활성화할 수있다.
    https://raw.githubusercontent.com/vercel/swr-site/master/.github/videos/refetch-interval.mp4
  • 재연결 시에 활성화하기
    사용자가 온라인으로 돌아올 때 갱신하는 것 또한 가능하다. 이는 사용자가 컴퓨터를 잠금 헤제하고 동시에 인터넷이 아직 연결되지 않았을 때 발생하는 경우이다.
    데이터를 항상 최신으로 보장하기 위해 네트워크가 회복될 때 SWR은 자동으로 데이터를 갱신한다.
    이 기능을 기본적으로 활성화되어 있고, revalidateOnReconnect옵선을 통해 비활성화 할 수 있다.
  • 자동생신 비활성화하기
    모든 종류의 자동갱신은 비활성화 될 수 있다.

9. 수동갱신 - 뮤테이션 Mutation
여기가 바로바로 이해가 너무 안돼서 주함오빠가 아주 잠깐 원망스러왔던 부분이죠..그냥 그렇다고

 

SWR은 revalidate를 사용하여 계속해서 서버에 요청을 보내면서 데이터를 자동으로 갱신한다. 하지만 이 기능이 불필요할 때 mutate를 사용한다.

revalidate는 매번 서버로 요청을 다시 보내서 데이터를 갱신하는 반면, mutate는 서버에 요청을 보내지 않고 데이터를 수정한다는 점에서차이가 있다.
공시문서 예시 : 유저가 "로그아웃" 버튼을 클릭할 때 로그인정보 (<Profile/> 내부)를 자동으로 갱신하는 방법이다.

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>
  )
}

고냥 간단하게 mutate()안에 데이터의 갱신을 요청하고싶은 api url을 넣어주면 된다.

 

piickle 예시 : 투표 취소를 했을 때 해당 투표 결과 데이터를 갱신하는 코드
( 근데 여기서 왜 mutate를 썼는지 아직 잘 모름 ㄹㅈㄷ)

//선언
export default function useBallotTopic(ballotId: string) {
  const { data, error } = useSWR<PiickleSWRResponse<BallotTopicData>>(`${PATH.BALLOTS}/${ballotId}`, realReq.GET_SWR);
  const { mutate } = useSWRConfig();
//...

  return {
    ballotTopic: data?.data,
    isLoading: !error && !data,
    isBeforeVotingState,
    mutateBallotState: () => {
      setTimeout(() => mutate(`${PATH.BALLOTS}/${ballotId}`), 200);
      handlingVotingState();
    },
  };
}

//실행
export default function AfterVoteList(props: AfterVoteListProps) {
  const { ballotTopic, mutateBallotState } = props;

  const cancelVote = () => {
    if (!ballotTopic.userSelect) throw new Error("투표 데이터 에러");
    voteApi.postVote({ ballotTopicId: ballotTopic.ballotTopic._id, ballotItemId: ballotTopic.userSelect.ballotItemId });

    mutateBallotState();
  };
  //...
  <St.VoteBtnContainer>
    <St.VoteBtn onClick={cancelVote}>다시 투표하기</St.VoteBtn>
  </St.VoteBtnContainer>

5. 전역설정

SWRconfig 컨텍스트는 모든 SWR hook에 대한 전역설정(options)을 제공한다.

import useSWR, { SWRConfig } from 'swr'

function Dashboard () {
  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())
      }}
    >
      <Dashboard />
    </SWRConfig>
  )
}

위 예시에서는, <Dashboard>컴포넌트 안의 모든 SWR hook은 SWRConfig 안에 제공된 fetcher함수를 사용해 JSON 데이터를 로드하고 기본적으로 3초마다 갱신한다.

'Web' 카테고리의 다른 글

[웹심화] SSR 과 CSR (우리가 옳은 선택을 했나?)  (1) 2023.01.13
[웹심화] Next.js  (0) 2022.11.30
스크린 리더로 UX개선하기  (1) 2022.07.10