UI5

Input vs MultiInput — UI5 Tokenizer 활용법 #shorts #SAP #UI5

▶ YouTube에서 보기

이 글에서 다룰 것

sap.m.MultiInput은 사용자가 여러 값을 토큰(Token) 형태로 입력하고 시각적으로 관리할 수 있는 컨트롤입니다. 검색 필터, 태그 입력, 수신자 목록처럼 "값을 하나씩 추가/제거하면서 전체 목록을 유지"해야 하는 UI에서 표준처럼 사용됩니다. 본문에서는 XML 뷰 선언부터 모델 바인딩, tokenUpdate 이벤트 처리, getTokens를 통한 일괄 수집까지 실무에서 자주 쓰는 패턴을 다룹니다.

  • MultiInput과 Token의 관계 이해
  • tokens 집합 바인딩으로 초기값과 양방향 동기화 구현
  • tokenUpdate 이벤트에서 added/removed 분리 처리
  • getTokens로 선택값을 서버에 전송하는 패턴 작성
  • 중복 토큰, 유효성 검증, ValueHelp 연동까지 확장

이 글을 보기 전에

SAPUI5/OpenUI5 기본 XML 뷰 구조, JSONModel 사용법, 컨트롤러 이벤트 핸들러 등록 방식을 알고 있어야 합니다. sap.m.Input을 한 번이라도 다뤄봤고, 집합 바인딩(aggregation binding)이 무엇인지 익숙하면 진도가 빠릅니다. 또한 OData V2/V4 또는 JSONModel을 모델로 사용하는 환경에서 양방향 바인딩(TwoWay)이 동작한다는 점을 이해하고 있어야 합니다.

환경 및 준비물

아래 환경에서 동작을 검증한 코드입니다. 버전별로 API 시그니처가 거의 동일하지만, 일부 이벤트 파라미터 이름이 바뀐 적이 있으므로 사용 중인 UI5 버전의 API Reference를 참고하는 것을 권장합니다.

  • SAPUI5 1.108 LTS 이상 (1.120 권장)
  • sap.m 라이브러리 (MultiInput, Token, Tokenizer 포함)
  • JSONModel 또는 OData V4 Model
  • Fiori Tools 또는 BAS(Business Application Studio)
  • 브라우저: Chromium 계열 최신 버전 권장

SAP BTP 환경에서 사용한다면 Launchpad 통합 시 컨트롤 ID 충돌이 발생할 수 있으므로 this.byId() 사용 습관을 유지하는 것이 좋습니다.

핵심 개념

MultiInput을 이해하려면 세 가지 구성 요소를 먼저 분리해 봐야 합니다. 입력 필드, 토큰(Token), 그리고 토큰을 담는 컨테이너인 Tokenizer입니다. 즉 MultiInput은 "Input + Tokenizer"의 합성 컨트롤로, 사용자가 텍스트를 치고 Enter를 누르면 그 값이 Token으로 변환되어 Tokenizer에 쌓입니다.

비유하자면 이메일 클라이언트의 "받는 사람" 필드와 같습니다. 주소를 입력하고 콤마를 치면 이름이 알약 모양의 라벨로 바뀌고, 라벨에 X를 누르면 제거되는 그 동작입니다.

이때 핵심은 토큰이 곧 데이터라는 점입니다. 각 Token은 key(내부 식별자)와 text(화면 표시 텍스트)를 가지며, 모델 바인딩 시 보통 key를 DB의 코드값, text를 사람이 읽는 라벨에 매핑합니다. tokens 집합은 컬렉션 바인딩 대상이므로 모델 배열에 객체를 push하면 토큰이 자동으로 화면에 추가됩니다.

이벤트 모델도 단순합니다. 사용자가 토큰을 추가하거나 제거하면 tokenUpdate가 발생하고, 파라미터로 addedTokensremovedTokens 배열이 함께 전달됩니다. 한 번의 동작이 여러 토큰을 동시에 바꿀 수 있으므로(예: 붙여넣기, 전체 삭제) 배열로 받는다는 점이 중요합니다. 값 검증과 자동 변환은 suggestionItems와 함께 사용하면 ValueHelp 다이얼로그로 확장할 수 있으며, 이는 Fiori 환경에서 가장 흔한 조합입니다.

실전 코드 3단계

1단계 — 기본 선언과 토큰 바인딩

가장 기본이 되는 XML 뷰입니다. /selectedTags 경로의 배열을 토큰 집합에 바인딩하고, 각 객체의 tag 속성을 key와 text 양쪽에 매핑했습니다. 초기 모델에 값이 들어 있다면 화면이 그려질 때 자동으로 토큰이 표시됩니다.

<mvc:View
    controllerName="com.example.tags.controller.Main"
    xmlns:mvc="sap.ui.core.mvc"
    xmlns="sap.m">
    <Page title="태그 입력 예제">
        <VBox class="sapUiSmallMargin">
            <Label text="관심 태그" labelFor="tagInput"/>
            <MultiInput
                id="tagInput"
                tokens="{
                    path: '/selectedTags',
                    template: {
                        Type: 'sap.m.Token',
                        key: '{tag}',
                        text: '{tag}'
                    }
                }"
                tokenUpdate=".onTokenUpdate"
                placeholder="태그 입력 후 Enter"/>
            <Button text="확인" press=".onConfirm"/>
        </VBox>
    </Page>
</mvc:View>

컨트롤러 초기화에서 JSONModel을 셋업합니다. 두 개의 초기 태그가 화면에 미리 표시됩니다.

sap.ui.define([
    "sap/ui/core/mvc/Controller",
    "sap/ui/model/json/JSONModel"
], function (Controller, JSONModel) {
    "use strict";
    return Controller.extend("com.example.tags.controller.Main", {
        onInit: function () {
            var oModel = new JSONModel({
                selectedTags: [
                    { tag: "SAPUI5" },
                    { tag: "Fiori" }
                ]
            });
            this.getView().setModel(oModel);
        }
    });
});

2단계 — tokenUpdate에서 추가·삭제 분리 처리

tokens 집합을 모델에 바인딩하면 UI 측 변경이 자동 반영되지만, 실무에서는 검증·중복 차단·로그 기록을 위해 tokenUpdate에서 직접 모델을 갱신하는 패턴을 더 자주 씁니다. 아래 예제는 addedTokens / removedTokens를 분리해 처리하고, 중복 태그 입력 시 MessageToast로 경고를 띄웁니다.

onTokenUpdate: function (oEvent) {
    var oLogger  = sap.ui.require("sap/base/Log");
    var aAdded   = oEvent.getParameter("addedTokens");
    var aRemoved = oEvent.getParameter("removedTokens");
    var oModel   = this.getView().getModel();
    var aTags    = oModel.getProperty("/selectedTags").slice();

    try {
        aAdded.forEach(function (oTok) {
            var sNew = (oTok.getText() || "").trim();
            if (!sNew) { return; }

            var bDup = aTags.some(function (t) {
                return t.tag.toLowerCase() === sNew.toLowerCase();
            });
            if (bDup) {
                sap.m.MessageToast.show("이미 등록된 태그: " + sNew);
                // 화면에서도 제거
                oTok.destroy();
                return;
            }
            aTags.push({ tag: sNew });
        });

        aRemoved.forEach(function (oTok) {
            aTags = aTags.filter(function (t) {
                return t.tag !== oTok.getText();
            });
        });

        oModel.setProperty("/selectedTags", aTags);
        oLogger && oLogger.info("tokens updated", JSON.stringify(aTags));
    } catch (e) {
        sap.m.MessageBox.error("토큰 처리 중 오류: " + e.message);
    }
}

여기서 slice()로 배열 복사본을 만들어 작업하는 이유는 모델 내부 참조를 직접 변형(mutate)할 때 발생할 수 있는 바인딩 갱신 누락을 방지하기 위함입니다. 또한 중복 토큰은 oTok.destroy()로 즉시 제거해 UI와 데이터의 정합성을 맞춥니다.

3단계 — getTokens로 일괄 수집 및 서버 전송

확인 버튼을 눌렀을 때 현재 화면에 떠 있는 토큰들을 한꺼번에 추출해 백엔드로 전송하는 패턴입니다. 모델 배열을 읽어도 되지만, 화면 상태가 진실의 원천(source of truth)이라는 보장이 필요할 때 getTokens()가 더 안전합니다.

onConfirm: function () {
    var oInput  = this.byId("tagInput");
    var aTokens = oInput.getTokens();

    if (aTokens.length === 0) {
        sap.m.MessageToast.show("태그를 1개 이상 입력해주세요");
        return;
    }
    if (aTokens.length > 20) {
        sap.m.MessageBox.warning("태그는 최대 20개까지 허용됩니다");
        return;
    }

    var aKeys = aTokens.map(function (oTok) {
        return oTok.getKey();
    });
    this._submit(aKeys);
},

_submit: function (aKeys) {
    var that = this;
    this.byId("tagInput").setBusy(true);

    jQuery.ajax({
        url: "/api/tags",
        method: "POST",
        contentType: "application/json",
        data: JSON.stringify({ tags: aKeys }),
        headers: {
            "X-CSRF-Token": this._csrf || "Fetch"
        }
    }).done(function () {
        sap.m.MessageToast.show("저장 완료");
    }).fail(function (jqXHR) {
        sap.m.MessageBox.error("저장 실패: " + jqXHR.status);
    }).always(function () {
        that.byId("tagInput").setBusy(false);
    });
}

프로덕션에서는 다음 항목을 추가로 고려해야 합니다. 첫째, CSRF 토큰은 OData/REST 호출 전에 별도 GET으로 발급받아 캐시해 둡니다. 둘째, 입력 길이 제한을 MultiInput의 maxTokens 속성으로도 강제할 수 있습니다. 셋째, 접근성을 위해 ariaLabelledBy를 Label과 연결하면 스크린리더가 항목을 정확히 안내합니다. 마지막으로 OPA5 또는 wdi5로 통합 테스트를 작성해 토큰 추가/삭제 시나리오를 회귀 검증합니다.

흔한 실수와 트러블슈팅

실제 프로젝트에서 반복적으로 마주치는 이슈를 FAQ 형식으로 정리합니다.

Q1. 토큰이 추가는 되는데 모델에 반영이 안 됩니다.
tokens 집합 바인딩을 사용하면서 동시에 tokenUpdate에서 push를 하면 중복 입력이 발생하거나 양방향 동기화가 깨질 수 있습니다. 둘 중 하나만 선택하세요. 단순 케이스에서는 바인딩만으로 충분하고, 검증이 필요한 경우 tokenUpdate 단일 경로로 통일하는 편이 디버깅이 쉽습니다.

Q2. 같은 값이 여러 번 들어갑니다.
MultiInput 자체에는 중복 차단 기능이 없습니다. 2단계 예제처럼 추가 직전에 some()으로 검사하거나, suggestionItems 기반으로만 선택 가능하도록 showValueHelp를 강제해야 합니다.

Q3. tokenUpdate가 두 번 발생합니다.
일부 버전에서 토큰 제거 시 내부적으로 add/remove가 순차적으로 발생하는 경우가 있습니다. oEvent.getParameter("type")("added"/"removed")을 확인해 분기 처리하거나, removedTokens 길이가 0인 호출은 무시하도록 가드를 두면 안전합니다.

Q4. ValueHelp 다이얼로그를 연결했는데 입력이 안 됩니다.
showValueHelp="true"만 설정하고 valueHelpRequest 핸들러를 만들지 않은 경우입니다. 다이얼로그를 직접 띄우고, 선택된 항목을 new Token({ key, text })로 생성해 addToken()으로 추가하면 됩니다.

Q5. 붙여넣기로 콤마 구분된 값을 한꺼번에 입력하고 싶습니다.
MultiInput의 tokenizer를 가져와 직접 addValidator를 등록하거나, onPaste 이벤트에서 문자열을 split해 토큰을 생성하는 방식이 일반적입니다.

다음 단계와 관련 주제

MultiInput을 다뤘다면 자연스럽게 이어지는 주제는 다음과 같습니다.

  • sap.m.MultiComboBox — 선택 가능한 값이 제한적일 때 더 적합
  • ValueHelpDialog — 대용량 후보 목록에서 검색·선택 UX 제공
  • SmartMultiInput — OData 어노테이션 기반 자동 바인딩
  • OPA5/wdi5 테스트 — 토큰 추가/삭제의 회귀 검증
  • FilterBar 통합 — 다중 값 필터를 ListReport에 연동

특히 Fiori Elements 환경이라면 SmartMultiInput으로 어노테이션 기반 바인딩을 활용하는 편이 유지보수에 유리합니다.

참고 자료

댓글 0

아직 댓글이 없습니다.