2026. 1. 22. 07:28ㆍAI Product Building/HalloGerman
이 글의 핵심
- 독일어 학습 앱에서 ‘읽기’와 ‘말하기(음성 인식)’를 하나로 묶지 않은 이유
- 사용자가 읽고 말한 단어를 안정적으로 처리하기 위한 Android 구현 구조
9편에서 말하기 학습 기능을 설명했다면, 이번 10편에서는 그 바로 앞 단계인 읽기 학습과 음성 인식을 어떻게 연결했는지를 다룬다.
HalloGerman에서는 읽기 학습과 말하기 학습을 하나의 기능처럼 보이게 만들었지만, 내부 구조는 의도적으로 분리되어 있다. 이 분리가 앱의 안정성과 확장성을 좌우한다.
1. 읽기 학습을 별도 기능으로 둔 이유
처음 앱을 설계할 때, 읽기 + 말하기를 한 화면에서 동시에 처리하는 구조도 고려했다. 하지만 테스트를 진행하면서 몇 가지 문제가 분명히 드러났다.
- 음성 인식 실패 시 전체 흐름이 끊어진다
- 마이크 권한, 네트워크 상태에 따라 불안정해진다
- 읽기 연습만 하고 싶은 사용자를 배려하기 어렵다
그래서 HalloGerman에서는 읽기 학습을 독립된 단계로 분리했다. 사용자는 먼저 단어를 보고, 읽고, 이해한다. 그 다음 단계에서 말하기로 넘어간다.
1-1. 읽기 학습의 역할 정의
이 앱에서 ‘읽기 학습’의 역할은 명확하다.
- 단어 철자에 익숙해지기
- 의미를 다시 한 번 확인하기
- 말하기 전에 머릿속으로 발음을 정리하기
즉, 읽기 학습은 말하기를 위한 완충 단계다. 이 단계가 없으면 말하기 학습의 진입 장벽이 급격히 올라간다.
2. 읽기 화면의 기본 구조
읽기 학습 화면은 최대한 단순하게 구성했다. 불필요한 버튼이나 효과는 의도적으로 제거했다.
- 단어(Text)
- 간단한 의미
- TTS 재생 버튼
사용자는 이 화면에서 ‘보고 → 읽고 → 듣는’ 흐름을 자연스럽게 반복하게 된다.
2-1. 읽기 화면에서 TTS 재생 코드 예시
fun speakWord(word: String) {
tts.speak(
word,
TextToSpeech.QUEUE_FLUSH,
null,
"READ_WORD"
)
}
말하기 학습과 달리, 읽기 단계에서는 음성 인식이 전혀 개입하지 않는다. 이 분리가 안정성을 크게 높여준다.
3. 음성 인식은 왜 별도 단계로 처리했을까
Android의 음성 인식은 생각보다 변수가 많다.
- 네트워크 상태
- 기기 마이크 성능
- 사용자 발음 편차
이 모든 변수가 읽기 학습 단계까지 영향을 주면, 앱 전체 경험이 불안정해진다. 그래서 음성 인식은 사용자가 명시적으로 선택했을 때만 시작되도록 설계했다.
3-1. 음성 인식 트리거 구조
HalloGerman에서는 사용자가 ‘말하기 버튼’을 눌렀을 때만 음성 인식이 시작된다.
// 읽기(발음 확인) 학습 화면/VM에서 STT 엔진을 시작하는 예시.
// 핵심은 "STT 엔진은 재사용"하고, "읽기 모드에 맞는 판정만 여기서" 한다는 점.
speechEngine.startListening(
onResult = { results ->
// SpeechRecognizer는 보통 "후보 문장 리스트"를 반환한다.
// results[0]이 가장 신뢰도가 높은 1순위 후보인 경우가 많아서
// 읽기 학습에서는 우선 1순위를 기준으로 판정한다.
val recognized = results.firstOrNull() ?: ""
// 읽기 학습의 판정은 엔진 내부가 아니라
// 화면(ViewModel/UseCase) 쪽에서 수행한다.
// (말하기/읽기/기타 모드가 같은 엔진을 공유할 수 있게 됨)
evaluateReadingResult(targetText, recognized)
},
onError = {
// 엔진이 실패하더라도 학습 흐름이 끊기면 안 된다.
// - 안내 문구 표시
// - 재시도 버튼 노출
// - 혹은 부분 결과(partial result)로 대체(9편에서 적용한 정책)
showErrorMessage()
}
)
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
putExtra(RecognizerIntent.EXTRA_LANGUAGE, "de-DE")
putExtra(
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM
)
}
speechRecognizer.startListening(intent)
이 방식의 장점은 명확하다.
- 읽기 학습 중 불필요한 오류가 발생하지 않는다
- 사용자에게 명확한 컨트롤 권한을 준다
- 앱의 전체 안정성이 높아진다
4. 읽기 결과와 말하기 결과를 비교하지 않는 이유
읽기 단계에서 본 단어와 음성 인식 결과를 직접 비교하는 방식도 고려했다. 하지만 이 방식은 학습자에게 불필요한 스트레스를 준다.
HalloGerman에서는
- 읽기 = 입력(Input)
- 말하기 = 출력(Output)
으로 역할을 명확히 나눈다. 두 결과를 엄격하게 비교하지 않기 때문에, 사용자는 틀릴까 봐 주저하지 않고 말할 수 있다.
5. 이 구조가 이후 기능 확장에 주는 이점
읽기와 말하기를 분리해 둔 덕분에, 이후 기능 확장이 훨씬 수월해졌다.
- 쓰기 학습(타이핑, 필기) 추가
- 플래시 카드 암기 기능 연결
- 홈 화면 진도 모니터링
각 기능이 서로 간섭하지 않고, 하나의 학습 흐름으로 자연스럽게 연결된다.
6. 다음 편 예고
다음 11편에서는 쓰기 학습 중 첫 번째 단계인 타이핑 기반 쓰기 학습을 어떻게 구현했는지, 그리고 왜 필기 입력과 분리했는지를 설명할 예정이다.
참고 자료
- Android TextToSpeech 공식 문서
https://developer.android.com/reference/android/speech/tts/TextToSpeech - Android SpeechRecognizer 공식 문서
https://developer.android.com/reference/android/speech/SpeechRecognizer
'AI Product Building > HalloGerman' 카테고리의 다른 글
| [독일어 학습 앱 개발기 12편] 쓰기 학습 ② 필기 입력은 왜 분리했을까 – 손으로 쓰는 학습의 기준 (1) | 2026.01.24 |
|---|---|
| [독일어 학습 앱 개발기 11편] 쓰기 학습 ① 타이핑 입력은 왜 가장 먼저 구현했을까 (0) | 2026.01.23 |
| [독일어 학습 앱 개발기 9편] 말하기 학습 구현 – Android 음성인식(STT)을 실제 서비스에 적용하는 방법 (0) | 2026.01.21 |
| [독일어 학습 앱 개발기 8편] CSV 단어장 업로드 UX – 사용자가 실패하지 않게 만드는 설계 (0) | 2026.01.20 |
| [독일어 학습 앱 개발기 7편] 뉴스 읽기 기능 구현 – 독일어 콘텐츠를 학습 도구로 바꾸는 구조 (0) | 2026.01.19 |