Expo 앱 다국어: 사전의 키를 한국어 원문으로 둔 이유
성경 지도 앱에 언어 다섯 개를 더했습니다. 영어, 일본어, 중국어 간체와 번체, 스페인어입니다. 화면이 하나뿐이고 서버도 없는 앱이라 붙이는 일 자체는 간단할 줄 알았는데, 번역문을 어디에 둘지에서 막혔습니다.
고칠 수 없는 파일 안에 번역할 문자열이 있었습니다
이 앱의 지도 데이터는 웹 버전에서 그대로 복사해 온 것입니다. 바울의 전도여행 경로, 구약 지명 여든두 곳, 12지파 분배 영역, 분열 왕국의 경계. 전부 좌표와 설명을 손으로 적어 둔 TypeScript 파일 다섯 개입니다.
복사본이라는 점이 중요합니다. 웹 쪽이 원본이고 앱은 사본이라, 두 곳의 차이가 0이어야 나중에 원본이 바뀌었을 때 무엇을 가져와야 하는지 알 수 있습니다. 그래서 이 다섯 파일은 앱에서 한 글자도 고치지 않는다는 규칙을 두고 있었습니다.
그런데 번역할 문자열이 전부 그 안에 있습니다. 세어 보니 화면에 그려지는 문자열이 753번 나왔고, 중복을 걷어내면 634개였습니다. 지명, 오늘날 지명, 한 줄 설명, 여정 제목, 시대 이름, 성경 책 이름. 파일에 손을 못 대니 번역문은 바깥에 두고 데이터를 가리키는 수밖에 없습니다.
구조 경로로 가리키면 어긋나도 모릅니다
가리키는 방법은 두 가지입니다.
첫 번째는 구조 경로입니다. route.first.stop.3.note 처럼 「1차 전도여행의 네 번째 정거장의 설명」이라고 위치로 적는 방식이고, i18n 라이브러리들이 대체로 이 모양입니다. 저도 처음에는 이쪽으로 잡았습니다.
문제는 웹 원본이 바뀔 때 드러납니다. 정거장이 하나 끼어들면 그 뒤가 통째로 한 칸씩 밀립니다. 사전은 그 사실을 모릅니다. 「루스드라」 자리에 「더베」의 설명이 붙고, 앱은 아무 오류 없이 잘 돕니다. 영어로 보고 있는 사람은 그게 틀렸다는 걸 알 방법이 없습니다. 저도 모릅니다.
두 번째는 한국어 원문 자체를 키로 쓰는 방식입니다.
'브엘세바': 'Beersheba',
'텔 셰바, 이스라엘': 'Tel Sheva, Israel',
'다윗이 처음 일곱 해 반 동안 유다를 다스린 도읍.':
'The capital from which David ruled Judah for his first seven and a half years.',원문이 한 글자라도 바뀌면 사전에서 빗나갑니다. 빗나가면 그 자리에 한국어가 그대로 나옵니다. 영어 화면 한복판에 「브엘세바」가 떠 있으면 이상하다는 걸 누구나 압니다.
이게 이 설계의 전부입니다. 번역이 안 된 것은 눈에 띄고, 틀린 번역은 눈에 안 띕니다. 어긋났을 때 어느 쪽으로 실패할지를 고른 것입니다.
되풀이가 줄어드는 효과도 있습니다. 「예루살렘」은 정거장이자 지명이자 남유다의 수도로 열한 번 나오는데 사전에는 한 줄입니다. 「튀르키예」가 들어간 지명은 스물여섯 개인데 나라 이름은 그중 한 번만 정하면 됩니다. 753개가 634개로 준 게 이 때문입니다. 극적인 차이는 아니지만, 같은 말을 두 번 다르게 옮길 여지가 없어지는 쪽이 더 중요합니다.
같은 한국어가 다른 뜻이면 이 방식이 깨집니다
원문을 키로 쓰면 자리에 따라 뜻이 다른 말이 한 칸을 두고 싸우게 됩니다. 이건 이 방식의 명백한 약점이라, 붙이기 전에 실제로 얼마나 겹치는지부터 셌습니다.
두 역할 이상에 걸친 문자열이 80개 나왔습니다. 하나씩 보니 전부 같은 것을 가리키고 있었습니다. 「우르」는 아브라함 여정의 정거장이면서 구약 지명 목록에도 있는데 둘 다 Ur 입니다. 「단」은 지명이자 지파인데 둘 다 Dan 입니다. 「텔 엘무카야르, 이라크」도 양쪽에 똑같이 나옵니다. 같은 자리를 가리키는 데이터가 두 층에 들어 있는 것이니 번역이 같은 편이 오히려 맞습니다.
진짜로 갈리는 곳은 화면 문구 쪽에 있었습니다. 상단 층 이름의 「왕국」은 분열 왕국 면을 켜는 버튼이고, 시대 구분의 「왕국」은 지명을 시기별로 나눈 이름입니다. 영어로는 Kingdoms 와 Kingdom 으로 갈립니다. 「복음서」도 경로 묶음 이름과 시대 이름 양쪽에 있습니다. 그래서 화면 문구만 뜻이 정해진 키를 쓰는 별도 파일로 뺐습니다.
점검 스크립트가 오래된 버그를 하나 잡았습니다
키를 원문으로 잡으면 어긋남이 화면에 드러나기는 하는데, 그때는 이미 배포된 뒤입니다. 원본을 다시 복사해 왔을 때 바로 알아야 해서 점검 스크립트를 만들었습니다. 데이터를 실제로 불러와 화면이 그리는 문자열을 전부 모으고, 언어별 사전과 맞춰 보는 것입니다.
여기서 예상하지 못한 게 나왔습니다.
성경 참조를 검사하면서 정거장 참조 136개는 전부 원문 그대로여야 한다고 단정해 뒀는데, 열세 개가 걸렸습니다. 제 단정이 틀린 게 아니라 앱이 틀려 있었습니다.
참조 데이터가 두 가지 꼴로 섞여 있었습니다. 바울 여정은 경로 전체가 사도행전이라 "13:4" 처럼 장과 절만 적고, 화면이 앞에 「사도행전」을 붙여 줍니다. 그런데 갈릴리 사역 경로는 마태와 마가와 누가를 오가서 정거장마다 "눅 4:16-30" 처럼 책 약어를 스스로 달고 있습니다. 화면은 그걸 가리지 않고 앞에 책 이름을 붙였습니다.
마태복음 눅 4:16-30
에스라 스 1:1-4한국어로 볼 때는 책 이름이 두 번 적힌 게 눈에 잘 들어오지 않습니다. 영어로 가면 Matthew Luke 4:16-30 이 됩니다. 다국어를 붙이지 않았으면 계속 못 봤을 자리입니다.
참조를 조립하는 자리를 함수 하나로 모으고, 참조가 책 이름을 스스로 달고 있으면 경로의 책 이름을 붙이지 않도록 고쳤습니다. 열세 곳이 정리됐습니다.
그 점검 스크립트도 틀렸습니다
고치고 다시 돌리니 영어에서만 두 건이 남았습니다.
책 이름 중복: 스 1:1-4 + 에스라 → Ezra 1:1-4Ezra 1:1-4 는 맞는 출력입니다. 제가 짠 검사가 「결과 안에 책 이름이 들어 있으면 중복」이라고 부분 문자열로 보고 있었는데, 영어에서는 에스라의 약어가 곧 Ezra 라 자기 자신에 걸렸습니다. 느헤미야는 Neh 와 Nehemiah 로 달라서 안 걸렸고, 그래서 하필 두 건만 남아 진짜 버그처럼 보였습니다.
앞머리를 정확히 맞춰 보도록 검사를 고쳤습니다. 점검 도구가 무엇을 단정하고 있는지 먼저 봐야 한다는 걸 한 작업에서 두 번 배웠습니다. 한 번은 도구가 옳았고 한 번은 도구가 틀렸는데, 출력만 봐서는 둘이 똑같이 생겼습니다.
지명은 음역하지 않았습니다
번역에서 세운 원칙이 하나 있습니다. 지명을 소리 나는 대로 옮기지 않는 것입니다.
개역개정의 「바보」를 Babo 로 적으면 영어 성경을 읽은 사람은 그게 Paphos 인 줄 모릅니다. 「가버나움」은 영어로 Capernaum, 일본어로 カファルナウム, 중국어로 迦百农 입니다. 각 언어의 성경이 이미 쓰고 있는 이름이 있으니 그것을 찾아 넣는 게 번역이고, 음역은 아무도 모르는 새 이름을 만드는 일입니다.
그래서 영어는 ESV/NIV 계열, 일본어는 新共同訳, 중국어는 和合本, 스페인어는 Reina-Valera 의 표기를 따랐습니다. 책 약어도 같습니다. 「왕상」은 영어에서 1 Kgs, 스페인어에서 1 R, 중국어에서 王上 입니다. 이런 약어가 21개 있습니다.
중국어는 간체와 번체를 따로 적었습니다. 성경 지명은 글자만 바꾸면 되지만 오늘날 지명이 갈립니다. 키프로스가 간체로는 塞浦路斯, 번체로는 賽普勒斯 입니다. 이탈리아도 意大利 와 義大利 로 다릅니다. 글자 변환기에 맡겼으면 오늘날 지명 쪽이 어색해졌을 겁니다.
화면 쪽에서 걸린 것
지도 라벨은 따로 다뤄야 했습니다. 지명 점을 누르면 그 자리에 말풍선이 뜨는데, 누른 점이 어느 데이터인지 되찾는 열쇠가 지명 이름이었습니다. 라벨을 번역해서 넣으면 그 열쇠까지 같이 바뀌어 말풍선이 안 뜹니다. 지도에 그릴 글자와 되찾을 열쇠를 GeoJSON 속성에 따로 뒀습니다. 지파 면은 원래 키로 되찾고 있어서 이 문제가 없었습니다.
사소한데 한참 헤맨 것도 하나 있습니다. 화면 문구를 내주는 함수를 t 라는 이름으로 받았는데, 지파 칩을 그리는 자리가 이미 ALL_TRIBES.map((t) => ...) 였습니다. 안쪽에서 t('noLand') 를 부르니 지파 객체를 함수로 호출하고 있었습니다.
아직 확신하지 못하는 부분
지명과 책 이름은 각 언어의 성경을 기준으로 삼았으니 근거를 댈 수 있습니다. 하지만 한 줄 설명 219개는 제가 옮긴 것입니다. 영어와 스페인어는 그럭저럭 봐 줄 만하다고 보는데, 일본어와 중국어 쪽은 원어민 검토를 받아야 한다고 생각합니다. 뜻이 틀린 것보다 어색한 쪽이 걱정입니다.
네이티브 모듈이 두 개 늘어서 이번 변경은 OTA 로 내보낼 수 없습니다. 새 빌드가 필요합니다. runtimeVersion 정책이 앱 버전을 따라가게 되어 있어서, 버전을 올리지 않은 채 OTA 를 발행하면 그 모듈이 없는 바이너리에 새 JS 가 내려갑니다. 이건 다국어와 상관없이 네이티브 의존성을 더할 때마다 걸리는 자리입니다.
함께 읽기
- 빌드 우회 셋 정리: 어디까지가 내가 고칠 수 있는 문제인가앱 하나를 두 스토어에 올리는 동안 빌드가 여러 번 깨졌고, 그때마다 우회를 하나씩 넣었습니다. 다 끝나고 저장소를 보니 임시방편이 셋 쌓여 있었습니다.
- EAS 배포 구조: Build·Submit·Update 는 왜 따로인가앱 하나를 두 스토어에 올리면서 EAS 를 처음 제대로 썼습니다. 그 전까지는 "배포 명령을 돌리면 스토어에 올라간다" 정도로만 알고 있었는데, 실제로 해보니 그 한 문장 안에 서로 다른 세 가지 일이 섞여 있었습니다. 그걸 구분하고 나서야 어디서 막혔는지가 보였습니다.
- Metro 포트 충돌로 배운 것: RN이 OTA 업데이트를 할 수 있는 이유새로 만든 앱을 아이폰에 설치했습니다. 빌드는 성공했고 설치도 됐는데, 앱을 열자 빨간 에러 화면이 떴습니다. 그런데 에러가 가리키는 파일이 이상했습니다.
- Expo의 역사: SDK 0에서 CNG와 개발 빌드까지Expo를 설명할 때 가장 자주 붙는 말은 "React Native를 쉽게 쓰게 해 주는 도구"입니다. 틀린 설명은 아니지만 지금의 Expo를 이해하기에는 범위가 너무 좁습니다. Expo는 미리 만들어진 앱에서 JavaScript를 실행하던 초기 경험을 출발점으로 삼았고, 지금은 네이티브 프로젝트 생성과 모듈 개발, 라…
- React Native Debug·Release 구성 차이: No script URL provided 가 뜨는 이유앱 두 개를 아이폰에 올려놓고 하나를 열었더니 빨간 화면에 이 문구가 떴습니다.