낙관적 업데이트(Optimistic Update)는 사용자의 경험을 향상시키기 위해 UI의 업데이트를 즉시 반영하는 기술적인 접근 방식이다.
낙관적 업데이트는 주로 네트워크 요청이나 비동기 작업과 관련된 상황에서 사용된다. 사용자가 어떤 작업을 요청하고 그에 대한 응답을 기다리는 동안, 서버에서의 응답을 받을 때까지 기다리지 않고 사용자 인터페이스를 미리 업데이트하는 것을 의미한다.
만약 요청에 실패할 경우 UI와 실제 데이터 간의 불일치가 발생할 수 있으므로 이에 대비하여 에러 처리를 해주어야 한다. 사용자는 값이 수정되었으니 업데이트되었다고 인지하겠지만 사실 요청에 실패하여 서버에는 값이 반영되지 않은 상태이다. 이후 사용자가 재접속하거나 새로고침을 하면 수정되기 이전의 값이 여전히 보이기 때문이다.
이로 인해 낙관적 업데이트는 사용자의 경험이 중요한 실시간 업데이트가 필요한 채팅이나 요청이 실패해도 위험도가 낮은 좋아요 버튼과 같은 부분에서 주로 사용된다.
낙관적 업데이트(Optimistic Update)와 비관적 업데이트(Pessimistic Update)
- Optimistic : 사용자 입력 -> 바로 화면 업데이트 -> 서버에 수정 요청
- Pessimistic : 사용자 입력 -> 서버에 수정 요청 -> 성공 시 화면 업데이트
아래의 코드를 보면 기존(비관적 업데이트)에는 수정 모드일 때는(isEditMode === true) input을 띄우고 수정이 완료되면(isEditMode === false) todo 값을 보여주고 있다.
그런데 <TodoBody>{list.todo}</TodoBody>
이 부분에서 서버의 todo 값을 그대로 보여주고 있다 보니 updateTodo
가 실행되고 서버에 값이 반영될 때까지 약간의 시간이 걸리게 되는데 이 찰나에 수정되기 이전의 값이 잠깐 보였다가 사라지는 현상이 발생했다.
function TodoItem({ list, getTodos }: TodoTypeProps) {
const [editTodoInput, setEditTodoInput] = useState<string>(list.todo);
const [isEditMode, setIsEditMode] = useState<boolean>(false);
const updateTodo = async () => {
try {
if (editTodoInput.length !== 0) {
const editTodo = {
todo: editTodoInput,
isCompleted: list.isCompleted,
};
await TODO_API.put(`/todos/${list.id}`, editTodo);
getTodos();
setIsEditMode(false);
} else {
alert('수정할 내용이 비어있습니다.');
inputRef.current?.focus();
}
} catch (err) {
setIsEditMode(false);
}
};
// ...
return (
<TodoItemContainer>
<Label htmlFor={`todo_${list.id}`}>
<Checkbox
id={`todo_${list.id}`}
type='checkbox'
defaultChecked={list.isCompleted}
onChange={updateTodoCheck}
/>
{isEditMode ? (
<EditTodoBodyInput
data-testid='modify-input'
type='text'
value={editTodoInput}
onChange={onChangeEditTodoInput}
ref={inputRef}
/>
) : (
<TodoBody>{list.todo}</TodoBody> // 서버의 값을 바로 보여주고 있다.
)}
</Label>
// ...
</TodoItemContainer>
);
}
export default TodoItem;
function TodoItem({ list, getTodos }: TodoTypeProps) {
const [editTodoInput, setEditTodoInput] = useState<string>(list.todo);
const [isEditMode, setIsEditMode] = useState<boolean>(false);
const updateTodo = async () => {
try {
if (editTodoInput.length !== 0) {
const editTodo = {
todo: editTodoInput,
isCompleted: list.isCompleted,
};
await TODO_API.put(`/todos/${list.id}`, editTodo);
getTodos();
setIsEditMode(false);
} else {
alert('수정할 내용이 비어있습니다.');
inputRef.current?.focus();
}
} catch (err) {
setIsEditMode(false);
}
};
// ...
return (
<TodoItemContainer>
<Label htmlFor={`todo_${list.id}`}>
<Checkbox
id={`todo_${list.id}`}
type='checkbox'
defaultChecked={list.isCompleted}
onChange={updateTodoCheck}
/>
{isEditMode ? (
<EditTodoBodyInput
data-testid='modify-input'
type='text'
value={editTodoInput}
onChange={onChangeEditTodoInput}
ref={inputRef}
/>
) : (
<TodoBody>{editTodoInput}</TodoBody> // state 값을 보여주고 있다.
)}
</Label>
// ...
</TodoItemContainer>
);
}
export default TodoItem;
const updateTodo = async () => {
try {
if (editTodoInput.length !== 0) {
const editTodo = {
todo: editTodoInput,
isCompleted: list.isCompleted,
};
await TODO_API.put(`/todos/${list.id}`, editTodo);
getTodos();
setIsEditMode(false);
} else {
alert('수정할 내용이 비어있습니다.');
inputRef.current?.focus();
}
} catch (err) {
alert(err);
// 에러 발생 시 수정 모드를 종료하고
// 수정하기 이전의 값(서버의 값)으로 변경하여 리렌더링 시킨다.
setIsEditMode(false);
setEditTodoInput(list.todo);
}
};