개요 및 이 글에서 다루는 것
SAP Fiori 앱을 데스크톱에서만 테스트하고 배포한 뒤 "모바일에서 표가 잘려 보인다"는 컴플레인을 받아본 개발자가 많을 것입니다. Fiori 디자인 가이드라인은 세 가지 폼팩터(Phone/Tablet/Desktop)를 동시에 지원해야 한다고 명시하지만, 실제로는 sap.m.Table을 그냥 배치하고 끝내는 경우가 대다수입니다. 이 글에서는 sap.f.GridList와 sap.ui.layout.cssgrid를 활용한 반응형 레이아웃 패턴을 실전 예제 코드와 함께 다룹니다.
이 글을 끝까지 읽으면 얻는 것:
- 왜
sap.m.Table의popinDisplay만으로는 부족한지 이해 - GridList의
autoPopin,fixedLayout,customBreakpoints패턴 구분 사용 - CustomerContract 관리 화면을 Phone·Tablet·Desktop에서 모두 정상 동작시키는 XML/JS 코드
- autoPopin의 성능 비용과 Lazy Loading 병행 전략
이 글을 읽기 전에 알아두면 좋은 것
SAPUI5/OpenUI5의 XML View와 Controller 구조에 익숙하고, sap.m.Table·sap.m.List를 한 번이라도 다뤄본 개발자를 대상으로 합니다. sap.ui.Device API의 개념, JSON 모델 바인딩, 그리고 CSS Grid의 grid-template-columns 기본 문법을 알면 이해가 빠릅니다. UI5 버전 1.60 이상을 기준으로 서술합니다.
환경 및 준비물
본 예제는 다음 스택에서 검증되었습니다.
- SAPUI5 1.108 LTS (SAP BTP, ABAP Environment 2308 기본 탑재 버전)
- SAP Business Application Studio 또는 VS Code + Fiori Tools 확장
- Chrome 최신 버전 + DevTools의 Device Toolbar (모바일 시뮬레이션)
sap.f라이브러리 (GridList 사용을 위해manifest.json의sap.ui5.dependencies.libs에 등록 필요)sap.ui.layout라이브러리 (cssgrid 하위 클래스 사용)
manifest.json에 다음이 포함되어야 합니다.
{
"sap.ui5": {
"dependencies": {
"minUI5Version": "1.108.0",
"libs": {
"sap.m": {},
"sap.f": {},
"sap.ui.layout": {}
}
}
}
}
핵심 개념 - 왜 GridList가 필요한가
Fiori 앱이 모바일에서 실패하는 대표적 시나리오를 먼저 보겠습니다. 계약 관리 화면에 sap.m.Table을 두고 컬럼 8개를 배치했습니다. 데스크톱에서는 완벽하지만, 아이폰에서 열면 가로 스크롤이 생기거나 컬럼이 짓눌려서 텍스트가 세로로 나열됩니다. 이때 개발자가 흔히 시도하는 것이 컬럼별 minScreenWidth와 demandPopin 설정입니다. 하지만 컬럼 하나하나 픽셀 값을 계산해서 넣는 것은 유지보수 지옥의 시작입니다.
sap.f.GridList는 접근 방식 자체가 다릅니다. "행 = 카드"라는 관점으로, 화면 폭이 좁아지면 카드가 세로로 쌓이고 넓어지면 그리드 형태로 정렬됩니다. 표(Table)를 뒤집어서 카드 컬렉션으로 만든다고 생각하면 됩니다.
비유:
sap.m.Table이 "엑셀 시트를 그대로 보여주려는 시도"라면,sap.f.GridList는 "명함을 데스크에 정렬하는 방식"입니다. 명함은 세로로 쌓아도, 3열로 배열해도 그 자체가 완결된 정보 단위입니다.
GridList가 제공하는 레이아웃 전략을 정리하면 다음과 같습니다.
| 패턴 | 클래스 | 특징 | 적합한 상황 |
|---|---|---|---|
| autoPopin | GridBoxLayout | 화면 폭에 따라 자동 열 개수 조정 | 카드형 대시보드, 상품 목록 |
| fixedLayout | GridBasicLayout | 고정된 grid-template-columns 정의 | 디자인 스펙이 명확한 화면 |
| customBreakpoints | GridResponsiveLayout | S/M/L/XL 4단계 브레이크포인트별 설정 | 폼팩터별 UX가 달라야 하는 화면 |
세 패턴 모두 내부적으로 CSS Grid를 사용하므로, 브라우저의 네이티브 렌더링 성능을 그대로 활용합니다. 이는 JavaScript로 resize 이벤트마다 다시 계산하는 방식보다 훨씬 가볍습니다.
1단계 예제 - GridBoxLayout 기본형
가장 간단한 GridBoxLayout 방식부터 시작합니다. 고객 계약(CustomerContract) 목록을 카드 형태로 보여주는 화면입니다.
<mvc:View
controllerName="com.acme.contract.controller.List"
xmlns:mvc="sap.ui.core.mvc"
xmlns="sap.m"
xmlns:f="sap.f"
xmlns:grid="sap.ui.layout.cssgrid">
<Page title="{i18n>contractListTitle}">
<f:GridList
id="contractGrid"
items="{/Contracts}"
growing="true"
growingThreshold="20">
<f:customLayout>
<grid:GridBoxLayout
boxMinWidth="18rem"
boxesPerRowConfig="XL4 L3 M2 S1"/>
</f:customLayout>
<CustomListItem>
<VBox class="sapUiSmallMargin">
<Title text="{contractName}" level="H3"/>
<Text text="{customerName}"/>
<ObjectStatus text="{status}" state="Success"/>
</VBox>
</CustomListItem>
</f:GridList>
</Page>
</mvc:View>
핵심은 boxesPerRowConfig="XL4 L3 M2 S1"입니다. XL(1440px+)에서는 4열, L(1024px+)에서 3열, M(600px+)에서 2열, S(모바일)에서 1열로 자동 전환됩니다. boxMinWidth는 카드 하나의 최소 폭을 강제해 지나치게 좁아지는 것을 방지합니다.
2단계 예제 - GridResponsiveLayout + Controller 로직
실무에서는 단순히 카드 개수만 바뀌면 안 되고, 폼팩터별로 표시되는 정보량 자체가 달라져야 합니다. 예를 들어 모바일에서는 계약명·상태만 보여주고, 데스크톱에서는 매출·담당자·갱신일까지 표시하는 요구가 흔합니다.
<f:GridList
id="contractResponsiveGrid"
items="{path:'/Contracts'}"
updateFinished=".onGridUpdated">
<f:customLayout>
<grid:GridResponsiveLayout
layoutChange=".onLayoutChange">
<grid:layoutS>
<grid:GridSettings gridTemplateColumns="1fr" gridGap="0.5rem"/>
</grid:layoutS>
<grid:layoutM>
<grid:GridSettings gridTemplateColumns="repeat(2, 1fr)" gridGap="1rem"/>
</grid:layoutM>
<grid:layoutL>
<grid:GridSettings gridTemplateColumns="repeat(3, minmax(20rem, 1fr))" gridGap="1.5rem"/>
</grid:layoutL>
<grid:layoutXL>
<grid:GridSettings gridTemplateColumns="repeat(4, minmax(18rem, 1fr))" gridGap="2rem"/>
</grid:layoutXL>
</grid:GridResponsiveLayout>
</f:customLayout>
<CustomListItem type="Navigation" press=".onContractPress">
<VBox class="sapUiContentPadding contractCard">
<HBox justifyContent="SpaceBetween">
<Title text="{contractName}" level="H4"/>
<ObjectStatus text="{status}" state="Success"/>
</HBox>
<Text text="{customerName}" class="sapUiTinyMarginTop"/>
<HBox visible="{device>/system/desktop}">
<ObjectNumber number="{annualValue}" unit="EUR"/>
<Text text="갱신: {renewalDate}" class="sapUiSmallMarginBegin"/>
</HBox>
</VBox>
</CustomListItem>
</f:GridList>
Controller에서는 sap.ui.Device 모델을 등록하고, 레이아웃 변경 이벤트를 로깅합니다.
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/model/json/JSONModel",
"sap/ui/Device",
"sap/base/Log"
], function (Controller, JSONModel, Device, Log) {
"use strict";
return Controller.extend("com.acme.contract.controller.List", {
onInit: function () {
const oDeviceModel = new JSONModel(Device);
oDeviceModel.setDefaultBindingMode("OneWay");
this.getView().setModel(oDeviceModel, "device");
},
onLayoutChange: function (oEvent) {
const sLayout = oEvent.getParameter("layout");
Log.info("GridList 레이아웃 전환: " + sLayout, "ContractList");
},
onGridUpdated: function (oEvent) {
const iTotal = oEvent.getParameter("total");
if (iTotal === 0) {
Log.warning("계약 데이터 없음", "ContractList");
}
},
formatCurrency: function (vValue) {
if (!vValue) return "";
return new Intl.NumberFormat("de-DE", { minimumFractionDigits: 2 }).format(vValue);
},
onContractPress: function (oEvent) {
const oItem = oEvent.getSource();
const sPath = oItem.getBindingContext().getPath();
this.getOwnerComponent().getRouter().navTo("detail", {
contractPath: encodeURIComponent(sPath.substring(1))
});
}
});
});
3단계 예제 - autoPopin과 Lazy Loading을 결합한 프로덕션 코드
프로덕션 환경에서는 성능 이슈가 함께 옵니다. 계약 데이터가 5000건이라면 한 번에 그리기가 부담스럽습니다. growing="true"와 growingThreshold로 지연 로딩을 걸고, autoPopin 개념을 sap.m.Table에 적용한 하이브리드 방식도 함께 살펴봅니다.
<Table
id="contractTable"
items="{path:'/Contracts'}"
autoPopinMode="true"
contextualWidth="Auto"
growing="true"
growingThreshold="50"
growingScrollToLoad="true"
sticky="ColumnHeaders,HeaderToolbar">
<headerToolbar>
<OverflowToolbar>
<Title text="계약 목록"/>
<ToolbarSpacer/>
<SearchField width="16rem" search=".onSearch"/>
</OverflowToolbar>
</headerToolbar>
<columns>
<Column importance="High"><Text text="계약명"/></Column>
<Column importance="High"><Text text="고객"/></Column>
<Column importance="Medium" hAlign="End"><Text text="연 계약금액"/></Column>
<Column importance="Low"><Text text="담당자"/></Column>
<Column importance="Low"><Text text="갱신일"/></Column>
</columns>
<ColumnListItem>
<cells>
<ObjectIdentifier title="{contractName}" text="{contractId}"/>
<Text text="{customerName}"/>
<ObjectNumber number="{annualValue}" unit="EUR"/>
<Text text="{ownerName}"/>
<Text text="{renewalDate}"/>
</cells>
</ColumnListItem>
</Table>
autoPopinMode="true"는 UI5 1.76+에서 도입된 속성으로, 각 컬럼의 importance 값(High/Medium/Low)에 따라 화면이 좁아질 때 자동으로 팝인(pop-in) 영역으로 이동시킵니다. 개발자가 minScreenWidth를 픽셀 단위로 계산할 필요가 없어집니다.
Controller에서 성능 관련 로직을 추가합니다.
onInit: function () {
const oTable = this.byId("contractTable");
oTable.attachUpdateStarted(function (oEvent) {
const sReason = oEvent.getParameter("reason");
if (sReason === "Growing") {
sap.ui.core.BusyIndicator.show(0);
}
});
oTable.attachUpdateFinished(function () {
sap.ui.core.BusyIndicator.hide();
});
},
onSearch: function (oEvent) {
const sQuery = oEvent.getParameter("query");
const oBinding = this.byId("contractTable").getBinding("items");
if (sQuery) {
oBinding.filter([
new sap.ui.model.Filter({
filters: [
new sap.ui.model.Filter("contractName", "Contains", sQuery),
new sap.ui.model.Filter("customerName", "Contains", sQuery)
],
and: false
})
]);
} else {
oBinding.filter([]);
}
}
흔한 실수와 문제 해결
Q1. GridList가 아예 렌더링되지 않아요.
가장 흔한 원인은 manifest.json에 sap.f 라이브러리를 등록하지 않은 것입니다. 콘솔에 "sap.f.GridList is not a constructor" 오류가 뜬다면 이것부터 확인해야 합니다. 또한 sap.ui.layout.cssgrid의 GridBoxLayout은 별도 임포트가 필요합니다.
Q2. autoPopin을 켰는데 데스크톱에서도 컬럼이 팝인으로 접혀 있어요.
contextualWidth 속성이 "Inherit"(기본값)로 되어 있으면 상위 컨테이너의 폭이 좁을 때 그 폭을 기준으로 팝인이 결정됩니다. contextualWidth="Auto"로 명시해서 실제 뷰포트 폭을 사용하도록 하거나, 픽셀 값(예: "1200px")을 직접 지정하면 해결됩니다.
Q3. 카드 하나가 너무 커서 나머지 여백이 낭비돼요.
GridBoxLayout의 boxWidth와 boxMinWidth를 혼동하는 경우입니다. boxWidth는 고정 폭이고 boxMinWidth는 최소 폭입니다. 반응형이 되려면 반드시 boxMinWidth를 사용해야 하며, 1fr로 자동 확장되도록 해야 합니다.
Q4. autoPopin이 성능에 영향을 주나요?
그렇습니다. autoPopin은 리사이즈 이벤트마다 각 컬럼의 폭을 재계산하므로, 500행 이상의 대용량 테이블에서는 렌더링 지연이 체감됩니다. 이런 경우 growingThreshold를 25~50 사이로 낮추고 growingScrollToLoad와 병행하는 것을 권장합니다.
Q5. 태블릿 가로/세로 회전 시 레이아웃이 안 바뀌어요.
sap.ui.Device의 orientation 변경 이벤트에 리스너를 붙여야 합니다. Device.orientation.attachHandler(fn, this)로 등록하고, 필요 시 GridResponsiveLayout의 layout 프로퍼티를 명시적으로 재설정하면 됩니다.
이어서 살펴볼 만한 주제
- sap.f.Card와 결합해 카드형 대시보드 구성 (Integration Card 규격 활용)
- FlexibleColumnLayout과 조합해 마스터-디테일 반응형 패턴 구현
- sap.ui.table.Table과의 차이점 (대량 데이터 처리 시 유리)
- Fiori Elements의 List Report + Object Page 템플릿에서 GridList 활용법
- OData V4
$filter·$top·$skip을 활용한 서버 사이드 페이지네이션
댓글 0
아직 댓글이 없습니다.