🐝매일 한입
개발일지📖 12분 읽기

Utakoto — AI 대화 추천 답에 한글 발음과 듣기 버튼, 그리고 곡 요청 선정을 월·목 두 번으로

가나를 못 읽으면 따라 말할 수 없던 AI 대화 추천 답에 가사와 같은 규칙의 한글 발음과 보내지 않는 스피커 버튼을 붙였습니다. 같은 날 곡 요청 선정이 주 2회로 바뀌어, 요일을 앱이 아니라 서버 시각에서 읽게 고쳤습니다.

📖 12분 읽기
#Utakoto#React Native#TTS#AI 대화#OTA#1인개발

따라 말할 문장인데 읽는 법이 없었다

Utakoto 에는 "이 줄로 대화하기"라는 기능이 있습니다. 가사 한 줄의 표현을 가지고 AI 와 최대 다섯 번 주고받는 대화이고, 한 번 답할 때마다 교정 하나와 추천 답 세 개가 나옵니다. 추천 답은 화면 아래에 칩으로 가로로 늘어서고, 칩을 누르면 그 답이 그대로 보내집니다.

문제는 그 칩에 일본어와 뜻만 있었다는 점입니다. intent 에 적은 문제를 그대로 옮기면, 가나를 못 읽는 학습자는 추천 답을 따라 말할 수 없고 소리를 들어 볼 방법도 없었습니다. 마이크로 답하는 흐름까지 만들어 두었는데, 정작 말해야 할 문장의 읽는 법과 소리가 칩에 없었던 것입니다. 11월 8일에 PO 요청으로 들어온 일이었습니다.

칩 안에 발음 한 줄과 스피커 하나

바꾼 것은 두 가지입니다. 칩에 한글 발음 한 줄이 생겼고, 오른쪽 위에 스피커 버튼이 생겼습니다. 발음은 서버가 만들어 pron 이라는 필드로 붙여 주고, 스피커는 기기의 일본어 TTS 로 그 답을 읽습니다. 단어 발음을 들을 때 쓰는 함수(speakJa)를 그대로 씁니다.

        {/* 듣기만 — 칩을 누르면 보내고, 스피커는 보내지 않는다(안쪽 Pressable 이 누름을 가져간다). */}
        <Pressable
          onPress={() => onSpeak(suggestion)}
          hitSlop={10}
          accessibilityRole="button"
          accessibilityLabel={`발음 듣기: ${suggestion.ja}`}
          className="h-6 w-6 items-center justify-center rounded-full"
          style={{ backgroundColor: colors.surface2 }}
        >
          <Ionicons name="volume-medium" size={13} color={colors.accent} />
        </Pressable>
      </View>
      <Text className="font-jp text-body leading-body text-text-hi" numberOfLines={2}>
        {suggestion.ja}
      </Text>
      {suggestion.pron ? (
        <Text className="font-sans text-caption text-text-mid" numberOfLines={2}>
          {suggestion.pron}
        </Text>
      ) : null}

칩 전체가 이미 "누르면 보내는" 버튼이라, 그 안에 버튼을 하나 더 넣는 구조입니다. 주석에 적은 대로 안쪽 Pressable 이 누름을 먼저 가져가기 때문에 스피커를 누르면 소리만 나고 답은 보내지지 않습니다. spec 에도 "스피커는 보내지 않는다"를 따로 한 줄로 적어 두었습니다. 들어 보려고 눌렀는데 답이 나가 버리면 다섯 번 중 한 번이 그냥 사라지니까요. 스피커 자체는 24pt 로 작게 두고 hitSlop 10 으로 누를 수 있는 범위를 넓혔습니다.

줄이 하나 늘어난 만큼 칩의 최소 높이도 84 에서 100 으로 올렸습니다. 원래 주석을 보면 이 높이는 칩이 새로 오거나 턴이 바뀔 때 칩 줄이 커졌다 줄었다 하지 않게 고정해 둔 값이었고, 불러오는 동안 보이는 뼈대도 같은 값을 씁니다. 그래서 둘을 함께 바꿨습니다.

발음은 가사와 같은 규칙으로, 없어도 깨지지 않게

pron 은 앱에서 계산하지 않고 서버가 붙입니다. spec 에는 가사 발음과 같은 생성기를 쓰고, 1.5초 상한을 두고, 실패하면 생략한다고 적었습니다. 발음이 늦거나 만들어지지 않아도 추천 답 자체는 그대로 온다는 뜻입니다.

같은 생성기를 쓰는 이유는 이번 intent 에 따로 적혀 있지 않습니다. 다만 지난 8월 한자 쓰기에 한글 발음을 붙일 때(a4673e0) 커밋에 남긴 이유가 있습니다. 가사와 단어 발음이 전부 백엔드의 정본 변환기로 만들어졌으니, 규칙을 다른 곳에 옮겨 적으면 같은 「キョウ」가 화면 위치에 따라 '쿄-'와 '쿄우'로 갈린다는 것입니다. 대화 칩도 같은 앱 안에서 가사와 나란히 보는 글자라, 저는 같은 이유로 읽습니다. 백엔드 쪽 검증으로 plan 에 적힌 것도 이 규칙과 이어집니다. 실제 형태소 분석기(kuromoji)로 조사 は 가 '와'로 나오는지까지 테스트 5개에 넣었습니다.

Utakoto 가사 화면, 가사 한 줄 위에 후리가나, 아래에 한글 발음과 뜻이 붙어 있고 단어 카드가 열려 있다 Utakoto 가사 화면(스토어 스크린샷)입니다. 가사 줄 밑의 한글 발음과 같은 생성기가 이제 AI 대화 추천 답 칩의 발음 줄도 만듭니다.

앱 쪽에서 지킨 제약은 "선택 필드"였습니다. 옛 서버나 이미 진행 중이던 옛 대화에는 pron 이 없으니, 없으면 발음 줄만 숨기고 칩은 예전 모양 그대로 둡니다. 스피커는 ja 를 읽기 때문에 발음 줄이 없어도 남습니다. 파서는 빈 문자열도 버립니다.

        ...(typeof o.pron === 'string' && o.pron.trim() ? { pron: o.pron } : {}),

파서 테스트도 이 세 경우(발음 있음, 공백뿐, 필드 없음)를 그대로 넣었습니다. AI 말풍선과 교정 문장의 발음, 서버에서 만든 TTS 음성은 비범위로 적어 두었습니다. 왜 이번에 빼었는지는 "다음 단계"라는 말 외에 기록이 없습니다.

같은 날, 곡 요청 선정이 주 두 번으로

같은 날 곡 요청 보드 문구도 고쳤습니다(a8e2622). 보드는 사람들이 듣고 싶은 곡을 요청하고 포인트로 투표하면, 표가 가장 많은 곡이 정해진 날 카탈로그에 들어오는 화면입니다. intent 에 남은 PO 의 말은 "생각보다 노래 요청이 많아서, 운영 추천은 없애고 선정 주기를 짧게 가져가야 할 것 같아"였습니다. 결정은 월요일 주 1회에서 월·목 주 2회, 회차마다 1곡이었고, 운영이 미리 채워 둔 추천 곡은 내리고 그 표는 돌려주는 것으로 서버 쪽에서 정했습니다.

앱이 바뀌어야 했던 이유는 문구에 "월요일"을 박아 두었기 때문입니다. 목요일 선정을 앞두고도 "3일 뒤 월요일 선정"이라고 말하게 되는 상태였습니다. 그래서 요일을 문자열로 쓰지 않고, 서버가 준 다음 선정 시각을 한국 시간으로 읽어 정하게 했습니다.

  const weekday = WEEKDAY_KO[new Date(t + KST_OFFSET_MS).getUTCDay()];
  return `${days}일 뒤 ${weekday}요일 선정`;

9시간을 더한 뒤 UTC 기준 요일을 읽으니, 기기의 시간대 설정과 상관없이 한국 시간 요일이 나옵니다. 보드 머리말도 서버 값(selectionDays, picksPerSelection)으로 그리고, 그 값이 없는 옛 서버 응답에는 "월·목"으로 떨어지게 했습니다. intent 의 제약에 적은 원칙이 "얇은 클라이언트"입니다. 선정 요일, 곡 수, 다음 선정 시각은 서버 값을 옮길 뿐 앱이 판정하지 않습니다. 그래서 순서도 서버 배포가 먼저였고, OTA 가 늦어도 옛 문구가 남을 뿐 기능은 깨지지 않는다고 적어 두었습니다.

작지만 판단이 들어간 문구가 하나 있습니다. "이번 주 선정"을 "선정된 곡"과 "선정"으로 바꾼 것입니다. 목요일에 뽑힌 곡이 다음 주 월요일까지 준비 중일 수 있어서, "이번 주"라는 말이 맞지 않게 됩니다.

확인한 것과 남은 것

AI 대화 발음은 plan 의 체크박스가 모두 채워져 있습니다. iOS 시뮬레이터(1.11.0 release)와 Android 16 에뮬레이터에서 실제 운영 대화로 칩의 발음과 스피커를 확인했고, TTS 를 누른 뒤에도 앱이 정상이었다고 적혀 있습니다. 앱 테스트 744개 통과, 타입 검사 오류 0, eslint 91건은 기존과 같은 수였습니다. 발행은 OTA 7bb4ed01(runtime 1.11.0, iOS·Android)로, 같은 날 디자인 시스템 정리 묶음과 함께 나갔습니다(d8ff278). 판정은 11월 22일에 스피커 이벤트(ai_chat_suggestion_tts) 수와 추천 답 탭률로 봅니다.

곡 요청 쪽은 별도 plan 이 없습니다. 커밋에 요일 표기와 머리말 테스트가 들어갔고(목요일 선정, 화·금 3곡 같은 서버 값, 값이 없는 옛 응답), OTA a1af5b7e 발행 기록(c517ea5)이 있습니다. 화면에서 확인했다는 기록은 남아 있지 않습니다. 두 intent 모두 Open questions 는 없음으로 닫혀 있습니다.

Utakoto 는 J-POP·애니송 가사로 일본어를 배우는 앱입니다. 가사마다 후리가나·한국어 발음·뜻이 붙고, 노래 속 단어가 JLPT 단어장으로 모여 복습할 수 있습니다. iOS·Android 에서 무료입니다.

📚 관련 글

💬 댓글