UI5

오류 수집 직접 코딩? — UI5 MessageModel로 자동화 #shorts #SAP #UI5

▶ YouTube에서 보기

1. 수동 오류 수집의 문제점

UI5 애플리케이션을 처음 작성할 때 가장 흔히 마주치는 안티 패턴은 컨트롤러마다 onError, onValidationError, onParseError 핸들러를 따로 두고 sap.m.MessageBox.error()를 호출하는 방식입니다. 입력 필드가 10개를 넘어가는 화면이라면 검증 로직이 컨트롤러를 가득 채우고, OData 백엔드에서 내려오는 sap-message 헤더는 별도로 파싱해야 하며, 같은 오류가 두 번 표시되거나 닫힌 뒤 사라져 사용자가 무엇을 잘못 입력했는지 추적하지 못하는 사례가 빈번합니다.

이 글은 그 분산된 오류 처리 코드를 제거하고, UI5가 기본 제공하는 MessageManagerMessageModel로 모든 메시지를 한 곳에 모아 자동으로 수집·표시하는 실전 패턴을 다룹니다. 끝까지 읽고 나면 컨트롤러에서 try/catch와 MessageBox 호출이 거의 사라지고, 화면 상단의 단일 MessagePopover가 모든 검증·서버·비즈니스 오류를 일관되게 노출하는 구조를 만들 수 있습니다.

  • 수동 핸들러 vs 중앙 수집 방식 비교
  • MessageManager 인스턴스 획득과 MessageModel 바인딩
  • OData V2/V4의 handleValidationsap-message 자동 흡수
  • 커스텀 메시지 등록, 타입별 필터링, 컨텍스트 연결 패턴

2. 사전에 알아두면 좋은 것들

이 글은 SAPUI5 1.96 LTS 이상을 기준으로 작성되었으며, sap.ui.core.message 네임스페이스와 sap.m.MessagePopover가 기본 라이브러리로 로드되어 있다고 가정합니다. JSONModel/ODataModel을 manifest.json에 정의해 본 경험, MVC 컨트롤러에서 this.getView().getModel()로 모델을 끌어다 쓰는 기본기, 그리고 BindingMode와 OneWay/TwoWay 동작 차이에 대한 이해가 있으면 충분합니다.

3. 환경, 버전, 사전 준비

예제는 SAPUI5 1.108 (LTS) 기준이며 SAP BTP의 SAP Build Work Zone, ABAP Environment, S/4HANA Cloud 어디에서 동작시키든 동일하게 적용됩니다. 의존 라이브러리는 sap.m, sap.ui.core, sap.ui.layout 세 가지면 충분하고, OData 서비스가 없는 경우에도 JSONModel과 커스텀 메시지만으로 패턴 전체를 검증할 수 있습니다.

{
  "sap.ui5": {
    "dependencies": {
      "minUI5Version": "1.108.0",
      "libs": {
        "sap.m": {},
        "sap.ui.core": {},
        "sap.ui.layout": {}
      }
    },
    "models": {
      "": {
        "dataSource": "mainService",
        "settings": {
          "useBatch": true,
          "defaultBindingMode": "TwoWay"
        }
      }
    }
  }
}

로컬 개발은 npm install -g @sap/ux-ui5-toolingui5 serve로 띄우며, BAS(Business Application Studio)를 쓰는 경우 별도 설치 없이 launch 구성이 동일하게 동작합니다. 권장 브라우저는 Chromium 계열 최신 버전이며, MessagePopover의 a11y(ARIA) 동작을 확인하려면 NVDA/VoiceOver를 함께 켜두는 것이 좋습니다.

4. MessageManager와 MessageModel의 동작 원리

핵심 개념은 단순합니다. UI5 코어에는 싱글톤으로 살아 있는 MessageManager가 있고, 이 매니저는 내부에 MessageModel이라는 JSONModel을 들고 있습니다. 애플리케이션 어디서 발생한 메시지든 매니저의 addMessages()를 거치면 MessageModel의 / 경로(루트 배열)에 자동으로 추가됩니다. 화면의 어떤 컨트롤이든 이 모델을 바인딩하면 동일한 데이터를 동시에 보게 됩니다.

비유하자면 회사의 공용 게시판입니다. 각 부서(컨트롤러, OData 모델, 검증기)가 각자 게시물을 붙이고, 로비의 모니터(MessagePopover) 한 대가 모든 게시물을 실시간으로 비춰주는 구조죠. 각 부서에 모니터를 따로 두지 않아도 됩니다.

메시지 객체는 sap.ui.core.message.Message 클래스의 인스턴스이며 다음과 같은 속성을 가집니다.

  • message — 사용자에게 보여줄 본문
  • type — Error, Warning, Information, Success, None 중 하나
  • target — 메시지가 가리키는 모델 경로 (예: /Orders('4711')/Quantity)
  • processor — 메시지를 만든 모델 인스턴스 (OData/JSON 모델)
  • validation — true면 검증 오류, false면 비즈니스/서버 오류
  • persistent — true면 사용자가 닫기 전까지 유지

OData V2 모델은 handleValidation: true로 인스턴스화하면 입력 컨트롤의 parseError/validationError를 자동으로 매니저에 등록하고, 서버 응답의 sap-message 헤더에 담긴 비즈니스 메시지도 자동으로 흡수합니다. 즉 이벤트 핸들러를 직접 작성하지 않아도 모델 계층이 알아서 게시판에 글을 붙이는 셈입니다. 컨트롤러 측에서는 모델을 구독하는 일에만 집중하면 됩니다.

5. getMessageModel로 중앙 저장소 참조하기

가장 작은 예제부터 시작합니다. Component.js의 init에서 매니저 인스턴스를 받고 MessageModel을 뷰 모델로 등록합니다. 이후 어떤 뷰든 message>/로 접근할 수 있습니다.

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

  return UIComponent.extend("kr.acme.orders.Component", {
    metadata: { manifest: "json" },

    init: function () {
      UIComponent.prototype.init.apply(this, arguments);

      var oMsgManager = Core.getMessageManager();
      oMsgManager.registerObject(this.getRootControl(), true);

      // 'message' 라는 이름으로 전 뷰에서 공유
      this.setModel(oMsgManager.getMessageModel(), "message");
    }
  });
});

registerObject(control, true)의 두 번째 인자가 핵심입니다. true로 주면 해당 컨트롤 트리 아래의 모든 입력 컨트롤에서 발생하는 검증 오류가 자동으로 매니저에 등록됩니다. 루트에 한 번만 걸어두면 됩니다.

6. 실전 시나리오 — OData 오류 자동 수집과 MessagePopover 연결

주문 등록 화면을 가정합니다. 수량(Quantity) 필드는 양의 정수여야 하고, 저장 시 서버는 재고 부족이면 400 응답과 sap-message 헤더로 한국어 메시지를 내려줍니다. 수동 핸들러 없이 두 종류의 오류를 모두 잡아내는 구조는 다음과 같습니다.

<mvc:View
  controllerName="kr.acme.orders.controller.OrderForm"
  xmlns="sap.m"
  xmlns:mvc="sap.ui.core.mvc">
  <Page title="주문 등록">
    <headerContent>
      <Button
        id="btnMsg"
        icon="sap-icon://message-popup"
        text="{= ${message>/}.length }"
        type="Emphasized"
        press=".onOpenMessages" />
    </headerContent>
    <content>
      <Input
        id="inpQty"
        value="{
          path: '/Orders(4711)/Quantity',
          type: 'sap.ui.model.odata.type.Int32',
          constraints: { minimum: 1, maximum: 9999 }
        }" />
      <Button text="저장" press=".onSave" type="Accept" />
    </content>
  </Page>
</mvc:View>
sap.ui.define([
  "sap/ui/core/mvc/Controller",
  "sap/m/MessagePopover",
  "sap/m/MessageItem",
  "sap/ui/core/Core"
], function (Controller, MessagePopover, MessageItem, Core) {
  "use strict";

  return Controller.extend("kr.acme.orders.controller.OrderForm", {

    onInit: function () {
      var oItemTpl = new MessageItem({
        title: "{message>message}",
        subtitle: "{message>additionalText}",
        type: "{message>type}",
        description: "{message>description}",
        activeTitle: { path: "message>controlIds", formatter: function (a) {
          return Array.isArray(a) && a.length > 0;
        }}
      });

      this._oMsgPopover = new MessagePopover({
        items: { path: "message>/", template: oItemTpl },
        activeTitlePress: this._onMsgTitlePress.bind(this)
      });
      this.getView().addDependent(this._oMsgPopover);
    },

    onOpenMessages: function (oEvent) {
      this._oMsgPopover.toggle(oEvent.getSource());
    },

    _onMsgTitlePress: function (oEvent) {
      var aIds = oEvent.getParameter("item").getBindingContext("message")
                 .getObject().controlIds;
      if (aIds && aIds[0]) {
        sap.ui.getCore().byId(aIds[0]).focus();
      }
    }
  });
});

이 코드만으로 입력값이 0이면 Int32 타입이 ValidationException을 던지고 매니저가 자동으로 메시지 객체를 만들어 게시판에 올립니다. 헤더 버튼의 text="{= ${message>/}.length }" 표현식이 즉시 카운트를 갱신하고, MessagePopover의 items 바인딩이 같은 모델을 보고 있으므로 별도 갱신 코드도 필요 없습니다. OData 모델에 handleValidation: true가 설정되어 있다면 서버 오류 역시 같은 경로로 들어옵니다.

7. 프로덕션 코드 — 커스텀 메시지, 타입 필터, 일괄 검증

실무에서는 비즈니스 규칙(예: "VIP 고객은 50개 이상 주문 시 승인자가 필요")처럼 모델 타입만으로 표현하기 어려운 검증이 많습니다. 이때는 addMessages()sap.ui.core.message.Message를 직접 등록합니다. 저장 버튼 클릭 시 전체 화면을 한 번에 검사하는 패턴까지 묶으면 다음과 같습니다.

sap.ui.define([
  "sap/ui/core/mvc/Controller",
  "sap/ui/core/Core",
  "sap/ui/core/message/Message",
  "sap/ui/core/MessageType",
  "sap/ui/core/ValueState"
], function (Controller, Core, Message, MessageType, ValueState) {
  "use strict";

  return Controller.extend("kr.acme.orders.controller.OrderForm", {

    onSave: function () {
      var oMgr  = Core.getMessageManager();
      var oView = this.getView();

      // 1) 이전에 우리가 직접 올린 메시지만 정리 (자동 수집분은 유지)
      var aOld = oMgr.getMessageModel().getProperty("/")
                 .filter(function (m) { return m.code === "BIZ_RULE"; });
      oMgr.removeMessages(aOld);

      // 2) 화면 단위 검증
      var iQty = parseInt(oView.byId("inpQty").getValue(), 10);
      var sCustomer = oView.byId("inpCustomer").getValue();

      if (sCustomer && sCustomer.startsWith("VIP-") && iQty >= 50) {
        oMgr.addMessages(new Message({
          message: "VIP 고객 대량 주문은 관리자 승인이 필요합니다.",
          additionalText: sCustomer,
          type: MessageType.Warning,
          code: "BIZ_RULE",
          target: "/Orders(4711)/Quantity",
          processor: oView.getModel(),
          persistent: false
        }));
      }

      // 3) Error 타입이 하나라도 있으면 저장 차단
      var aErr = oMgr.getMessageModel().getProperty("/").filter(function (m) {
        return m.type === MessageType.Error;
      });
      if (aErr.length > 0) {
        oView.byId("btnMsg").setType("Reject");
        this._oMsgPopover.openBy(oView.byId("btnMsg"));
        return;
      }

      // 4) 정상 저장
      oView.getModel().submitChanges({
        success: this._onSaveOk.bind(this),
        error:   this._onSaveFail.bind(this)
      });
    },

    _onSaveOk: function () {
      Core.getMessageManager().addMessages(new Message({
        message: "주문이 저장되었습니다.",
        type: sap.ui.core.MessageType.Success,
        persistent: true
      }));
    },

    _onSaveFail: function (oErr) {
      // handleValidation:true 라면 이 핸들러는 거의 비어 있어도 됩니다.
      // 서버가 내려준 sap-message는 매니저가 이미 수집했습니다.
      console.warn("submitChanges failed", oErr);
    }
  });
});

핵심 트릭 세 가지를 짚어두면 좋습니다. 첫째, 비즈니스 메시지는 고유 code를 부여해 두면 다음 검증 사이클에서 그 코드만 정확히 제거할 수 있습니다. 둘째, processortarget을 지정하면 MessagePopover의 activeTitle 클릭으로 해당 입력 컨트롤에 포커스가 가는 동작이 자동으로 동작합니다. 셋째, persistent: true는 Success 알림처럼 사용자가 직접 닫을 때까지 유지되어야 하는 메시지에 적합하며 기본값은 false입니다.

8. 흔한 실수와 트러블슈팅

Q1. MessagePopover에 메시지가 두 번씩 보입니다. ODataModel을 handleValidation: true로 두면서 동시에 컨트롤러에서 onValidationError로 또 addMessages를 하면 중복됩니다. 자동 수집을 켰다면 수동 등록은 비즈니스 규칙용으로만 제한하세요.

Q2. 화면을 전환해도 이전 메시지가 그대로 남습니다. MessageModel은 컴포넌트 단위 싱글톤입니다. 라우터의 routeMatched 이벤트에서 해당 뷰의 컨텍스트와 무관한 메시지를 removeAllMessages() 또는 target 경로 기준으로 정리하는 훅을 두는 것이 일반적으로 권장됩니다.

Q3. 입력 필드의 빨간 테두리(ValueState)가 풀리지 않습니다. 매니저가 자동으로 컨트롤 ValueState를 세팅하지만, 해제는 사용자가 값을 다시 입력해 ValidationSuccess가 발생할 때 일어납니다. 코드로 메시지를 제거했다면 oInput.setValueState(ValueState.None)을 함께 호출해야 시각적으로도 풀립니다.

그 밖에 자주 마주치는 함정으로는, MessageItem 템플릿에서 type 속성을 문자열 리터럴로 묶어버려 색상이 항상 None으로 나오는 경우, 모델 이름을 message가 아닌 다른 별칭으로 등록하고 바인딩 경로를 갱신하지 않은 경우, OData V4에서 $$updateGroupId가 다르게 설정되어 메시지 처리 순서가 꼬이는 경우 등이 있습니다. 모두 콘솔에서 sap.ui.getCore().getMessageManager().getMessageModel().getData()를 찍어 보면 원인이 거의 즉시 드러납니다.

9. 더 깊이 파고들 만한 주제들

중앙 수집 구조가 자리잡으면 자연스럽게 이어지는 확장 포인트가 있습니다. 첫째는 Fiori Elements의 메시지 처리로, ListReport/ObjectPage가 동일한 MessageManager를 공유하므로 Custom Extension에서 동일 패턴을 그대로 재사용할 수 있습니다. 둘째는 OData V4requestSideEffects와 메시지 lifecycle 연계로, target이 컨텍스트 상대 경로로 바뀌는 점만 주의하면 됩니다. 셋째는 OPA5 통합 테스트에서 MessageModel의 길이를 단언(assert)하여 검증 시나리오를 자동화하는 패턴이며, 마지막으로 접근성 관점에서 MessagePopover의 ARIA live region을 활용해 스크린리더에 즉시 안내하는 기법까지 익혀두면 실무 품질이 한 단계 올라갑니다.

10. 더 읽어볼 곳

댓글 0

아직 댓글이 없습니다.