최근에 어르신들을 위한 앱을 만들면서 음성 안내 기능을 넣어야 했어요. Compose로 작업하다 보니 TTS도 Compose스럽게 쓰고 싶더라구요. 그래서 간단한 커스텀 Composable을 만들어봤습니다.
왜 Compose에서 TTS가 까다로울까?
TTS는 Android의 전통적인 API라서 Compose의 선언적 패러다임과 잘 안 맞아요. 특히 이런 부분들이 신경 쓰였습니다:
- 초기화가 비동기로 진행됨
- Activity 생명주기에 맞춰 정리해야 함
- Recomposition 때마다 새로 생성되면 안 됨
rememberTextToSpeech 만들기
그래서 이런 식으로 Compose에 맞게 래핑했어요:
@Composable
fun rememberTextToSpeech(
onInit: (TextToSpeech) -> Unit = {}
): MutableState {
val context = LocalContext.current
val tts = remember { mutableStateOf(null) }
DisposableEffect(context) {
val textToSpeech = TextToSpeech(context) { status ->
if (status == TextToSpeech.SUCCESS) {
tts.value?.language = Locale.KOREA
tts.value?.setSpeechRate(1.0f)
tts.value?.setPitch(1.0f)
onInit(tts.value!!)
} else {
Log.e("TTS", "TTS 초기화 실패: $status")
}
}
tts.value = textToSpeech
onDispose {
textToSpeech.stop()
textToSpeech.shutdown()
}
}
return tts
}
포인트는 DisposableEffect
를 사용한 거예요. Composable이 화면에서 사라질 때 자동으로 TTS 리소스를 정리해줍니다. 메모리 릭 걱정 없어요!
실제 사용 예시
@Composable
fun FaceRecognitionResultScreen(
matchResult: MatchResult?,
isAllSuccess: Boolean
) {
val tts = rememberTextToSpeech()
val settingViewModel = hiltViewModel()
val ttsOn = settingViewModel.isSeniorTTS
LaunchedEffect(Unit) {
if(ttsOn) {
if (isAllSuccess) {
delay(500) // UI 전환 후 잠시 대기
tts.value?.speak(
matchResult?.message,
TextToSpeech.QUEUE_FLUSH,
null,
"description"
)
} else {
delay(500)
tts.value?.speak(
"안녕하세요.",
TextToSpeech.QUEUE_FLUSH,
null,
"description"
)
}
}
}
// UI 구성...
}
LaunchedEffect(Unit)
를 사용해서 화면이 처음 표시될 때 한 번만 음성 안내가 나가도록 했어요. delay를 준 건 화면 전환 애니메이션이 끝난 후에 말하게 하려고요.
설정 화면에서 TTS 켜기/끄기
어르신들은 음성 안내가 오히려 번거로울 수도 있어서 설정에서 끌 수 있게 했습니다:
@Composable
fun SettingsScreen() {
val viewModel = hiltViewModel()
var isTTSEnabled by remember { mutableStateOf(viewModel.isSeniorTTS) }
Column(
modifier = Modifier.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("음성 안내")
Switch(
checked = isTTSEnabled,
onCheckedChange = {
isTTSEnabled = it
viewModel.updateTTSSetting(it)
}
)
}
}
}
개선하면서 배운 점들
1. null 체크가 중요해요
TTS 초기화가 비동기라서 tts.value
가 null일 수 있어요. 항상 체크하고 사용해야 합니다:
tts.value?.let { textToSpeech ->
textToSpeech.speak(message, TextToSpeech.QUEUE_FLUSH, null, null)
}
2. 적절한 딜레이 필요
화면 전환 직후 바로 말하면 사용자가 놀랄 수 있어요. 500ms 정도 딜레이를 주니 자연스럽더라구요.
3. 에러 처리
val result = textToSpeech.speak(message, TextToSpeech.QUEUE_FLUSH, null, null)
if (result == TextToSpeech.ERROR) {
Log.e("TTS", "TTS 발화 오류 발생")
// 대체 수단 (예: Toast 메시지)
}
더 나아가기
기본 기능은 이 정도로 충분했지만, 나중에 이런 것들도 추가해볼 생각이에요:
- 음성 속도 조절 (어르신들은 느리게 듣고 싶어하심)
- 다국어 지원 (영어 음성도 필요할 때가 있음)
- 음성 종료 콜백 (다음 동작 연결)
마무리
Compose에서 TTS 쓰는 게 처음엔 어색했는데, DisposableEffect
와 remember
를 활용하니 깔끔하게 구현할 수 있었어요. 특히 생명주기 관리가 자동으로 되는 게 정말 편하더라구요.
간단한 음성 안내 기능 정도는 이 정도로도 충분한 것 같아요. 혹시 더 좋은 방법 있으면 알려주세요!
전체 코드가 궁금하신 분들은 댓글 남겨주세요 🙂