온디바이스 번역의 시대가 왔다: ML Kit, Apple Translation, Chrome Translator API 살펴보기

온디바이스 번역의 시대가 왔다: ML Kit, Apple Translation, Chrome Translator API 살펴보기

서비스를 만들다 보면 어느 순간 “이 기능을 서버에서 계속 처리하는 게 맞나?”라는 생각을 하게 된다. 번역 기능도 그중 하나였다.

내가 운영하는 서비스에는 영어 원어민 강사와 한국 학부모 사이의 커뮤니케이션이 있다. 알림장, 채팅, 댓글, 피드백처럼 짧지만 자주 발생하는 문장이 많다. 처음에는 당연히 번역 API를 생각했다. 하지만 개인사업자 입장에서 모든 번역 요청을 외부 API로 태우는 것은 사용량과 비용 예측 면에서 부담이 있었다.

또 다른 생각도 들었다. 지금 사용자가 들고 있는 스마트폰, 태블릿, 노트북의 성능은 과거의 데스크톱을 훌쩍 뛰어넘는다. 그런데 이 기기들을 단순히 HTTP 응답을 받아 화면에 그리는 최종 뷰어로만 쓰는 것은 어딘가 아깝다.

그래서 번역을 로컬에서 처리하는 방법을 찾아보기 시작했다.

ML Kit 먼저 보기

가장 먼저 검토한 것은 Google ML Kit의 온디바이스 번역 기능이었다.

ML Kit 소개

ML Kit은 Google이 제공하는 모바일용 머신러닝 SDK다. Android와 iOS를 모두 지원하고, 번역뿐 아니라 텍스트 인식, 바코드 스캔, 얼굴 인식, 언어 감지 같은 기능도 제공한다.

ML Kit 자체는 2018년 Google I/O 시기에 Firebase와 함께 소개되었고, 이후 2020년에는 Firebase 프로젝트 없이도 사용할 수 있는 standalone ML Kit SDK로 분리되었다. 2021년에는 대부분의 API가 GA가 되었다.

내가 관심을 가진 것은 그중에서도 Translation API였다. 문서를 보면 50개 이상의 언어 간 번역을 온디바이스로 처리할 수 있고, 필요한 언어 모델은 처음 사용할 때 동적으로 다운로드하는 방식이다.

내 서비스처럼 한국어 ↔ 영어 문장을 자주 다루는 경우에는 꽤 매력적으로 보였다.

iOS에서 ML Kit 사용하기

iOS에서는 CocoaPods로 ML Kit Translate를 추가한다.

pod 'GoogleMLKit/Translate'

Swift에서는 대략 이런 식으로 사용할 수 있다.

import MLKitTranslate

let options = TranslatorOptions(
    sourceLanguage: .korean,
    targetLanguage: .english
)

let translator = Translator.translator(options: options)

let conditions = ModelDownloadConditions(
    allowsCellularAccess: false,
    allowsBackgroundDownloading: true
)

translator.downloadModelIfNeeded(with: conditions) { error in
    guard error == nil else {
        print("model download failed")
        return
    }

    translator.translate("오늘 숙제를 잘 해왔습니다.") { translatedText, error in
        guard error == nil, let translatedText else {
            print("translation failed")
            return
        }

        print(translatedText)
    }
}

핵심은 모델 다운로드다.
번역을 바로 호출하는 것이 아니라, 해당 언어쌍에 필요한 모델이 디바이스에 있는지 확인하고, 없으면 먼저 내려받아야 한다.

ML Kit 문서상 언어 모델은 대략 30MB 정도이고, Wi-Fi 환경에서 필요한 모델만 받는 것을 권장한다. 따라서 실서비스에서는 “첫 번역 시 모델 다운로드 안내 → 다운로드 완료 후 번역 가능” 같은 UX가 필요하다.

또 하나의 실무 포인트는 iOS 패키지 관리다. ML Kit iOS는 CocoaPods 중심이다. 이미 Firebase Messaging 같은 라이브러리를 Swift Package Manager로 쓰고 있다면, 일부 라이브러리는 SPM, 일부는 Pods로 섞이면서 빌드 구성이 지저분해질 수 있다.

Android에서 ML Kit 사용하기

Android에서는 Gradle dependency를 추가한다.

dependencies {
    implementation 'com.google.mlkit:translate:17.0.3'
}

Kotlin 예시는 다음과 같다.

val options = TranslatorOptions.Builder()
    .setSourceLanguage(TranslateLanguage.KOREAN)
    .setTargetLanguage(TranslateLanguage.ENGLISH)
    .build()

val translator = Translation.getClient(options)

val conditions = DownloadConditions.Builder()
    .requireWifi()
    .build()

translator.downloadModelIfNeeded(conditions)
    .addOnSuccessListener {
        translator.translate("오늘 숙제를 잘 해왔습니다.")
            .addOnSuccessListener { translatedText ->
                println(translatedText)
            }
            .addOnFailureListener { exception ->
                exception.printStackTrace()
            }
    }
    .addOnFailureListener { exception ->
        exception.printStackTrace()
    }

Android 쪽도 구조는 비슷하다.
Translator를 만들고, 모델을 다운로드하고, 번역을 호출한다.

사용이 어렵지는 않다. 오히려 “모바일에서 로컬 번역을 붙인다”는 관점에서는 가장 빨리 실험해볼 수 있는 선택지다.

ML Kit의 단점

그런데 실제로 써보면 몇 가지 아쉬움이 있었다.

첫 번째는 개발 환경이다. iOS 쪽에서는 시뮬레이터 지원에 제약이 있어 실제 기기 테스트가 필요했다. (구글이 컴파일 막아둠) 불가능한 일은 아니지만, 개발 루프가 느려진다. 다른 구글 패키지 사용시 cocoapod과 spm으로 이원화되는 것도 별로다.

두 번째는 품질이다. 이 부분이 가장 컸다. 간단한 문장은 어느 정도 쓸 만 했지만, 여러 케이스를 테스트 해 본 결과 ML Kit은 “번역이 되긴 한다”에 가깝고, 자연스러움과 의미 보존이 부족한 경우가 많았다.

그래서 ML Kit은 우선 온디바이스 번역의 최저선으로 잡고 좀 더 찾아보기로 했다.

웹 번역: Chrome Translator API

사실 Chrome은 오래전부터 번역 기능을 제공해왔다.
사용자가 외국어 페이지에 들어가면 주소창에 번역 아이콘이 뜨고, 우클릭 메뉴나 브라우저 UI를 통해 페이지 전체를 번역할 수 있었다.

하지만 이 기능은 어디까지나 최종 사용자를 위한 브라우저 UI였다.
웹앱 개발자가 JavaScript로 “이 문장을 한국어에서 영어로 번역해줘”라고 호출할 수 있는 API는 아니었다.

내가 처음 ML Kit을 찾아봤던 이유도 여기에 있었다. 웹앱에서 바로 로컬 번역을 호출할 수 없다면, 모바일 앱의 네이티브 영역에서 ML Kit을 붙이거나, 서버 API를 써야 했다.

당시에는 꽤 무리한 실험도 생각해봤다.
아이폰 공기계에 간단한 HTTP 서버를 띄우고, 내 백엔드 서버가 그 아이폰으로 번역 요청을 보내면, 아이폰에서 ML Kit 번역을 돌린 뒤 다시 response를 주는 구조였다.

아이디어만 보면 “집에 굴러다니는 공기계를 번역 서버로 쓰자”에 가깝다.
하지만 프로덕션에 올리기에는 무리가 있었다.

공기계가 강제 업데이트를 하면?
화면 꺼짐이나 절전 정책에 걸리면?
서비스가 죽었을 때 원격으로 재시작할 방법은?
모니터링은 어떻게 할 것인가?
동시 요청이 늘어나면 어떻게 할 것인가?

결국 이건 서버가 아니라 “서버처럼 쓰고 싶은 개인 기기”였다.
재미있는 실험은 될 수 있어도, 운영 가능한 시스템은 아니었다.

그런데 2024년 말부터 상황이 바뀌기 시작했다. Chrome이 Built-in AI 흐름 안에서 Translator API를 공개했고, Chrome 131부터 origin trial로 사용할 수 있게 했다. 이후 Chrome 138부터는 Translator API가 stable로 올라왔다.

Chrome Translator API 사용 예시

현재 Chrome Translator API는 대략 이런 식으로 사용할 수 있다.

type TranslatorAvailability =
  | 'unavailable'
  | 'downloadable'
  | 'downloading'
  | 'available';

declare const Translator: {
  availability(options: {
    sourceLanguage: string;
    targetLanguage: string;
  }): Promise<TranslatorAvailability>;

  create(options: {
    sourceLanguage: string;
    targetLanguage: string;
    monitor?: (monitor: EventTarget) => void;
  }): Promise<{
    translate(text: string): Promise<string>;
    translateStreaming?(text: string): ReadableStream<string>;
    destroy?(): void;
  }>;
};

async function translateKoToEn(text: string) {
  if (typeof Translator === 'undefined') {
    throw new Error('Translator API is not supported in this browser.');
  }

  const availability = await Translator.availability({
    sourceLanguage: 'ko',
    targetLanguage: 'en',
  });

  if (availability === 'unavailable') {
    throw new Error('This language pair is not available.');
  }

  const translator = await Translator.create({
    sourceLanguage: 'ko',
    targetLanguage: 'en',
    monitor(monitor) {
      monitor.addEventListener('downloadprogress', (event) => {
        const e = event as ProgressEvent;
        console.log(`downloaded: ${Math.round(e.loaded * 100)}%`);
      });
    },
  });

  const result = await translator.translate(text);
  translator.destroy?.();

  return result;
}

브라우저에 내장된 번역 모델을 쓰기 때문에, 서버로 원문을 보내지 않고도 번역할 수 있다.
특히 웹앱 입장에서는 이 변화가 크다.

기존 구조는 보통 이랬다.

사용자 입력
  → 내 서버
  → 번역 API
  → 내 서버
  → 사용자 화면

Chrome Translator API를 쓰면 가능한 경우에는 이렇게 바뀐다.

사용자 입력
  → 사용자 브라우저에서 번역
  → 필요한 결과만 서버 저장 또는 전송

임시로 보여주는 번역, 채팅창에서 사용자가 눌렀을 때만 보는 번역, 관리자 화면에서 참고용으로 보는 번역처럼 “반드시 서버에 원문과 번역문을 모두 저장할 필요가 없는 기능”에는 꽤 잘 맞는다.

Chrome Translator API의 제한

다만 아직 모든 플랫폼에서 쓸 수 있는 것은 아니다.

현재 문서 기준으로 Language Detector API와 Translator API는 Chrome desktop에서 동작하고, 모바일에서는 동작하지 않는다. 또한 Translator 객체 생성에는 사용자의 최근 상호작용, 즉 transient user activation이 필요하다. 쉽게 말해 페이지가 로드되자마자 몰래 대량 번역을 돌리는 식의 사용은 제한된다.

모델도 처음부터 항상 준비되어 있는 것은 아니다. 언어팩은 필요할 때 다운로드되고, 이 과정에서 시간이 걸릴 수 있다. 따라서 UX상으로는 “번역 준비 중”, “모델 다운로드 중” 같은 상태 처리가 필요하다.

또한 MDN에서는 아직 이 API를 experimental, limited availability로 설명한다. Chrome에서는 stable로 올라왔지만, 웹 표준 전체 관점에서는 아직 모든 브라우저에서 믿고 쓸 수 있는 Baseline API는 아니다.

그래서 내 결론은 이렇다.

PC 사용자에게는 Chrome을 권장하고, Chrome Translator API가 가능하면 로컬 번역을 사용한다.
불가능하면 기존 서버 번역이나 다른 fallback을 둔다.
모바일 웹뷰에서는 아직 Chrome Translator API에 기대기 어렵다.

Apple Translation Framework

Chrome Translator API를 실제로 돌려보니 ML Kit과 품질 차이가 꽤 컸다.
그래서 iOS에서도 ML Kit 대신 더 나은 온디바이스 번역이 가능한지 찾아보게 되었다.

그때 찾은 것이 Apple Translation Framework다.

Apple은 이미 iOS에 Translate 앱과 시스템 번역 기능을 제공해왔다. iOS 17.4부터는 SwiftUI의 translationPresentation을 통해 앱 안에서 시스템 번역 UI를 띄울 수 있었다.

하지만 이 방식은 내가 원하던 형태와는 조금 달랐다.

translationPresentation은 이름 그대로 “번역 화면을 보여주는” 기능에 가깝다. 사용자가 특정 텍스트를 보고, 시스템이 제공하는 번역 UI를 통해 결과를 확인하는 흐름이다. 앱 입장에서는 번역 기능을 빠르게 붙일 수 있지만, 개발자가 원하는 시점에 문장을 넘기고, 결과 문자열을 받아서, 내 서비스의 데이터 흐름 안에 자연스럽게 넣는 API형 사용과는 거리가 있었다.

예를 들어 채팅 메시지 옆에 번역문을 바로 붙이거나, 알림장 내용을 저장 전에 미리 번역하거나, 여러 문장을 한 번에 번역해 화면에 다시 반영하는 식의 기능을 만들려면 단순한 시스템 UI 표시만으로는 부족하다.

즉, iOS 17.4의 translationPresentation은 “사용자에게 번역 UI를 보여주기”에는 좋았지만, 서비스 로직 안에서 번역 결과를 다루기에는 애매했다.

이 부분이 iOS 18에서 달라졌다.
iOS 18부터는 TranslationSessiontranslationTask를 통해 앱 안에서 번역 결과를 더 유연하게 받을 수 있게 되었다.

SwiftUI에서 사용하는 방식

Apple Translation은 일반적인 SDK처럼 TranslationClient() 같은 객체를 아무 곳에서나 만들어 호출하는 방식과는 조금 다르다. SwiftUI view에 .translationTask를 붙이면, 그 안에서 TranslationSession을 받아 번역을 수행하는 형태다.

간단한 예시는 다음과 같다.

import SwiftUI
import Translation

struct LocalTranslateView: View {
    @State private var sourceText = "오늘 숙제를 잘 해왔습니다."
    @State private var translatedText = ""
    @State private var configuration: TranslationSession.Configuration?

    var body: some View {
        VStack(spacing: 12) {
            Text(sourceText)

            Button("번역") {
                configuration = TranslationSession.Configuration(
                    source: Locale.Language(identifier: "ko"),
                    target: Locale.Language(identifier: "en")
                )
            }

            Text(translatedText)
        }
        .translationTask(configuration) { session in
            do {
                let response = try await session.translate(sourceText)
                translatedText = response.targetText
            } catch {
                print(error)
            }
        }
    }
}

이 구조가 처음에는 조금 낯설다.
번역 엔진을 백그라운드 서비스처럼 하나 만들어두고 어디서든 호출하는 방식이 아니라, UI에 연결된 task를 통해 session을 얻는 방식이기 때문이다.

하지만 Apple 입장에서는 이 설계가 자연스럽다.
번역 모델이 없으면 사용자의 동의가 필요할 수 있고, 모델 다운로드 상태를 시스템 UI로 안내해야 할 수도 있다. 이미 시스템에 받아둔 언어 모델이 있다면 앱은 그 모델을 활용할 수 있다. 앱마다 같은 번역 모델을 제각각 관리하는 구조가 아니라, 시스템이 언어 모델을 관리하고 앱은 그 능력을 빌려 쓰는 방식에 가깝다.

Apple Translation의 장점과 주의점

iOS에서는 이 선택지가 꽤 매력적이다.

별도의 Google ML Kit 의존성을 붙이지 않아도 된다.
언어 모델은 시스템이 관리한다.
사용자는 익숙한 Apple 번역 품질과 다운로드 UI를 경험한다.
무엇보다 내가 실험한 교육 현장 문장 기준으로는 ML Kit보다 번역 결과가 훨씬 자연스러웠다.

다만 주의점도 있다.

우선 iOS 18 이상이 필요하다. 구형 iOS 사용자를 위해서는 fallback이 필요하다.
또한 API 구조가 SwiftUI view에 붙는 방식이라, 완전히 headless한 번역 서버처럼 생각하면 안 된다. 필요할 때 시스템 UI가 개입할 수 있고, 모델이 없는 경우 다운로드 흐름도 고려해야 한다.

그래도 iOS 앱에서 온디바이스 번역을 붙인다면, 이제 ML Kit만 볼 필요는 없다.
iOS 18 이상에서는 Apple Translation Framework가 훨씬 강력한 후보가 되었다.

번역 기능의 기본값을 다시 생각할 때

지금은 온디바이스 AI가 화두인 시대다.
LLM이나 이미지 생성처럼 큰 기능만 떠올리기 쉽지만, 실제 서비스에서는 번역 같은 작은 기능부터 체감될 수 있다.

사용자 기기는 이미 충분히 강하다.
그 리소스를 단순히 서버 응답을 보여주는 뷰어로만 쓰기보다, 가능한 작업은 기기 안에서 처리하게 만들 수 있다.

온디바이스 번역은 제공자의 API 비용 부담을 줄이고, 사용자 문장이 외부 서버로 나가는 일을 줄이며, 응답속도 측면에서도 장점이 있다. 물론 모든 환경에서 동일하게 쓸 수는 없고, 플랫폼별 fallback도 필요하다.

그래도 방향은 분명하다.

이제 번역 기능을 설계할 때 기본값을 다시 생각해볼 만하다.
모든 문장을 서버 API로 보내기 전에, 먼저 물어볼 수 있다.

“이 번역, 사용자의 기기에서 처리할 수는 없을까?”

PC에서는 Chrome Translator API, iOS에서는 Apple Translation Framework, Android에서는 ML Kit 같은 선택지가 생기면서 온디바이스 번역은 더 이상 실험적인 아이디어만은 아니게 되었다.