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