본문 바로가기
Frontend2025년 4월 22일8분 읽기

SWR 심화 — 낙관적 업데이트와 뮤테이션 전략

YS
김영삼
조회 154

SWR 뮤테이션 기본 개념

SWR은 "stale-while-revalidate" 전략으로 데이터를 패칭하는 React 훅 라이브러리입니다. 읽기(GET) 뿐 아니라 쓰기(POST/PUT/DELETE) 이후의 캐시 갱신도 중요합니다. mutate 함수를 활용하면 서버 응답을 기다리지 않고 UI를 즉시 업데이트하는 낙관적 업데이트가 가능합니다.

기본 뮤테이션 패턴

import useSWR, { mutate } from 'swr';

function TodoList() {
  const { data: todos, mutate: boundMutate } = useSWR('/api/todos', fetcher);

  // 1. 기본 뮤테이션 - 재검증 트리거
  const addTodo = async (text) => {
    await fetch('/api/todos', {
      method: 'POST',
      body: JSON.stringify({ text }),
    });
    // 캐시 무효화 → 자동 재요청
    boundMutate();
  };

  // 2. 서버 응답으로 캐시 직접 업데이트
  const addTodoWithData = async (text) => {
    const newTodo = await fetch('/api/todos', {
      method: 'POST',
      body: JSON.stringify({ text }),
    }).then(r => r.json());

    // 재요청 없이 캐시 직접 갱신
    boundMutate([...todos, newTodo], false);
  };
}

낙관적 업데이트 구현

낙관적 업데이트는 서버 응답을 기다리지 않고 UI를 먼저 갱신합니다. 요청이 실패하면 이전 상태로 롤백합니다. 이 패턴은 사용자 경험을 크게 향상시킵니다.

function TodoItem({ todo }) {
  const { data: todos, mutate } = useSWR('/api/todos', fetcher);

  const toggleComplete = async () => {
    const updatedTodo = { ...todo, completed: !todo.completed };

    // 낙관적 업데이트: UI 먼저 갱신
    await mutate(
      async (currentTodos) => {
        // 실제 API 호출
        const result = await fetch(`/api/todos/${todo.id}`, {
          method: 'PATCH',
          body: JSON.stringify({ completed: !todo.completed }),
        }).then(r => r.json());

        return currentTodos.map(t => t.id === todo.id ? result : t);
      },
      {
        // 낙관적 데이터 (즉시 반영)
        optimisticData: todos.map(t =>
          t.id === todo.id ? updatedTodo : t
        ),
        // 실패 시 롤백
        rollbackOnError: true,
        // 성공 후 재검증 생략
        revalidate: false,
      }
    );
  };

  const deleteTodo = async () => {
    await mutate(
      fetch(`/api/todos/${todo.id}`, { method: 'DELETE' }).then(() =>
        todos.filter(t => t.id !== todo.id)
      ),
      {
        optimisticData: todos.filter(t => t.id !== todo.id),
        rollbackOnError: true,
        revalidate: false,
      }
    );
  };
}

글로벌 뮤테이션과 캐시 무효화

import { mutate } from 'swr';

// 특정 키의 캐시 무효화
mutate('/api/todos');

// 정규표현식으로 여러 키 무효화
mutate(
  key => typeof key === 'string' && key.startsWith('/api/todos'),
  undefined,
  { revalidate: true }
);

// 모든 캐시 무효화 (로그아웃 시 유용)
mutate(() => true, undefined, { revalidate: false });

// 관련 캐시 연쇄 갱신
async function createTodo(text) {
  const newTodo = await api.createTodo(text);
  // 목록 캐시 갱신
  mutate('/api/todos', todos => [...todos, newTodo], false);
  // 카운트 캐시 갱신
  mutate('/api/todos/count', count => count + 1, false);
  // 대시보드 통계도 갱신
  mutate('/api/dashboard/stats');
}

useSWRMutation으로 명시적 트리거

import useSWRMutation from 'swr/mutation';

async function createTodo(url, { arg }) {
  return fetch(url, {
    method: 'POST',
    body: JSON.stringify(arg),
  }).then(r => r.json());
}

function AddTodoForm() {
  const { trigger, isMutating, error } = useSWRMutation(
    '/api/todos',
    createTodo
  );

  const handleSubmit = async (e) => {
    e.preventDefault();
    const text = e.target.text.value;
    try {
      const result = await trigger({ text });
      // 성공 처리
    } catch (e) {
      // 에러 처리
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="text" disabled={isMutating} />
      <button disabled={isMutating}>
        {isMutating ? '추가 중...' : '추가'}
      </button>
      {error && <span>오류 발생</span>}
    </form>
  );
}

뮤테이션 패턴 비교

패턴UX데이터 정합성구현 복잡도
재검증만느림 (로딩 표시)항상 최신낮음
서버 응답 반영보통최신중간
낙관적 업데이트빠름 (즉시 반영)실패 시 롤백 필요높음
  • 간단한 토글/삭제는 낙관적 업데이트가 효과적이지만, 복잡한 비즈니스 로직은 서버 응답을 기다리는 것이 안전합니다
  • useSWRMutation은 form 제출같은 명시적 액션에 적합하며, 자동 재검증과 분리됩니다
  • 에러 바운더리와 함께 사용하면 뮤테이션 실패를 우아하게 처리할 수 있습니다

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록