Next.js - SWR

nooori·2024년 6월 13일
post-thumbnail

이전 react 프로젝트들을 진행할 때는 react-query로 백엔드에서 데이터를 가져왔는데 이번 인스타그램 프로젝트에서는 SWR를 사용해서 데이터를 가져와서 업데이트하려고 한다.

SWR이란?

SWR은 Next.js팀에서 개발한 React 어플리케이션에서 데이터를 가져오고 관리하기 위한 라이브러리이다. SWR은 데이터를 가져오는 동안 로딩 상태를 처리하고, 캐싱을 통해 데이터를 최적화하며, 데이터를 다시 가져오는 시점을 자동으로 관리하는 등의 기능을 제공한다. 주로 서버 사이드 렌더링(SSR)과 클라이언트 사이드 렌더링(CSR)을 지원하는 프레임워크와 함께 사용된다. 데이터를 가져오는 과정에서 편리한 캐싱 기능과 상태 관리를 제공하기 때문에 개발자가 간단하고 효율적으로 데이터 통신을 할 수 있도록 돕는다.

SWR Getting started
🏴 https://swr.vercel.app/ko/docs/getting-started

기본적인 사용 방법

기본적인 사용방법은 아래와 같다.

const fetcher = (...args) => fetch(...args).then(res => res.json())
import useSWR from 'swr'
 
function Profile () {
  const { data, error, isLoading } = useSWR('/api/user/123', fetcher)
 
  if (error) return <div>failed to load</div>
  if (isLoading) return <div>loading...</div>
 
  // 데이터 렌더링
  return <div>hello {data.name}!</div>
}

swr이 여기저기 많이 사용될 것 같아서 모든 컴포넌트가 일관된 로직을 공유할 수 있도록 전역 설정을 해보려고 한다. SWR 공식 사이트에서 전역 설정을 어떻게 할 수 있는지 보여주고 있어서 참고했다. 차근차근 하나보면 그렇게 어렵지 않게 할 수 있다.
SWR 전역 설정
🏴 https://swr.vercel.app/ko/docs/global-configuration

  1. 로그인 세션에서 AuthContext을 만들었던 것처럼 swr도 SWRConfigContext 컴포넌트를 따로 만들어주었다.
'use client'  // context는 항상 클라이언트 컴포넌트에서만 동작하니까 반드시 명시해주기!

import { SWRConfig } from 'swr'

type Props = {
  children: React.ReactNode
}
export default function SWRConfigContext({ children }: Props) {
  return (
    <SWRConfig value={{
      fetcher: (url: string) => fetch(url).then((res) => res.json())
    }}>
      {children}
    </SWRConfig>
  )
}

👉 SWRConfig의 value prop에 기본 fetcher 함수를 정의한다. 이 fetcher는 어디로 갈 것인지 url을 string 타입으로 받아오면 브라우저에서 제공해주는 fetch를 사용해서 전달받은 url로 데이터를 가져온 후에 응답을 JSON으로 변환하는 함수이다. 이렇게 해주면 swr을 사용하는 모든 곳에서 JSON으로 변환해주지 않아도 된다.

  1. 이렇게 만든 SWRConfig를 어플리케이션 전반에 걸쳐 사용할 수 있도록 감싸줘야한다.
return (
    <html lang="en" className={openSans.className}>
      <body className='w-full bg-neutral-50 overflow-auto'>
        <AuthContext>
          <header className='sticky top-0 bg-white z-10 border-b'>
            <div className='max-w-screen-xl mx-auto'>
              <Navbar />
            </div>
          </header>
          <main className='w-full flex justify-center max-w-screen-xl mx-auto'>
            <SWRConfigContext>
              {children}
            </SWRConfigContext>
          </main>
        </AuthContext>
        <div id='portal' />
      </body>
    </html>
  )

👉 Navbar에서는 따로 데이터를 불러오는 작업이 필요하지 않아서 swr을 사용할 일이 없다. 그래서 main 아래에 있는 children을 감싸준다.

  1. 그럼 이제 swr을 한번 사용해보자! 사용자 검색하는 기능을 구현한 코드 중 일부이다.
const [keyword, setKeyword] = useState('');
  const { data: users, isLoading, error } = useSWR(`/api/search/${keyword}`);

👉 useSWR을 사용해서 데이터를 가져올 URL을 전달한다. 여기서는 /api/search/${keyword} 형태로 keyword 값을 포함시킨 URL을 사용해서 요청을 보냈다. 그리고 useSWR을 사용하면 데이터를 가져오는 것 뿐만 아니라 isLoading로 로딩 상태를 나타내거나 error로 데이터를 가져오는 과정에서 오류가 발생했을 때 어떻게 할 것인지를 처리할 수 있다.

  1. 그리고 전달받은 URL인 api > search > [keyword] > route.ts로 와서 GET 함수로 데이터를 받아온다.
import { searchUsers } from '@/service/user';
import { NextRequest, NextResponse } from 'next/server';


export async function GET(_: NextRequest, context: Context) {
  return searchUsers(context.params.keyword)
  .then(data => NextResponse.json(data))
}

위 함수는 HTTP GET 요청을 처리하는 함수이다. 요청에 관련된 정보를 전달받는 NextRequest는 사용하지 않을 거기 때문에 _ 로 설정해서 무시했다.그리고 함수 실행에 필요한 추가적인 정보를 전달받기 위해 Context 타입의 매개변수가 필요하다. 이 코드에서는 'context.params.keyword'를 통해 URL의 keyword 파라미터 값을 추출하여 searchUser 함수에 전달했다. searchUsers는 'context.params.keyword'값을 사용하여 특정 키워드를 기반으로 사용자 검색을 수행하는 함수이고 이 함수가 완료되면 '.then()' 메서드를 사용해서 data를 JSON 형식으로 반환한다.

=> 이렇게 useSWR에 경로만 명시하면 내부적으로 자동으로 fetch API를 사용해서 요청을 보내고 반응이 오면 그것을 JSON 형태로 반환까지 해준다. 이전에 사용했던 react-query와 크게 다르지 않다.

업데이트 Revalidate

revalidate는 SWR 라이브러리가 제공하는데 데이터를 새로고침하고 업데이트하는 방법을 제어하는 데 사용된다. 주로 데이터를 다시 가져오는 시점을 설정하거나 데이터의 상태를 갱신할 때 활용된다.

SWR 라이브러리에 대한 설명과 사용 예시
🏴 https://swr.vercel.app/ko/docs/advanced/understanding

아래 코드는 좋아요를 업데이트하는 내용을 담고있다. useSWR을 사용하여 포스트 데이터를 가져오고 관리하는 기능을 포함한다.

import { SimplePost } from '@/model/post';
import useSWR from 'swr';

async function updateLike(id: string, like: boolean) {
  return fetch('api/likes', {
    method: "PUT",
    body: JSON.stringify({id, like})
  }).then(res => res.json());
}

export default function usePosts() {
  const {data: posts, isLoading, error, mutate} = useSWR<SimplePost[]>('/api/posts');

  const setLike = (post: SimplePost, username: string, like: boolean) => {
    const newPost = {
      ...post, 
      likes: like 
      ? [
          ...post.likes, 
          username
        ] 
      : post.likes.filter(item => item !== username)
    };
    const newPosts = posts?.map(p => p.id === post.id ? newPost : p);

    return mutate(updateLike(post.id, like), { 
      optimisticData: newPosts,  // UI를 즉각적으로 먼저 업데이트할 newPosts 전달
      populateCache: false,  // 기존 데이터 덮어씌지 않도록
      revalidate: false,   // 이미 로컬상에 새로운 post 배열이 있으므로 likes가 변경이 된다고 해서 새로운 post에 대한 정보를 다시 받아오지 않도록 false
      rollbackOnError: true  // UI상에서 먼저 업데이트 해두었는데 그 후에 네트워크의 문제로 인해서 백엔드 업데이특라 이루어지지 않는다면 rollback으로 error를 알려달라고 true 
    })
  }
  return {posts, isLoading, error, setLike};
}

🤌 useSWR을 사용하여 '/api/posts'에서 포스트 데이터를 가져온다.SWR을 사용할 때 데이터뿐만 cache key('/api/posts')와 밀접하게 연결된 mutate 함수를 받아와서 직접적으로 호출 할 수 있다. 즉, /api/posts 요청을 위한 mutate 함수이다. mutate 함수를 호출하면 최신 데이터로 캐시를 업데이트하고 UI를 최신 상태로 유지할 수 있다.
🤌 setLike를 호출하면 mutate된 최신으로 업데이트 된 like에 대한 정보를 반환환해준다.'mutate' 함수를 호출할 때 옵션들(optimisticData, populateCache, revalidate, rollbackOnError)을 통해 필요한 처리를 할 수 있다. 각 옵션의 기능은 주석으로 설명해두었다.
🤌 updatelike 함수를 사용하여 서버로 좋아요를 업데이트하는 PUT 요청을 보낸다. setLike 함수는 호출되면 기존 포스트 정보를 복사하여 newPost를 생성한다. like 값이 true이면 포스트의 likes 배열에 username을 추가하고, false이면 likes 배열에서 username을 제거한다.

0개의 댓글