[독일어 학습 앱 개발기 5편] 단어 암기 UI 설계 – 듣기·말하기·쓰기를 하나의 흐름으로 만드는 법

2026. 1. 17. 06:36AI Product Building/HalloGerman

반응형

이 글의 핵심:
단어 암기 앱의 성패는 “얼마나 많은 기능이 있느냐”가 아니라 “사용자가 어떤 순서로 행동하게 만드느냐”에 달려 있다. 5편에서는 HalloGerman에서 실제로 사용한 단어 암기 UI 설계 방식과, 듣기·말하기·쓰기를 하나의 학습 흐름으로 묶은 이유를 설명한다.


1. 단어 암기 UI에서 가장 흔한 실수

많은 단어 암기 앱이 같은 실수를 반복한다.

  • 기능은 많은데 무엇부터 해야 할지 모른다
  • 듣기, 말하기, 쓰기가 각각 다른 화면에 흩어져 있다
  • 사용자가 “공부 중”이라는 피로감을 느낀다

이런 UI는 사용자가 앱을 오래 쓰지 못하게 만든다. 그래서 HalloGerman에서는 UI 설계의 출발점을 기능이 아니라 학습 순서로 잡았다.


2. 단어 암기의 기본 순서는 이미 정해져 있다

언어 학습에서 단어 암기의 자연스러운 순서는 다음과 같다.

  1. 먼저 소리를 듣는다
  2. 그다음 따라 말해본다
  3. 마지막으로 직접 써본다

이 순서를 거꾸로 배치하면 학습 효율이 급격히 떨어진다. 그래서 UI는 이 순서를 강요하지 않되, 자연스럽게 유도하도록 설계했다.


3. 하나의 화면, 하나의 단어

HalloGerman의 단어 암기 화면은 매우 단순하다.

  • 한 화면에 한 단어만 표시
  • 불필요한 정보 제거
  • 행동 버튼은 3개만 제공

이 구조는 사용자가 “다음에 무엇을 해야 할지” 고민하지 않게 만든다.


4. 듣기 UI: 가장 먼저, 가장 눈에 띄게

듣기 기능(TTS)은 단어 암기의 시작점이다. 그래서 가장 먼저, 가장 쉽게 누를 수 있어야 한다.

  • 단어 텍스트 근처에 배치
  • 아이콘은 스피커 하나만 사용
  • 재생 중임을 시각적으로 표시

Compose에서는 다음과 같이 단순한 버튼으로 구현했다.

IconButton(onClick = { tts.speak(word.term) }) {
    Icon(
        imageVector = Icons.Default.VolumeUp,
        contentDescription = "Listen"
    )
}

중요한 점은 UI에서 TTS 로직을 직접 다루지 않는다는 것이다. 실제 발음 처리는 speech.tts 패키지에 위임한다.


5. 말하기 UI: 성공 경험만 남기기

음성 인식 기능은 학습 효과가 크지만, 실패 경험이 쌓이면 바로 이탈로 이어진다.

그래서 HalloGerman에서는 다음 원칙을 적용했다.

  • 정확도 점수 표시하지 않기
  • 틀렸을 때 “실패”라는 표현 쓰지 않기
  • 다시 말해볼 수 있는 기회를 바로 제공

UI는 단순히 “말해보기” 버튼 하나만 제공한다.

Button(onClick = { speechRecognizer.start(word.term) }) {
    Text("말해보기")
}

사용자가 말을 하면, 결과 비교는 내부적으로만 처리하고 UI에는 “다시 시도해보세요” 정도의 피드백만 준다.


6. 쓰기 UI: 타이핑과 필기를 분리한 이유

쓰기 학습은 두 가지 방식이 전혀 다르다.

  • 키보드로 철자 입력
  • 손으로 직접 써보는 필기

이 둘을 하나의 UI로 묶으면 복잡해진다. 그래서 쓰기 버튼을 누르면 “타이핑 / 필기”를 선택하게 했다.

타이핑 입력은 매우 단순하다.

TextField(
    value = input,
    onValueChange = { input = it },
    label = { Text("단어를 입력하세요") }
)

필기 입력은 별도 화면으로 분리해, 집중을 방해하지 않도록 설계했다.


7. 학습 상태를 눈에 띄게 보여주지 않은 이유

많은 앱이 “완료”, “미완료”, “정답률” 같은 정보를 강조한다. 하지만 이런 정보는 초반 학습자에게 압박이 된다.

HalloGerman에서는 학습 상태를 UI 전면에 드러내지 않았다. 대신, 단어장을 다시 열었을 때 자연스럽게 “봤던 단어”가 느껴지도록만 처리했다.

이 결정 덕분에 앱은 ‘공부 앱’보다는 ‘도구’에 가까운 인상을 준다.

 

[주요 코드]

//모드 카드 기반의 통일된 진입점
//LearnModeScreen.kt
LearnItem(
    title = stringResource(R.string.learn_mode_listen_title),
    description = stringResource(R.string.desc_listen_mode),
    icon = Icons.Default.Hearing,
    color = PastelPalette.LearnListen,
    onClick = { navController.navigate("listen?topic=$encodedTopic") }
),
LearnItem(
    title = stringResource(R.string.learn_mode_read_title),
    description = stringResource(R.string.desc_speak_mode),
    icon = Icons.Default.Mic,
    color = PastelPalette.LearnRead,
    onClick = { navController.navigate("read?topic=$encodedTopic") }
),
LearnItem(
    title = stringResource(R.string.learn_mode_write_title),
    description = stringResource(R.string.desc_write_mode),
    icon = Icons.Default.Edit,
    color = PastelPalette.LearnWrite,
    onClick = { navController.navigate("write?topic=$encodedTopic") }
)
//사용자는 “듣기/말하기/쓰기”를 하나의 학습 흐름으로 인식한다.

//듣기 모드는 자동 TTS로 리듬 유지
//ListenScreen.kt
val tts = remember {
    lateinit var inst: TextToSpeech
    inst = TextToSpeech(context) { status ->
        if (status == TextToSpeech.SUCCESS) {
            inst.language = Locale.GERMAN
            ttsReady = true
        }
    }
    inst
}

LaunchedEffect(ttsReady, uiState.current, listenMode, language) {
    if (ttsReady) {
        uiState.current?.let { word ->
            val (text, locale) = if (listenMode == ListenMode.MeaningToWord) {
                meaningForLanguage(word, language) to localeForMeaning(language)
            } else {
                word.german to Locale.GERMAN
            }
            tts.speak(text, TextToSpeech.QUEUE_FLUSH, null, null)
        }
    }
}
//리듬이 끊기지 않도록 자동 음성 재생을 기본 흐름으로 둔다.

//쓰기 모드는 입력 방식만 바꾸면 끝
//WriteScreen.kt
val writeMode by settingsViewModel.learnWriteMode.collectAsState(initial = "Alphabet")
val isTypingMode = writeMode == "Typing" || writeMode == "Handwriting"
val isHandwriting = writeMode == "Handwriting"

if (isTypingMode) {
    OutlinedTextField(
        value = typingInput,
        onValueChange = { typingInput = it },
        label = { Text(stringResource(R.string.write_input_prompt)) },
        modifier = Modifier.fillMaxWidth().padding(horizontal = 0.dp),
        singleLine = true,
        shape = RoundedCornerShape(12.dp)
    )
} else {
    // Selection Mode (Alphabet Chips)
}
//UI는 동일하게 유지하고 “입력 방식만 교체”하면 흐름이 자연스럽다.

8. 이 UI 구조가 주는 확장성

이 단어 암기 UI는 이후 확장에도 유리하다.

  • 문장 학습 → 단어 카드 아래 문장 추가
  • 예문 듣기 → 듣기 버튼 재사용
  • 복습 모드 → 같은 UI 재활용

처음부터 단순하게 만든 UI는, 나중에 살을 붙이기 쉽다.


9. 다음 편 예고

6편. 음성 인식과 TTS 실제 구현 – Android에서 가장 까다로운 부분 다루기
다음 글에서는 TextToSpeech와 SpeechRecognizer를 안정적으로 다루기 위해 어떤 구조를 사용했는지, 그리고 크래시를 어떻게 피했는지 실제 코드 중심으로 설명한다.


참조 라이브러리 및 도구 모음

 

반응형