UI5

XMLModel 30초 만에 — UI5 외부 XML 데이터 바인딩 #shorts #SAP #UI5

▶ YouTube에서 보기

개요 및 이 글의 목표

SAP UI5 애플리케이션을 개발하다 보면 백엔드가 항상 JSON이나 OData를 내려주지는 않습니다. 레거시 ERP 인터페이스, 외부 카탈로그 시스템, RSS/Atom 피드, SOAP 응답, 또는 ABAP에서 직접 직렬화한 XML 응답을 그대로 화면에 묶어야 할 때 sap.ui.model.xml.XMLModel이 가장 자연스러운 선택지가 됩니다. 이 글에서는 XMLModel의 내부 동작 원리부터 XPath 기반 바인딩, 그리고 실무에서 자주 마주치는 카탈로그 데이터 처리까지를 단계적으로 다룹니다.

  • XMLModel 인스턴스 생성과 외부 XML 적재 흐름 이해
  • XPath 경로 표현식으로 속성/엘리먼트 바인딩
  • bindElement를 활용한 컨텍스트 고정과 상대 경로 단축
  • aggregation 바인딩으로 XML 배열을 리스트/테이블에 표시
  • JSONModel·ODataModel 대비 XMLModel의 적정 사용 시점 판단

먼저 알고 있어야 할 것

UI5의 MVC 구조, XMLView 정의 방식, 데이터 바인딩 모드(One Way / Two Way / One Time)에 대한 기본 이해가 필요합니다. JSONModel을 한 번이라도 컨트롤러에서 다뤄봤다면 XMLModel 학습은 거의 1:1로 매핑되어 진행됩니다. 또한 XPath 1.0 수준의 경로 표현식(/root/item, @attr, text())을 알아두면 본문 이해가 훨씬 빨라집니다.

환경, 버전, 준비물

이 글의 예제는 SAPUI5 1.120 LTS(2024년 4월 릴리스 기준)와 OpenUI5 동일 버전에서 모두 검증되어 있습니다. sap.ui.model.xml.XMLModel은 UI5 초창기부터 제공된 안정 API로, 별도 의존성 추가가 필요 없습니다.

  • SAPUI5/OpenUI5 1.96 이상 (1.120 권장)
  • Node.js 18 LTS + ui5-tooling 3.x (로컬 개발 서버용)
  • BTP Build Work Zone 또는 Fiori Launchpad 통합 시 destination에서 CORS 헤더 허용 필요
  • 외부 XML 응답에 Content-Type: application/xml 또는 text/xml 지정 권장

주의할 점은 XMLModel이 브라우저의 DOMParser를 활용해 문서를 파싱한다는 사실입니다. 따라서 XML 선언부의 인코딩 정보와 실제 바이트 인코딩이 일치하지 않으면 한글이 깨질 수 있어 백엔드 헤더 설정이 중요합니다.

핵심 개념 — XMLModel은 어떤 식으로 데이터를 들고 있는가

JSONModel이 자바스크립트 객체 트리를 메모리에 그대로 보관한다면, XMLModel은 파싱된 DOM Document 객체를 내부에 보관합니다. 비유하자면 JSONModel이 "잘 정리된 가계부 엑셀"이라면 XMLModel은 "원문 그대로 스캔한 종이 문서"에 가깝습니다. 이 차이가 곧 바인딩 경로 표기 방식을 결정합니다.

XMLModel의 경로 규칙을 한 줄로 정리하면 다음과 같습니다.

  • 슬래시(/)는 자식 엘리먼트로의 진입을 의미
  • @ 접두어는 XML 속성(attribute) 접근
  • 루트 엘리먼트는 경로에 포함되지 않음 — 가장 흔한 함정
  • 같은 이름의 형제 엘리먼트는 0, 1 인덱스로 접근하거나 aggregation 바인딩으로 처리

또한 XMLModel은 양방향 바인딩을 지원하지만, DOM 노드를 직접 수정하는 방식이라 setProperty 호출 시 내부적으로 setAttribute 또는 textContent 갱신이 일어납니다. 따라서 양방향 바인딩으로 사용자 입력을 받은 뒤 다시 XML 문자열로 직렬화하려면 getData()가 아니라 getXML()(또는 XMLSerializer)을 사용해야 한다는 점이 JSONModel과 결정적으로 다릅니다.

실전 코드 1단계 — 외부 XML을 모델로 적재하기

가장 단순한 시나리오부터 시작합니다. 사내 자재 마스터를 XML로 제공하는 레거시 엔드포인트가 있다고 가정하고, 컨트롤러의 onInit에서 모델을 적재해 뷰 루트에 붙입니다.

sap.ui.define([
    "sap/ui/core/mvc/Controller",
    "sap/ui/model/xml/XMLModel",
    "sap/m/MessageToast"
], function (Controller, XMLModel, MessageToast) {
    "use strict";

    return Controller.extend("zmat.controller.CatalogOverview", {
        onInit: function () {
            const oMatModel = new XMLModel();
            oMatModel.loadData("/legacy/materials.xml", null, true);
            oMatModel.attachRequestCompleted(function () {
                MessageToast.show("자재 카탈로그 로딩 완료");
            });
            this.getView().setModel(oMatModel, "mat");
        }
    });
});

실전 코드 2단계 — 에러 처리와 XPath 기반 바인딩

실무에서는 네트워크 오류, XML 파싱 실패, 빈 노드 등 예외 케이스가 더 많습니다. attachRequestFailed로 에러를 잡고, 뷰에서는 XPath 경로로 속성·텍스트를 모두 묶어보겠습니다.

onInit: function () {
    const oCatalog = new XMLModel();
    oCatalog.setSizeLimit(2000);

    oCatalog.attachRequestFailed(function (oEvent) {
        const sMessage = oEvent.getParameter("message");
        const iStatus  = oEvent.getParameter("statusCode");
        sap.base.Log.error("Catalog XML 적재 실패: " + sMessage, "statusCode=" + iStatus);
        sap.m.MessageBox.error("카탈로그를 불러오지 못했습니다.");
    });

    oCatalog.loadData("/legacy/catalog.xml", null, true, "GET", false, false, {
        "Accept": "application/xml"
    });

    this.getView().setModel(oCatalog, "cat");
}
<mvc:View xmlns="sap.m" xmlns:mvc="sap.ui.core.mvc" controllerName="zmat.controller.CatalogOverview">
    <Panel headerText="{cat>/Header/Title}">
        <ObjectStatus text="{cat>/Header/IssuedAt}" state="Information"/>
        <Text text="버전: {cat>/@version}"/>
    </Panel>
</mvc:View>

루트 <Catalog>는 경로에 포함되지 않습니다. /Header/Title이 곧 루트의 자식인 Header의 자식 Title을 가리킵니다. 루트 자체의 속성은 /@version처럼 접근합니다.

실전 코드 3단계 — bindElement, aggregation 바인딩

<List items="{cat>/Items/Item}" selectionChange="onItemPick" mode="SingleSelectMaster">
    <ObjectListItem
        title="{cat>Name}"
        number="{cat>Price}"
        numberUnit="{cat>Price/@currency}">
        <attributes>
            <ObjectAttribute text="SKU {cat>@sku}"/>
            <ObjectAttribute text="재고 {cat>@stock}"/>
        </attributes>
    </ObjectListItem>
</List>
onItemPick: function (oEvent) {
    const oItem = oEvent.getParameter("listItem");
    const sPath = oItem.getBindingContext("cat").getPath();
    this.byId("detailPage").bindElement({
        path: sPath,
        model: "cat"
    });
}

흔한 실수와 트러블슈팅

  • 루트 엘리먼트를 경로에 포함하면 바인딩이 빈값으로 표시됨 — oModel.getProperty("/Header/Title")로 확인
  • 한글 깨짐: 백엔드 Content-Type: text/xml; charset=utf-8 명시 필수
  • 형제 엘리먼트 인덱스: XPath [2]가 아닌 /Items/Item/1 (0-based) 사용
  • 양방향 바인딩 후 직렬화: getData() 대신 getXML() 또는 XMLSerializer 사용

JSONModel·ODataModel과의 비교 — XMLModel이 답인 상황

  • 레거시 SOAP/XML 엔드포인트 — OData 전환이 불가능한 경우
  • ABAP에서 cl_xml_document로 직렬화한 응답을 프론트가 그대로 소비
  • RSS/Atom 피드 또는 외부 카탈로그 XML 파일 직접 렌더링
  • 설정 파일(config.xml)을 앱 구동 시 한 번 로드해 정적 뷰에 바인딩
  • OData V4 마이그레이션 전 과도기 — XMLModel로 화면을 먼저 구성 후 모델만 교체

댓글 0

아직 댓글이 없습니다.