SWR을 통한 서버 데이터 가져오기 전략

Muru·2025년 8월 14일

[React] 지식 저장소

목록 보기
30/30
post-thumbnail

SWR먼저 캐시된 데이터를 반환하면서 동시에 백그라운드에서는 데이터를 요청하고 최종적으로 최신 데이터를 반환하는 전략이다. 사용자 경험을 향상시키면서 데이터는 최신 상태를 유지 할 수 있다.

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

  • 파라미터

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

  • 반환 값

data: fetcher가 이행한 주어진 키에 대한 데이터(로드되지 않았다면 undefined)
error: fetcher가 던진 에러(또는 undefined)
isLoading: 진행 중인 요청이 있고 "로드된 데이터"가 없는 경우. 폴백 데이터와 이전 데이터는 "로드된 데이터"로 간주하지 않음.
isValidating: 요청이나 갱신 로딩의 여부
mutate(data?, options?): 캐시 된 데이터를 뮤테이트하기 위한 함수

SWR을 사용해볼 수 있는 상황

1. 여러 페이지에서 서버의 동일한 데이터를 사용하는 경우

여러 페이지에서 동일한 데이터를 사용한다고 가정해보자. 보통 useEffect훅을 사용하여 다음과 같이 구현할 것이다.

// fetcher.js
export const fetchUser = () => fetch('/api/user').then(r => r.json());

// A.jsx
import { useEffect, useState } from 'react';
import { fetchUser } from './fetcher';

export default function A() {
  const [data, setData] = useState();
  useEffect(() => { fetchUser().then(setData); }, []);
  return <div>{data?.name}</div>;
}

// B.jsx (다시 API 호출, 캐시 X)
import { useEffect, useState } from 'react';
import { fetchUser } from './fetcher';

export default function B() {
  const [data, setData] = useState();
  useEffect(() => { fetchUser().then(setData); }, []);
  return <div>{data?.name}</div>;
}

위 코드의흐름은 다음과 같다.

  1. A페이지에서 user 데이터를 가져온다. (API 호출)
  2. B페이지로 이동
  3. B페이지에서 user 데이터를 가져온다. (API 호출)

사용자 입장에서는 1번과정과 3번과정에서 값이 undefined인 상태를 반드시 거치게된다. 즉 로딩상태가 필연적으로 발생한다. 왜냐하면 캐싱된 데이터가 아니기 때문이다.

Data.name을 표시하는 과정...
마운트 → 캐시 조회 → 캐시 없음 → undefined(로딩상태) → fetch 후 값 표시


SWR을 도입해서 위 문제를 해결해보자.

// fetcher.js
export const fetcher = (url) => fetch(url).then(r => r.json());

// A.jsx
import useSWR from 'swr';
import { fetcher } from './fetcher';
export default function A() {
  const { data } = useSWR('/api/user', fetcher);
  return <div>{data?.name}</div>;
}

// B.jsx (다른 페이지에서도 캐시 재사용)
import useSWR from 'swr';
import { fetcher } from './fetcher';
export default function B() {
  const { data } = useSWR('/api/user', fetcher);
  return <div>{data?.name}</div>;
}

위 코드의 흐름은 다음과 같다.
1. A페이지에서 user 데이터를 가져온다. (API 호출)
2. B페이지이동
3. B페이지에서 user 데이터를 가져온다. (API 호출하지 않고 바로 캐싱된 데이터)

사용자 입장에서는 A페이지에서 B페이지로 이동할 경우 user 데이터에 관한것은 바로 출력이 된다. 왜냐하면 렌더링 전에 미리 useSWR훅이 작동하여 캐싱된 데이터가 있기 때문이다.

SWR을 사용할때 Data.name을 표시하는 과정...
마운트 → 캐시 조회 → 캐시 존재 → 바로 값 표시 → (백그라운드 API 호출)

물론 마지막의 백그라운드에서 API 호출을 원하지 않을경우 의도적으로 막을수 있다. 데이터가 분초를 다투는것이 필요하지 않을경우 의미없는 API호출을 막아서 더 나은 성능 최적화를 할 수 있는것이다.

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

// 1. 컴포넌트가 마운트되면 /api/user 요청 실행
// 2. 5초 이내에 같은 키(/api/user)로 다른 컴포넌트에서 호출하거나 다시 마운트해도 API 재호출 안 함
→ 대신 캐시 데이터 즉시 반환
// 3. 5초가 지나면 같은 키로 호출했을 때 다시 네트워크 요청


실제로 한번 프로젝트에 적용하여 사용해보자. SWR을 사용했을때 동일한 메모 데이터를 가져오는 흐름이다.

메인 페이지(localhost:3000) => 영화메모1 페이지(localhost:3000/usermemo1) => 영화메모2 페이지(localhost:3000/usermemo2)

바라는 동작 흐름은 영화메모1 페이지에서 API요청을 하고나서 동일한 데이터를가지고 영화메모2 페이지를 이동했을때 API요청을 하지 않고 캐싱데이터를 사용하는지 볼것이다.
(캐싱데이터를 사용하는지 보기 위하여 API요청에 의도적으로 3초지연을 설정하였다.)

영화메모2 페이지로 이동할때 API요청을 하지 않고 바로 캐싱된 데이터가 렌더링된것을 볼 수 있다.

usermemo1.tsx (usermemo2.tsx도 해당 코드와 거의 동일하다)

'use client';

import useSWR from 'swr';
import { fetcher } from '@/lib/fetcher';
import Link from 'next/link';

interface MemoData {
  success: boolean;
  data: {
    id: number;
    title: string;
    content: string;
    createdAt: string;
    author: string;
  };
  timestamp: string;
}

export default function UserMemo1() {
  const { data, error, isLoading } = useSWR<MemoData>('/api/latest-memo', fetcher, {
    // 5초 동안 중복 요청 방지 - 같은 키로 요청 시 캐시된 데이터 반환
    dedupingInterval: 5000,
  });

  if (error) {
    return (
      <div>
        <div>
          <div>
            <h1>오류 발생</h1>
            <p>메모를 불러오는 중 오류가 발생했습니다.</p>
            <p>{error.message}</p>
          </div>
        </div>
      </div>
    );
  }

  return (
    <div>
      <div>
        <div>
          <h1>영화메모1 페이지</h1>
          <br/>
        
          {/* 메모 내용 */}
          <div>
            {isLoading ? (
              <div>
                <div></div>
                <p>API에서 최신 메모 한개를 불러오는 중...</p>
                <p>
                  (3초 지연을 의도적으로 추가 했어요)
                </p>
              </div>
            ) : data ? (
              <div>
                <h2>
                  {data.data.title}
                </h2>
                <p>
                  <strong>메모 내용:</strong>{data.data.content}
                </p>
                
                <div>
                  <p><strong>작성자:</strong> {data.data.author}</p>
                  <p><strong>작성일:</strong> {new Date(data.data.createdAt).toLocaleString('ko-KR')}</p>
                  <p><strong>메모 ID:</strong> {data.data.id}</p>
                </div>
                <div>
                  <p>
                    API 호출 및 응답 시간: {data.timestamp}
                  </p>
                </div>
              </div>
            ) : null}
          </div>
        </div>
      </div>
    </div>
  );
}

2. 기존 데이터 패칭시 많은 보일러플레이트의 문제

useEffect로 데이터 패칭을 한다고 했을때 로직 구현에 있어서 많은 보일러 플레이트가 존재 할 수 있다.

const [data, setData] = useState();
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState();

useEffect(() => {
  setIsLoading(true);
  
  fetch(url)
    .then(res => res.json())
    .then(json => setData(json))
    .catch(err => setError(err))
    .finally(() => setIsLoading(false));
}, [url]);

위 코드를 SWR을 사용하여 개선한다고 해보았을때

import useSWR from 'swr';
const { data, error, isLoading } = useSWR('/api/user', fetcher);

매우 간결하게 코드를 작성할 수 있음을 볼 수 있다.

3. SWR훅의 옵션객체를 통한 편리한 로직

첫번째로 SWR훅의 옵션객체중 refreshInterval를 통해 실시간 폴링을 손쉽게 구현이 가능하다.

실시간 폴링 : 주기적으로 서버에 요청을 보내 최신 데이터를 가져오는 방식

useEffect로 실시간 폴링을 구현한다고 했을때

import { useEffect, useState } from 'react';

export default function LiveScore() {
  const [data, setData] = useState();

  useEffect(() => {
    const fetchData = () => {
      fetch('/api/score').then(r => r.json()).then(setData);
    };
    fetchData();
    const id = setInterval(fetchData, 5000);
    return () => clearInterval(id);
  }, []);

  return <div>{data?.score}</div>;
}

위 코드처럼 폴링 로직을 매번 작성하며 clearInterval을 사용한 클린업까지 수동적으로 작성해야한다.

위 코드를 SWR을 사용하여 개선한다고 해보았을때

import useSWR from 'swr';
const fetcher = (url) => fetch(url).then(r => r.json());

export default function LiveScore() {
  const { data } = useSWR('/api/score', fetcher, { refreshInterval: 5000 });
  return <div>{data?.score}</div>;
}

refreshInterval : time(ms)을 작성해주기만 한다면 자동적으로 클린업까지 작동하여 간편하게 구현이 가능하다.

그외에도 많은 옵션들이 있다. 기본값으로 설정되있는것을 기능구현에 맞게 의도적으로 변경하여 맞게 쓰면된다.
revalidateOnFocus : 탭 포커스시 자동 재검증 ( 기본값 : true )
revalidateOnReconnect : 네트워크 재연결시 자동 재검증 ( 기본값 : true )
shouldRetryOnError : fetcher에 에러가 있을 때 재시도 ( 기본값 : true )

SWR을 사용하면서..

SWR을 써보면서 서버 최신 데이터를 가져오면서 캐싱전략으로 성능 최적화까지 손쉽게 잡는것을 볼 수 있었다.
또한, 수많은 보일러 플레이트를 단 몇줄로 코드의 수가 매우 줄어든것으로 유지보수 및 가독성면에서 좋다고 생각한다.

다만, 사용하는게 좋지 않는 상황도 숙지를 해야한다. 예를들어 한번만 실행되는 CRUD 로직에서 SWR의 특징이 필요할까? 캐시 및 재검증을 사용함으로써 얻는 이점이 거의 없다고 보면된다.
다른 예시로 데이터가 거의 안바뀌고 그 데이터 크기도 클때 재검증을 지속적으로 하는것이 올바르지는 않을것이다.

참조 : SWR 공식 문서

profile
Developer

0개의 댓글