본문으로 바로가기
728x90
반응형
SMALL

[Troubleshoot] Next.js TTS 모바일 발음 이슈 및 Gemini API 서버 사이드 캐싱 해결기

Next.js 환경에서 학습 서비스를 개발하며 겪은 두 가지 핵심 이슈인 '모바일 TTS 발음 파편화''API 호출 비용 최적화를 위한 서버 캐싱'에 대한 트러블슈팅 과정을 공유합니다.


1. TTS 발음 이슈: 왜 내 폰에서는 영어가 한국어처럼 들릴까?

📌 문제 상황

PC 웹 브라우저에서는 유창한 영어로 들리던 TTS(Text-to-Speech)가 모바일(Android/iOS) 기기에서 접속하면 한국어 억지로 영어를 읽는 듯한 '한국어식 발음'으로 출력되는 현상이 발생했습니다.

🔍 원인 분석

브라우저 내장 window.speechSynthesis는 OS 및 브라우저 엔진에 따라 제공하는 음성 데이터(Voices)가 다릅니다. 단순히 lang = 'en-US'만 설정할 경우, 모바일 기기에서는 기본 설정된 한국어 엔진이 영어를 읽어버리는 경우가 발생하기 때문입니다.

✅ 해결 방법: 음성 엔진 우선순위 할당 로직 도입

모바일 환경을 감지하여, 품질이 검증된 엔진(Google, Apple 등)을 강제로 매칭하고 모바일 특유의 먹먹함을 해결하기 위해 pitchrate를 보정했습니다.

이전로직

 async playTTS(text: string, options?: AudioServiceOptions): Promise<void> {
    return new Promise((resolve, reject) => {
      if (!this.isSupported()) {
        reject(new Error('Browser does not support speech synthesis'));
        return;
      }

      const opts = { ...this.defaultOptions, ...options };

      // 이전 재생 중지
      window.speechSynthesis.cancel();

      const utterance = new SpeechSynthesisUtterance(text);
      utterance.lang = opts.lang || 'en-US';
      utterance.rate = opts.rate || 0.9;
      utterance.pitch = opts.pitch || 1;

      // 최고 품질 음성 선택
      const voice = this.selectBestVoice(utterance.lang);
      if (voice) {
        utterance.voice = voice;
      }

      utterance.onend = () => resolve();
      utterance.onerror = (error) => {
        console.error('Speech synthesis error:', error);
        reject(error);
      };

      window.speechSynthesis.speak(utterance);
    });
  }

이후 로직

async playTTS(text: string, options?: AudioServiceOptions): Promise<void> {
    return new Promise((resolve, reject) => {
      if (typeof window === 'undefined' || !window.speechSynthesis) {
        return reject(new Error('지원하지 않는 브라우저입니다.'));
      }
      window.speechSynthesis.cancel();

      const opts = { ...this.defaultOptions, ...options };
      const utterance = new SpeechSynthesisUtterance(text);
      utterance.lang = opts.lang || 'en-US';

      const isMobile = this.checkIsMobile();
      if(isMobile){
        utterance.rate = options?.rate || 0.85; 
        utterance.pitch = 1.2;  보
      } else {

        utterance.rate = options?.rate || 0.9;
        utterance.pitch = options?.pitch || 1.0;
      }

// 핵심 로직: 영어 발음 강제를 위한 Voice Selection
      const voices = window.speechSynthesis.getVoices();
      const enVoices = voices.filter(v => v.lang.startsWith('en'));

     // 모바일 품질이 좋은 엔진 순서대로 매칭
      const selectedVoice = 
        enVoices.find(v => v.name.includes('Google') && v.lang === 'en-US') ||
        enVoices.find(v => v.name.includes('Apple') && v.lang === 'en-US') ||
        enVoices.find(v => v.lang === 'en-US') ||
        enVoices[0];

      if (selectedVoice) {
        utterance.voice = selectedVoice;
      }

      utterance.onend = () => resolve();
      utterance.onerror = (e) => reject(e);

      window.speechSynthesis.speak(utterance);
    });
  }




2. Gemini API 재요청 이슈: LocalStorage의 한계와 서버 캐싱

📌 문제 상황

Gemini API는 호출당 비용이나 할당량이 제한되어 있습니다. 처음에는 사용자의 새로고침을 막기 위해 LocalStorageClient-side 메모리 캐시를 사용했으나, 이는 '개별 사용자'에게만 유효했습니다. 서비스에 1,000명의 새로운 사용자가 들어오면 여전히 1,000번의 API 호출이 발생하는 구조였습니다.

🔍 원인 분석

  • LocalStorage/SWR: 클라이언트 브라우저에만 데이터가 존재하므로 기기 간 공유가 불가능합니다.
  • 서버 메모리 변수: 서버가 재시작되면 초기화되며, 인스턴스가 여러 개일 경우 동기화되지 않습니다.

✅ 해결 방법: Next.js Route Segment Config를 통한 정적 캐싱

모든 사용자가 동일한 학습 데이터를 본다면, 매번 생성할 필요 없이 서버 레벨에서 정적 결과물로 빌드하거나 긴 주기(24시간 등)로 캐싱하는 것이 가장 효율적입니다.

TypeScript

// ✅ Next.js App Router 서버 캐시 전략 적용

export const revalidate = 86400; // 24시간 동안 캐시 유지
export const dynamic = 'force-static'; // 해당 라우트를 정적 페이지로 강제

export async function GET(request: NextRequest) {
  try {
    // API 호출 및 데이터 처리 로직...
    return NextResponse.json(grammarData);
  } catch (error) {
    return NextResponse.json({ error: 'Failed' }, { status: 500 });
  }
}

💡 마치며: 무엇을 배웠는가?

  1. TTS는 엔진 선택이 핵심이다: 단순 lang 설정만으로는 부족하며, 디바이스별 보이스 리스트를 직접 필터링해야 일관된 UX를 제공할 수 있습니다.
  2. 캐싱의 주체를 명확히 하자: * 개인화된 데이터 👉 LocalStorage / SWR
    • 공통된 리소스 (콘텐츠) 👉 Server-side Caching (Next.js Cache)

불필요한 API 호출을 줄임으로써 비용 절감은 물론, 서버 응답 속도를 획기적으로 개선할 수 있었습니다.

728x90
반응형
LIST