728x90
리액트 데이터 패칭 라이브러리

 

❓상황

Next.js CSR에서의 데이터 패칭하는 방법을 배우기 위해, swr를 학습하고 정리함.

 

📖 swr 이란?

"SWR"이라는 이름은 HTTP RFC 5861에 의해 알려진 HTTP 캐시 무효 전략인 stale-while-revalidate에서 유래되었습니다.

 

SWR은 먼저 캐시(스태일)로부터 데이터를 반환하여 UI를 만들고, 데이터 fetch 요청(재검증)을 하고, 최종적으로 최신화된 데이터를 가져와 UI에 표시하는 전략입니다.

 

네트워크 재연결되거나, 뷰포트가 클릭되면 다시 데이터 패치가 발생합니다.

쉽게 말하자면, 사용자의 노트북이 대기상태에서 깨어나거나, 브라우저 탭을 전환할 때 자동으로 데이터 패치가 발생합니다.

 

🐸 설치

yarn add swr

 

🚀 빠른 시작!

처음 시작하기

swr을 빠르게 시작하기 위해서는, fetch 함수를 생성한다.

const fetcher = async (url) => {
    const res = await fetch(url);
    const data = res.json();
    return data;
}

 

다음, useSWR을 import하고, 사용하면 된다.

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의 첫번째 파라미터는 fetcher의 인자(url)로 사용된다.

 

재사용하기

useQuery의 경우엔, isLoading, isError, data로 렌더링을 조절할 수 있지만, useSWR의 경우에는 data와 error밖에 없다.

 

그래서, isLoading, isError, data를 반환하는 hook으로 새롭게 정의해서 사용해보자.

import { useSWR } from 'swr';

const fetcher = (url) => {
    const res = fetch(url);
    return res.json();
}

function useUser(url) {
    const {data, error} = useSWR(url, fetcher)
    
    return {
    	users: data, # 데이터
        isLoading: !data && !error, # boolean
        isError: !!error, # boolean
    }
}

function App () {
    const {users, isLoading, isError} = useUser(url);
    
    if (isLoading) {
        return <div>Loading....</div>
    }
    
    if (isError) {
        return <div>Error!</div>
    }
	return (
        {users.map(user => (
            ...
    ))}
    )
}

 

🧮 정리

useSWR : props

useSWR는 props를 4가지를 갖는다.

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

data, error는 사전적 의미로 해당 변수가 가지고 있는 값을 유추할 수 있다.

 

isValidating은 isLoading과 의미가 혼동되기도 한다.

 

isLoading은 data 또는 error가 없어 fetch가 한 번 발생할때 사용되고,

isValidating은 data 또는 error가 있어도 fetch가 여러 번 발생할때 사용된다.

 

즉, useSWR은 데이터 패치가 여러번 발생할때(뷰포트 포커스, 노트북 슬립모드 해제), 이에 따른 적절한 반응을 하기 위해서는 isValidating이 사용된다... 라고 친절히 설명해주셨다.

 

Best practice for isLoading. What different with isValidating? · Discussion #563 · vercel/swr

In SWR documentation, I cannot found about isLoading explicitly. This page show like this isLoading: !error && !data, Similirary, [quick start] section show like this https://swr.vercel.app...

github.com

 

mutate는, 이미 useSWR로 인해 한번 fetch되어 가지고 있는 캐시 데이터를 변경할때 사용된다.

 

useSWR : parameter

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

 

key는 데이터 패치 요청을 위한 고유한 key로, 해당 key가 fetcher의 인자로 사용되어 데이터 패치가 일어난다.

그래서, key의 값은 url을 넣어야한다.(꼭 아니어도 되긴한다. 자세한 설명은 아래 줄을 참고하자)

 

fetcher는 데이터 패치 함수를 넣어준다.

fetcher는 key값을 인자로 받아, api를 요청하기도 하지만, 다르게 받을 수도 있다.

function App (url) {
    const { data, error, isValidating, mutate } = useSWR(['api/user', url], fetcher);
    ...
}

 

options에는 useSWR이 기본 동작을 재설정할 수 있는 파라미터이다.

 

useSWR : options

상세히 설명하고 싶지만, 너무 많아서 혼절 할 뻔했다...

내가 설명하지 않아도 공식문서에 잘 설명되어 있기때문에, 필요할때 공식문서를 참고해서 사용하자

 

SWRconfig

각 useSWR에 똑같은 option을 넣고 싶다면, 아래와 같이 작성해야한다.

  const { data: events } = useSWR('/api/events', { refreshInterval: 0 })
  const { data: projects } = useSWR('/api/projects', { refreshInterval: 0 })
  const { data: user } = useSWR('/api/user', { refreshInterval: 0 })

 

위의 방법대로 해도되지만, 3개가 아닌 1억개가 된다면, 일일이 option을 넣는게 힘들 것이다.

이를 해결하기위해, useSWR에 전역으로 설정할 수 있는 기능을 제공하는 SWRconfig가 생겼다.

 

사용방법은 아래와 같다.

 

<SWRConfig value={{refreshInterval: 3000}}>
  <Component/>
</SWRConfig>

function Component () {
    const { data: events } = useSWR('/api/events')
    const { data: projects } = useSWR('/api/projects')
    const { data: user } = useSWR('/api/user')
}

 

추가적으로, SWRconfig는 useSWR에 없는 옵션인 provider를 사용할 수 있다.

provider는 fetcher함수를 전역으로 사용할 수 있도록 설정할 수 있다.

 

useSWRconfig

useSWRconfig hook은 SWRconfig로 설정된 전역값과 mutate, cache를 얻을 수 있다.useSWRconfig의 mutate는 useSWR의 mutate와 같은 기능을 가진다.

import { useSWRConfig } from 'swr'

function Component () {
  const { refreshInterval, mutate, cache, ...restConfig } = useSWRConfig()

  // ...
}

 

에러처리

에러가 발생하더라도, 이미 한번 데이터 패치가 되어서 캐시 데이터를 갖고 있다면

fetch 함수는 에러를 반환하고, UI는 캐시 데이터를 이용하여 UI를 만든다.

import React from "react";
import useSWR from "swr";

// a fake API that returns data or error randomly
const fetchCurrentTime = async () => {
  // wait for 1s
  await new Promise((res) => setTimeout(res, 1000));

  // error!
  if (Math.random() < 0.4) throw new Error("An error has occurred!");

  // return the data
  return new Date().toLocaleString();
};

export default function App() {
  const { data, error, mutate, isValidating } = useSWR(
    "/api",
    fetchCurrentTime,
    { dedupingInterval: 0 }
  );

  return (
    <div className="App">
      <h2>Current time: {data}</h2>
      <p>Loading: {isValidating ? "true" : "false"}</p>
      <button onClick={() => mutate()}>
        <span>Refresh</span>
      </button>
      {error ? <p style={{ color: "red" }}>{error.message}</p> : null}
    </div>
  );
}

 

위의 내용의 단점으로는, useSWR이 많이 사용된다면 어느 API 요청이 에러가 발생했는지 확인하기가 어렵다.

그래서 SWRconfig에 전역으로 error를 받아서 화면에 표시해줄 수 있다.

<SWRConfig value={{
  onError: (error, key) => {
    if (error.status !== 403 && error.status !== 404) {
      // Sentry로 에러를 보내거나,
      // 알림 UI를 보여줄 수 있습니다.
    }
  }
}}>
  <MyApp />
</SWRConfig>

 

조건부 데이터 패치

useSWR은 key, fetcher, options 를 파라미터로 갖는다.

만약 if 조건문처럼, 어느 상황일때만 데이터 패치를 할 수는 없을까?

 

그건 바로, useSWR의 key값으로 null이나, falsy를 반환하는 콜백함수를 넣으면 된다.

// 조건부 가져오기
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)

 

세번째 줄의 user.id가 정의되지 않았을 때 에러 throw는 아래의 코드를 보면 더욱 쉽게 이해할 수 있다.

function MyProjects () {
  const { data: user } = useSWR('/api/user')
  const { data: projects } = useSWR(() => '/api/projects?uid=' + user.id)
  // 함수를 전달할 때, SWR은 반환 값을 `key`로 사용합니다.
  // 함수가 falsy를 던지거나 반환한다면,
  // SWR은 일부 의존성이 준비되지 않은 것을 알게 됩니다.
  // 이 예시의 경우 `user.id`는 `user`가 로드되지 않았을 때
  // 에러를 던집니다.

  if (!projects) return 'loading...'
  return 'You have ' + projects.length + ' projects'
}

 

useSWR : 다중 인자

기본적으로 useSWR에 key 인자를 제공하는 표현식은 3가지 정도가 있다.

useSWR('/api/user', () => fetcher('/api/user'))
useSWR('/api/user', url => fetcher(url))
useSWR('/api/user', fetcher)

 

만약, 2개의 인자를 fetcher 함수에 주고 싶다면, key에 배열로 넣어주면된다.

const { data: user } = useSWR(['/api/user', token], fetchWithToken)

 

또한, key에 배열 대신, 객체를 넣을 수 있다.

const { data: orders } = useSWR({ url: '/api/orders', args: user }, fetcher)

 

mutate

mutate는 key를 인자로 받아, key를 이용하여 데이터 패치를 하는 useSWR을 다시 호출하는 기능을 가진다.

 

useSWR과 useSWRConfig 둘다 mutate를 props로 가지고 있다.

 

차이점은, 전역적으로 사용할지, 지역적으로 사용할지 구분되어 사용된다는 것이고,

useSWRConfig의 mutate는 key을 인자로 받아야하지만, useSWR의 mutate는 key 인자를 받지 않아도 된다.

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

 

로컬 데이터 즉시 업데이트

만약 API 호출이 시간이 오래 걸리는 작업이라면, 사용자가 느끼기에는 API 호출 완료할때까지, 화면에 있는 UI가 변경되지 않아 느리다고 느낄것이다.

 

그래서, 로컬 데이터를 즉시 업데이트 하되, 실제 백엔드단의 데이터는 작업이 진행중인 상태로 바꿀 수 있다.

import useSWR, { useSWRConfig } from 'swr'

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

  return (
    <div>
      <h1>My name is {data.name}.</h1>
      <button onClick={async () => {
        const newName = data.name.toUpperCase()
        const user = { ...data, name: newName }
        const options = { optimisticData: user, rollbackOnError: true }

        // optimisticData에 바꿀 데이터를 넣어주면, 로컬 데이터는 즉시 업데이트된다.
        // 그 다음에, 실제 API에 데이터를 변경하도록 요청한다.
        mutate('/api/user', updateFn(user), options);
      }}>Uppercase my name!</button>
    </div>
  )
}

 

여기서, updateFn에서 return된 값은 mutate에 넣어진 key에 해당되는 useSWR에 모두 영향을 준다.

 

캐시 데이터 중, 일부만 업데이트

이번에는, 내가 가진 캐시 데이터 중, 일부분만 변경하는 코드이다.

mutate('/api/todos', async todos => {
  // ID `1`을 갖는 todo를 업데이트해 완료되도록 해봅시다
  // 이 API는 업데이트된 데이터를 반환합니다
  const updatedTodo = await fetch('/api/todos/1', {
    method: 'PATCH',
    body: JSON.stringify({ completed: true })
  })

  // 리스트를 필터링하고 업데이트된 항목을 반환합니다
  const filteredTodos = todos.filter(todo => todo.id !== '1')
  return [...filteredTodos, updatedTodo]
})

 

코드를 보면, mutate 두번째 파라미터에서 url이 /api/todos/1인 api를 호출하여 반환된 updatedTodo를 기존의  데이터인 todos를 업데이트하고, [...filteredTodos, updatedTodo]를 반환한다.

 

return된 값은 /api/todos/를 key로 가진 useSWR에 영향을 준다.

 

mutate 실행 중, 에러 핸들링

mutate 실행하는 것이 오류가 발생하면, try-catch 구문을 이용하여 에러를 핸들링 할 수 있다.

try {
  const user = await mutate('/api/user', updateUser(newUser))
} catch (error) {
  // 여기에서 user를 업데이트하는 동안 발생하는 에러를 처리
}

 

페이지네이션

페이지네이션을 구현하기 위해서는 두가지 hook을 사용하면 된다.

 

일반적인 페이지네이션 구현

일반적으로 블로그와 같이 숫자 형식으로 되어있는 페이지네이션을 구현하려면, useSWR을 사용하면 된다.

function Page ({ index }) {
  const { data } = useSWR(`/api/data?page=${index}`, fetcher);

  // ... 로딩 및 에러 상태를 처리

  return data.map(item => <div key={item.id}>{item.name}</div>)
}

function App () {
  const [pageIndex, setPageIndex] = useState(0);

  return <div>
    <Page index={pageIndex}/>
    
    # pre-fetch를 위한 코드
    <div style={{ display: 'none' }}><Page index={pageIndex + 1}/></div>
    
    <button onClick={() => setPageIndex(pageIndex - 1)}>Previous</button>
    <button onClick={() => setPageIndex(pageIndex + 1)}>Next</button>
  </div>
}

 

또한, 다음 데이터를 가져와 태그를 숨겨놓으면, 다음 페이지로 이동될때, 이미 캐시된 데이터가 있음으로 로딩시간 없이 바로 보여줄 수 있다.

이는, prefetch를 구현한 것이다.

 

또한 for 반복문을 사용하여,페이지 네이션을 구현할 수 있다.

 

function App () {
  // 다음 컴포넌트를 생성한다.
  const [cnt, setCnt] = useState(1)
  const pagesData = []
  
  // 더보기 버튼을 누르면, for 반복문이 돈다.
  for (let i = 0; i < cnt; i++) {
    pagesData.push(<Page index={i} key={i} />)
  }

  return <div>
    {pages}
    <button onClick={() => setCnt(cnt + 1)}>더보기</button>
  </div>
}

 

무한 페이지네이션 구현 : 특정 개수만큼만 마운트하기

우선 무한 스크롤이던가, 더보기 버튼을 클릭해서 무한 페이지네이션을 구현할 때는, useSWRInfinite hook을 사용한다.

 

useSWRInfinite hook은 useSWR이 가지는 props에 size와 setSize를 추가로 갖는다.

import useSWRInfinite from 'swr/infinite'

// ...
const { data, error, isValidating, mutate, size, setSize } = useSWRInfinite(
  getKey, fetcher?, options?
)

 

size는 가져올 페이지를 뜻하고,

setSize는 가져올 페이지 수를 설정한다.

 

useSWRInfinite는 getKey와 fetcher, options를 파라미터로 가지는데,getKey에서 url을 반환하면, fetcher 함수가 API를 호출하여 데이터를 가져온다.

GET /users?page=0&limit=10

[
  { name: 'Alice', ... },
  { name: 'Bob', ... },
  { name: 'Cathy', ... },
  ...
]
# 11번 데이터 ~ 
[]

const getKey = (pageIndex, previousPageData) => {
  # pageIndex는 size를 뜻하고, previousPageData는 현재 캐시된 data를 뜻한다.
  # 만약 첫 렌더링인경우, size는 0이다.
  
  # 끝에 도달
  # []은 true를 가진다 && 길이가 0이어서 false인데 !으로 인해 true로 바뀐다.
  # 만약 데이터가 10번까지 밖에 없다면, 11번까지 데이터 url을 리턴하고,
  # 11번에서 button을 클릭해, size가 12인 상태인 경우에 null을 리턴한다.
  if (previousPageData && !previousPageData.length) return null
  
  # 다음 페이지의 url을 반환한다.
  return `/users?page=${pageIndex}&limit=10`                   
}

function App () {
  # getKey에서 반환된 url을 fetcher함수가 받아서 데이터를 가져온다.
  const { data, size, setSize } = useSWRInfinite(getKey, fetcher)
  
  if (!data) return 'loading'

  // 이제 모든 users의 수를 계산할 수 있습니다
  let totalUsers = 0
  for (let i = 0; i < data.length; i++) {
    totalUsers += data[i].length
  }

  return <div>
    <p>{totalUsers} users listed</p>
    {data.map((users, index) => {
      // `data`는 각 페이지의 API 응답 배열입니다.
      return users.map(user => <div key={user.id}>{user.name}</div>)
    })}
    <button onClick={() => setSize(size + 1)}>Load More</button>
  </div>
}

 

만약, 데이터에 다음 데이터를 뜻하는 cursor가 있다면 아래와 같이 코드를 작성하면 된다.

GET /users?cursor=123&limit=10
{
  data: [
    { name: 'Alice' },
    { name: 'Bob' },
    { name: 'Cathy' },
    ...
  ],
  nextCursor: 456
}
...
# 마지막 데이터
{
  nextCursor: 456
}


const getKey = (pageIndex, previousPageData) => {
  // 끝에 도달
  if (previousPageData && !previousPageData.data) return null

  // 첫 페이지, `previousPageData`가 없음
  if (pageIndex === 0) return `/users?limit=10`

  // API의 엔드포인트에 커서를 추가
  return `/users?cursor=${previousPageData.nextCursor}&limit=10`
}

 

pre-fetch

link 태그를 이용하여, 데이터를 prefetch할 수 있다. : 이 방식이 적극 권장된다고 한다.

<link rel="preload" href="/api/data" as="fetch" crossorigin="anonymous">

 

만약, 다음 페이지를 불러오기 전에 이전 페이지의 데이터를 우선 보여주기를 원한다면, fallback 옵션을 사용해라.

useSWR('/api/data', fetcher, { fallbackData: prefetchedData })

 

타입스크립트

useSWR의 key와 fetcher의 타입은 swr에서 제공해준다.

import useSWR, { Key, Fetcher } from 'swr'

const uid: Key = '<user_id>'
const fetcher: Fetcher<User, string> = (id) => getUserById(id)

const { data } = useSWR(uid, fetcher)
// `data` will be `User | undefined`.

 

useInfinite의 key의 타입 또한, swr에서 제공해준다.

import { SWRInfiniteKeyLoader } from 'swr/infinite'

const getKey: SWRInfiniteKeyLoader = (index, previousPageData) => {
  // ...
}

const { data } = useSWRInfinite(getKey, fetcher)

 

만약 데이터 타입만 제네릭으로 지정하고 싶다면 아래와 같이 작성하면 된다.

// `fetcher` 는 any 타입으로 지정된다.
// data는 User 타입으로 지정된다.
const { data } = useSWR<User>('/api/user', fetcher)

'공부 > 프론트엔드' 카테고리의 다른 글

공부 | HTML, CSS 웹 접근성(스크린리더) 정리  (0) 2022.10.11
공부 | Redux Toolkit 정리  (0) 2022.08.12
공부 | recoil 정리(ft. react)  (0) 2022.07.30
공부 | next.js 정리  (0) 2022.07.20
공부 | Line Awesome 정리  (0) 2022.07.16
복사했습니다!