빠르게 만들고 나서야 보이는 것들: Optimistic UI가 실제 사용자를 만났을 때

빠르게 만들고 나서야 보이는 것들: Optimistic UI가 실제 사용자를 만났을 때

데모에서 완벽했던 UI가 다섯 번의 클릭에 무너지기까지—MVP를 배포하고 나서야 발견되는 인터랙션 버그의 본질.

Optimistic UI useOptimistic 레이스 컨디션 Next.js 16 MVP 배포 Server Actions UX 품질 프로덕트 사고
광고

'완벽하게 작동한다'는 확신은 대개 혼자 테스트했을 때의 이야기다. 체크박스는 즉각 반응하고, 스피너도 없고, 지연도 없다. 데모는 깔끔하다. 그리고 그 순간, 우리는 배포 버튼을 누른다.

dev.to에 올라온 한 글이 이 흐름의 균열을 정확하게 포착했다. Next.js 16의 useOptimistic으로 태스크 리스트를 구현한 개발자가 스스로 "다섯 번 연속으로 빠르게 클릭해봤을 때"를 묘사한 장면이다. UI는 멀쩡해 보였다. 그러나 데이터베이스에는 다섯 개의 요청이 제각각 순서로 도착했고, 같은 boolean 값이 레이스 컨디션 속에서 뒤섞였다. 에러 로그는 없었다. 화면도 조용했다. UI는 그냥 조용히 현실과 어긋나 있었다.

이것이 Optimistic UI가 가진 고유한 함정이다. 이 패턴의 본질은 '일단 맞다고 보여주는 것'이기 때문에, 실제로 틀렸을 때도 맞아 보인다. 보이는 것과 실제로 일어난 것이 같다는 보장은 없다. 그 간극을 개발자 본인은 데모 단계에서 절대 발견할 수 없다.


핵심 이슈: 레이스 컨디션은 느린 사용자가 만들지 않는다

문제의 해법 자체는 복잡하지 않다. 폼 전송 중 버튼을 비활성화하는 것과 같은 원리를 체크박스 단위로 적용하면 된다. 요청이 처리 중인 아이템 ID를 pendingId로 추적하고, 동일한 행의 체크박스를 disabled 처리한 뒤 요청이 완료되면 해제한다. 전체 리스트에 글로벌 락을 거는 것이 아니라 행 단위로 스코프를 좁혀야 한다는 점이 핵심이다.

이 패턴이 중요한 이유는 단순히 버그를 막아서가 아니다. 실제로 발생하지 말아야 할 요청 자체를 차단한다는 점에서 데이터베이스 부하와 네트워크 비용까지 함께 줄인다. 두 번째 클릭이 첫 번째 요청과 경합하지 않으면, 불필요한 쓰기 연산도 사라진다.

롤백 처리에서도 흔한 오해가 있다. 많은 예제가 실패 시 setOptimistic을 다시 호출해서 수동으로 되돌리는 패턴을 보여주지만, useOptimistic은 트랜지션이 완료되는 순간 자동으로 원래 상태로 복귀한다. 요청이 실패하고 서버 상태가 바뀌지 않았다면 체크박스는 알아서 원래 값으로 돌아온다. 예외는 서버가 아직 확인하지 않은 새 항목—예를 들어 임시 ID로 추가한 댓글—처럼 클라이언트에만 존재했던 값이다. 이 경우에만 catch 블록에서 직접 제거 로직을 처리해야 한다.

Next.js 16의 캐시 무효화 전략도 선택이 중요하다. revalidatePath는 단일 경로, revalidateTag는 태그 단위의 stale-while-revalidate, updateTag는 현재 페이지에 즉각 반영이 필요할 때 사용한다. 사용자가 클릭하며 바라보고 있는 체크박스라면 updateTag가 맞고, 사이드바의 통계처럼 실시간성이 덜한 곳은 revalidateTag로 비동기 갱신하면 된다. 최신 Next.js 16에서는 revalidateTag에 두 번째 인자가 필수가 됐고, 단독 호출은 TypeScript 에러를 낸다는 점도 주의해야 한다.


맥락 해석: 이건 Next.js 버그가 아니라 프로덕트 사고의 문제다

같은 날 dev.to에서 읽은 또 다른 글이 이 문제를 다른 각도에서 설명한다. MVP를 조기에 배포해야 하는 이유를 다루는 글인데, 핵심 문장이 이렇다. "제품이 공개되기 전에는 모든 결정이 아직 통제 가능하다고 느껴진다." 온보딩을 다시 쓸 수 있고, 빈 상태를 더 예쁘게 만들 수 있고, 엣지 케이스는 나중에 처리하면 된다고 생각한다.

Optimistic UI 레이스 컨디션은 바로 이 구간에서 만들어지는 버그다. 개발자 혼자 테스트하는 환경에서는 절대로 다섯 번을 빠르게 클릭하지 않는다. 실제 사용자만이 그렇게 행동한다. 그 피드백 루프 없이는 버그가 보이지 않는다.

Claude Code로 300개가 넘는 브라우저 도구를 혼자 만든 사례도 같은 맥락을 보여준다. AI 도구 덕분에 빠르게 프로토타입을 만들고 배포까지 이어갈 수 있었지만, 진짜 사용자를 어떻게 만날 것인지, 신뢰를 어떻게 얻을 것인지는 여전히 풀리지 않은 문제로 남아있다. 도구가 빨라질수록 '코드 완성'과 '사용자와의 만남' 사이의 간극이 더 뚜렷하게 드러난다.


시사점: AI가 속도를 올려줄수록, 인터랙션 엣지케이스는 더 빨리 만나게 된다

세 사례가 가리키는 방향은 하나다. 빠르게 만드는 능력이 올라갈수록, 실제 사용자 인터랙션에서 터지는 엣지케이스를 더 빨리 마주하게 된다. AI 도구가 Optimistic UI 코드를 뚝딱 생성해준다고 해도, 다섯 번 연속 클릭 시나리오는 프롬프트에서 나오지 않는다. 그건 사용자가 알려주는 정보다.

프로덕트 사고로 다시 읽으면, Optimistic UI 자체는 훌륭한 UX 패턴이다. 사용자에게 즉각적인 피드백을 주고 지연을 숨겨 경험을 매끄럽게 만든다. 문제는 이 패턴이 '보이는 것'과 '실제 상태'의 간극을 의도적으로 활용한다는 점이고, 그 간극이 잘못 관리될 때 조용히 데이터를 망가뜨린다는 것이다. 화려한 인터랙션일수록 실패 경로 설계가 더 정밀해야 한다.

개발 흐름으로 보면 권장할 수 있는 순서는 이렇다. 먼저 낙관적 업데이트로 빠른 피드백 경험을 만들고 배포한다. 실제 사용자가 붙으면 예상하지 못한 인터랙션 패턴이 나온다. 그 신호를 받아 요청 직렬화, 롤백 전략, 캐시 무효화 범위를 세밀하게 조정한다. 이 사이클을 거치지 않으면 아무리 완성도 높은 UI도 실험실 밖에서는 다른 얼굴을 한다.


전망: '빠른 배포' 이후가 진짜 프론트엔드 엔지니어링이다

Next.js 16의 error.tsxunstable_retry prop을 추가한 것은 상징적이다. reset은 에러 화면을 지우고 같은 컴포넌트를 다시 렌더링하지만, 데이터를 다시 불러오지는 않는다. 데이터 페칭 실패로 생긴 에러라면 unstable_retry를 써야 서버에서 세그먼트를 새로 렌더링해 실제 문제를 복구할 수 있다. 프레임워크가 이 구분을 명시적으로 드러낸다는 것 자체가, 실패 경로에 대한 설계가 이제 선택이 아닌 필수가 됐음을 의미한다.

결국 지금 프론트엔드 개발의 핵심 긴장은 여기에 있다. AI 도구와 현대적인 프레임워크가 '빠르게 만드는 것'의 진입 장벽을 낮추는 동안, '실제 사용자가 경험하는 품질'의 책임은 오히려 더 선명하게 개발자에게 남는다. 데모에서 완벽했던 체크박스가 다섯 번의 클릭에 조용히 무너지는 순간, 우리는 비로소 진짜 프로덕트를 만들고 있는 것이다.

출처

더 많은 AI 트렌드를 Seedora 앱에서 확인하세요