
발생 환경: iOS Safari, iOS Chrome, Android Chrome
요약
Web Speech API 기반 음성인식이 모바일(iOS, Android Chrome)에서 동작하지 않던 문제를 단계적으로 해결.
최종 원인은 스펙트럼 시각화의 getUserMedia 호출이 Web Speech API의 마이크 접근을 차단한 것.
문제 1. iOS에서 음성인식이 아예 시작되지 않음
증상
- 마이크 버튼을 눌러도 음성인식이 시작되지 않음
- 스펙트럼은 움직이나 인식된 내용이 표시되지 않음
원인
continuous: true 설정이 iOS Safari / iOS Chrome(WebKit)에서 미지원.
iOS는 Android Chrome(Blink)과 달리 WebKit 엔진을 사용하며,continuous: true 상태에서 인식 세션이 즉시 종료되거나 아예 시작되지 않음.
// 문제 코드
recognition.continuous = true;
recognition.interimResults = true;
해결
continuous: false로 변경하고, onend 이벤트에서 자동 재시작하는 방식으로 전환.
recognition.continuous = false;
recognition.interimResults = true;
문제 2. 취소 후 다른 필드 음성인식이 동작하지 않음
증상
- 경조사 구분 음성인식 시작 → 취소
- 대상자 이름 음성인식 시작 → 동작 안 함
- 다시 경조사 구분 음성인식 시작 → 동작 안 함
원인
Race condition. 이전 recognition 인스턴스의 onend가 늦게 발화(async)되는 시점에
새 recognition이 이미 시작된 상태였고, stale onend가 새 인스턴스를 방해.
[취소] rec1.stop() → speechRecognitionRef = null → isVoiceActive = false
[새 시작] isVoiceActive = true → rec2 생성 → rec2.start()
[늦은 onend] rec1.onend 발화 → isVoiceActive = true → 재시작 로직 실행
→ rec2의 pending timeout 취소 + 새 timeout 설정 → 충돌
해결
onend에서 현재 활성 인스턴스가 자신이 아닌 경우(stale) 재시작 로직 스킵.
recognition.onend = () => {
if (speechRecognitionRef.current === recognition) {
speechRecognitionRef.current = null;
}
if (!isVoiceActiveRef.current) return;
// 다른 인식 인스턴스가 이미 활성 상태면 stale onend → 스킵
if (speechRecognitionRef.current !== null) return;
// 재시작 로직...
};
문제 3. Android Chrome에서 transcript가 비어있음
증상
listening=true확인됨 (인식 시작은 됨)- 스펙트럼 움직임 (마이크 접근 성공)
- 말을 해도 인식된 내용이 표시되지 않음
onresult이벤트 미발화
원인 (초기 추정 - 오답)
interimResults: false로 변경한 것이 원인이라 판단하여 true로 복원.
→ 증상 동일, 근본 원인 아님.
실제 원인
스펙트럼 시각화 코드의 getUserMedia 호출이 마이크를 선점하여
Web Speech API가 실제 오디오 데이터를 수신하지 못한 것.
setVoiceListeningField(field) 호출
→ 스펙트럼 useEffect 발동
→ navigator.mediaDevices.getUserMedia({ audio: true }) → 마이크 선점
→ 350ms 후 SpeechRecognition.startListening()
→ listening = true (세션 시작은 됨)
→ 그러나 마이크 스트림이 getUserMedia에 묶여 있어 오디오 미전달
→ onresult 미발화 → transcript 없음
| 플랫폼 | 동작 |
|---|---|
| iOS | OS 레벨 음성인식 → getUserMedia와 공존 가능 → 정상 동작 |
| Android Chrome | 브라우저 레벨 Web Speech API → getUserMedia와 충돌 → transcript 없음 |
해결
스펙트럼 시각화의 getUserMedia 코드 전체 제거.listening 상태(react-speech-recognition)를 활용한 CSS 애니메이션으로 대체.
// 제거: getUserMedia 기반 canvas 스펙트럼
// 대체: listening 상태 기반 막대 애니메이션
{Array.from({ length: 16 }, (_, i) => (
<div
key={i}
style={{
width: 6,
borderRadius: 3,
background: listening ? "#6366f1" : "#cbd5e1",
animation: listening
? `voiceBar 0.9s ease-in-out ${(i * 0.06).toFixed(2)}s infinite alternate`
: "none",
}}
/>
))}
문제 4. react-speech-recognition 전환 후 자동 재시작 루프
증상
react-speech-recognition 라이브러리로 전환 후 iOS에서 다시 인식 안 됨.
원인
자동 재시작 useEffect가 초기 상태(voiceListeningField 설정됨, listening 아직 false)에서
즉시 발화하여 300ms마다 재시작 루프를 형성, 첫 번째 startListening을 방해.
// 문제 코드: listening=false이기만 하면 즉시 재시작 예약
useEffect(() => {
if (!voiceListeningField || listening) return;
const timer = setTimeout(() => {
SpeechRecognition.startListening(...);
}, 300);
return () => clearTimeout(timer);
}, [listening, voiceListeningField]);
해결
wasListeningRef로 실제로 listening=true를 한 번 확인한 이후에만 재시작 허용.
const wasListeningRef = useRef(false);
useEffect(() => {
if (listening) {
wasListeningRef.current = true; // 실제 시작됨을 기록
return;
}
// 한 번도 listening=true가 된 적 없으면 재시작 안 함
if (!voiceListeningField || !wasListeningRef.current) return;
wasListeningRef.current = false;
const timer = setTimeout(() => {
SpeechRecognition.startListening({ language: "ko-KR", continuous: false });
}, 300);
return () => clearTimeout(timer);
}, [listening, voiceListeningField]);
최종 아키텍처
마이크 버튼 클릭
→ startVoiceInput(field)
→ wasListeningRef = false
→ resetTranscript()
→ setVoiceListeningField(field) ← 모달 오픈
→ setTimeout 350ms
→ SpeechRecognition.startListening({ language: "ko-KR", continuous: false })
listening = true
→ wasListeningRef = true
→ 막대 애니메이션 활성화
말하면 → transcript 업데이트 → UI 표시
침묵 or 짧은 발화 후 인식 종료
→ listening = false
→ wasListeningRef = true → 300ms 후 자동 재시작
인식 완료 버튼 클릭
→ confirmVoiceInput()
→ wasListeningRef = false
→ SpeechRecognition.abortListening()
→ transcript 파싱 → formData 업데이트
→ 모달 닫기
핵심 교훈
getUserMedia와 Web Speech API는 Android Chrome에서 동시 사용 불가.
두 API 모두 마이크를 점유하려 하며,getUserMedia가 먼저 선점하면 Web Speech API가 오디오를 받지 못함.- iOS는 OS 레벨 음성인식이라 마이크 공유가 가능하지만
continuous: true를 지원하지 않음.continuous: false+ 자동 재시작 패턴으로 대응해야 함. - react-speech-recognition의
listening상태는startListening호출 즉시true가 되지 않음.
첫listening=true확인 후에만 재시작 로직을 허용해야 초기 루프를 방지할 수 있음.
사용 라이브러리
react-speech-recognitionv3.x — Web Speech API React 래퍼- 설치:
npm install react-speech-recognition @types/react-speech-recognition
'TroubleShooting' 카테고리의 다른 글
| 외부 API 연동 시 세션(loginInfo) 초기화 현상 (1) | 2026.03.06 |
|---|---|
| 사용자 이탈 방지 로직(뒤로가기/새로고침 차단) 최적화 (0) | 2026.01.14 |
| 리액트 API 호출 구조 리팩토링: useEffect에서 React Query로의 전환 (0) | 2026.01.07 |
| Jenkins-SonarQube 연동 에러: "The ‘report’ parameter is missing" (HTTP 400) (0) | 2026.01.06 |