관광 앱 다국어 데이터 모델 및 검색 색인 설계 (자꾸 까먹어서 정리)

관광 앱을 개발하거나 서비스를 운영하다 보면 외국인 여행자를 위해 다국어를 지원해야 하는 일이 꼭 생깁니다. 처음에는 단순히 앱 내 UI 문자열(strings.xml)만 번역하면 끝날 줄 알았는데, 실제 서비스를 돌려보니 장소명이나 지역명 같은 콘텐츠 데이터 처리가 진짜 번역의 핵심이더군요.

특히 공공데이터를 가져와서 쓰다 보면 한국어만 달랑 있거나, 영문 표기 방식이 데이터 소스마다 제각각이라 데이터가 다 깨집니다. 저의 경우도 "제주시", "Jeju-si", "Jeju City"가 마구 뒤섞여서 검색도 안 되고 화면도 깨지는 삽질을 좀 했습니다.

나중에 또 같은 문제로 고생하지 않으려고 표시 이름, 내부 지역 코드 표준화, Fallback 처리, 그리고 검색 색인 구조까지 실무에서 바로 쓸 수 있게 정리해 둡니다.

1. 기본 개념: 표시 이름과 원본 이름 분리하기

공공데이터에서 가져온 원본 이름을 그대로 덮어써 버리면, 나중에 번역 데이터가 바뀌거나 원본 데이터가 업데이트될 때 매칭할 기준이 사라집니다. 원본 이름은 그대로 보존하고, UI 표시용과 로마자 표기를 별도로 분리해서 관리해야 유지보수가 쉬워집니다.

항목예시용도
원본 이름성산일출봉공공데이터 원본 데이터 보존 (비교용)
한국어 표시명성산일출봉한국어 UI 표시
영어 표시명Seongsan Ilchulbong영어 UI 표시
로마자 표기Seongsan Ilchulbong외국어 검색 보조용
검색 키워드sunrise peak, seongsan검색 색인 확장용
출처명공공데이터 제공 기관명저작권 및 출처 표시 요구사항 대응

2. 데이터 모델 설계 예시 (Kotlin)

저의 경우, 이 문제를 해결하기 위해 아래처럼 장소 기본 정보와 번역 맵(Map<AppLanguage, String>)을 분리한 데이터 모델을 사용합니다. 외부 노출을 막기 위해 실제 API URL이나 내부 코드는 더미 값으로 대체한 구조입니다.

Kotlin
data class TourPlaceName(
    val placeId: String,
    val sourceName: String,
    val localizedNames: Map<AppLanguage, String>,
    val romanizedName: String?,
    val searchKeywords: List<String>,
    val sourceUpdatedAt: String?,
)

enum class AppLanguage {
    KO, // 한국어
    EN, // 영어
    JA, // 일본어
    ZH, // 중국어
}

fun TourPlaceName.displayName(language: AppLanguage): String {
    return localizedNames[language]
        ?: localizedNames[AppLanguage.EN]
        ?: localizedNames[AppLanguage.KO]
        ?: sourceName
}

이 모델의 핵심: Fallback 순서 보장

displayName 확장 함수를 보면 Fallback 순서가 명확하게 잡혀 있습니다. 사용자가 선택한 언어 데이터가 없으면 영어 -> 한국어 -> 원본 이름 순으로 꽂아줘야 화면에 빈 값이 노출되는 대참사를 막을 수 있습니다. 이 기준이 명확하지 않으면 목록 화면과 상세 화면에서 서로 다른 이름이 노출되는 엉뚱한 버그가 터지기 쉽습니다.

3. 지역명 표준화 기준 (내부 코드 매핑)

지역명은 필터, 검색, 상세 주소, 지도 화면 등 안 쓰이는 곳이 없습니다. 공공데이터에서 주는 원본 지역 코드를 앱 전역에서 그대로 가져다 쓰면, 나중에 데이터 제공처가 바뀌었을 때 소스코드를 다 뜯어고쳐야 하는 재앙이 옵니다.

  • 해결책: 앱 내부에서 쓸 안정적인 공통 지역 코드를 정의하고, 외부 원본 코드를 매핑해서 쓰는 구조로 설계해야 합니다.

구분예시관리 기준
원본 지역 코드provider_area_001공공데이터 원본 코드 (언제든 바뀔 수 있음)
앱 내부 지역 코드JEJU_CITY앱 내부 공통 코드 (안정성 확보)
한국어명제주시한국어 UI 표시용
영어명Jeju City영어 UI 표시용
로마자Jeju-si검색 보조용
상위 지역제주특별자치도계층형 필터 구성용

4. Android 리소스와 서버 데이터의 역할 분담

처음 개발할 때 자꾸 하는 실수가 모든 텍스트를 strings.xml에 밀어 넣으려는 것입니다. 앱 업데이트(릴리즈) 없이 콘텐츠를 실시간으로 바꾸거나 관리하려면 UI 문구와 다국어 콘텐츠 데이터를 철저히 분리해야 운영이 편해집니다.

데이터 유형추천 저장 위치분리 이유
버튼 / 메뉴 / 설정 문구Android string resources고정된 앱 UI 문구이므로 앱 빌드 시 함께 관리
지역명로컬 DB 또는 서버 데이터필터링 및 검색 조건과 유기적으로 연결됨
장소명서버 API 또는 로컬 캐시실시간 데이터 갱신 및 대용량 데이터 관리 필요
카테고리명리소스 + 코드 매핑 데이터앱 내부 UI 아이콘 매핑과 데이터 코드가 둘 다 필요
출처 표시 문구서버 또는 로컬 데이터제공 기관별, 데이터별로 저작권 표시가 다를 수 있음

5. 외국어 검색을 위한 검색 색인(Index) 빌더

화면에는 깔끔하게 공식 영어명인 "Seongsan Ilchulbong"을 보여주더라도, 외국인 사용자는 "Seongsan"이나 의미상 단어인 "Sunrise peak"으로 검색할 수 있습니다. 한글 장소명에만 매칭을 걸어두면 외국인들은 아무것도 찾지 못합니다.

표시용 문자열과 별개로, 아래처럼 검색 전용 키워드 색인을 생성하는 구조를 두는 것이 좋습니다.

Kotlin
class TourSearchIndexBuilder {
    fun buildKeywords(place: TourPlaceName): Set<String> {
        return buildSet {
            add(place.sourceName.normalizeKeyword())
            place.localizedNames.values.forEach { add(it.normalizeKeyword()) }
            place.romanizedName?.let { add(it.normalizeKeyword()) }
            place.searchKeywords.forEach { add(it.normalizeKeyword()) }
        }.filter { it.isNotBlank() }.toSet()
    }

    private fun String.normalizeKeyword(): String {
        return trim()
            .lowercase()
            .replace(Regex("\\s+"), " ") // 연속된 공백을 하나로 처리
            .take(80) // 인덱스 길이 제한
    }
}

이렇게 소문자 변환(lowercase)과 공백 정규화(normalizeKeyword)를 거친 키워드 셋을 따로 인덱싱해 두면 검색 품질이 확 올라갑니다. 경험상 검색 기능은 출시 후에 품질 체감이 가장 크게 드러나는 영역이라, 처음부터 쪼개놓는 걸 추천합니다.

6. 현실적인 Fallback UI 처리 기준

다국어 앱이라고 해서 오픈 시점에 모든 콘텐츠를 100% 완벽하게 번역할 수는 없습니다. 번역 데이터 누락은 예외 상황이 아니라 언제든 발생할 수 있는 기본 흐름으로 잡고 UI 대책을 세워야 화면이 깨지지 않습니다.

  • 현재 언어 번역이 있을 때: 해당 언어로 깔끔하게 표시

  • 현재 언어 번역이 없을 때: 영어(EN) 또는 한국어(KO)로 Fallback 표시

  • 모든 다국어 번역이 누락됐을 때: 공공데이터 원본 이름(sourceName)이라도 표시

  • 상세 설명문 번역이 없을 때: "본 콘텐츠는 원문(한국어)으로 제공됩니다" 안내 문구와 함께 원문 표시

  • 검색 키워드가 없을 때: 원본 이름과 로마자 표기를 기준으로 최소한의 검색 허용

7. 보안 및 실무 운영 체크리스트

실무에서 서비스를 운영하다 보면 의외로 보안이나 개인정보 처리에서 뒤통수를 맞는 경우가 생깁니다. 아래 가이드라인은 설계 시 꼭 체크하시길 바랍니다.

  • 번역 출처 및 검수 상태 관리: 파파고나 구글 번역 API를 써서 자동 번역을 돌렸다면 내부 데이터에 번역 방식과 검수 상태(reviewed, pending, machineTranslated)를 반드시 태깅해 두세요. 나중에 오역 신고 들어왔을 때 일괄 수정하기 편합니다.

  • 사용자 검색 로그 및 개인정보 주의: 검색 품질 개선을 위해 외국인들이 검색한 키워드를 수집할 때가 많습니다. 이때 사용자 식별자 + 현재 GPS 좌표 + 검색어 원문을 한 테이블에 통으로 쌓으면 민감한 개인정보 이슈가 생길 수 있습니다. 로그를 남길 때는 식별자를 제거하거나 집계 형태로 최소화해야 합니다.

  • 민감 정보 공개 제한: 당연한 이야기지만, 코드 예제나 설정 파일 관리 시 실제 운영 API URL(https://example.com 형태로 대체 필요), 내부 지역 코드, 외부 번역 API 키 등은 외부 깃허브나 공개 저장소에 절대 흘러 들어가지 않도록 주의하셔야 합니다.

요약 및 한 줄 결론

관광 앱의 다국어 처리는 단순한 strings.xml 번역 노가다가 아니라, 표시명, 검색어, 데이터 출처, Fallback 흐름을 유기적으로 관리하는 데이터 모델 및 검색 색인 설계의 문제로 접근해야 합니다.

처음부터 이 구조를 탄탄하게 잡아두면, 나중에 서비스 대상 국가(언어)가 추가되거나 공공데이터 제공처가 통째로 바뀌더라도 소스코드를 크게 뒤흔들지 않고 안정적으로 서비스를 운영할 수 있습니다.

댓글

이 블로그의 인기 게시물

안드로이드 화면 꺼짐 방지 FLAG_KEEP_SCREEN_ON 및 WakeLock 적용 방법

안드로이드 백그라운드 타이머 구현 및 Foreground Service 알림 설계 정리