관광 앱 다국어 데이터 모델 및 검색 색인 설계 (자꾸 까먹어서 정리)
- 공유 링크 만들기
- X
- 이메일
- 기타 앱
관광 앱을 개발하거나 서비스를 운영하다 보면 외국인 여행자를 위해 다국어를 지원해야 하는 일이 꼭 생깁니다. 처음에는 단순히 앱 내 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이나 내부 코드는 더미 값으로 대체한 구조입니다.
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"으로 검색할 수 있습니다. 한글 장소명에만 매칭을 걸어두면 외국인들은 아무것도 찾지 못합니다.
표시용 문자열과 별개로, 아래처럼 검색 전용 키워드 색인을 생성하는 구조를 두는 것이 좋습니다.
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 흐름을 유기적으로 관리하는 데이터 모델 및 검색 색인 설계의 문제로 접근해야 합니다.
처음부터 이 구조를 탄탄하게 잡아두면, 나중에 서비스 대상 국가(언어)가 추가되거나 공공데이터 제공처가 통째로 바뀌더라도 소스코드를 크게 뒤흔들지 않고 안정적으로 서비스를 운영할 수 있습니다.
댓글
댓글 쓰기