SWR

Happhee·2022년 4월 25일
3

💙  React 💙

목록 보기
14/18
post-thumbnail

SWR은 비동기 작업을 도와주는 React Hooks 라이브러리이다.

Cache된 데이터를 보여주고, 데이터 요청을 보낸 후 새롭게 받은 데이터를 보여주는 SWR에 대해 알아보자.


✨ SWR

Stale-While-Revaildate : React Hooks library for data fetching으로 데이터를 얻는 GET에 특화된 훅이다.


특징

useSWR은 한 번의 fetch한 원격 상태의 데이터를 내부적으로 캐시한다.
즉, 다른 component에서 동일한 상태를 사용하고자 하는 경우에 서버로 재요청을 하는 것이 아니라 이전에 캐시했던 상태를 그대로 사용한다.

서로 다른 component에서 동일한 상태를 공유할 수 있다

🤔 캐시란?

주어진 리소스의 복사본을 저장하고 있다가 해당 리소스를 요청할 때 복사본을 제공하는 기술이다.
서버에 도달하기 전에 브라우저가 요청을 중간에 가져가 캐시에 있는 정보로 리소스를 응답한다.

비교

axios or fetch

import axios from "axios";

function Todo() {
  // 상태 정의
  const [ data, setData ] = useState(null);
  // mount 되었을때 실행
  useEffect(() => {
    ( async() => {
      // 서버로부터 가져와서 보여주기
      const { data } = await axios.get("/api/todos");
      setData(data);
    })();
  },[])
  return <div> { data }</div>
}

위의 코드에서의 단점은 다음과 같다.

  • 비동기 처리를 우리가 직접 구현해야 한다.
  • 서버의 데이터를 반복적으로 호출하는 것을 방지하기 위해서는 useEffect안에서 사용해야 한다.
  • 가져온 데이터를 사용하기 위해 useState로 정의해주어야 한다.
  • 다른 component에서 해당 데이터를 사용하기 위해서는 재통신을 진행하는 불편함이 존재한다.

useSWR

import useSWR from 'swr';

const fetcher = url => fetch(url).then(res => res.json());
// axios.get(url).then(res => res.data)

function Todo() {
  const { data, error } = useSWR('/api/todos', fetcher, options)

  // 밑에 자체가 에러처리!
  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>
  return <div> { data }</div>
}

앞서 살펴본 단점을 모두 보완하게 된다.

  • 비동기 처리를 따로 해줄 필요가 없다.
  • 가져온 데이터를 사용하기 위해 useState로 정의할 필요가 없다.
  • 데이터 캐싱(store 느낌)기능으로 다른 component에서 호출할 때 캐싱된 데이터를 가져온다.
    👉 서버에서 가져온 데이터를 상태로 관리해서 사용할 수 있는 장점이 생긴다.

장점

  • 데이터 패치 상태 ( react 의 suspense )

  • 전역적으로 데이터 관리하여 데이터 캐싱 기능이 가능하다. ( swr cache )
    즉, 반복적인 호출에 따른 비용이 발생하지 않는다.

  • 일정 시간마다 원격 데이터를 자동 동기화한다 👉 revalidate

    • 클릭시 자동으로 데이터 refetch 👉 revalidateOnFocus
    • 네트워크를 연결할 때 동기화 👉 revalidateOnReconnect
    • 특정 시간 주기로 데이터 refetch 👉 refreshInterval
  • 수동 동기화가 가능하다 👉 mutate


✨ 사용 방법

설치

yarn add swr

npm install swr

문법

import useSWR from "swr";

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

✅ key

첫 번째 인자 👉 key

  • 요청에 대한 유일한 식별자 역할이다.
  • API URL을 사용하기 위해 string 또는 함수( string을 return)를 사용한다.

✅ fetcher

두 번째 인자 👉 fetcher

  • 데이터를 받아오는 함수이며 key 값이 첫 번째 파라미터로 전달된다.
  • promise를 return 하는 모든 함수가 올 수 있기에 기본 fetch 함수 뿐만 아니라 axios 등과 같은 별도 라이브러리도 사용이 가능하다.

key를 파라미터로 받고, 응답이 정상적으로 이루어지면 respnse.data를 useSWR의 data로 사용한다.

✅ options

세 번째 인자 👉 options

  • refreshInterval = 0
    기본적으로 비활성화, 설정 ms만큼 poling 주기 설정
  • revaildateOnFocus = true
    사용자가 페이지를 탐색하는 중 다른 탭을 보다가 다시 돌아올 때의 재 실행 여부
  • revalidateOnReconnect = true
    네트워크가 끊겼다가 다시 연결되었을 때 재 실행 여부

✅ 반환 값

  1. data
    fetcher 통신 전에는 undefined가 할당되어 있으며, 통신 후에는 응답의 결과 data가 저장된다.

  2. error
    fetcher의 error가 저장된다.

  3. revalidate
    요청 또는 요청 재검증을 통해 SWR에 정의된 API를 다시 요청하여 데이터를 검증한다.

  4. mutate(datam shouldRevaildate)
    캐시되는 데이터를 저장한다.

    • mutate(data)를 설정하면 data의 값이 변경된다.
    • shouldRevaildate는 검증의 여부를 true, false로 받는다.
mutate("newData", false)
// data 값을 newData로 바꾸고, 2번째 옵션이 false면 서버로 재요청을 하지 않는다.

사용 순서

  1. fetcher를 만든다.
    여기서 url을 지정하고 이 fetcher로부터 데이터를 가져온다. fetcher를 만드는 방식은 Fetch, Axios, GraphQL 등이 가능하다.

👇 axios로 만든 fetcher코드이다.

import axios from 'axios';

const fetcher = (url: string) =>
  axios
    .get(url, {
      withCredentials: true,
    })
    .then((response) => response.data);

export default fetcher;
  1. 데이터를 요청할 때 사용할 key값을 fetcher와 같이 넘겨준다.
const { data, error, isValidating, mutate } = useSWR(key, fetcher, options)

key에는 API 명세서에 적힌 url 끝부분을 넣어준다.

import useSWR from 'swr'

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

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

  // render data
  return <div>hello {data.name}!</div>
}

✨ 기능

useSWR 훅

👇 사용자의 정보를 받아오는 hook 코드를 만들어보자.

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

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

아래와 같은 방법으로 사용이 가능하다.

function UserProfile({ id }) {
  const { user, isLoading, isError } = useUser(id)

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

❗️ 비교하기 ❗️

// 페이지 컴포넌트
// useEffect로 첫 렌더링에 정보를 가져와서
// 아직 정보가 없다면 Spinner라는 로딩 페이지를 보여주고
// 정보가 들어오면 그에 맞는 자식 컴포넌트들을 보여줍니다.

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로 넘겨준다.
context를 사용해서 props 드릴링을 피할 수 있지만, 동적 콘텐츠 문제가 있을 수 있다. 더불어 페이지 콘텐츠 내 컴포넌트들은 동적일 수 있으며, 최상위 레벨 컴포넌트는 그 자식 컴포넌트가 필요로하는 데이터가 무엇인지 알 수 없을 수도 있다.

👇 그래서 SWR로 리팩토링을 진행하는 것이 필요하다.

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로 전송된다는 것이 핵심이다.

Global Configuration

SWRConfig Component의 자식 컴포넌트들에게 사용된 swr에 동일한 options을 부여하는 기능이다.

function App () {
  return (
    <SWRConfig 
      value={{
        refreshInterval: 3000,
        fetcher: (resource, init) => fetch(resource, init).then(res => res.json())
      }}
    >
      <Dashboard />
    </SWRConfig>
  )
}

조건부 Fetching

어떤 state에 따라 api 요청을 제어하고자 하는 기능이다.

👇 key값이 null이거나 key 대신 함수를 사용할 경우에 false한 값을 반환받으면 fetch를 멈추도록 하는 것도 가능하다.

//  state에 따라 key 값을 null로 만들면 fetch가 실행되지 않는다.
const { data } = useSWR(shouldFetch ? "/api/data" : null, fetcher)

//  key 함수가 falsy 값을 리턴하면 fetch가 실행되지 않는다.
const { data } = useSWR(() => shouldFetch ? "/api/data" : null, fetcher)

// user가 존재하지 않아서 user.id에 접근할 때 error가 발생해 fetch가 실행되지 않는다.
const { data } = useSWR(() => "/api/data?uid=" + user.id, fetcher)

Arguments

  • 3가지는 모두 동일한 표현식이다.
useSWR("/api/user", () => fetcher("/api/user"))
useSWR("/api/user", url => fetcher(url))
useSWR("/api/user", fetcher)
  • 토큰이 달라도 key는 같기에 동일한 key로 간주한다.
useSWR("/api/user", url => fetchWithToken(url, token))
  • 다중 인자를 포함하는 배열을 key 파라미터로 사용가능하다.
const { data : user } = useSWR(["/api/user", token], fetchWithToken)

mutate

useSWRConfig() hook으로부터 mutate 함수를 얻을 수 있으며, mutate(key)를 호출하여 동일한 키에 해당하는 전역의 모든 SWR을 revaildation한다.

import useSWR, { useSWRConfig } from "swr";

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

mutation + POST 요청

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()
        
        // 로컬 데이터를 즉시 업데이트하지만, 갱신은 비활성화
        mutate('/api/user', { ...data, name: newName }, false)
        
        // 소스 업데이트를 위해 API로 요청 전송
        await requestUpdateUsername(newName)
        
        // 로컬 데이터가 올바른지 확인하기 위해 갱신(refetch) 트리거
        mutate('/api/user')
      }}>Uppercase my name!</button>
    </div>
  )
}

🤔 여기서 refetch가 아닌 즉시 update를 하고 싶다면?

👇 mutate 함수에 현재 캐시 된 값을 받는(있으면 업데이트된 문서를 반환) 비동기 함수를 전달하면 update가 가능하다.

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

📚 학습할 때, 참고한 자료 📚

profile
즐기면서 정확하게 나아가는 웹프론트엔드 개발자 https://happhee-dev.tistory.com/ 로 이전하였습니다

0개의 댓글