📖 개요: 바인딩 없이 화면을 갱신해야 하는 순간들
UI5 개발을 하다 보면 "모든 화면 갱신은 데이터 바인딩으로"라는 원칙을 배우게 됩니다. 하지만 실무에서는 바인딩이 오히려 과한 경우가 있습니다. 일회성 상태 메시지, 초당 수십 번 갱신되는 실시간 카운터, 모델에 담을 필요가 없는 순수 UI 상태 같은 것들입니다. 이 글은 창고 재고 대시보드라는 가상의 실무 시나리오를 통해, 바인딩 없이 컨트롤 프로퍼티를 직접 갱신하는 기법들을 비교하고 언제 어떤 방식이 적합한지 단계별로 정리합니다.
- ManagedObject의 setProperty/getProperty 동작 원리를 이해한다
- 컨트롤 세터(setText, setVisible 등) 직접 호출로 화면을 갱신할 수 있다
- jQuery/DOM 직접 조작이 왜 위험한지 렌더링 관점에서 설명할 수 있다
- 바인딩 vs 직접 갱신의 선택 기준(성능·유지보수)을 판단할 수 있다
📚 시작 전에 알고 있으면 좋은 것
XML View와 Controller의 기본 구조, this.byId()로 컨트롤을 얻는 방법, JSONModel을 이용한 기초적인 바인딩 경험이 있다면 충분합니다. JavaScript의 setInterval, try/catch 정도의 문법을 이해하고 있다고 가정합니다. 렌더링 내부 구조는 몰라도 되며, 필요한 부분은 본문에서 설명합니다.
🔧 테스트 환경과 사용한 버전
이 글의 코드는 SAPUI5 1.120 LTS 기준으로 작성했으며, OpenUI5 1.120 이상에서도 동일하게 동작합니다. 1.71 이상이라면 대부분의 API가 그대로 적용됩니다. 개발 환경은 SAP Business Application Studio 또는 로컬 UI5 CLI(@ui5/cli 3.x) 어느 쪽이든 무방합니다.
- SAPUI5 1.120 LTS (또는 OpenUI5 1.120+) — 1.71+에서도 대부분 호환
- Node.js 18 이상 + UI5 CLI 3.x (로컬 개발 시)
- 브라우저 개발자 도구 (렌더링 확인용)
예제 시나리오는 창고 재고 대시보드입니다. 자재 코드별 가용 수량을 보여주는 패널에서, 서버 폴링으로 수신한 수량을 바인딩 없이 화면에 반영하는 상황을 다룹니다.
💡 핵심 개념: 프로퍼티 갱신의 세 가지 경로
UI5에서 화면의 텍스트 하나를 바꾸는 방법은 크게 세 갈래입니다. 비유하자면 우체국(바인딩), 직접 방문(컨트롤 세터), 담장 넘기(DOM 조작)입니다.
- 모델 바인딩: 모델의
setProperty를 호출하면 변경 이벤트가 발생하고, 해당 경로를 구독하는 모든 바인딩이 자동으로 컨트롤을 갱신합니다. 우체국처럼 여러 수신자에게 한 번에 전달되지만, 변경 감지→바인딩 평가→포맷터 실행이라는 중간 단계 비용이 있습니다. - 컨트롤 세터 직접 호출:
oText.setText("120 EA")처럼 대상에게 직접 값을 건넵니다. 내부적으로 모든 UI5 컨트롤의 조상인sap.ui.base.ManagedObject의setProperty가 실행되어, 값 검증(타입 체크) → 내부 상태 저장 → invalidate(무효화) 순으로 진행됩니다. 무효화된 컨트롤은 다음 렌더링 사이클에서 다시 그려지며, 1.x의 시맨틱 렌더링 덕분에 일반적으로 변경된 DOM 조각만 패치됩니다. - jQuery/DOM 직접 조작:
document.getElementById(...).innerText = ...처럼 담장을 넘어 결과물(DOM)만 고치는 방식입니다. 컨트롤의 내부 상태는 이전 값 그대로이므로, 다른 이유로 재렌더링이 발생하는 순간 수정 내용이 흔적 없이 사라집니다. 상태와 화면이 어긋나는 전형적인 원인이라 지양이 권장됩니다.
선택 기준을 표로 정리하면 다음과 같습니다.
| 기법 | 적합한 경우 | 주의점 |
|---|---|---|
| 모델 바인딩 | 업무 데이터, 여러 컨트롤이 같은 값을 참조, 양방향 입력 | 고빈도 갱신 시 오버헤드 누적 |
| 컨트롤 세터 | 일회성 UI 상태(busy, 안내 문구), 실시간 카운터, 모델에 둘 이유가 없는 값 | 바인딩된 프로퍼티에 세터를 섞으면 값이 되돌아감 |
| DOM 직접 조작 | 사실상 없음 (측정/디버깅 정도) | 재렌더링 시 소실, XSS 위험 |
핵심 원리는 하나입니다. UI5 컨트롤의 진짜 상태는 DOM이 아니라 ManagedObject 내부의 프로퍼티 저장소이며, DOM은 그 상태의 렌더링 결과물일 뿐입니다. 따라서 상태를 바꾸는 정상 경로는 세터(또는 setProperty)까지이고, 그 아래(DOM)로 내려가면 안 됩니다.
💻 직접 해보기: 재고 대시보드로 익히는 직접 갱신
1단계 — 기본: 이벤트 핸들러에서 세터 직접 호출
먼저 View입니다. 어떤 프로퍼티에도 바인딩을 걸지 않았다는 점에 주목하세요.
<mvc:View controllerName="warehouse.dash.controller.StockPanel"
xmlns:mvc="sap.ui.core.mvc" xmlns="sap.m">
<Panel headerText="자재별 가용 재고">
<ObjectStatus id="stsStockLevel" text="조회 전" state="None" />
<Text id="txtLastSync" text="-" visible="false" />
<Button text="재고 새로고침" press=".onRefreshStock" />
</Panel>
</mvc:View>
// controller/StockPanel.controller.js
sap.ui.define([
"sap/ui/core/mvc/Controller"
], function (Controller) {
"use strict";
return Controller.extend("warehouse.dash.controller.StockPanel", {
onRefreshStock: function () {
const oStatus = this.byId("stsStockLevel");
const oSync = this.byId("txtLastSync");
// 세터 호출 = ManagedObject.setProperty 실행 = 상태 저장 + invalidate
oStatus.setText("MAT-4711 : 128 EA");
oStatus.setState("Success");
// 제네릭 API로도 동일하게 가능 (동적 프로퍼티명이 필요할 때 유용)
oSync.setProperty("text", "마지막 동기화: " + new Date().toLocaleTimeString());
oSync.setVisible(true);
// 읽기도 대칭적으로: getText() 또는 getProperty("text")
console.log(oStatus.getProperty("text"));
}
});
});
setText와 setProperty("text", ...)는 결국 같은 내부 경로를 타지만, 타입 세터는 코드 가독성이 좋고 setProperty는 프로퍼티명을 변수로 다룰 때 유리합니다. 같은 렌더링 사이클 안에서 여러 세터를 연달아 호출해도 무효화는 병합되어 한 번만 다시 그려지므로, 호출 횟수를 과도하게 걱정할 필요는 없습니다.
2단계 — 실무: 폴링 갱신 + 에러 처리와 로깅
이제 5초마다 재고 API를 폴링해 화면을 갱신하는 상황입니다. 실무에서는 비동기 응답이 도착했을 때 화면이 이미 파괴됐을 수 있다는 점과, 실패를 로그로 남기는 것이 중요합니다.
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/base/Log",
"sap/m/MessageToast"
], function (Controller, Log, MessageToast) {
"use strict";
return Controller.extend("warehouse.dash.controller.StockPanel", {
onInit: function () {
this._iPollTimer = setInterval(this._pollStock.bind(this), 5000);
},
onExit: function () {
// 타이머 미해제 → 파괴된 컨트롤에 setText 시도 → 콘솔 오류의 단골 원인
clearInterval(this._iPollTimer);
},
_pollStock: async function () {
const oStatus = this.byId("stsStockLevel");
try {
const oRes = await fetch("/api/warehouse/stock/MAT-4711");
if (!oRes.ok) {
throw new Error("HTTP " + oRes.status);
}
const oData = await oRes.json();
// 응답 대기 중 뷰가 파괴됐다면 갱신 스킵
if (!oStatus || oStatus.isDestroyed()) {
Log.info("뷰가 이미 파괴되어 갱신 생략", null, "StockPanel");
return;
}
const bLow = oData.availableQty < oData.safetyQty;
oStatus.setText(oData.materialCode + " : " + oData.availableQty + " EA");
oStatus.setState(bLow ? "Warning" : "Success");
if (bLow) {
Log.warning("안전재고 미달: " + oData.materialCode,
"가용 " + oData.availableQty + " / 안전 " + oData.safetyQty,
"StockPanel");
}
} catch (oErr) {
Log.error("재고 폴링 실패", oErr.message, "StockPanel");
if (oStatus && !oStatus.isDestroyed()) {
oStatus.setState("Error");
oStatus.setText("재고 조회 실패 — 재시도 중");
}
MessageToast.show("재고 서버 연결이 불안정합니다");
}
}
});
});
이 시나리오가 직접 갱신에 적합한 이유를 짚어보면, 이 수량 값은 이 컨트롤 하나만 사용하고, 5초마다 덮어써지는 휘발성 값이라 모델에 보관할 업무적 이유가 없습니다. 만약 같은 값을 차트·테이블 등 여러 컨트롤이 함께 보여준다면 그때는 JSONModel + 바인딩이 유지보수 면에서 낫습니다.
3단계 — 프로덕션: 고빈도 갱신 스로틀링, 보안, 테스트
WebSocket으로 초당 수십 건의 재고 이벤트가 들어오는 프로덕션 환경이라면, 이벤트마다 세터를 호출하는 대신 최신 값만 모아 프레임 단위로 반영하는 것이 일반적으로 권장됩니다.
// 고빈도 이벤트 → requestAnimationFrame 스로틀링
_onSocketMessage: function (oEvent) {
const oMsg = JSON.parse(oEvent.data);
this._mPending = this._mPending || {};
this._mPending[oMsg.materialCode] = oMsg.availableQty; // 최신값만 유지
if (!this._bFlushScheduled) {
this._bFlushScheduled = true;
requestAnimationFrame(() => {
this._bFlushScheduled = false;
const oStatus = this.byId("stsStockLevel");
const iQty = this._mPending["MAT-4711"];
if (iQty !== undefined && oStatus && !oStatus.isDestroyed()) {
oStatus.setText("MAT-4711 : " + iQty + " EA"); // 프레임당 1회만 반영
}
this._mPending = {};
});
}
}
보안 관점에서는, 서버 응답 문자열을 화면에 넣을 때 세터 경로를 쓰면 UI5 렌더러가 출력 시 이스케이프를 수행하므로 일반적으로 안전합니다. 반면 innerHTML이나 sap.ui.core.HTML에 원문을 넣는 방식은 XSS 통로가 될 수 있습니다. 불가피하게 문자열을 조립해야 한다면 sap/base/security/encodeXML로 인코딩하는 것이 권장됩니다.
테스트는 바인딩이 없어도 간단합니다. 세터로 갱신된 상태는 게터로 그대로 검증할 수 있기 때문입니다.
// QUnit — 세터 기반 갱신 검증
QUnit.test("안전재고 미달 시 Warning 상태로 갱신", function (assert) {
const oController = new StockPanelController();
const oStatusStub = new ObjectStatus(); // 실제 컨트롤 인스턴스
sinon.stub(oController, "byId").returns(oStatusStub); // 뷰 없이 주입
oController._applyStock({ materialCode: "MAT-4711",
availableQty: 3, safetyQty: 10 });
assert.strictEqual(oStatusStub.getState(), "Warning");
assert.ok(oStatusStub.getText().includes("3 EA"));
oStatusStub.destroy();
});
프로덕션 체크리스트: (1) onExit에서 타이머·소켓 해제, (2) 비동기 콜백마다 isDestroyed() 가드, (3) 고빈도 갱신은 스로틀링, (4) 갱신 로직을 _applyStock 같은 순수 메서드로 분리해 테스트 가능하게 유지.
⚠️ 자주 만나는 함정 FAQ
Q1. setText로 바꿨는데 잠시 후 원래 값으로 되돌아갑니다.
해당 프로퍼티에 바인딩이 걸려 있는 경우입니다. 바인딩된 프로퍼티에 세터를 호출하면 그 순간에는 값이 바뀌지만, 모델에서 change 이벤트가 오면 바인딩이 다시 값을 덮어씁니다. 한 프로퍼티는 바인딩 또는 직접 갱신 중 하나만 사용하세요. 직접 갱신으로 전환하려면 oControl.unbindProperty("text")를 먼저 호출합니다.
Q2. jQuery로 DOM을 고쳤는데 스크롤하거나 리사이즈하면 사라집니다.
정상 동작입니다. 재렌더링은 ManagedObject의 내부 상태로 DOM을 다시 만들기 때문에, 상태를 거치지 않은 DOM 수정은 언제든 소실됩니다. 반드시 세터/setProperty 경로로 상태 자체를 갱신하세요.
Q3. this.byId(...)가 undefined를 반환합니다.
세 가지를 확인하세요. (1) View XML의 id 오타, (2) 프래그먼트 안의 컨트롤이라면Fragment.byId(뷰ID조합, ...)사용 필요, (3) 뷰 파괴 후 비동기 콜백에서 호출한 경우. 전역sap.ui.getCore().byId()는 뷰 프리픽스가 붙은 전체 ID가 필요해 혼동의 원인이 되므로 컨트롤러에서는this.byId()가 권장됩니다.
Q4. 모델의 setProperty와 컨트롤의 setProperty는 뭐가 다른가요?
이름만 같은 별개 API입니다.oModel.setProperty("/stock/qty", 5)는 데이터 저장소를 바꿔 바인딩을 통해 간접 갱신하고,oControl.setProperty("text", "5")는 컨트롤 상태를 직접 바꿉니다. 전자는 구독자 전원에게, 후자는 그 컨트롤 하나에만 영향을 줍니다.
🚀 여기서 더 나아가기
직접 갱신 기법을 이해했다면, 반대편 극단인 바인딩 최적화도 함께 살펴보길 권합니다. OneTime 바인딩 모드는 "한 번만 그리는 값"을 바인딩 문법 안에서 처리하는 절충안이고, Expression Binding은 간단한 조건 표시를 컨트롤러 코드 없이 해결합니다. 또한 커스텀 컨트롤을 만들 때 setProperty의 bSuppressInvalidate 인자와 렌더러 작성법을 배우면 이 글에서 다룬 무효화 메커니즘을 더 깊이 이해할 수 있습니다. OData V4 모델 환경에서의 갱신 전략 비교도 좋은 다음 주제입니다.
📚 더 읽어볼 자료
- help.sap.com — SAPUI5 제품 문서 홈
- help.sap.com — SAPUI5 Documentation (최신 버전 뷰어)
- help.sap.com — SAP Business Application Studio 문서
- UI5 API Reference — sap.ui.base.ManagedObject (setProperty/getProperty)
- UI5 API Reference — sap.ui.core.Control (invalidate, 렌더링)
- UI5 API Reference — sap.ui.model.json.JSONModel
- OpenUI5 SDK — Demo Kit 및 개발자 가이드
댓글 0
아직 댓글이 없습니다.