개요 및 이 글에서 얻어갈 것
Fiori 앱을 오픈하는 순간 사용자 눈에 처음 보이는 것은 로딩 스피너입니다. 초기 렌더링이 3초를 넘어가면 이탈률이 급격히 상승한다는 UX 통계가 있는데, Fiori/UI5 앱의 경우 라이브러리 파일이 워낙 방대하다 보니 최적화 전략 없이는 첫 화면(TTI, Time-To-Interactive)까지 5초 이상 걸리는 경우가 다반사입니다. 이 글에서는 Lazy Loading(지연 로딩)과 Preloading(사전 로딩)을 상황별로 어떻게 조합해야 초기 로딩을 실질적으로 3배 이상 단축할 수 있는지 실전 코드와 함께 정리합니다.
- UI5 async 부트스트랩과 Component-preload.js 원리 이해
- Manifest.json의
sap.ui5.rootView,routing,models섹션 지연 전략 - 라우팅 기반 View Lazy Loading 구성
- OData 배치 요청과
$expand최적화 - 운영환경 배포용 preload 번들 생성 파이프라인
이 글을 편하게 읽기 위한 배경 지식
SAPUI5 컴포넌트 구조(Component.js, manifest.json), MVC 패턴, XML View, Routing/Targets 설정을 실무에서 다뤄본 경험이 있다면 무리 없이 따라올 수 있습니다. 추가로 Chrome DevTools Performance 탭과 Network 워터폴을 읽을 줄 알면 실측 검증까지 가능합니다. OData V2/V4의 $batch, $expand 차이도 알고 있으면 좋습니다.
환경 및 준비물
이 글은 다음 스택 기준으로 검증한 예제입니다.
- SAPUI5 1.120.x (LTS, 2024년 릴리스) — 일부 API는 1.108+ 필요
- BTP ABAP Environment 또는 S/4HANA 2023 (OData V4)
- SAP Business Application Studio 또는 VS Code + Fiori Tools
- UI5 Tooling 3.x (
@ui5/cli), Node.js 18 LTS - Cloud Foundry HTML5 Application Repository (배포 대상)
구버전(UI5 1.71 이하)에서는 sap.ui.core.mvc.View.create 등 Promise 기반 API가 제한적이라 동일한 코드가 동작하지 않을 수 있습니다. 가능하면 최신 LTS로 마이그레이션한 뒤 적용하기를 권장합니다.
핵심 개념: Lazy Loading과 Preloading의 경계
둘 다 "필요한 것을 필요한 시점에" 로딩한다는 목표는 같지만 방향이 정반대입니다. Lazy Loading은 "당장 필요 없으면 미룬다", Preloading은 "곧 필요할 것 같으면 미리 당겨온다"입니다. 비유하자면 Lazy는 손님이 주문한 뒤에야 재료를 사러 가는 방식이고, Preload는 오픈 전에 그날 인기 메뉴 재료를 창고에 쟁여두는 방식입니다.
Fiori 앱의 로딩 파이프라인은 대략 다음 단계로 진행됩니다.
- Bootstrap:
sap-ui-core.js로드 → 코어 라이브러리 파싱 - Component 초기화:
Component.js+manifest.json파싱, 모델 인스턴스 생성 - Root View 렌더링: 최초 View의 컨트롤 트리 생성
- Routing 매칭: URL 해시에 따른 Target View 로드
- Data Binding: OData 요청 → 화면 렌더링
이 중 1~3단계에서 지연이 발생하면 "흰 화면 오래 뜬다"는 컴플레인이 나오고, 4~5단계에서 지연되면 "화면은 보이는데 데이터가 안 뜬다"가 됩니다. 두 문제는 원인이 다르므로 해결 전략도 달라야 합니다.
일반적으로 Preloading은 1~2단계에, Lazy Loading은 3~5단계에 적용합니다. Preload는 Component-preload.js라는 하나의 큰 JSON 번들로 앱 리소스를 묶어 HTTP 요청 수를 줄이는 반면, Lazy는 라우팅 시점까지 View/Fragment/Controller 파일 로드를 미룹니다. 잘못 조합하면 서로 상쇄되므로 "무엇을 미리, 무엇을 나중에" 판단이 성능의 핵심입니다.
실전 코드 1단계: async 부트스트랩과 Component-preload 기본
UI5 앱의 시작점인 index.html에서 data-sap-ui-async="true"를 활성화하면 라이브러리 병렬 로딩이 켜집니다. 이 한 줄이 없으면 아래 모든 최적화가 무력화되므로 반드시 확인해야 합니다.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Purchase Order Cockpit</title>
<script
id="sap-ui-bootstrap"
src="resources/sap-ui-core.js"
data-sap-ui-theme="sap_horizon"
data-sap-ui-libs="sap.m,sap.ui.core"
data-sap-ui-resourceroots='{"com.acme.pocockpit": "./"}'
data-sap-ui-async="true"
data-sap-ui-preload="async"
data-sap-ui-compatVersion="edge"
data-sap-ui-oninit="module:sap/ui/core/ComponentSupport">
</script>
</head>
<body class="sapUiBody">
<div data-sap-ui-component
data-name="com.acme.pocockpit"
data-id="container"
data-settings='{"id" : "pocockpit"}'></div>
</body>
</html>
이후 ui5 build --clean-dest를 실행하면 dist/ 폴더에 Component-preload.js가 생성됩니다. 이 파일은 앱의 View/Controller/i18n을 하나의 문자열 맵으로 병합해 단일 요청으로 내려주는 구조입니다.
실전 코드 2단계: Routing 기반 View Lazy Loading + 에러 핸들링
구매오더 목록(POList)에서 상세(PODetail)로 넘어가는 흐름을 예로 들면, 초기 진입 시 상세 View까지 다 로드할 필요가 없습니다. manifest.json의 라우팅 설정에서 target을 명시하고 상세 View는 라우팅 매칭 시점에 처음 로드되도록 구성합니다.
{
"sap.ui5": {
"rootView": {
"viewName": "com.acme.pocockpit.view.App",
"type": "XML",
"async": true,
"id": "app"
},
"routing": {
"config": {
"routerClass": "sap.m.routing.Router",
"viewType": "XML",
"async": true,
"viewPath": "com.acme.pocockpit.view",
"controlId": "app",
"controlAggregation": "pages",
"transition": "slide"
},
"routes": [
{ "pattern": "", "name": "list", "target": "list" },
{ "pattern": "detail/{poId}", "name": "detail", "target": "detail" }
],
"targets": {
"list": { "viewName": "POList", "viewLevel": 1 },
"detail": { "viewName": "PODetail", "viewLevel": 2 }
}
}
}
}
여기서 핵심은 "async": true 두 군데입니다. rootView와 routing config 모두에서 true로 두어야 View 인스턴스화가 Promise 기반으로 진행되고, 하나가 실패해도 앱 전체가 멈추지 않습니다. 상세 View에서 무거운 Fragment(예: 첨부파일 미리보기)를 지연 로드하는 컨트롤러 예제입니다.
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/core/Fragment",
"sap/base/Log"
], function (Controller, Fragment, Log) {
"use strict";
return Controller.extend("com.acme.pocockpit.controller.PODetail", {
onAttachmentPress: async function () {
const oView = this.getView();
try {
if (!this._pAttachDialog) {
this._pAttachDialog = Fragment.load({
id: oView.getId(),
name: "com.acme.pocockpit.view.fragment.AttachmentPreview",
controller: this
}).then(function (oDialog) {
oView.addDependent(oDialog);
return oDialog;
});
}
const oDialog = await this._pAttachDialog;
oDialog.open();
} catch (oErr) {
Log.error("Attachment fragment load failed",
oErr, "com.acme.pocockpit.PODetail");
sap.m.MessageToast.show("첨부 미리보기를 열 수 없습니다.");
}
}
});
});
Fragment.load Promise를 this._pAttachDialog에 캐싱해두면 두 번째 클릭부터는 네트워크 요청 없이 즉시 열립니다. try/catch로 감싸 실패 시에도 앱이 멈추지 않도록 하고, sap/base/Log로 컴포넌트 스코프의 로그를 남기는 것이 운영 관점에서 중요합니다.
실전 코드 3단계: 프로덕션 배포용 Preload + OData Prefetch
운영 환경에서는 CDN 캐시와 HTTP/2 멀티플렉싱을 감안해 preload 전략을 세워야 합니다. ui5.yaml에서 preload 번들을 세분화하면 첫 진입은 빠르게, 이후 라우팅은 background prefetch로 커버할 수 있습니다.
specVersion: "3.0"
metadata:
name: com.acme.pocockpit
type: application
framework:
name: SAPUI5
version: "1.120.4"
libraries:
- name: sap.m
- name: sap.ui.core
- name: sap.f
- name: sap.ui.table
optional: true
builder:
bundles:
- bundleDefinition:
name: com/acme/pocockpit/Component-preload.js
defaultFileTypes: [".js", ".xml", ".json", ".properties"]
sections:
- mode: preload
filters:
- "com/acme/pocockpit/manifest.json"
- "com/acme/pocockpit/Component.js"
- "com/acme/pocockpit/view/App.view.xml"
- "com/acme/pocockpit/view/POList.view.xml"
- "com/acme/pocockpit/controller/App.controller.js"
- "com/acme/pocockpit/controller/POList.controller.js"
- "com/acme/pocockpit/i18n/i18n.properties"
resolve: false
renderer: false
- bundleDefinition:
name: com/acme/pocockpit/detail-preload.js
sections:
- mode: preload
filters:
- "com/acme/pocockpit/view/PODetail.view.xml"
- "com/acme/pocockpit/controller/PODetail.controller.js"
- "com/acme/pocockpit/view/fragment/AttachmentPreview.fragment.xml"
이렇게 나누면 초기 Component-preload.js는 목록 화면까지만 포함하므로 크기가 줄고, 상세 화면 번들은 목록 렌더링이 끝난 뒤 idle 시간에 prefetch할 수 있습니다. Component.js에서 idle 시점을 잡아 background 로딩하는 패턴입니다.
sap.ui.define([
"sap/ui/core/UIComponent",
"sap/ui/model/odata/v4/ODataModel",
"sap/base/Log"
], function (UIComponent, ODataModel, Log) {
"use strict";
return UIComponent.extend("com.acme.pocockpit.Component", {
metadata: { manifest: "json" },
init: function () {
UIComponent.prototype.init.apply(this, arguments);
this.getRouter().initialize();
this._prefetchDetailBundle();
this._warmupOData();
},
_prefetchDetailBundle: function () {
const cb = window.requestIdleCallback || function (fn) {
return setTimeout(fn, 1500);
};
cb(() => {
sap.ui.require(["com/acme/pocockpit/detail-preload"], () => {
Log.info("Detail bundle prefetched");
}, (err) => Log.warning("Prefetch failed", err));
});
},
_warmupOData: function () {
const oModel = this.getModel();
if (!oModel) { return; }
const oList = oModel.bindList("/PurchaseOrders", null, null, null, {
$$groupId: "$auto.warmup",
$select: "PurchaseOrderId,Supplier,GrossAmount,Status",
$top: 25
});
oList.requestContexts(0, 25).catch((e) =>
Log.warning("OData warmup skipped", e));
}
});
});
requestIdleCallback으로 브라우저 유휴 시간에 상세 번들을 당겨오고, OData 모델은 $$groupId를 별도 그룹으로 지정해 초기 렌더링 요청과 병렬로 워밍업합니다. 이렇게 하면 목록에서 상세로 넘어갈 때 View 로드+데이터 로드가 이미 완료되어 사용자 체감 반응이 즉각적으로 됩니다. 보안 관점에서는 warmup 요청도 CSRF 토큰과 인증 헤더를 정상 흐름과 동일하게 태워야 하므로, ODataModel이 관리하는 요청을 그대로 쓰는 것이 안전합니다. 커스텀 fetch로 우회하면 세션이 어긋날 수 있습니다.
자주 밟는 지뢰와 트러블슈팅
실제 프로젝트에서 반복적으로 마주치는 3가지 상황과 대응책입니다.
- Q1. async를 켰는데도 로딩이 느립니다.
data-sap-ui-async="true"만 있고manifest.json의rootView.async나routing.config.async가 빠진 경우가 많습니다. 세 지점 모두 true여야 진짜 async가 됩니다. Chrome DevTools에서 라이브러리 파일이 폭포수처럼 순차 로드되면 어딘가 sync 지점이 남아있다는 신호입니다. - Q2. Fragment를 캐싱했는데 두 번째 열 때 오히려 에러가 납니다. Fragment 인스턴스는 View의 dependent로 등록되어 있어 View가 파괴되면 함께 파괴됩니다. 컨트롤러 인스턴스 변수(
this._pDialog)에 캐싱한 뒤 라우팅으로 View가 재생성되면 stale 참조가 됩니다.onExit에서this._pDialog = null로 초기화하거나, Component 레벨의 캐시(getOwnerComponent().getModel근처)로 옮기는 방식이 안정적입니다. - Q3. Preload를 만들었는데 개발 중 변경사항이 반영되지 않습니다.
data-sap-ui-preload="async"가 켜져 있으면 Component-preload.js가 우선 로드됩니다. 개발 시에는?sap-ui-xx-componentPreload=offURL 파라미터 또는ui5 serve의 개발 모드로 preload를 우회하세요. 운영 배포 전 반드시ui5 build로 재생성해야 최신 소스가 번들에 포함됩니다.
추가로 라이브러리 sap.ui.table이나 sap.viz는 크기가 커서 초기에 다 로드하면 손해입니다. manifest.json의 dependencies.libs에 "lazy": true를 지정해 실제 View에서 요구할 때만 로드되게 하는 것이 좋습니다.
이 다음으로 파볼 만한 주제
초기 로딩을 잡았다면 그 다음은 런타임 성능입니다. OData V4의 $$aggregation을 활용한 서버사이드 그룹핑, growing="true" 리스트의 paging 튜닝, Smart Controls의 Flex 처리 지연 등이 자연스러운 후속 주제입니다. 배포 파이프라인 쪽으로는 CAP 프로젝트와 통합된 mbt build로 MTA에 preload를 포함시키는 방법, HTML5 App Repo의 x-content-type-options/cache-control 헤더 튜닝을 함께 정리해두면 운영 안정성이 크게 올라갑니다. Fiori Elements 프로젝트라면 manifest.json의 sap.ui.generic.app 설정에서 preload flow를 조정하는 방식이 별도로 존재하니 참고하세요.
추가로 훑어볼 링크
- SAPUI5 SDK — Performance: Speed Up Your App (help.sap.com)
- SAPUI5 SDK — Routing and Navigation (help.sap.com)
- SAPUI5 SDK — Descriptor for Applications (manifest.json) (help.sap.com)
- UI5 Tooling — Project Configuration (ui5.yaml)
- SAP Community Blogs — Fiori Performance 태그
- SAP Developers — Optimizing UI5 Apps with Preload Bundles
댓글 0
아직 댓글이 없습니다.