Toolbar와 Bar를 혼용할 때 생기는 문제
SAP Fiori 화면을 만들다 보면 상단 헤더에 제목과 검색 버튼, 하단 푸터에 저장/취소 버튼을 배치하는 일이 빈번합니다. 이때 많은 개발자가 sap.m.Bar와 sap.m.Toolbar를 거의 같은 컨트롤로 착각하고 아무 곳에나 끼워 넣습니다. 결과적으로 다음과 같은 증상이 나타납니다.
- 모바일에서 헤더의 제목이 가운데 정렬되어야 하는데 왼쪽으로 쏠림
- 버튼이 많을 때 화면 밖으로 잘려 나가지만 오버플로 메뉴가 뜨지 않음
Page의showHeader속성과customHeader가 충돌해 헤더가 두 줄로 나타남- 테마 변경 시 헤더 배경색이 다른 페이지와 어긋남
이 모든 문제의 근본 원인은 두 컨트롤의 설계 의도가 다르다는 점을 놓쳤기 때문입니다. Bar는 페이지의 골격(header/footer)을 잡는 레이아웃 컨트롤이고, Toolbar는 그 안에 들어가는 액션 묶음입니다. 둘은 경쟁 관계가 아니라 보완 관계입니다. 이 글에서는 두 컨트롤의 역할, 동작 원리, 그리고 실전에서 어떻게 조합해야 깨지지 않는 레이아웃을 만들 수 있는지 단계별로 풀어보겠습니다.
점검해야 할 핵심 포인트는 다음과 같습니다.
- Toolbar와 OverflowToolbar의 차이를 설명할 수 있는가
- Bar의 contentLeft/contentMiddle/contentRight 슬롯 의미를 이해하는가
Page컨트롤의 customHeader/customFooter에 무엇을 넣어야 하는가- 버튼 5개 이상이 들어가는 헤더를 반응형으로 만들 수 있는가
이 글을 따라오기 전에 알아두면 좋은 배경
SAPUI5 1.96 이상(현 글에서는 1.120 LTS 기준)에서의 sap.m 라이브러리 기본 구조, XML 뷰 문법, 그리고 Page 컨트롤이 header/subHeader/footer aggregation을 가진다는 사실 정도면 충분합니다. 컨트롤러에서 이벤트 핸들러를 연결해 본 경험, id와 byId로 컨트롤을 조회해 본 경험이 있다면 5단계부터의 동적 조작 부분이 한층 자연스럽게 읽힐 것입니다. CSS 커스터마이징보다는 표준 컨트롤 조합으로 해결하는 방향을 권장합니다.
실습 환경과 준비물
이 글의 예제는 다음 환경을 가정합니다.
- SAPUI5 또는 OpenUI5 1.108 이상 (LTS 1.120 권장, 1.96+에서도 OverflowToolbar 대부분 기능 동작)
- BAS(Business Application Studio) 혹은 VS Code + UI5 Tooling (
@ui5/cli3.x) - Node.js 18 LTS 이상, npm 9.x
- SAP Fiori elements가 아닌 freestyle 프로젝트 (Toolbar/Bar를 직접 다루는 시나리오)
- 테마:
sap_horizon또는sap_fiori_3
로컬에서 빠르게 띄우려면 npm init @sap/ui5-application으로 스캐폴딩한 뒤 ui5 serve -o test.html로 실행하면 됩니다. 모바일 반응형 동작을 확인하려면 브라우저 개발자 도구의 디바이스 에뮬레이션을 켜고 320px, 600px, 1024px 폭을 번갈아 테스트하는 것을 권장합니다.
sap.m.Toolbar — 액션 버튼 컨테이너의 역할
sap.m.Toolbar는 한 줄짜리 가로 컨테이너입니다. 안에 버튼, 입력 필드, 레이블, 셀렉트 박스 등 어떤 컨트롤이든 넣을 수 있고, ToolbarSpacer를 사이에 끼워 좌/우 분리를 만듭니다. 시각적으로는 단순한 막대처럼 보이지만, 내부적으로는 flex 레이아웃으로 동작하며 자식 컨트롤의 layoutData에 따라 신축성이 결정됩니다.
비유하자면 Toolbar는 "주방의 도마"입니다. 어떤 재료(컨트롤)든 그 위에 올려놓을 수 있지만, 도마 자체는 주방(페이지)의 어느 위치에 둘지 결정하지 않습니다. 그래서 Toolbar는 단독으로 쓰이기보다 테이블의 headerToolbar, 폼의 상단, 또는 Bar의 자식으로 들어가는 일이 많습니다.
가장 기본적인 형태는 다음과 같습니다.
<mvc:View
xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc"
controllerName="zorder.controller.OrderList">
<Toolbar id="orderActionBar">
<Title text="주문 목록" level="H2"/>
<ToolbarSpacer/>
<SearchField
id="orderSearch"
width="18rem"
placeholder="주문번호 또는 고객명"
search=".onSearchOrder"/>
<Button
icon="sap-icon://refresh"
tooltip="새로고침"
press=".onRefreshOrders"/>
</Toolbar>
</mvc:View>
여기서 핵심은 ToolbarSpacer입니다. 이 빈 컨트롤이 가용 공간을 모두 흡수해 좌측 제목과 우측 액션을 자동으로 떨어뜨립니다. CSS justify-content: space-between을 직접 작성할 필요가 없습니다. design 속성을 Solid, Transparent, Info 등으로 바꾸면 배경 톤도 조정할 수 있습니다.
OverflowToolbar — 반응형 버튼 오버플로 처리
버튼이 4개를 넘어가는 순간 일반 Toolbar는 좁은 화면에서 문제가 됩니다. 마지막 버튼들이 화면 밖으로 밀려나거나 줄바꿈이 되어 레이아웃이 깨집니다. 이를 해결하는 컨트롤이 sap.m.OverflowToolbar입니다.
OverflowToolbar는 자식 컨트롤마다 OverflowToolbarLayoutData를 통해 우선순위(priority)를 부여합니다. 폭이 줄어들면 우선순위가 낮은 항목부터 자동으로 우측의 "..."(more) 버튼 아래 팝오버로 이동합니다. 이때 항목이 단순히 숨겨지는 것이 아니라 실제 DOM에서 팝오버 자식으로 이동하기 때문에 이벤트 바인딩이 유지됩니다.
<OverflowToolbar id="invoiceActions">
<Title text="송장 관리" level="H2">
<layoutData>
<OverflowToolbarLayoutData priority="NeverOverflow"/>
</layoutData>
</Title>
<ToolbarSpacer/>
<Button text="발행" type="Emphasized" press=".onIssue">
<layoutData>
<OverflowToolbarLayoutData priority="High"/>
</layoutData>
</Button>
<Button text="복제" press=".onClone">
<layoutData>
<OverflowToolbarLayoutData priority="Low" group="1"/>
</layoutData>
</Button>
<Button text="삭제" press=".onDelete">
<layoutData>
<OverflowToolbarLayoutData priority="Low" group="1"/>
</layoutData>
</Button>
<Button text="감사 로그" press=".onAudit">
<layoutData>
<OverflowToolbarLayoutData priority="Disappear"/>
</layoutData>
</Button>
</OverflowToolbar>
우선순위는 NeverOverflow, High, Low, Disappear 4단계로 구분됩니다. NeverOverflow는 화면이 아무리 좁아져도 항상 보입니다(보통 제목에 사용). Disappear는 오버플로 영역에도 노출되지 않고 완전히 숨겨집니다(중요도가 낮은 부가 기능). group 속성을 같은 숫자로 묶으면 함께 이동합니다.
sap.m.Bar — 페이지 header/footer 레이아웃 뼈대
sap.m.Bar는 Toolbar와 달리 3분할 슬롯 구조를 가집니다. contentLeft, contentMiddle, contentRight aggregation이 따로 정의되어 있어 좌/중/우 영역에 컨트롤을 명시적으로 배치합니다. 모바일에서 가운데 슬롯은 자동으로 중앙 정렬되며, 일반적으로 페이지 제목이 들어갑니다.
비유하자면 Bar는 "신문 1면의 상단 마스트헤드"입니다. 좌측에 로고, 중앙에 신문 이름, 우측에 날짜가 들어가는 고정된 골격이죠. 그래서 Bar는 Page, Dialog, QuickView, MessagePopover 등 컨테이너의 header/footer 영역에 들어가도록 설계되어 있습니다.
<Bar id="catalogHeader" design="Header">
<contentLeft>
<Button
icon="sap-icon://nav-back"
tooltip="뒤로"
press=".onNavBack"/>
</contentLeft>
<contentMiddle>
<Title text="제품 카탈로그" level="H1"/>
</contentMiddle>
<contentRight>
<Button
icon="sap-icon://action-settings"
press=".onOpenSettings"/>
</contentRight>
</Bar>
Bar의 design 속성은 Auto(기본), Header, SubHeader, Footer의 4가지 값을 받습니다. 각 값에 따라 테마가 적용하는 배경색과 그림자 처리가 달라지므로, Page의 어느 슬롯에 넣느냐에 맞춰 지정하는 것이 좋습니다. Auto로 두면 부모 컨테이너의 슬롯을 감지해 자동 결정합니다.
Bar + Toolbar 조합 패턴 — 실전 표준
두 컨트롤의 역할이 갈리면 자연스럽게 떠오르는 의문이 있습니다. "그럼 헤더에 액션 버튼이 4~5개 있고 오버플로도 필요하면 Bar는 어떻게 처리하나?" 답은 간단합니다. Bar의 contentRight 슬롯 안에 OverflowToolbar를 넣는다입니다.
<Page id="dashboardPage" showHeader="false">
<customHeader>
<Bar design="Header">
<contentLeft>
<Button icon="sap-icon://menu2" press=".onToggleMenu"/>
</contentLeft>
<contentMiddle>
<Title text="운영 대시보드" level="H1"/>
</contentMiddle>
<contentRight>
<OverflowToolbar style="Clear">
<Button icon="sap-icon://filter" press=".onFilter">
<layoutData>
<OverflowToolbarLayoutData priority="High"/>
</layoutData>
</Button>
<Button icon="sap-icon://download" press=".onExport">
<layoutData>
<OverflowToolbarLayoutData priority="Low"/>
</layoutData>
</Button>
<Button icon="sap-icon://print" press=".onPrint">
<layoutData>
<OverflowToolbarLayoutData priority="Low"/>
</layoutData>
</Button>
<Button icon="sap-icon://share" press=".onShare">
<layoutData>
<OverflowToolbarLayoutData priority="Disappear"/>
</layoutData>
</Button>
</OverflowToolbar>
</contentRight>
</Bar>
</customHeader>
</Page>
이 패턴이 실전 표준인 이유는 세 가지입니다. 첫째, Bar가 좌/중/우 골격을 잡아 제목이 항상 중앙에 정렬됩니다. 둘째, OverflowToolbar가 contentRight 슬롯 안에서 동작하므로 좁은 화면에서 우측 버튼들만 자동으로 오버플로 메뉴로 이동합니다. 셋째, 좌측의 메뉴/뒤로 버튼은 우선순위 영향을 받지 않고 항상 보입니다.
Page customHeader/customFooter에서의 활용과 동적 제어
컨트롤러에서 헤더의 일부를 런타임에 갱신하거나, 페이지 상태에 따라 풋터 버튼을 활성/비활성으로 바꾸는 경우도 흔합니다. 이때는 컨트롤에 id를 부여해 byId로 접근합니다.
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/m/MessageToast"
], function (Controller, MessageToast) {
"use strict";
return Controller.extend("zorder.controller.OrderDetail", {
onInit: function () {
this._oFooterSave = this.byId("footerSaveBtn");
this._toggleFooterByMode("display");
},
_toggleFooterByMode: function (sMode) {
const bEditable = sMode === "edit";
this._oFooterSave.setEnabled(bEditable);
this._oFooterSave.setType(bEditable ? "Emphasized" : "Default");
},
onPressEdit: function () {
try {
this._toggleFooterByMode("edit");
this.getView().getModel("ui").setProperty("/editMode", true);
} catch (oErr) {
jQuery.sap.log.error(
"편집 모드 전환 실패",
oErr,
"zorder.OrderDetail"
);
MessageToast.show("편집 모드로 전환할 수 없습니다.");
}
},
onPressSave: function () {
const oModel = this.getView().getModel();
oModel.submitChanges({
success: () => {
MessageToast.show("저장되었습니다.");
this._toggleFooterByMode("display");
},
error: (oError) => {
jQuery.sap.log.error("저장 실패", oError);
MessageToast.show("저장 중 오류가 발생했습니다.");
}
});
}
});
});
여기서 주의할 점은 customFooter에 Bar를 넣되 그 안의 contentRight에 OverflowToolbar를 두는 일관성입니다. 풋터에 Toolbar만 단독으로 넣으면 가운데 슬롯이 비어 시각적 균형이 무너집니다. 또한 Page의 showFooter 속성을 false로 두고 모드에 따라 토글하면 데이터 미로딩 상태에서 빈 풋터가 깜빡이는 현상을 막을 수 있습니다.
프로덕션 단계 — 접근성, 다국어, 테스트
실제 서비스에 올릴 때는 추가로 세 가지를 챙겨야 합니다. 첫째, 접근성입니다. Bar의 contentMiddle에 들어가는 Title은 level="H1"처럼 시맨틱 레벨을 명시해야 스크린 리더가 페이지 제목으로 인식합니다. 버튼은 텍스트가 없는 아이콘 버튼이라면 tooltip을 반드시 부여하고, 더 정확하게는 ariaLabelledBy 또는 i18n 키로 묶인 라벨을 연결합니다.
둘째, 다국어입니다. 한국어/영어/독일어 텍스트 길이가 크게 달라 Bar의 contentMiddle Title이 contentRight를 침범할 수 있습니다. Title에 wrapping="false"를 설정하고 OverflowToolbar의 우선순위를 보수적으로 잡아 두면 안전합니다. 셋째, OPA5 통합 테스트로 오버플로 메뉴 동작을 검증합니다. iWaitForOverflowButton 같은 사용자 정의 매처를 만들어 좁은 폭에서 특정 버튼이 팝오버로 이동했는지 확인합니다.
자주 하는 실수와 트러블슈팅
Q1. Bar의 contentMiddle에 버튼만 잔뜩 넣었더니 제목이 사라졌습니다.
A. Bar는 좌/중/우 3분할 골격을 위한 컨트롤이지, 버튼 묶음을 위한 컨테이너가 아닙니다. 버튼이 많다면 contentRight 안에 OverflowToolbar를 넣고, contentMiddle에는 Title 하나만 유지하세요.
Q2. OverflowToolbar에서 특정 버튼이 절대 오버플로 메뉴로 안 들어갑니다.
A. 해당 버튼의 OverflowToolbarLayoutData priority가 NeverOverflow로 잡혀 있거나 부모 너비가 충분한 상태입니다. 또한 SearchField처럼 너비가 큰 컨트롤은 우선순위와 별개로 shrinkable="true" 옵션도 함께 검토하세요.
Q3. Page의 기본 header와 customHeader가 동시에 보입니다.
A. Page에 customHeader를 지정하면 기본 header는 자동으로 무시되어야 하지만, title과 showHeader를 동시에 설정해 둔 경우 충돌이 발생할 수 있습니다. showHeader="false"로 명시하거나 title 속성을 제거해 customHeader만 남기는 것을 권장합니다.
기타 자주 발생하는 함정
- 풋터에 직접 Toolbar만 넣고 우측 정렬을 위해 ToolbarSpacer를 빼먹는 경우 — 결과적으로 버튼이 좌측에 붙음
- OverflowToolbar 안에 또 다른 Toolbar를 중첩 — 오버플로 계산이 깨지므로 피해야 함
- Bar의
design을Auto로 두고 Dialog 안에서 사용 — Dialog의 footer 영역에서는 명시적으로Footer로 지정하는 편이 안전
이 글 이후에 살펴보면 좋은 주제
Toolbar/Bar 조합에 익숙해졌다면 다음 단계로 넘어갈 수 있습니다. sap.f.DynamicPageHeader는 스크롤에 따라 헤더가 접히는 패턴을, sap.m.SemanticPage는 액션 버튼을 의미 단위(positive/negative/share)로 자동 배치하는 방식을 제공합니다. Fiori 3에서 추가된 ShellBar는 앱 전체 상단의 글로벌 헤더를 담당하므로 Bar와 역할이 다르다는 점도 함께 이해해 두면 좋습니다. 또한 sap.ui.table.Table의 extension aggregation, sap.m.Table의 headerToolbar/infoToolbar 차이도 동일한 사고방식으로 정리해 보세요.
Toolbar vs Bar 비교 정리표
| 구분 | sap.m.Toolbar / OverflowToolbar | sap.m.Bar |
|---|---|---|
| 주된 용도 | 액션/입력 컨트롤 묶음 | 페이지 header/footer 레이아웃 골격 |
| 슬롯 구조 | 단일 content (flex) | contentLeft / contentMiddle / contentRight |
| 중앙 정렬 | ToolbarSpacer로 수동 조정 | contentMiddle이 자동 중앙 정렬 |
| 반응형 오버플로 | OverflowToolbar가 지원 | 자체 지원 없음 (내부에 OverflowToolbar 권장) |
| 대표 사용처 | Table headerToolbar, Form 상단 | Page customHeader/customFooter, Dialog |
| design 속성 | Solid/Transparent/Info/Auto | Header/SubHeader/Footer/Auto |
| 조합 권장 | Bar의 contentRight 안에 배치 | contentRight에 OverflowToolbar 포함 |
정리하면, Bar는 페이지의 골격을 잡고 Toolbar는 그 안의 액션을 모은다는 역할 분담을 지키는 것이 핵심입니다. 이 구분 하나만 명확히 해도 헤더/풋터 레이아웃 버그의 대부분이 사라지고, 모바일/태블릿/데스크톱에서 일관된 Fiori 룩앤필을 손쉽게 유지할 수 있습니다.
더 깊이 파고들 수 있는 자료
댓글 0
아직 댓글이 없습니다.