PPI-957 분석 및 수정 내용 정리

📅 2026-05-19 🌿 브랜치: PPI-955 📁 변경 파일: apps/web/hooks/use-sanitized-input.ts

🔍 이슈 배경

발생 현상

게스트 입장 페이지의 아동 이름 입력 필드에서 iOS/iPad의 10키 한글 키보드로 입력 시, 글자가 누락되는 현상.

예시: 전해온 입력 시 → 전온 으로 저장됨 ("해" 누락)

영향 범위

⌨️ iOS 10키 한글 키보드 동작 원리

iOS 10키 한글 키보드(천지인·모아)는 복합 모음을 만들 때 중간점(·, U+00B7)을 내부 조합 문자로 사용합니다.

모음10키 조합내부 표현 예시
ㅏ + ·ㅏ·
ㅓ + ·ㅓ·
ㅏ + · + ·ㅏ··

"해"(ㅎ + ㅐ) 를 입력할 때 브라우저가 받는 조합 중간 상태:

ㅎ 입력 → → ㅏ 입력 → 해ㅏ중간점(·) 적용 완성

중간점이 적용되는 순간에 브라우저가 compositionend 이벤트를 발화합니다.

🐛 근본 원인 분석

apps/web/hooks/use-sanitized-input.ts

문제 코드

onCompositionEnd: (e: CompositionEvent<HTMLInputElement>) => {
  isComposingRef.current = false;
  setValue(sanitize(e.currentTarget.value)); // ← 원인
},

버그 발생 시퀀스 ("전해온" 입력 시)

① compositionstart

isComposing = true

② onChange — "전ㅎ"

isComposing=true → raw 값 그대로 저장

③ onChange — "전ㅎㅏ"

isComposing=true → raw 값 그대로 저장

④ compositionend 발화 (중간점 · 적용 시점)

e.currentTarget.value = "전ㅎ" (ㅐ 완성 전 중간 상태)
setValue(sanitize("전ㅎ")) 호출
React가 DOM input value를 즉시 "전ㅎ"으로 덮어씀

⑤ iOS IME 조합 상태 파괴

DOM이 "전ㅎ"으로 리셋되어 중간점(·)을 적용해 ㅐ를 완성하려던 IME 내부 상태가 깨짐
"해" 전체 유실

⑥ "온" 입력 정상 처리

새 composition이 시작되어 "온"은 정상 입력 → 최종값 "전온"

removeSpecialChars가 문제를 심화하는 이유

// apps/web/lib/utils.ts
export function removeSpecialChars(value: string): string {
  return value.replace(/[^a-zA-Z0-9ㄱ-ㅎㅏ-ㅣ가-힣]/g, "");
}

중간점(·)은 [^…가-힣] 범위에 포함되지 않으므로 제거됩니다. 조합 복구에 필요한 중간점이 사라지면서 IME가 이전 상태로 되돌아갈 수도 없게 됩니다.

핵심: compositionend 시점의 e.currentTarget.value는 iOS에서 조합이 완성되기 의 중간 상태를 담고 있습니다. 여기서 setValue를 호출하면 React가 DOM을 조기에 덮어씌워 IME 조합을 파괴합니다.

🛠 수정 내용

apps/web/hooks/use-sanitized-input.ts

변경 전 → 후

// 변경 전
onCompositionEnd: (e: CompositionEvent<HTMLInputElement>) => {
  isComposingRef.current = false;
  setValue(sanitize(e.currentTarget.value));
},

// 변경 후
onCompositionEnd: () => {
  isComposingRef.current = false;
  // compositionend 시점에 setValue를 호출하면 React가 DOM을 덮어써서
  // iOS 10키 한글 키보드의 모음 조합(예: ㅏ+· → ㅐ)을 깨뜨린다.
  // compositionend 직후 onChange가 발화하며 isComposing=false 상태로 sanitize를 적용한다.
},

수정 근거 — onChange 이벤트 의존

모든 주요 브라우저(iOS Safari 포함)에서 compositionend 직후 onChange(input 이벤트)가 발화됩니다. 이 시점에 isComposingfalse이므로 sanitize가 정상 적용됩니다.

환경compositionend 후 onChange 발화 여부결과
iOS Safari (10키 한글)✅ 발화 (최종 조합값 포함)sanitize 정상 적용
Chrome (macOS, QWERTY 한글)✅ 발화sanitize 정상 적용
Firefox (Windows)✅ 발화sanitize 정상 적용
Safari (macOS)✅ 발화sanitize 정상 적용

✅ 수정 후 정상 흐름

"전해온" 입력 시 수정 후 이벤트 시퀀스:

① compositionstart → isComposing = true

② onChange — "전ㅎ", "전ㅎㅏ"

isComposing=true → raw 값 저장 (DOM 덮어쓰기 없음)

③ compositionend

isComposing = false로만 설정. setValue 호출 없음 → DOM 그대로 유지

④ onChange — "전해" (iOS IME가 정상 조합 완성)

isComposing=false → sanitize("전해") = "전해" ✅

⑤ "온" 입력 후 동일 흐름 → 최종값 "전해온"

📋 변경 사항 요약

apps/web/hooks/use-sanitized-input.ts
항목변경 전변경 후
onCompositionEnd 파라미터 (e: CompositionEvent<HTMLInputElement>) ()
onCompositionEnd 내 setValue 호출 setValue(sanitize(e.currentTarget.value)) 제거
sanitize 적용 시점 compositionend + onChange(비조합시) onChange(비조합시)만
사용하지 않는 import CompositionEvent import 있음 CompositionEvent import 제거