UI5

날짜·금액·상태 변환 코드 90% 줄이는 Formatter #shorts #SAP #UI5

▶ YouTube에서 보기

UI5 Formatter가 해결하는 실무 문제

SAP UI5 애플리케이션을 개발하다 보면 OData 서비스에서 내려오는 원본 데이터와 사용자가 실제로 화면에서 보고 싶어하는 형태 사이에 큰 간극이 존재합니다. 예를 들어 백엔드는 날짜를 /Date(1717200000000)/ 형태의 Edm.DateTime으로 보내주고, 금액은 1234567.89라는 순수 숫자로, 주문 상태는 A, P, C, X 같은 단일 문자 코드로 전달합니다. 이걸 그대로 화면에 뿌리면 사용자는 "이게 무슨 뜻이지?"라며 당혹스러워합니다.

이 글에서 다룰 Formatter는 바로 이 간극을 메우는 핵심 장치입니다. 뷰(View)의 바인딩 표현식과 모델(Model)의 원본 데이터 사이에 얇은 변환 레이어를 삽입해서, 컨트롤러 로직을 오염시키지 않고도 표시 형식을 자유롭게 바꿀 수 있게 해줍니다.

  • Edm.DateTime, Edm.Decimal 등 OData 원시 타입을 사람이 읽을 수 있는 형태로 변환
  • 상태 코드를 다국어 텍스트 및 sap.ui.core.ValueState 아이콘 색상으로 매핑
  • 컨트롤러의 onInit이나 onBeforeRendering에서 수동 변환하는 코드 제거
  • 재사용 가능한 유틸 모듈로 분리해 여러 뷰에서 공유

먼저 알고 있으면 좋은 개념

이 글은 beginner 난이도로 작성되었지만, 몇 가지 배경 지식이 있으면 훨씬 매끄럽게 읽을 수 있습니다. XML View에서 {property} 형태로 데이터를 바인딩해 본 경험, sap.ui.define으로 모듈을 선언하는 AMD 방식, 그리고 JSONModel 또는 ODataModel을 컨트롤러에 연결해 본 경험이면 충분합니다.

실습 환경과 준비물

본 예제는 SAP UI5 1.120 LTS(장기지원) 기준으로 작성되었으며 1.108 이상에서 동일하게 동작합니다. SAP Business Application Studio 또는 VS Code + Fiori Tools 확장이면 충분하고, 로컬에서는 ui5 serve 또는 npm start로 미리보기를 실행합니다.

  • SAP UI5 SDK 1.120.x (또는 SAPUI5/OpenUI5 1.108+)
  • Node.js 18 LTS 이상, UI5 CLI 3.x
  • manifest.json에 sap.ui.core, sap.m 라이브러리 선언
  • 실습 시나리오: B2B 판매주문(SalesOrder) 관리 화면 - 주문일자, 총액, 처리상태 3개 컬럼

Formatter가 내부에서 어떻게 동작하는가

UI5에서 Formatter는 "바인딩된 원본 값을 화면에 표시하기 직전에 한 번 통과시키는 순수 함수(pure function)"라고 이해하면 쉽습니다. 마치 공장의 컨베이어 벨트 끝에 달린 라벨링 기계처럼, 흘러오는 데이터에 예쁜 포장지를 씌워 최종 소비자(사용자)에게 전달하는 역할을 합니다.

기술적으로는 sap.ui.model.PropertyBinding이 모델에서 값을 읽어올 때, 바인딩 옵션에 formatter 함수가 지정되어 있으면 그 함수를 호출해 반환값을 컨트롤의 프로퍼티에 세팅합니다.

중요한 원칙은 다음과 같습니다. 첫째, Formatter는 반드시 순수 함수여야 합니다. 둘째, Formatter 내부에서 모델을 갱신(setProperty)하면 무한 루프에 빠질 수 있으므로 절대 금지입니다. 셋째, null과 undefined에 대한 방어 코드는 필수입니다.

1단계: 날짜 Formatter 기본 예제

먼저 webapp/model/formatter.js 파일을 만들어 재사용 가능한 유틸 모듈로 설계합니다. B2B 판매주문 화면에서 주문일자를 "2026년 7월 8일" 형태로 표시하는 시나리오입니다.

sap.ui.define([
    "sap/ui/core/format/DateFormat"
], function (DateFormat) {
    "use strict";

    var _oDateFormat = DateFormat.getDateInstance({
        style: "long",
        calendarType: "Gregorian"
    });

    return {
        formatOrderDate: function (vOrderDate) {
            if (!vOrderDate) { return ""; }
            var oDate = vOrderDate instanceof Date
                ? vOrderDate
                : new Date(vOrderDate);
            if (isNaN(oDate.getTime())) { return ""; }
            return _oDateFormat.format(oDate);
        }
    };
});

모듈 스코프에 DateFormat 인스턴스를 한 번만 생성해 캐싱합니다. 대용량 테이블 렌더링 시 매 행마다 getDateInstance를 호출하면 옵션 파싱 오버헤드가 누적되어 체감 성능이 저하됩니다.

2단계: 금액 Formatter + 에러 처리 시나리오

실무에서는 통화(currency)가 항상 KRW만 있는 게 아닙니다. 같은 판매주문 테이블에 USD, EUR, JPY가 뒤섞여 있고, 결측치와 음수(반품)도 처리해야 합니다.

sap.ui.define([
    "sap/ui/core/format/NumberFormat",
    "sap/base/Log"
], function (NumberFormat, Log) {
    "use strict";

    var _mCurrencyCache = {};

    function _getCurrencyFormatter(sCurrencyCode) {
        if (!_mCurrencyCache[sCurrencyCode]) {
            _mCurrencyCache[sCurrencyCode] = NumberFormat.getCurrencyInstance({
                currencyCode: false,
                showMeasure: true
            });
        }
        return _mCurrencyCache[sCurrencyCode];
    }

    return {
        formatOrderAmount: function (vAmount, sCurrency) {
            if (vAmount === null || vAmount === undefined || vAmount === "") {
                return "-";
            }
            var fAmount = parseFloat(vAmount);
            if (isNaN(fAmount)) {
                Log.warning("formatOrderAmount: 변환 불가 - " + vAmount, null, "sales.formatter");
                return "-";
            }
            var sCcy = sCurrency || "KRW";
            return _getCurrencyFormatter(sCcy).format(fAmount, sCcy);
        }
    };
});

parseFloat을 쓰는 이유는 OData V2가 Decimal 타입을 문자열로 내려보내는 경우가 많기 때문입니다. sap/base/Log는 UI5 표준 로거로 프로덕션에서 로그 레벨로 필터링됩니다.

3단계: 상태 Formatter + 다국어 + ValueState 매핑

프로덕션 수준에서는 상태 코드 하나로 텍스트와 색상을 동시에 변환해야 합니다.

sap.ui.define([
    "sap/ui/core/ValueState",
    "sap/base/Log"
], function (ValueState, Log) {
    "use strict";

    var _mStatusMap = {
        "A": { i18nKey: "status.approved",  state: ValueState.Success },
        "P": { i18nKey: "status.pending",   state: ValueState.Warning },
        "C": { i18nKey: "status.completed", state: ValueState.Success },
        "X": { i18nKey: "status.cancelled", state: ValueState.Error },
        "H": { i18nKey: "status.onHold",    state: ValueState.Information }
    };

    return {
        formatStatusText: function (sCode) {
            if (!sCode) { return ""; }
            var oEntry = _mStatusMap[sCode];
            if (!oEntry) {
                Log.warning("Unknown status code: " + sCode, null, "sales.formatter");
                return sCode;
            }
            var oBundle = this.getModel && this.getModel("i18n")
                ? this.getModel("i18n").getResourceBundle()
                : null;
            return oBundle ? oBundle.getText(oEntry.i18nKey) : oEntry.i18nKey;
        },

        formatStatusState: function (sCode) {
            if (!sCode) { return ValueState.None; }
            var oEntry = _mStatusMap[sCode];
            return oEntry ? oEntry.state : ValueState.None;
        }
    };
});

XML View 바인딩 등록

컨트롤러에서 formatter를 프로퍼티로 노출해야 XML View에서 .formatter.xxx 형식으로 접근할 수 있습니다.

sap.ui.define([
    "sap/ui/core/mvc/Controller",
    "com/example/sales/model/formatter"
], function (Controller, formatter) {
    "use strict";

    return Controller.extend("com.example.sales.controller.OrderList", {
        formatter: formatter,

        onInit: function () {
            // 모델 초기화
        }
    });
});
<Table items="{/SalesOrderSet}">
    <columns>
        <Column><Text text="주문일"/></Column>
        <Column><Text text="총액"/></Column>
        <Column><Text text="상태"/></Column>
    </columns>
    <items>
        <ColumnListItem>
            <cells>
                <Text text="{ path: 'OrderDate', formatter: '.formatter.formatOrderDate' }"/>
                <Text text="{ parts: ['GrossAmount','CurrencyCode'], formatter: '.formatter.formatOrderAmount' }"/>
                <ObjectStatus
                    text="{ path: 'Status', formatter: '.formatter.formatStatusText' }"
                    state="{ path: 'Status', formatter: '.formatter.formatStatusState' }"/>
            </cells>
        </ColumnListItem>
    </items>
</Table>

앞쪽 점(.)은 "컨트롤러 스코프에서 찾아라"라는 지시어입니다. 점을 빠뜨리면 전역 스코프를 뒤지다가 undefined가 되어 값이 그대로 표시됩니다.

자주 마주치는 실수와 해결법

Q1. Formatter가 호출되지 않고 값이 undefined로 표시됩니다.
컨트롤러에서 formatter: formatter로 프로퍼티를 노출하지 않은 경우가 가장 흔한 원인입니다. 두 번째는 XML 바인딩 문법에서 앞쪽 점(.) 누락입니다.

Q2. 테이블 스크롤 시 화면이 버벅입니다.
Formatter 내부에서 getDateInstance나 getCurrencyInstance를 매번 호출하는 게 원인입니다. 모듈 스코프에 캐싱하거나 sap.ui.model.type.Currency, sap.ui.model.type.Date 같은 Type 클래스로 대체하세요.

Q3. 복합 바인딩에서 나머지 인자가 undefined입니다.
parts 배열의 각 항목이 서로 다른 모델에 속해 있는데 모델 이름을 지정하지 않은 경우입니다. parts: [{path: 'GrossAmount'}, {path: '/Currency', model: 'settings'}]처럼 명시해야 합니다.

더 깊이 들어가고 싶을 때

Formatter를 익혔다면 sap.ui.model.SimpleType을 상속받아 양방향 바인딩 지원 커스텀 Type을 만들어보세요. 입력 폼에서 "1,234,567원"으로 입력해도 모델에는 1234567이 저장되는 패턴입니다. Expression Binding을 활용하면 간단한 삼항 연산은 Formatter 없이 XML만으로 처리할 수 있어 코드량이 줄어듭니다. Fiori Elements에서 Annotation 기반 Formatter 주입, TypeScript UI5 프로젝트에서 Formatter 타입 정의도 자연스러운 다음 주제입니다.

댓글 0

아직 댓글이 없습니다.