iOS 모바일 onBlur 처리 순서

3 minute read
2024-08-21

iOS 모바일에서 form 에 바깥을 클릭했을 때 confirm 으로 확인하도록 로직을 작성했다. 그래서 아래와 같은 커스텀 훅을 작성하면 될 것 같았다.


초기 hook
function useFormFocusState() {
  const [isAnyFocus, setIsAnyFocus] = useState(false)
 
  const checkFocus = useCallback(() => setIsAnyFocus(true), [])
  const uncheckFocus = useCallback(() => setIsAnyFocus(false), [])
 
  return {
    isAnyFocus,
    onFocus: checkFocus,
    onBlur: uncheckFocus,
  }
}


그러고 기쁜 마음에 컴포넌트에 적용해보았다.


onFocus onBlur 적용
function MyComponent() {
  const { isAnyFocus, ...anyFocusProps } = useFormFocusState()
  
  const handleOpenChange = useCallback(
    (open: boolean) => {
      if (!open) {
        // 닫을려고 할때
        if (isAnyFocus) {
          // NOTE: 해당 요소의 onBlur 에서 직접 처리. 여기서 호출 X
          return
        }
 
        const result = confirm(
          '입력하신 정보가 저장되지 않습니다.\n취소하시겠습니까?'
        )
        if (result) {
          setOpen(false)
          form.reset()
          return
        }
      } else {
        setOpen(true)
      }
    },
    [isAnyFocus, form]
  )
  
  return (
    <Modal onOpenChange={handleOpenChange}>
    	{/* ... */}
    	<FormControl {...anyFocusProps}>
    </Modal>
  )
}


PC에서 확인했을 때, 의도한 대로 동작했다. 끝난 줄 알았다.


하지만, 모바일에선 바로 confirm 이 먼저 나왔다. window.alert() 로 확인해보니 순서가 아래와 같이 진행되었기 때문에 문제가 되었다.


이벤트 순서
focus 
  -> (isAnyFouc true)
  -> 바깥 클릭 
  -> (isAnyFocus false)
  -> onOpenChange()


개선 코드

개선 hook
function useFormFocusState() {
	// ...
  
  const uncheckFocus = useCallback(() => {
    const isIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
 
    if (isIos) {
      setTimeout(() => setIsAnyFocus(false), 0)
    } else {
      setIsAnyFocus(false)
    }
  }, [])
 
	// ...
}


비동기로 처리되는 상태 변경 흐름에서 후순위 로 처리하기 위해 macro queue 에 콜백이 적재되도록 setTimeout 을 호출해서 해결하였다.