이 장에서는 서버 상태를 클라이언트 상태로 복제해 사용할 때 발생하는 문제와, 이에 대응하는 방법을 살펴본다.
이 문제는 문법 오류처럼 즉시 드러나는 버그가 아니다. 오히려 팀 프로젝트에서 백엔드 API 구현을 기다리며 프론트엔드 개발자가 작성한, 로컬 환경에서는 아무 문제 없이 동작하던 코드에서 시작된다.
API를 연결해 배포하는 과정에서 우리는 화면에 보이는 데이터가 서버의 현재 상태를 반영한다고 가정하기 쉽다. 그러나 배포 환경에서는 다양한 외부 변수로 인해 화면에 보이는 상태가 서버의 현재 상태와 어긋날 수 있으며, 이에 따라 발생하는 차이를 어떤 기준으로 다룰 것인지는 더 이상 서버가 아니라 클라이언트의 몫이 된다.
이 장의 핵심은 단순하다. 서버 상태를 컴포넌트 상태로 복제하는 순간, 클라이언트는 서버의 현재 상태를 직접 확인할 수 없는 상황에서 자신이 들고 있는 스냅샷을 기준으로 동작하게 되며, 그로 인해 발생하는 불일치를 처리해야 하는 책임을 떠안게 된다. 이 장에서는 이러한 책임을 이후부터 정합 책임이라고 부른다.
이번 장에서는 서버 상태를 로컬 상태로 복제해 사용하는 구조에서 어떤 문제가 발생하는지를 단계적으로 살펴본다.
실제 프로젝트에서는 API를 연결하기 전, 요구사항을 기준으로 화면의 동작부터 구현하는 경우가 많다. 이번 절에서는 그런 흐름에 따라, 간단한 카운터 프로그램을 먼저 만들어본다.
카운터 프로그램의 요구사항은 다음과 같다.
import { useState } from "react";
export default function Counter() {
const [items, setItems] = useState([
{ id: crypto.randomUUID(), value: 1 },
]);
const handleCreate = () => {
const created = { id: crypto.randomUUID(), value: 0 };
setItems((prev) => [created, ...prev]);
};
const handleUpdate = (id) => {
setItems((prev) =>
prev.map((p) => (p.id === id ? { ...p, value: p.value + 1 } : p))
);
};
const handleDelete = (id) => {
setItems((prev) => prev.filter((p) => p.id !== id));
};
return (
<>
<button onClick={handleCreate}>Create</button>
<ul>
{items.map((item) => (
<li key={item.id}>
{item.value}
<button onClick={() => handleUpdate(item.id)}>+1</button>
<button onClick={() => handleDelete(item.id)}>Delete</button>
</li>
))}
</ul>
</>
);
}
코드 1_1
이렇게 개발 환경에서 요구사항을 충족하는 프로그램이 완성되었다. 이 시점에서 이 코드는 화면의 동작만을 기준으로 보면 문제가 없다.
다만, 변화는 이 코드에 외부 데이터가 개입되기 시작하면서 나타난다.