
(서버 응답을 기다리지 않는 UI 업데이트)
데이터 양이 많은 테이블에서 토글 상태를 변경하는 기능을 구현하던 중,
서버 응답 속도에 따라 UI 반응이 눈에 띄게 느려지는 문제를 겪었다
기능적으로는 문제가 없었지만,
사용자 입장에서는 “클릭했는데 왜 바로 안 바뀌지?” 라는 불편함을 느낄 수 있는 상황이었고,
이를 개선하기 위해 서버 응답 이전에 UI를 먼저 업데이트하는 방식인 낙관적 업데이트(Optimistic UI)를 적용했다
이 글에서는
✔️ 왜 기존 방식이 UX를 저해했는지
✔️ 이를 어떻게 개선했는지
✔️ 적용하면서 고려했던 장단점과 설계 포인트
를 정리해보려 한다
서버에 상태 변경 요청을 보낸 뒤,
응답을 기다리지 않고 UI를 먼저 변경하는 방식이다
1️⃣ 서버 응답 속도와 무관하게 즉각적인 사용자 피드백 제공
2️⃣ 네트워크 상태가 좋지 않거나 응답 시간이 길어도 체감 UX 저하 최소화
특히 토글, 체크박스처럼 즉각적인 반응이 중요한 UI에 효과적
1️⃣ 서버에서 오류가 발생하면 잠시 동안 잘못된 상태가 화면에 표시될 수 있음
⚠️ 반드시 오류 발생 시 상태를 되돌릴 수 있는 설계(롤백)가 필요


토글 변경 시:
뮤테이션 요청 👉 서버 응답 완료 👉 refetch로 데이터 재조회
👉 그 후에야 UI 반영
const handleUpdateMappingInfoComplete = async (
isComplete: boolean,
mappingInfoUuidList: string[],
refetch: () => void,
) => {
await updateMappingInfoShopComplete({
variables: {
isComplete,
mappingInfoUuidList,
},
onCompleted: () => {
refetch();
getSnackbar("작업 완료 처리되었습니다.", "success");
},
onError: () => {
getSnackbar("작업 완료 처리에 실패했습니다.", "error");
},
});
};
<Toggle
checked={item.isRebarShopComplete}
handleChange={(e) => {
handleUpdateMappingInfoComplete(
e.target.checked,
[item.uuid],
handleRefetchMappingInfo,
);
}}
/>
서버 응답이 느릴 경우 토글이 한 박자 늦게 바뀌는 현상
⚠️ 대용량 테이블일수록 refetch 비용이 커져 체감 지연이 더 커짐
사용자는 “클릭이 안 된 것처럼” 느끼게 됨
UI 상태 변경과 서버 요청을 분리 👉 UI는 로컬 state를 즉시 변경 👉 서버 요청은 비동기로 처리
👉 최종 정합성은 refetch로 보장
const handleMappingInfoComplete = (
uuidList: string[],
isComplete: boolean,
) => {
const updatedFloorShopDrawing = floorShopDrawing.map((item) => ({
...item,
MappingInfo: item.MappingInfo.map((m) =>
uuidList.includes(m.uuid)
? { ...m, isRebarShopComplete: isComplete }
: m,
),
}));
setPaginatedData(updatedFloorShopDrawing.slice(startIndex, endIndex));
};
👉 사용자는 클릭 즉시 토글이 바뀐 것을 확인할 수 있음
const handleUpdateMappingInfoComplete = async (
isComplete: boolean,
mappingInfoUuidList: string[],
refetch: () => void,
) => {
await updateMappingInfoShopComplete({
variables: {
isComplete,
mappingInfoUuidList,
},
onCompleted: () => {
getSnackbar("작업 완료 처리되었습니다.", "success");
},
onError: () => {
getSnackbar("작업 완료 처리에 실패했습니다.", "error");
},
});
// 성공/실패와 관계없이 refetch
refetch();
};
<Toggle
checked={item.isRebarShopComplete}
handleChange={(e) => {
handleMappingInfoComplete([item.uuid], e.target.checked);
handleUpdateMappingInfoComplete(
e.target.checked,
[item.uuid],
handleRefetchMappingInfo,
);
}}
/>
서버 요청이 실패한 경우 로컬 state는 성공한 것처럼 보일 수 있음
이를 방치하면 UI와 서버 상태가 불일치하기 때문에 성공/실패 여부와 상관없이 refetch 실행
실패 시 서버의 실제 데이터로 다시 덮어씌워져 자동 롤백
👉 사용자에게는 에러 스낵바로 명확한 피드백 제공
서버 응답을 기다리는 UI는 기능적으로는 맞지만, UX 관점에서는 부족할 수 있음
특히 토글 / 체크박스 / 상태 변경 UI에서는 즉각적인 피드백이 중요
로컬 state를 활용한 선행 UI 업데이트 + refetch 기반 정합성 보장은
UX와 데이터 신뢰성을 모두 잡을 수 있는 현실적인 선택지이다
“서버를 기다리게 하지 말고, 사용자는 바로 반응하게 하자.”