2026. 1. 19. 06:44ㆍAI Product Building/HalloGerman
이 글의 핵심:
언어 학습 앱에서 “콘텐츠”는 그 자체로 목적이 아니라 재료에 가깝다. 중요한 것은 그 콘텐츠를 어떻게 학습 흐름으로 연결하느냐다. 7편에서는 HalloGerman에서 독일어 뉴스 콘텐츠를 불러오고, 읽어주고, 단어를 추출해 단어장으로 연결한 전체 구조를 설명한다.
1. 왜 뉴스 콘텐츠를 선택했는가
독일어 학습에서 뉴스는 매우 좋은 학습 재료다.
- 실제 독일에서 사용되는 표현
- 반복적으로 등장하는 어휘
- 시사·생활·경제 등 다양한 주제
하지만 대부분의 학습 앱은 뉴스를 “읽기 자료”로만 제공한다. HalloGerman에서는 뉴스를 단순 소비용 콘텐츠가 아니라 단어를 수집하는 출발점으로 사용했다.
2. 뉴스 기능의 핵심 목표는 단 하나
뉴스 기능의 목표는 명확했다.
“읽다가 막힌 단어를 즉시 단어장으로 옮길 수 있어야 한다.”
이 목표를 기준으로 UI와 구조를 설계했다. 그래서 뉴스 화면에는 불필요한 기능을 넣지 않았다.
3. 뉴스 화면 UI 설계 원칙
뉴스 화면은 최대한 단순해야 한다.
- 기사 제목
- 본문 텍스트
- 듣기 버튼(TTS)
스크롤 중에도 언제든지 듣기 버튼을 누를 수 있어야 하고, 텍스트는 선택 가능해야 한다. 강조, 밑줄, 주석 같은 요소는 과감히 제거했다.
4. 뉴스 데이터 구조: 단어와 분리하기
뉴스 데이터는 단어 데이터와 섞이지 않는다. 뉴스는 “출처”일 뿐이다.
data class NewsItem(
val id: String,
val title: String,
val content: String
)
뉴스 자체에는 학습 정보가 없다. 학습은 사용자의 행동에서 발생한다.
5. TTS로 뉴스 읽어주기
뉴스 읽기에서 가장 중요한 기능은 TTS다. 사용자는 눈으로 읽다가, 귀로 다시 한 번 들을 수 있어야 한다.
구현 방식은 단순하다.
IconButton(onClick = {
ttsManager.speak(news.content)
}) {
Icon(Icons.Default.VolumeUp, contentDescription = "Read News")
}
TTS 로직은 이미 6편에서 설명한 매니저를 그대로 재사용한다. 이 재사용성이 구조 설계의 힘이다.
6. 단어 추출은 자동이 아니라 ‘선택’이다
자동 단어 추출은 겉보기에는 좋아 보이지만, 실제 학습에는 도움이 되지 않는 경우가 많다. 그래서 HalloGerman에서는 자동 추출을 사용하지 않았다.
대신 사용자가 직접 텍스트를 선택하도록 했다.
- 길게 눌러 단어 선택
- 선택된 텍스트로 단어 추가
이 과정에서 사용자는 “이 단어를 외워야겠다”는 의사결정을 한 번 더 하게 된다.
7. 선택된 단어를 단어장으로 보내는 흐름
뉴스에서 단어를 선택하면 다음과 같은 흐름이 실행된다.
뉴스 텍스트 선택
→ 단어 추가 다이얼로그
→ Word 생성 (source = NEWS)
→ VocabularyRepository.addWord()
이때 단어는 단어장에 추가되지만, 뉴스와의 연결 정보는 별도로 저장된다. (4편에서 설명한 NewsWordLink 구조)
8. 왜 문장을 함께 저장했는가
뉴스에서 가져온 단어는 반드시 문맥과 함께 기억되어야 한다.
그래서 단어 자체와 함께 “이 단어가 쓰인 문장”을 저장했다. 하지만 이 문장은 단어 데이터에 직접 포함하지 않았다.
이 설계 덕분에 다음과 같은 확장이 가능해졌다.
- 문장 복습 기능
- 문장 듣기
- 문장 학습 모드
9. 이 구조가 주는 사용자 경험의 변화
뉴스 기능이 추가되면서 사용자의 학습 패턴이 바뀌었다.
- 단어장을 먼저 열지 않는다
- 뉴스를 읽다가 자연스럽게 단어를 모은다
- 모아둔 단어를 나중에 암기한다
학습이 “계획된 행동”이 아니라 “자연스러운 흐름”이 되었다.
[주요코드]
//JS → Android 문장 전달 인터페이스
//NewsWebViewScreen.kt
@JavascriptInterface
fun sendSentences(json: String) {
try {
val list = mutableListOf<TtsSentence>()
val ja = JSONArray(json)
for (i in 0 until ja.length()) {
val jo = ja.getJSONObject(i)
list.add(TtsSentence(jo.getString("id"), jo.getString("text")))
}
onSentencesReceived(list)
} catch (e: Exception) {
e.printStackTrace()
}
}
//웹의 텍스트를 앱 내부 학습 데이터로 변환하는 브리지다.
//WebView에 JS 인터페이스 등록
//NewsWebViewScreen.kt
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.settings.cacheMode = WebSettings.LOAD_DEFAULT
webView.addJavascriptInterface(NewsWebInterface(context) { list ->
webView.post { currentOnSentencesReceived.value(list) }
}, "AndroidTTS")
//단순한 “브라우저”가 아니라 “학습기”로 바꾸는 핵심 지점.
//페이지에서 문장 추출 후 전달
//NewsWebViewScreen.kt
val js = """
(function() {
try {
var nodes = document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, blockquote, figcaption, div, article, section');
var sentenceId = 0;
var sentences = [];
var existing = document.querySelectorAll("span[id^='tts_']");
if(existing.length > 0) {
for(var k=0; k<existing.length; k++) {
sentences.push({id: existing[k].id, text: existing[k].innerText});
}
window.AndroidTTS.sendSentences(JSON.stringify(sentences));
return;
}
if(sentences.length > 0) {
window.AndroidTTS.sendSentences(JSON.stringify(sentences));
} else {
window.AndroidTTS.showToast("${context.getString(R.string.news_no_text_found)}");
}
} catch(e) {
window.AndroidTTS.showToast("JS Error: " + e.message);
}
})()
""".trimIndent()
webViewRef.value?.evaluateJavascript(js, null)
//뉴스 페이지를 “문장 단위 학습 데이터셋”으로 변환하는 로직.
10. 다음 편 예고
8편. CSV 단어장 업로드 UX – 사용자 데이터를 안전하게 받아들이는 방법
다음 글에서는 CSV 업로드 과정에서 발생하는 오류, UX 설계, 그리고 사용자를 혼란스럽게 하지 않는 방법을 다룬다.
참조 라이브러리 및 도구 모음
'AI Product Building > HalloGerman' 카테고리의 다른 글
| [독일어 학습 앱 개발기 9편] 말하기 학습 구현 – Android 음성인식(STT)을 실제 서비스에 적용하는 방법 (0) | 2026.01.21 |
|---|---|
| [독일어 학습 앱 개발기 8편] CSV 단어장 업로드 UX – 사용자가 실패하지 않게 만드는 설계 (0) | 2026.01.20 |
| [독일어 학습 앱 개발기 6편] TTS와 음성 인식 구현 – Android에서 가장 까다로운 기능 다루기 (0) | 2026.01.18 |
| [독일어 학습 앱 개발기 5편] 단어 암기 UI 설계 – 듣기·말하기·쓰기를 하나의 흐름으로 만드는 법 (0) | 2026.01.17 |
| [독일어 학습 앱 개발기 4편] 단어장 데이터 구조 설계 – CSV, 뉴스, 로컬 데이터를 하나로 묶는 법 (1) | 2026.01.16 |