개요 및 핵심 포인트
SAPUI5/OpenUI5의 UIComponent는 애플리케이션의 진입점이자 라우팅, 모델, 의존성 관리를 총괄하는 구성 단위입니다. 컴포넌트가 어떤 시점에 살아나고 어떤 시점에 사라지는지를 라이프사이클 훅으로 명확히 다루지 못하면, 라우터가 두 번 등록되거나 타이머가 영원히 돌아가는 등 잠재적인 메모리 누수가 누적될 수 있습니다.
본 문서에서 다루는 핵심은 다음 세 가지 훅입니다.
- init — 컴포넌트 생성 시 최초 1회 호출. 라우터 초기화, JSON/OData 모델 설정, 이벤트버스 구독 등 1회성 작업.
- onBeforeRendering — 렌더링 직전마다 호출. DOM이 아직 없는 상태에서 데이터 가공.
- exit — 컴포넌트 소멸 시 호출. 타이머 해제, 구독 취소, 모델 destroy, 외부 리소스 정리.
학습 체크리스트.
- UIComponent.prototype.init.apply(this, arguments) 호출 위치 이해
- onBeforeRendering이 호출되는 빈도와 적절한 활용 범위 구분
- exit에서 누수 없이 정리해야 할 자원 4종(타이머, 이벤트버스, 모델, 커스텀 이벤트)
- Controller 라이프사이클(onInit/onExit)과 Component 라이프사이클의 차이 인식
사전에 알아두면 좋은 배경
UI5의 MVC 구조에서 Component는 애플리케이션 루트, View는 화면 선언, Controller는 화면 단위 로직을 담당합니다. 라이프사이클 훅 이름이 비슷해 보여도 호출 시점과 범위가 다릅니다.
- UIComponent: 앱 전체 한 번 생성, 라우터·전역 모델 보유.
- Controller: View가 만들어질 때마다 인스턴스화, View에 종속된 로직 처리.
또한 manifest.json(Descriptor)에 선언된 모델, 라우팅, 의존성은 init 시점에 컴포넌트가 자동으로 읽어들이므로, 부모 클래스의 init을 반드시 먼저 호출해야 자동 처리가 정상 동작합니다.
실습 환경 및 버전
본 예제는 다음 환경을 기준으로 작성되었습니다.
- SAPUI5 1.108 LTS 이상 (1.120 LTS 권장)
- UI5 Tooling 3.x (
@ui5/cli3.7 이상) - Node.js 18 LTS 또는 20 LTS
- TypeScript 5.x (선택, JavaScript로도 동일 패턴 적용 가능)
- SAP Business Application Studio 또는 VS Code + UI5 Language Assistant 확장
프로젝트 골격은 npm init @sapui5/easy-ui5 또는 yo easy-ui5 제너레이터로 생성하는 것을 일반적으로 권장합니다. 본 문서의 코드는 표준 폴더 구조(webapp/Component.js, webapp/manifest.json, webapp/controller/*.controller.js)를 가정합니다.
specVersion: "3.0"
metadata:
name: com.example.lifecycle
type: application
framework:
name: SAPUI5
version: "1.120.0"
libraries:
- name: sap.m
- name: sap.ui.core
핵심 개념과 동작 원리
UI5 컴포넌트는 다음과 같은 순서로 살아갑니다.
- 인스턴스 생성 —
sap.ui.core.ComponentContainer또는Component.create()가 호출되면 컴포넌트 클래스가 인스턴스화됩니다. - manifest.json 로드 — 디스크립터에 정의된 모델/라우팅/의존성이 자동 처리됩니다.
- init() 호출 — 사용자가 추가로 등록할 라우터 초기화, 전역 모델 설정 등을 실행합니다.
- 루트 View 생성 및 렌더링 —
onBeforeRendering-> 실제 DOM 부착 ->onAfterRendering순으로 호출됩니다. - 런타임 동안 onBeforeRendering이 N회 반복 — 모델 변경, 리렌더링 요청마다 발생합니다.
- exit() 호출 — 컴포넌트가 destroy되면 종료 훅이 발생합니다.
비유하자면 init은 매장 오픈 첫날 인테리어와 직원 채용에 해당하고, onBeforeRendering은 매일 영업 시작 직전 진열대 정리에 해당하며, exit는 매장 폐업 시 임대차 해지·전기 차단·키 반납에 해당합니다. exit에서 정리를 빠뜨리면 매장은 사라졌는데 전기 요금만 계속 청구되는 상황(메모리 누수)이 발생합니다.
manifest.json의 sap.ui5.routing 블록은 컴포넌트가 자동으로 라우터 인스턴스를 만들 수 있게 해주지만, 라우터의 initialize() 호출과 타깃 매핑은 코드에서 명시적으로 수행해야 합니다. 이 작업이 들어가는 자리가 바로 init입니다.
실전 코드
예제 A — init: 라우터와 디바이스 모델 등록
가장 기본적인 UIComponent 골격입니다. UIComponent.prototype.init.apply(this, arguments)를 가장 먼저 호출해야 manifest 기반 자동 처리(createContent 직전 초기화)가 깨지지 않습니다.
sap.ui.define([
"sap/ui/core/UIComponent",
"sap/ui/Device",
"sap/ui/model/json/JSONModel"
], function (UIComponent, Device, JSONModel) {
"use strict";
return UIComponent.extend("com.example.lifecycle.Component", {
metadata: {
manifest: "json"
},
init: function () {
// 1) 부모 클래스 init을 먼저 호출 - 누락 시 manifest 라우팅이 동작하지 않음
UIComponent.prototype.init.apply(this, arguments);
// 2) 디바이스 모델 (반응형 UI에 활용)
var oDeviceModel = new JSONModel(Device);
oDeviceModel.setDefaultBindingMode("OneWay");
this.setModel(oDeviceModel, "device");
// 3) 라우터 초기화 - hash 변화 감지 시작
this.getRouter().initialize();
}
});
});
예제 B — onBeforeRendering: 렌더 직전 데이터 가공과 로깅
실무에서는 컴포넌트 자체의 onBeforeRendering보다 Controller 레벨이 더 자주 쓰입니다. 다만 컴포넌트 전역 모델을 매 렌더링 직전 동기화하거나, 권한 컨텍스트를 다시 평가해야 할 때 컴포넌트 훅이 유용합니다.
sap.ui.define([
"sap/ui/core/UIComponent",
"sap/base/Log"
], function (UIComponent, Log) {
"use strict";
return UIComponent.extend("com.example.lifecycle.Component", {
metadata: { manifest: "json" },
init: function () {
UIComponent.prototype.init.apply(this, arguments);
this._iRenderCount = 0;
},
onBeforeRendering: function () {
// 렌더 직전에는 DOM이 아직 없음 - this.getDomRef()는 null
this._iRenderCount++;
Log.debug("Component pre-render #" + this._iRenderCount, "lifecycle");
try {
var oAppModel = this.getModel("app");
if (oAppModel) {
// 비싼 작업은 금지. 가벼운 derived 값 정도만 갱신
oAppModel.setProperty("/renderedAt", new Date().toISOString());
}
} catch (oErr) {
Log.error("onBeforeRendering 처리 실패", oErr, "lifecycle");
}
}
});
});
주의:
onBeforeRendering은 매우 빈번하게 호출될 수 있습니다. 네트워크 호출, 무거운 계산은 일반적으로 피하는 것이 권장됩니다.
예제 C — exit: 자원 정리와 메모리 누수 방지 프로덕션 패턴
프로덕션 컴포넌트는 타이머, 이벤트버스 구독, 외부 라이브러리 인스턴스, 커스텀 모델까지 명시적으로 해제해야 합니다. 다음은 누수 방지 체크리스트를 코드화한 예시입니다.
sap.ui.define([
"sap/ui/core/UIComponent",
"sap/ui/model/json/JSONModel",
"sap/base/Log"
], function (UIComponent, JSONModel, Log) {
"use strict";
return UIComponent.extend("com.example.lifecycle.Component", {
metadata: { manifest: "json" },
init: function () {
UIComponent.prototype.init.apply(this, arguments);
// 전역 모델
this._oAppModel = new JSONModel({ user: null, theme: "light" });
this.setModel(this._oAppModel, "app");
// 주기적 헬스 체크 타이머 (예: 토큰 만료 감지)
this._iHealthTimer = setInterval(this._checkHealth.bind(this), 60000);
// EventBus 구독 - channel/event 명을 보관해 두는 것이 안전
this._oEventBus = this.getEventBus();
this._fnAuthHandler = this._onAuthChanged.bind(this);
this._oEventBus.subscribe("auth", "changed", this._fnAuthHandler);
// 외부 window 이벤트도 흔히 누수 원인이 됨
this._fnOnline = this._onOnline.bind(this);
window.addEventListener("online", this._fnOnline);
},
_checkHealth: function () {
Log.debug("health ping", "lifecycle");
},
_onAuthChanged: function (sChannel, sEvent, oData) {
this._oAppModel.setProperty("/user", oData && oData.user);
},
_onOnline: function () {
Log.info("network online", "lifecycle");
},
exit: function () {
// 1) 타이머 해제
if (this._iHealthTimer) {
clearInterval(this._iHealthTimer);
this._iHealthTimer = null;
}
// 2) EventBus 구독 해제 - subscribe와 동일한 핸들러 참조 필요
if (this._oEventBus && this._fnAuthHandler) {
this._oEventBus.unsubscribe("auth", "changed", this._fnAuthHandler);
this._fnAuthHandler = null;
}
// 3) window 이벤트 해제
if (this._fnOnline) {
window.removeEventListener("online", this._fnOnline);
this._fnOnline = null;
}
// 4) 수동 생성한 모델 destroy
if (this._oAppModel) {
this._oAppModel.destroy();
this._oAppModel = null;
}
Log.info("Component exit cleanup done", "lifecycle");
}
});
});
테스트 측면에서는 QUnit으로 oComponent.destroy() 호출 후 타이머/구독이 모두 해제되었는지 검증하는 시나리오를 추가하는 것이 권장됩니다. 보안 측면에서는 exit에서 토큰/세션 정보를 담은 모델을 반드시 destroy하여 메모리 덤프에 잔여가 남지 않도록 하는 것이 일반적으로 안전합니다.
실전 패턴 — 메모리 누수 방지 체크리스트
컴포넌트가 destroy될 때 다음 네 가지를 점검하면 대부분의 누수를 차단할 수 있습니다.
- 타이머 —
setInterval,setTimeout은 인스턴스 변수에 ID를 저장해 두고exit에서clear*호출. - EventBus 구독 —
subscribe시 사용한 동일 핸들러 함수 참조로unsubscribe. 익명 함수로 등록하면 해제가 불가능합니다. - 수동 생성 모델 —
setModel으로 등록한 모델도 GC가 자동 회수하지 않는 경우가 있으므로destroy()호출 권장. - 외부 라이브러리/Window 이벤트 —
addEventListener, 차트 라이브러리 인스턴스, WebSocket 등은 명시적으로 닫기.
또한 컴포넌트와 동일한 정리 책임을 Controller에서도 가져야 합니다. Controller의 onExit에서 컨트롤 fragment, Dialog 인스턴스를 destroy()해 주지 않으면 View 단위 누수가 누적됩니다.
흔한 실수와 트러블슈팅
FAQ 1. 라우팅이 동작하지 않거나 manifest 모델이 비어 있어요.
가장 흔한 원인은 init에서 부모 호출을 빠뜨린 경우입니다. UIComponent.prototype.init.apply(this, arguments)가 manifest의 모델/라우팅 자동 등록을 수행하므로, 사용자 정의 로직보다 먼저 호출되어야 합니다. 또한 this.getRouter().initialize()를 호출하지 않으면 해시 변경이 감지되지 않습니다.
FAQ 2. exit를 정의했는데도 메모리 사용량이 계속 증가합니다.
다음을 점검하세요. (1) subscribe한 핸들러를 bind로 매번 새로 만든 뒤 다른 참조로 unsubscribe하지 않았는지. (2) setInterval ID를 저장하지 않아 clearInterval을 호출하지 못했는지. (3) 자식 컴포넌트나 Dialog를 this.addDependent로 부착하지 않아 부모가 destroy되어도 함께 정리되지 않는지. addDependent로 묶어 두면 라이프사이클이 함께 관리됩니다.
FAQ 3. onBeforeRendering에서 OData 호출을 했더니 화면이 깜빡이고 느려져요.
onBeforeRendering은 모델 변경, 리사이즈, 바인딩 갱신 등 다양한 이유로 자주 트리거됩니다. 네트워크 호출 같은 무거운 작업은 init 또는 라우팅 패턴 매칭 핸들러(예: attachPatternMatched) 안에서 1회성으로 수행하는 것이 일반적으로 권장됩니다. onBeforeRendering에서는 이미 메모리에 있는 데이터의 가벼운 가공만 수행하세요.
FAQ 4. Controller의 onInit과 Component의 init 중 어디서 모델을 만들어야 하나요?
앱 전역에서 공유할 모델은 Component.init에서, 특정 View에만 종속된 임시 상태 모델은 Controller.onInit에서 만드는 것이 일반적으로 권장됩니다. 전역 모델을 Controller에서 만들면 View가 destroy될 때 모델도 함께 사라져 다른 View에서 참조할 수 없게 됩니다.
참고 자료
- SAPUI5 SDK — Components 개요
- SAPUI5 API — sap.ui.core.UIComponent
- SAPUI5 SDK — Descriptor for Applications (manifest.json)
- help.sap.com — SAPUI5 Component 개요 (NetWeaver)
- help.sap.com — Developing SAPUI5 Applications on SAP BTP
- help.sap.com — UI5 Tooling 가이드
- UI5 Tooling 공식 문서
- SAPUI5 SDK — Routing and Navigation
댓글 0
아직 댓글이 없습니다.