UI5

UI5 i18n 없이 다국어? 이 3단계면 끝 #shorts #SAP #UI5

▶ YouTube에서 보기

📖 이 글에서 다루는 내용과 목표

UI5 앱에서 버튼 라벨과 메시지를 뷰에 하드코딩하면, 영어 사용자에게도 한국어 화면이 그대로 노출되고 문구 하나를 바꿀 때마다 뷰 파일을 전부 뒤져야 합니다. 이 글은 SAPUI5의 i18n(ResourceModel) 모델과 .properties 파일로 다국어 텍스트를 관리하는 방법을, 전자상거래 주문 관리 앱 시나리오로 3단계에 걸쳐 다룹니다. 끝까지 읽으면 아래 항목을 직접 수행할 수 있습니다.

  • i18n 모델이 언어별 .properties 파일을 찾아가는 폴백 체인 이해
  • XML 뷰 바인딩과 컨트롤러 getText()로 플레이스홀더 치환
  • supportedLocales·fallbackLocale로 불필요한 404 요청 제거
  • 텍스트 분류 주석과 QUnit 테스트로 번역 품질 관리

📚 미리 갖춰두면 좋은 배경

XML 뷰와 컨트롤러의 기본 구조, manifest.json이 앱 설정의 중심이라는 점, 그리고 {model>path} 형태의 바인딩 문법을 알고 있으면 충분합니다. JSONModel을 한 번이라도 써봤다면, ResourceModel은 "읽기 전용·텍스트 전용 모델"이라는 점만 추가로 이해하면 됩니다.

🔧 환경 · 버전 · 준비물

이 글의 예제는 SAPUI5 1.120 LTS(OpenUI5 동일 API) 기준이며, 1.71 이상이면 대부분 동일하게 동작합니다. 다만 두 가지 버전 경계가 있습니다.

  • supportedLocales / fallbackLocale 설정: 1.77 이상에서 지원
  • .properties 파일의 UTF-8 우선 인코딩 처리: 일반적으로 1.90 이상에서 안정적

도구는 SAP Business Application Studio 또는 VS Code + UI5 CLI(ui5-tooling 3.x), 로컬 미리보기용 Node.js 18 이상을 권장합니다. 언어별 파일은 webapp/i18n/ 폴더에 배치하고, 브라우저 언어를 바꾸지 않고 테스트할 때는 URL 파라미터 ?sap-ui-language=en을 활용하는 방법이 일반적입니다.

💡 핵심 개념 — ResourceModel과 .properties의 동작 원리

i18n 모델의 실체는 sap.ui.model.resource.ResourceModel이며, 내부적으로 ResourceBundle이 언어별 .properties 파일을 로드합니다. .properties 파일은 키=값 한 줄 형식의 단순 텍스트로, 뷰는 "키"만 참조하고 실제 문장은 파일이 책임집니다. 식당에 비유하면 뷰는 "3번 메뉴 주세요"라고 번호(키)만 말하고, 어떤 언어의 메뉴판(.properties)이 손님 앞에 놓일지는 ResourceModel이 로케일을 보고 결정하는 구조입니다.

언어 결정은 폴백 체인(fallback chain)으로 이뤄집니다. 사용자의 로케일이 ko_KR이라면 UI5는 다음 순서로 파일을 탐색합니다.

  1. i18n_ko_KR.properties — 언어+국가 일치
  2. i18n_ko.properties — 언어만 일치
  3. i18n_en.properties — 설정된 폴백 로케일(기본값 en)
  4. i18n.properties — 최종 폴백(기본 파일)

중요한 점은 "파일 단위 교체"가 아니라 키 단위 병합이라는 것입니다. i18n_ko.properties에 없는 키는 자동으로 폴백 파일의 값이 사용되므로, 번역이 90%만 끝났어도 앱이 깨지지 않습니다. 반대로 이 특성 때문에 번역 누락을 눈치채기 어려워, 뒤에서 다룰 키 검증 테스트가 필요합니다.

또 하나의 축은 플레이스홀더입니다. msgOrderCount=총 {0}건의 주문이 있습니다처럼 자리 표시자를 두면, 언어마다 어순이 달라도(영어는 "You have {0} orders") 문장을 자연스럽게 유지할 수 있습니다. 문자열을 +로 이어 붙이는 방식은 어순이 다른 언어에서 반드시 어색해지므로 피하는 편이 좋습니다.

💻 실전 예제 3단계

1단계 — 기본: i18n 모델 등록과 뷰 바인딩

주문 관리 앱의 manifest.json에 ResourceModel을 선언합니다. 컴포넌트 코드에서 수동 생성하는 방식보다 매니페스트 선언이 일반적으로 권장됩니다.

{
  "sap.ui5": {
    "models": {
      "i18n": {
        "type": "sap.ui.model.resource.ResourceModel",
        "settings": {
          "bundleName": "shop.ordermgr.i18n.i18n"
        }
      }
    }
  }
}

webapp/i18n/i18n.properties(기본 파일)와 한국어 파일을 만듭니다. 기본 파일은 영어로 작성하는 것이 관행입니다.

# i18n.properties (기본/폴백)
orderListTitle=Order Management
btnRefreshOrders=Refresh
shippingStatusLabel=Shipping Status
# i18n_ko.properties
orderListTitle=주문 관리
btnRefreshOrders=새로 고침
shippingStatusLabel=배송 상태

XML 뷰에서는 모델명 i18n과 키를 바인딩합니다.

<mvc:View xmlns="sap.m" xmlns:mvc="sap.ui.core.mvc"
  controllerName="shop.ordermgr.controller.OrderList">
  <Page title="{i18n>orderListTitle}">
    <headerContent>
      <Button text="{i18n>btnRefreshOrders}" press=".onRefresh"/>
    </headerContent>
    <Label text="{i18n>shippingStatusLabel}"/>
  </Page>
</mvc:View>

브라우저 언어가 한국어면 i18n_ko.properties가, 그 외에는 기본 파일이 적용됩니다. ?sap-ui-language=en으로 즉시 전환 테스트가 가능합니다.

2단계 — 실무: 플레이스홀더 치환과 키 누락 로깅

주문 건수처럼 동적 값이 들어가는 메시지는 컨트롤러에서 getText(키, [인자 배열])로 처리합니다. 비동기 모델에서는 번들 취득이 Promise를 반환하므로 async로 다루고, 실패 상황과 키 누락에 대비한 로깅을 넣습니다.

# i18n_ko.properties 추가분
msgOrderCount=총 {0}건의 주문 중 {1}건이 배송 지연입니다
msgLoadFailed=주문 데이터를 불러오지 못했습니다
sap.ui.define([
  "sap/ui/core/mvc/Controller",
  "sap/m/MessageToast",
  "sap/base/Log"
], function (Controller, MessageToast, Log) {
  "use strict";

  return Controller.extend("shop.ordermgr.controller.OrderList", {

    onRefresh: async function () {
      const oBundle = await this.getOwnerComponent()
        .getModel("i18n").getResourceBundle();
      try {
        const aOrders = await this._fetchOrders(); // OData 조회 가정
        const iDelayed = aOrders.filter((o) => o.delayed).length;
        MessageToast.show(
          oBundle.getText("msgOrderCount", [aOrders.length, iDelayed])
        );
      } catch (oError) {
        Log.error("주문 조회 실패", oError, "shop.ordermgr");
        MessageToast.show(oBundle.getText("msgLoadFailed"));
      }
    },

    // 키 존재 여부를 방어적으로 확인하고 싶을 때
    _safeText: function (oBundle, sKey, aArgs) {
      if (!oBundle.hasText(sKey)) {
        Log.warning("i18n 키 누락: " + sKey, null, "shop.ordermgr");
      }
      return oBundle.getText(sKey, aArgs);
    }
  });
});

hasText()는 폴백 체인 전체에서 키 존재 여부를 확인하므로, 개발 단계에서 번역 누락을 콘솔 경고로 조기에 발견할 수 있습니다.

3단계 — 프로덕션: 성능·테스트·품질 관리

기본 설정만 쓰면 UI5는 존재하지 않는 언어 파일(i18n_ko_KR.properties 등)까지 순서대로 요청해 불필요한 404가 발생합니다. 실제 제공하는 언어를 명시해 네트워크 요청을 줄입니다.

{
  "sap.ui5": {
    "models": {
      "i18n": {
        "type": "sap.ui.model.resource.ResourceModel",
        "settings": {
          "bundleName": "shop.ordermgr.i18n.i18n",
          "supportedLocales": ["ko", "en", "de"],
          "fallbackLocale": "en",
          "async": true
        }
      }
    }
  }
}

번역가에게 문맥을 전달하는 분류 주석(X/Y 텍스트 타입)도 일반적으로 권장되는 관행입니다. 최대 길이 힌트를 함께 적어 번역 후 UI가 깨지는 문제를 예방합니다.

#XTIT,20: 주문 목록 페이지 제목
orderListTitle=Order Management
#XBUT,12: 새로 고침 버튼
btnRefreshOrders=Refresh
#XMSG: {0}=전체 건수, {1}=지연 건수
msgOrderCount=You have {0} orders, {1} delayed

QUnit 테스트로 "기본 파일과 언어 파일의 키 집합 일치"를 자동 검증하면 번역 누락이 빌드 단계에서 걸러집니다.

QUnit.test("ko 번들은 기본 번들의 모든 키를 포함한다", async function (assert) {
  const [sBase, sKo] = await Promise.all([
    fetch("i18n/i18n.properties").then((r) => r.text()),
    fetch("i18n/i18n_ko.properties").then((r) => r.text())
  ]);
  const fnKeys = (sText) => sText.split("\n")
    .filter((l) => l.trim() && !l.startsWith("#"))
    .map((l) => l.split("=")[0].trim());
  const aKoKeys = fnKeys(sKo);
  fnKeys(sBase).forEach((sKey) =>
    assert.ok(aKoKeys.includes(sKey), "누락 키: " + sKey));
});

보안 관점에서 한 가지 덧붙이면, .properties 값에 사용자 입력을 조합해 FormattedText처럼 HTML을 해석하는 컨트롤에 넣을 때는 이스케이프 여부를 반드시 확인해야 합니다. 표준 컨트롤의 text 속성은 자동 이스케이프되지만, HTML 렌더링 계열 속성은 예외이기 때문입니다.

⚠️ 자주 겪는 문제와 해결 FAQ

Q1. 화면에 번역 대신 키 이름(orderListTitle)이 그대로 보입니다. 해당 키가 폴백 체인 어디에도 없다는 뜻입니다. 키 오타(대소문자 구분), 기본 파일 i18n.properties에 키 자체가 빠진 경우가 대부분입니다. 새 텍스트는 항상 기본 파일에 먼저 추가하고 언어 파일에 복제하는 순서를 습관화하세요.

Q2. 한국어가 주문처럼 깨져 보입니다. 인코딩 문제입니다. .properties는 역사적으로 ISO-8859-1이 표준이었으나, 최신 UI5는 일반적으로 UTF-8을 우선 시도합니다. 에디터 저장 인코딩을 UTF-8(BOM 없음)로 통일하고, 1.90 미만 구버전 환경이라면 유니코드 이스케이프() 변환 빌드를 검토하세요.

Q3. ?sap-ui-language=ko를 붙여도 언어가 바뀌지 않습니다. supportedLocales에 해당 언어가 없으면 폴백 로케일로 강제됩니다. 또 뷰에 하드코딩된 텍스트는 당연히 바뀌지 않으므로, 전환되지 않는 문구가 실제로 i18n 키 바인딩인지 먼저 확인하세요.

Q4. Network 탭에 i18n 관련 404 요청이 여러 개 보입니다. 오류가 아니라 폴백 체인 탐색의 부산물입니다. 다만 프로덕션에서는 3단계처럼 supportedLocales를 명시해 제거하는 편이 성능상 유리합니다.

Q5. getResourceBundle() 결과에서 getText 호출이 실패합니다. async: true 모델에서는 Promise가 반환됩니다. await 또는 .then() 없이 곧바로 getText()를 호출하면 실패하므로 2단계 코드처럼 async 패턴을 사용하세요.

🚀 이어서 확장할 주제

텍스트 국제화가 자리 잡으면 자연스럽게 숫자·날짜·통화 로케일 포맷팅(sap.ui.core.format.DateFormat, NumberFormat)이 다음 과제입니다. 산업·고객사별 용어를 갈아 끼우는 terminologies 설정, Fiori launchpad 환경의 사용자 언어 설정 연동, 접근성 텍스트(ariaLabelledBy 등)의 i18n 키 일원화, 그리고 CAP 백엔드와 함께 쓰는 경우 CDS 기반 데이터 국제화까지 확장하면 앱 전체의 로케일 대응이 완성됩니다.

📚 더 읽어보면 좋은 문서

댓글 0

아직 댓글이 없습니다.