Next.js
React 모달의 Escape 처리가 입력 중에도 닫히던 실수
모달에 Escape 키 닫기를 추가했다가 자동완성 입력까지 닫아 버린 문제를 수정한 짧은 회고입니다.
ESC 하나가 폼을 날렸다
상품 편집 모달에 Escape 닫기를 넣은 뒤, 태그 입력의 자동완성 목록을 닫으려 ESC를 누르면 모달까지 사라졌다. 아직 저장하지 않은 제목과 설명도 함께 사라져서 꽤 불편했다. 키 이벤트가 전역으로 올라간다는 점을 가볍게 본 실수였다.
기존 코드는 너무 단순했다
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') onClose()
}
window.addEventListener('keydown', onKeyDown)
return () => window.removeEventListener('keydown', onKeyDown)
}, [onClose])
이 코드는 현재 포커스가 어디에 있는지, 하위 컴포넌트가 Escape를 이미 처리했는지 묻지 않는다. 특히 combobox나 date picker는 Escape를 자기 목록 닫기에 쓰는 경우가 많다.
닫기 전에 두 가지를 확인했다
하위 위젯이 이벤트를 처리했다면 defaultPrevented로 물러나고, IME 조합 중에는 키 입력을 건드리지 않도록 했다.
const onKeyDown = (event: KeyboardEvent) => {
if (event.key !== 'Escape') return
if (event.defaultPrevented || event.isComposing) return
const target = event.target as HTMLElement
if (target.closest('[data-escape-handled="true"]')) return
onRequestClose()
}
자동완성 컴포넌트가 목록을 닫을 때는 event.preventDefault()를 호출하거나 data-escape-handled 영역으로 표시했다. 프로젝트에서 이미 사용하는 접근성 라이브러리가 있다면, 직접 window 리스너를 만드는 대신 그 라이브러리의 Dialog 구현을 우선 쓰는 편이 낫다.
닫힘은 파괴적인 동작이다
마지막으로 onRequestClose에서 변경 사항이 있을 때 확인 대화상자를 띄웠다. Escape, 바깥 클릭, 닫기 버튼이 모두 같은 경로를 지나게 해서 한 군데만 예외가 되는 문제도 줄였다.
점검한 항목은 네 가지다.
- 자동완성 목록이 열린 입력에서 Escape를 누른다.
- 한글 조합 중 Escape를 누른다.
- 변경 사항이 있는 모달과 없는 모달을 각각 닫는다.
- Tab 포커스가 모달 밖으로 빠져나가지 않는지 본다.
키보드 단축키는 편의 기능이지만, 편집 내용을 버리는 기능과 연결될 때는 가장 안쪽 컴포넌트의 의도를 먼저 존중해야 했다.