iOS 사파리에서 React onBlur가 동작하지 않는 이유
🔗 관련 이슈
- iOS 키보드 ‘Done’ 버튼에서 onBlur가 누락되는 현상과 재현 과정을 정리해 React 팀에 이슈를 제기했으며, 현재까지도 동일한 증상이 논의되고 있습니다.
- React 공식 이슈 트래커 링크
🖐️ 개요
iOS 모바일 웹 환경에서는 입력 필드와 키보드 상호작용 과정에서 React의 onBlur 이벤트가 누락되는 특정 케이스가 존재합니다.
이 문제는 단순한 구현 실수가 아니라, React의 Event 시스템과 iOS WebKit의 독자적인 키보드 이벤트 처리 방식 사이의 간극에서 발생하는 구조적 이슈입니다.
이 글에서는 문제가 발생하는 정확한 조건을 정리하고, React의 이벤트 처리 구조와 iOS 키보드 동작을 함께 분석합니다. 이어서 이 현상이 단순한 버그가 아니라 설계적 트레이드오프의 결과임을 설명하고, 실무에서 바로 적용할 수 있는 우회 방법을 제시합니다.
TL;DR
iOS 모바일 웹에서 키보드 Accessory View의 Done 버튼을 누르면 blur 이벤트는 발생하지만 focusout 이벤트는 트리거되지 않습니다.
React의 onBlur는 focusout을 기반으로 동작하기 때문에, 이 경우 onBlur가 호출되지 않습니다.
iOS 환경에서는 필요할 경우 blur 이벤트를 직접 감지하는 방식으로 우회해야 합니다.
현상: 완료 버튼을 클릭했는데 UI가 갱신되지 않는다.
입력 필드에 포커스가 잡힌 상태에서 Done 버튼을 누르면 키보드는 내려가고 커서는 사라집니다.
사용자 입장에서는 입력이 종료되고 포커스도 해제된 것처럼 보입니다. 그러나 이 시점에 React 컴포넌트에 등록된 onBlur 핸들러는 호출되지 않습니다.
같은 입력 필드에서 다른 영역을 터치해 포커스를 이동시키면 onBlur는 정상적으로 호출됩니다. Android, 태블릿, PC 브라우저에서도 동일한 코드는 문제없이 동작합니다.
즉, 이 문제는 iOS 모바일 웹 + 키보드 Done 버튼이라는 매우 제한적인 조건에서만 발생합니다.
원인 분석
1. React의 onBlur는 blur가 아닌 focusout을 사용한다.
Native blur와 focus 이벤트는 기본적으로 버블링되지 않습니다. 이벤트 위임을 핵심 전략으로 사용하는 React는 이 제약을 피하기 위해, 버블링이 가능한 focusout 이벤트를 감지해 onBlur를 구현합니다.
따라서 React의 onBlur는 Native blur 이벤트와 1:1로 대응하지 않습니다. 내부적으로는 focusout을 기반으로 동작하는 Synthetic Event입니다.
// React DOM Event System 소스 코드 일부 (SimpleEventPlugin)
case 'focusout':
reactEventType = 'blur';
SyntheticEventCtor = SyntheticFocusEvent;
break;2. iOS 키보드 ‘Done’ 버튼의 실제 동작
iOS WebKit 기반 브라우저에서 키패드 Accessory View의 Done 버튼을 누르면 일반적인 터치 이벤트와는 다른 방식으로 처리됩니다. 키보드가 닫힐 때 DOM Native blur 이벤트는 발생하지만, focusout 이벤트는 트리거되지 않습니다. 결과적으로 React의 onBlur 핸들러는 해당 버튼 클릭 시점에 이벤트를 감지하지 못합니다.
그 결과, Native blur가 발생했지만 React의 onBlur는 호출되지 않는 상태가 됩니다.

3. 이것이 버그는 아니다.
React가 focusout을 선택한 것은 일관된 이벤트 모델과 성능 최적화를 위한 합리적인 설계 결정입니다. 대부분의 환경에서는 이 선택이 문제를 일으키지 않습니다.
반면 iOS는 키보드와 브라우저 이벤트 처리에서 독특한 엣지 케이스를 가지고 있으며, 이 지점에서 React의 전제가 깨집니다. 이 문제는 React의 실수라기보다, 라이브러리의 추상화와 브라우저 구현 사이에서 발생한 구조적 간극에 가깝습니다.
실제로 재현 코드와 함께 공식 이슈를 등록한 지 오래되었지만, 근본적인 수정이 어려운 이유도 여기에 있습니다.
해결책
이 문제는 React의 이벤트 시스템 내부에서 해결할 수 없습니다. 따라서 DOM을 직접 조작해서 Native blur 이벤트 처리로 해결이 가능합니다.
React의 이벤트 시스템을 우회하고 DOM을 직접 조작하는건 일반적으로는 안티패턴입니다. 하지만 이 경우처럼 문제 해결을 위한 적절한 해결책일 경우 필요한 선택지입니다.
blur 이벤트는 특정 입력 요소에만 국한되며 복잡한 사이드 이펙트를 유발하지 않습니다. 컴포넌트 언마운트 시 clean-up만 주의하면 실무에서도 안전하게 사용할 수 있습니다.
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
const element = inputRef.current;
if (!element) return;
const handleBlur = (e) => {
onBlur?.(e);
};
element.addEventListener('blur', handleBlur);
return () => {
element.removeEventListener('blur', handleBlur);
};
}, [onBlur]);
return <input ref={inputRef} />;
문제를 어떻게 재정의했는가
이 이슈를 해결하는 과정에서 핵심이었던 것은 클릭 이벤트와 트리거되는 이벤트들을 추적해서 정확한 문제 지점을 파악하는데 있었습니다.
1. 증상 기준의 문제 정의
- Done 버튼 클릭 시 UI가 갱신되지 않는다
- onBlur 핸들러가 호출되지 않는다
이 단계에서는 이벤트가 아예 발생하지 않는다고 판단하기 쉽습니다.
2. 브라우저 기준의 문제 재정의
- CSS :focus 상태는 해제된다
- 커서가 사라지고 키보드가 내려간다
이 단계에서는 DOM 기준으로 포커스가 해제되었음을 확인할 수 있었습니다.
3. 라이브러리 기준의 문제 재정의
- React의 onBlur는 Native blur가 아닌 focusout을 기반으로 동작한다.
- iOS Done 버튼 경로에서는 Native blur는 트리거되며, focusout이 트리거되지 않는다.
결국 문제는 "blur가 발생하지 않았다"가 아니라, "React가 감지하는 이벤트 경로에서 blur 핸들러가 실행되지 않았다"는 점에 있었습니다.
결론
프론트엔드 개발은 브라우저와 라이브러리 사이를 오가는 작업이 많습니다. 이번 이슈처럼, 표면적으로는 단순해 보이는 문제도 내부를 들여다보면 꽤 깊은 구조를 가지고 있습니다.
이 문제를 정리하며 재현 코드를 만들고 공식 이슈로 공유하는 과정에서 라이브러리의 설계 철학이 모든 환경과 엣지 케이스를 포괄하는 것은 현실적으로 쉽지 않다는 점을 다시 한 번 느꼈습니다.
이런 이유로 개발자에게는 의도적으로 추상화의 안쪽으로 들어가 보는 과정이 필요하다고 생각합니다.
완성도 높은 추상화 위에서 개발하더라도, 특정 환경에서는 그 가정이 깨질 수 있습니다.
중요한 것은 “누구의 잘못인가”가 아니라, 문제의 구조를 이해하고 필요하다면 추상화를 벗어난 선택을 할 수 있는가입니다.
이 글이 iOS 환경에서 React 이벤트를 다룰 때 한 번쯤 참고할 수 있는 사례가 되기를 바랍니다.