완성도 다듬기
4번째 글이 사고 대응기였다면, 이번엔 사고가 아니라 이미 잘 돌아가던 걸 더 낫게 만든 이야기다. 인터랙티브 UI를 처음 만든 순간부터 로그인 설계를 다시 고민한 것, 마지막엔 AI 에이전트한테 완성된 사이트를 감사시켜본 경험까지 — 기능을 추가하는 게 아니라 완성도에 시간을 쓴 기록이다.
커서 스포트라이트
라이브러리 없이 커서를 따라다니는 히어로 스포트라이트를 만들었다. --spot-x, --spot-y CSS 변수를 pointermove로 갱신하고, mask-image: radial-gradient(...)로 격자를 도려내는 방식이다.
el.style.setProperty("--spot-x", `${e.clientX - rect.left}px`);
el.style.setProperty("--spot-y", `${e.clientY - rect.top}px`);
.hero-grid {
mask-image: radial-gradient(280px circle at var(--spot-x, 50%) var(--spot-y, 42%), #000 0%, transparent 76%);
}
만들면서 진짜 삽질도 했다. 마스크를 건 요소랑 흐르는 배경 애니메이션을 같은 요소에 걸었더니, 마스크가 배경이랑 같이 밀려나면서 스포트라이트가 커서를 벗어나는 문제가 생겼다. 둘을 별도 DOM 노드로 분리해서 해결했다. 오로라 배경은 scale 대신 translate3d만 써서, blur가 걸린 큰 레이어의 재래스터화 비용을 피했다. prefers-reduced-motion과 pointer: fine 가드도 전 구간에 깔아서, 모션을 원치 않는 환경이나 터치 기기에서는 아예 아무 일도 일어나지 않게 처리했다.
후속으로 드롭다운도 손봤다. 트리거에서 4px 떨어져 있다 보니 마우스가 그 틈을 지나가는 순간 onMouseLeave가 먼저 발동해서 메뉴가 닫혀버리던 버그가 있었는데, 포인터가 그레이스 기간 안에 다시 들어오면 취소되는 지연 닫기 패턴으로 고쳤다. 이 타이밍에 손으로 그린 SVG 아이콘들도 phosphor-icons로 정리했다.
익명 로그인 설계
4번째 글의 KOE205 사고 이후 scope 요청 자체를 포기하고 나니, "그럼 닉네임은 어떻게 보여주나"라는 원래 문제가 남았다. 답은 의외로 간단했다. 카카오에서 아무 정보도 안 받아오고 사용자${kakaoId.slice(-4)}로 시작시킨 뒤, 로그인한 사람이 언제든 원하는 이름으로 바꾸게 하면 됐다. 오히려 실명이나 카카오 프로필에 덜 의존하는 구조라 더 나은 설계였다.
const kakaoNickname =
profile.properties?.nickname ??
profile.kakao_account?.profile?.nickname ??
`사용자`;
처음엔 "닉네임을 바꾸라"는 팝업이나 배너를 고민했다. 대신 거창한 안내 없이 계정 드롭다운 안에서 바로 텍스트를 눌러 인라인으로 바꾸는 쪽으로 결정했다. 기본값을 아직 안 바꾼 계정에는 작은 점 하나로만 살짝 신호를 준다. 닉네임을 바꾸면 서버가 새 JWT를 재발급해야 한다는 것도 짚어둘 만하다 — 토큰 안에 닉네임이 박혀 있어서다.

AI 에이전트로 UI 감사
기능은 다 있는데 "완성도"는 다른 문제라는 걸 깨닫고, UI 감사 전용 에이전트 스킬(Impeccable)로 사이트 전체를 훑게 했다. 이 블로그 자체가 AI와 협업해서 만들어지고 있다는 점에서, 다른 글들과는 결이 다른 소재다.
실제로 잡아낸 것들은 이랬다.
- -글·댓글 삭제는 밋밋한
window.confirm()인데, 카테고리 삭제는 실시간 게시글 수까지 보여주는 제대로 된 모달이었던 불일치. 공통ConfirmDialog(Esc로 취소, 포커스 자동 이동)로 통일했다. - -카테고리를 지운 뒤에도 예전 글이 그 슬러그를 그대로 들고 있어서, 그 글에 들어가면 브레드크럼이 비어있고 "목록으로" 링크가 존재하지 않는 페이지로 가던 고아 링크 버그.
- -카드 그리드 페이지들이
<h1>에서 바로 카드의<h3>로 건너뛰어서 스크린리더 탐색이 깨지던 헤딩 계층 문제. - -About 페이지 스킬 배지 글자색이 대비 4.4:1로, WCAG AA 기준(4.5:1)에 살짝 못 미쳤던 것.
- -StatsStrip과 댓글 목록만 유리질감 표면 처리가 빠져있어서 주변 카드들보다 밋밋해 보이던 시각적 불일치.
// window.confirm("삭제하시겠습니까?") 대신
<ConfirmDialog
message={`이 게시글을 삭제하시겠습니까? (현재 ${count}개)`}
onConfirm={handleDelete}
/>
기능 테스트는 통과해도 이런 건 안 걸린다. 사람이 직접 눈으로 보지 않으면 놓치기 쉬운 종류의 결함들이었다.
정리
기능을 채우는 시간과 완성도를 높이는 시간은 다른 종류의 일이었다. 4번째 글이 사고를 막는 이야기였다면, 이번 글은 아무도 시키지 않은 걸 다듬는 이야기다.