Skip to main content
Views 26

SWR 완벽 가이드: React 데이터 패칭과 캐싱 전략

Summary

개요

SWR은 Vercel에서 만든 React용 데이터 패칭 라이브러리로, 클라이언트 사이드에서 서버 데이터를 효율적으로 가져오고 캐싱하는 데 특화되어 있다.
useSWR 훅 하나로 요청, 로딩 상태, 에러, 캐싱, 재검증까지 대부분의 로직을 처리할 수 있도록 설계되어 있다.

이름은 캐시 전략인 "Stale-While-Revalidate"에서 따왔으며, 이미 캐시에 있는 데이터를 즉시 보여주면서 동시에 백그라운드에서 최신 데이터를 다시 가져오는 방식을 사용한다.
이를 통해 사용자에게는 빠른 응답성을 제공하면서도 데이터 최신성을 확보할 수 있다.


SWR의 핵심 개념

SWR의 핵심은 "stale-while-revalidate" 전략이다.
브라우저가 오래된 캐시를 바로 보여준 뒤, 뒤에서 새 데이터를 가져와 갱신하는 HTTP 캐시 개념을 라이브러리 수준에서 구현한 것이다.

즉, 컴포넌트는 항상 "가능한 한 최신에 가까운 데이터"를 사용하면서도, 네트워크 지연 때문에 화면이 비거나 느려지는 문제를 최소화한다.
이 전략 덕분에 UX 측면에서 빠른 체감 속도와 부드러운 업데이트 경험을 제공할 수 있다.


기본 사용법

SWR의 기본 사용법은 매우 단순하다.
핵심은 key(식별자)와 fetcher(실제 데이터를 가져오는 함수) 두 가지를 useSWR에 전달하는 것이다.

import useSWR from 'swr'

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

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

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Failed to load</div>

  return <div>hello, {data.name}</div>
}

여기서 '/api/user'는 캐시 키이자 요청 식별자 역할을 하고, 같은 키를 사용한 다른 컴포넌트는 동일한 캐시를 공유한다.
data, error, isLoading, isValidating 같은 값은 비동기 상태 관리를 거의 자동으로 처리해 준다.


캐싱과 키(key) 전략

SWR에서 캐시의 단위는 key이다.
같은 key를 쓰는 모든 useSWR 호출은 동일한 데이터를 공유하고, 한 곳에서 갱신되면 다른 컴포넌트도 자동으로 최신 상태를 반영한다.

키는 보통 문자열 URL을 사용하지만, 배열이나 함수도 사용할 수 있다.
예를 들어 쿼리 파라미터를 포함하는 요청은 배열을 사용해 의도를 명확히 표현할 수 있다.

const { data } = useSWR(['/api/posts', page, category], ([url, page, category]) =>
  fetch(`${url}?page=${page}&category=${category}`).then(r => r.json())
)

키를 null로 주면 요청을 비활성화할 수 있어, 로그인 후에만 호출해야 하는 API 같은 것을 조건부로 제어할 수 있다.

const { data } = useSWR(isLoggedIn ? '/api/me' : null, fetcher)

Stale-While-Revalidate 동작 방식

SWR의 동작 흐름은 대략 다음과 같다.

  1. 컴포넌트가 처음 마운트되면 캐시를 확인한다.

  2. 캐시에 데이터가 있으면 즉시 반환하고 화면에 렌더링한다(이 데이터는 stale일 수 있다).

  3. 동시에 백그라운드에서 서버로 새 요청을 보내 최신 데이터를 가져온다.

  4. 새 데이터가 도착하면 캐시를 갱신하고, 이 캐시를 쓰는 모든 컴포넌트를 자동으로 리렌더링한다.

이 덕분에 사용자는 "일단 보이는 화면"을 빠르게 보게 되고, 그 뒤에 살짝 자연스럽게 값이 최신 상태로 바뀌는 경험을 한다.
네트워크 품질에 따라 첫 요청이 느리더라도 UX가 크게 나빠지지 않는다.


재검증(revalidate) 트리거

SWR은 데이터 최신성을 유지하기 위해 여러 가지 자동 재검증 트리거를 제공한다.

  • 포커스 시 재검증: 탭 혹은 창이 다시 활성화될 때 자동으로 재검증

  • 네트워크 재연결 시: 오프라인에서 온라인으로 바뀔 때 데이터 재검증

  • 인터벌 폴링: 지정한 간격마다 주기적으로 재검증

예를 들어 폴링을 사용하면 다음과 같이 설정할 수 있다.

const { data } = useSWR('/api/stats', fetcher, {
  refreshInterval: 5000, // 5초마다 재검증
})

이 옵션들은 상황에 따라 켜거나 끌 수 있어, 비용이 큰 API는 폴링을 줄이고, 채팅·알림 등 실시간성이 중요한 데이터는 자주 갱신하도록 조절할 수 있다.


수동 갱신과 뮤테이션(mutate)

SWR은 mutate 함수를 통해 수동으로 캐시를 갱신할 수 있다.
이는 서버에 반영되기 전에 먼저 UI에 반영하는 낙관적 업데이트(optimistic UI)를 구현할 때 자주 사용한다.

import useSWR, { mutate } from 'swr'

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

async function updateUserName(name) {
  // 1. 낙관적 업데이트
  mutate('/api/user', (prev) => ({ ...prev, name }), false)

  // 2. 서버에 실제 요청
  const updated = await fetch('/api/user', {
    method: 'PATCH',
    body: JSON.stringify({ name }),
  }).then(r => r.json())

  // 3. 서버 응답으로 최종 동기화
  mutate('/api/user', updated, false)
}

위 패턴을 사용하면 사용자는 수정 버튼을 누르자마자 화면에서 이름이 바뀐 것을 볼 수 있고, 서버 요청은 그 뒤에 진행되므로 체감 속도가 매우 빨라진다.
에러 처리 시에는 롤백을 구현해 다시 기존 값으로 되돌릴 수도 있다.


전역 설정과 SWRConfig

SWR은 SWRConfig 컴포넌트를 통해 전역 기본 옵션을 설정할 수 있다.
애플리케이션 루트에서 공통 fetcher, 재검증 정책 등을 한 번만 정의하면 개별 컴포넌트에서 반복 설정할 필요가 없다.

import { SWRConfig } from 'swr'

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

function App({ children }) {
  return (
    <SWRConfig
      value={{
        fetcher,
        revalidateOnFocus: true,
        dedupingInterval: 2000, // 2초 내 중복 요청 방지
      }}
    >
      {children}
    </SWRConfig>
  )
}

이렇게 하면 모든 useSWR 호출에서 fetcher를 생략할 수 있고, 포커스 재검증, deduping 전략 등도 일관성 있게 적용된다.
대규모 애플리케이션일수록 전역 설정을 잘 설계하는 것이 중요하다.


에러 처리와 로딩 상태

SWR은 error, isLoading, isValidating 같은 상태를 제공해 로딩·에러 UI를 쉽게 구성할 수 있다.

  • isLoading: 최초 데이터가 아직 없고, 요청이 진행 중일 때

  • error: 요청 실패 시 에러 객체

  • isValidating: 백그라운드 재검증이 진행 중일 때

const { data, error, isLoading, isValidating } = useSWR('/api/posts', fetcher)

if (isLoading) return <Spinner />

if (error) return <ErrorMessage message="불러오기에 실패했습니다." />

return (
  <>
    {isValidating && <small>업데이트 중...</small>}
    <PostList posts={data} />
  </>
)

이 구분 덕분에 "첫 로딩 스피너"와 "이미 데이터는 있지만 뒤에서 업데이트 중"인 상태를 구체적으로 다르게 표현할 수 있다.


의존 관계가 있는 요청(Dependent Fetching)

어떤 요청은 다른 요청의 결과가 있어야만 호출할 수 있다.
예를 들어 사용자 ID를 먼저 받아온 뒤, 그 ID로 상세 정보를 다시 불러와야 하는 경우다.

SWR에서는 이 상황을 키를 조건부로 설정해 간단히 처리한다.

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

const { data: profile } = useSWR(
  user ? `/api/profile/${user.id}` : null,
  fetcher
)

user가 로딩 중일 때는 두 번째 요청의 키가 null이므로 호출되지 않고,
user가 준비된 이후에만 두 번째 API가 실행되므로 불필요한 에러나 중복 호출을 줄일 수 있다.


페이지네이션과 무한 스크롤

페이지네이션이나 무한 스크롤은 useSWRInfinite 훅을 사용해 구현할 수 있다.
각 페이지를 별도의 키로 관리하면서도 하나의 연속된 리스트처럼 다뤄준다.

import useSWRInfinite from 'swr/infinite'

const getKey = (pageIndex, previousPageData) => {
  if (previousPageData && !previousPageData.length) return null // 끝
  return `/api/posts?page=${pageIndex + 1}`
}

function PostList() {
  const { data, size, setSize, isLoading } = useSWRInfinite(getKey, fetcher)

  const posts = data ? data.flat() : []

  return (
    <>
      {posts.map(p => <PostItem key={p.id} post={p} />)}
      <button onClick={() => setSize(size + 1)} disabled={isLoading}>
        더 보기
      </button>
    </>
  )
}

이 패턴을 사용하면 페이지 단위의 캐시와 무한 스크롤 UX를 동시에 얻을 수 있다.


React Query 등과의 비교

SWR은 설계 철학상 "심플한 데이터 패칭 + 캐시"에 초점을 맞추고, API도 상대적으로 간단하다.
반면 React Query(TanStack Query)는 캐시 전략, 쿼리 상태, mutation 관리, DevTools, SSR 통합 등 훨씬 많은 기능을 포함한다.

  • SWR: 적은 개념으로 빠르게 도입 가능, URL 기반 캐싱에 강점, 가벼운 데이터 패칭에 적합

  • React Query: 복잡한 상태, 다양한 캐시 정책, 풍부한 옵션이 필요한 대규모 애플리케이션에 유리

단순한 REST API 중심의 프론트엔드라면 SWR만으로도 충분한 경우가 많고,
캐싱·동기화·백그라운드 업데이트를 세세하게 제어해야 하는 복잡한 도메인에서는 React Query를 더 선호하기도 한다.


SSR/ISR과의 연동

Next.js와 함께 사용할 때 SWR은 서버 사이드 렌더링(SSR) 또는 정적 생성(ISR)과 잘 결합할 수 있다.
서버에서 미리 데이터를 받아 HTML을 렌더링하고, 클라이언트에서는 SWR로 같은 키에 대해 데이터를 재검증하는 방식이다.

서버에서 가져온 초기 데이터를 SWRConfigfallback에 넣어두면, 클라이언트는 해당 데이터를 캐시에서 즉시 꺼내 쓰면서 필요 시 재검증을 진행한다.
이렇게 하면 SEO와 초기 로딩 속도, 그리고 클라이언트 측 최신 데이터 유지까지 모두 잡을 수 있다.


언제 SWR을 선택하면 좋은가

SWR을 선택하기 좋은 상황은 대략 다음과 같다.

  • React 앱에서 클라이언트 사이드 데이터 패칭을 단순하고 선언적으로 처리하고 싶을 때

  • 여러 컴포넌트가 동일 데이터를 공유하며, 자동 캐싱·공유가 필요할 때

  • UX 관점에서 "빠른 첫 화면 + 자연스러운 데이터 갱신"이 중요할 때

  • 현재 상태 관리 라이브러리(Redux, Zustand 등)로 비동기 로직을 복잡하게 관리하고 있다면 이를 대체하거나 보완하고 싶을 때

반대로 서버 상태가 매우 복잡하거나, 캐시 만료·의존성·비동기 플로우를 매우 세밀하게 제어해야 한다면 다른 도구(예: React Query)를 검토하는 것이 좋다.


마무리

SWR은 React 애플리케이션에서 서버 데이터를 다루는 일을 크게 단순화해 주는 라이브러리다.
stale-while-revalidate 전략, 자동 캐싱과 재검증, 간결한 API 덕분에 대부분의 일반적인 데이터 패칭 요구사항은 useSWR 하나로 해결할 수 있다.

특히 빠른 응답성과 부드러운 업데이트가 중요한 현대 웹 앱에서, SWR은 복잡한 상태 관리 없이도 "사용자 경험 좋은 데이터 패칭"을 구현할 수 있는 실용적인 선택지로 자리 잡고 있다.

SWR 완벽 가이드: React 데이터 패칭과 캐싱 전략

이 노트는 요약·비평·학습 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.