UI5

GridContainer vs CSSGrid — UI5 반응형 레이아웃 #shorts #SAP #UI5

1. 개요 및 이 글에서 얻어갈 것

SAPUI5에서 대시보드나 카드 기반 화면을 만들 때 가장 자주 마주치는 고민이 있습니다. HBoxVBox로 끝없이 중첩시키다 보면 반응형이 깨지고, Grid(sap.ui.layout.Grid)는 12-컬럼 float 기반이라 카드 크기를 픽셀 단위로 정밀하게 다루기 어렵습니다. sap.f.GridContainer(많은 자료에서 sap.ui.layout.cssgrid 패키지와 함께 언급되는 그것)는 CSS Grid를 SAPUI5 컨트롤로 감싼 컴포넌트로, Fiori 3 이후 카드 기반 Launchpad/Overview Page의 표준 레이아웃 컨테이너입니다.

이 글에서는 GridContainer의 동작 원리부터 시작해서, CustomLayout 설정, GridItemLayoutData로 반응형 span 처리, 대시보드 실전 예제, 중첩 패턴, 그리고 DynamicSideContent/FlexibleColumnLayout과 어떻게 역할이 겹치고 어떻게 선택해야 하는지까지 정리합니다.

  • float 방식 Grid와 CSS Grid 기반 GridContainer의 구조적 차이 이해
  • CustomLayout으로 columns, rowGap, columnGap을 명시적으로 제어
  • GridItemLayoutData로 S/M/L/XL breakpoint별 span 지정
  • 중첩 GridContainer로 카드 내부 sub-grid 구성
  • height collapse, VBox 안 100% height 문제 회피 패턴

버전 기준: SAPUI5 1.120 LTS (1.96 이상 호환). sap.f.GridContainer는 1.65부터 stable 상태이며, sap.ui.layout.cssgridCSSGrid 컨트롤은 1.60부터 제공됩니다.

2. 핵심 개념 — GridContainer와 CSSGrid API 동작 원리

먼저 용어 정리가 필요합니다. SAPUI5 안에는 CSS Grid와 관련된 컨트롤이 두 갈래로 존재합니다.

  • sap.ui.layout.cssgrid.CSSGrid: CSS Grid 속성(gridTemplateColumns, gridGap, gridAutoFlow 등)을 거의 그대로 노출한 저수준 컨테이너. 자유도가 높지만 반응형 처리를 직접 짜야 함
  • sap.f.GridContainer: Fiori 카드 격자 패턴에 특화된 고수준 컨테이너. GridContainerSettingslayout, layoutS, layoutM, layoutL, layoutXL로 분리해 breakpoint별 컬럼 수와 간격을 선언적으로 지정

두 컨트롤 모두 내부적으로 브라우저의 CSS Grid를 사용합니다. 차이는 "누가 미디어 쿼리를 책임지느냐"입니다. CSSGrid는 개발자가, GridContainer는 컨트롤 자체가 ResizeHandler를 통해 컨테이너 너비를 감지하고 breakpoint별 설정으로 자동 전환합니다.

비유하자면, sap.ui.layout.Grid(float 기반)는 "종이를 12등분으로 접고 그 칸을 채우는 방식"이고, GridContainer"바둑판 위에 카드를 놓되, 카드마다 가로/세로 칸 수를 선언하는 방식"입니다. float 방식은 카드가 한 줄에서 다음 줄로 떨어질 때 빈 공간이 생기는 "orphan column" 문제가 있지만, CSS Grid 기반은 grid-auto-flow: dense로 빈 칸을 채워줍니다.

breakpoint는 컨트롤이 차지하는 컨테이너의 실제 너비를 기준으로 판단합니다(미디어쿼리가 아닌 ResizeHandler). 일반적인 기준값은 다음과 같습니다.

  • S: 너비 ~ 599px
  • M: 600px ~ 1023px
  • L: 1024px ~ 1439px
  • XL: 1440px 이상

이 값들은 SAPUI5 표준 breakpoint(sap.ui.Device.media)와 동일한 컨벤션을 따르며, 컨트롤 단위로 결정되므로 같은 화면 안에 여러 GridContainer가 있을 때 각각 다른 breakpoint를 가질 수 있습니다(중첩 시 매우 중요).

3. 1단계: 기본 GridContainer 선언 (CustomLayout + columns/gap)

가장 간단한 형태부터 시작합니다. 6개의 Card를 카드 격자로 배치합니다.

<mvc:View
    xmlns:mvc="sap.ui.core.mvc"
    xmlns="sap.m"
    xmlns:f="sap.f"
    xmlns:card="sap.f.cards"
    controllerName="my.app.Dashboard">

  <Page title="GridContainer Basic">
    <content>
      <f:GridContainer id="grid"
          snapToRow="true"
          allowDenseFill="true">

        <f:layout>
          <f:GridContainerSettings
              rowSize="5rem"
              columnSize="5rem"
              gap="1rem" />
        </f:layout>

        <f:layoutS>
          <f:GridContainerSettings
              columns="4"
              rowSize="4rem"
              columnSize="4rem"
              gap="0.5rem" />
        </f:layoutS>

        <f:Card height="10rem" width="20rem">
          <f:header>
            <card:Header title="매출" subtitle="오늘" />
          </f:header>
        </f:Card>
        <!-- 나머지 카드 5개 동일 패턴 -->

      </f:GridContainer>
    </content>
  </Page>
</mvc:View>

핵심 속성을 짚어봅니다.

  • rowSize, columnSize: 격자 한 칸의 크기. CSS Grid의 grid-auto-rows/grid-template-columns에 매핑됩니다
  • gap: 칸 사이 간격
  • snapToRow="true": 카드 높이가 칸 크기의 배수가 되도록 강제. 시각적으로 정렬이 깔끔해집니다
  • allowDenseFill="true": CSS Grid의 grid-auto-flow: dense 활성화. 빈 칸을 뒷 카드가 자동으로 채웁니다
  • layout: 기본 설정. 특정 breakpoint 설정이 없으면 이 값이 사용됩니다
  • layoutS: S breakpoint(모바일) 전용 설정. M/L/XL도 동일 패턴

여기서 주의할 점은 columns 속성입니다. layoutS처럼 모바일에서는 명시적으로 컬럼 수를 고정하는 것이 일반적입니다(보통 4). 기본 layout에서는 columns를 비워두면 컨테이너 너비를 columnSize + gap으로 나눈 만큼 자동 계산됩니다.

4. 2단계: GridItemLayoutData로 반응형 span 설정

실무에서는 카드마다 차지하는 칸 수가 다릅니다. KPI 카드는 작게, 차트 카드는 크게. 이때 GridContainerItemLayoutData를 카드의 layoutData에 넣습니다.

<f:Card height="100%">
  <f:layoutData>
    <f:GridContainerItemLayoutData
        minRows="2"
        columns="2" />
  </f:layoutData>
  <f:header>
    <card:Header title="신규 주문" subtitle="실시간" />
  </f:header>
</f:Card>

minRows는 최소 차지 행 수, columns는 가로 칸 수입니다. 콘텐츠 높이가 더 커지면 자동으로 늘어나지만 최소 보장값이 있어 카드끼리 높이가 들쭉날쭉하지 않습니다.

여기서 SAPUI5 1.96 이후 추가된 rows(고정 행 수) 속성도 활용할 수 있는데, 일반적으로는 minRows를 사용하는 것이 콘텐츠 적응에 유리합니다.

화면 크기별 span을 다르게 하려면 컨트롤러나 model에서 breakpoint를 받아 columns 값을 바인딩하는 패턴을 씁니다. GridContainerlayoutChange 이벤트를 발생시킵니다.

// controller
onInit: function () {
  var oGrid = this.byId("grid");
  oGrid.attachLayoutChange(function (oEvent) {
    var sLayout = oEvent.getParameter("layout"); // "layoutS" 등
    this.getView().getModel("ui").setProperty("/layout", sLayout);
  }.bind(this));
},

그리고 XML에서 다음과 같이 바인딩합니다.

<f:GridContainerItemLayoutData
    columns="{= ${ui>/layout} === 'layoutS' ? 4 : 2 }"
    minRows="2" />

이 패턴은 같은 카드를 모바일에서는 한 줄 전체(4칸), 데스크톱에서는 2칸만 차지하게 만드는 가장 흔한 처리 방식입니다. "왜 layoutData에는 breakpoint별 속성이 따로 없냐"는 의문이 자주 나오는데, 카드 자체의 span은 콘텐츠 의도에 따라 다르기 때문에 컨테이너 레벨(layoutS/layoutM)과 분리되어 있다고 이해하면 됩니다.

5. 3단계: 대시보드 레이아웃 실전 예제

이제 헤더 KPI 카드 4개 + 차트 1개 + 목록 1개로 구성된 운영 대시보드를 만들어봅니다.

<f:GridContainer
    id="dashboardGrid"
    snapToRow="true"
    allowDenseFill="true"
    layoutChange=".onLayoutChange">

  <f:layout>
    <f:GridContainerSettings
        rowSize="5rem"
        columnSize="5rem"
        gap="1rem" />
  </f:layout>
  <f:layoutS>
    <f:GridContainerSettings
        columns="4"
        rowSize="4rem"
        columnSize="4rem"
        gap="0.5rem" />
  </f:layoutS>
  <f:layoutM>
    <f:GridContainerSettings
        columns="8"
        rowSize="5rem"
        columnSize="5rem"
        gap="0.75rem" />
  </f:layoutM>

  <!-- KPI 카드 4개 -->
  <f:Card>
    <f:layoutData>
      <f:GridContainerItemLayoutData minRows="2" columns="2" />
    </f:layoutData>
    <f:header>
      <card:NumericHeader title="매출" number="1.2M" unitOfMeasurement="KRW" />
    </f:header>
  </f:Card>
  <!-- 나머지 KPI 3개 동일 -->

  <!-- 차트 카드 (크게) -->
  <f:Card>
    <f:layoutData>
      <f:GridContainerItemLayoutData minRows="6" columns="6" />
    </f:layoutData>
    <f:header>
      <card:Header title="주간 매출 추이" />
    </f:header>
    <f:content>
      <viz:Popover xmlns:viz="sap.viz.ui5.controls" />
      <!-- 실제로는 VizFrame 또는 ChartContainer 사용 -->
    </f:content>
  </f:Card>

  <!-- 목록 카드 -->
  <f:Card>
    <f:layoutData>
      <f:GridContainerItemLayoutData minRows="6" columns="4" />
    </f:layoutData>
    <f:header>
      <card:Header title="최근 주문 10건" />
    </f:header>
    <f:content>
      <List items="{/orders}">
        <StandardListItem title="{customer}" description="{amount}" />
      </List>
    </f:content>
  </f:Card>

</f:GridContainer>

이 구성에서 큰 화면(XL, 컬럼 자동 계산)에서는 KPI 4개가 한 줄(2+2+2+2=8칸), 그 아래 차트(6) + 목록(4)이 한 줄에 배치됩니다. 중간 화면(M, 8 컬럼)에서는 KPI 4개가 한 줄, 차트와 목록이 각각 한 줄을 차지합니다. 모바일(S, 4 컬럼)에서는 KPI 2개씩 두 줄, 차트와 목록이 한 줄씩 쌓입니다.

allowDenseFill="true"가 핵심 역할을 하는데, 차트 카드(6칸)가 한 줄에 안 들어가면 다음 줄로 넘어가고 그 자리에 목록 카드(4칸)가 빈 칸을 채워 들어옵니다. float Grid에서는 불가능했던 동작입니다.

6. 4단계: GridContainer 중첩 패턴

큰 카드(예: 차트 카드) 내부에 또 격자가 필요한 경우, GridContainer를 중첩합니다. 외부 격자는 카드 단위, 내부 격자는 KPI 위젯 단위라고 생각하면 됩니다.

<f:Card>
  <f:layoutData>
    <f:GridContainerItemLayoutData minRows="8" columns="8" />
  </f:layoutData>
  <f:header>
    <card:Header title="복합 위젯" />
  </f:header>
  <f:content>
    <f:GridContainer
        snapToRow="true"
        allowDenseFill="true">
      <f:layout>
        <f:GridContainerSettings
            rowSize="3rem"
            columnSize="3rem"
            gap="0.5rem" />
      </f:layout>
      <Panel headerText="좌측 미니 차트">
        <layoutData>
          <f:GridContainerItemLayoutData minRows="4" columns="3" />
        </layoutData>
      </Panel>
      <Panel headerText="우측 통계">
        <layoutData>
          <f:GridContainerItemLayoutData minRows="4" columns="3" />
        </layoutData>
      </Panel>
    </f:GridContainer>
  </f:content>
</f:Card>

중첩 시 가장 중요한 것은 breakpoint가 독립적으로 동작한다는 점입니다. 외부 GridContainer가 L breakpoint여도 내부 GridContainer는 자신의 너비(예: 800px)에 따라 M으로 동작합니다. 이 덕분에 카드 내부 위젯은 카드 크기에 맞춰 자동 재배치됩니다.

다만 중첩 시 흔히 발생하는 문제는 내부 GridContainer의 부모 컨테이너(Cardcontent aggregation)가 너비를 제대로 전파하지 않는 경우입니다. height="100%"를 명시하거나, 부모를 FlexBox로 감싸 renderType="Bare", fitContainer="true"를 설정해야 할 수 있습니다.

7. 자주 겪는 함정과 FAQ

Q1. GridContainer 내부의 카드 높이가 0이 되어 안 보입니다.

가장 흔한 케이스입니다. snapToRow="false"(기본값)이면 카드 자체 높이가 0이거나 명시되지 않으면 minRows가 무시되는 것처럼 보일 수 있습니다. 해결책은 두 가지입니다.

  • snapToRow="true"로 설정해서 격자 칸 단위 높이를 강제
  • 카드의 height="100%"를 명시하고 GridContainerItemLayoutData에서 minRows를 지정

Q2. VBox 안에 GridContainer를 넣었더니 카드가 한 칸씩 세로로 쌓입니다.

VBox는 기본적으로 display: flex; flex-direction: column이라 자식의 너비를 콘텐츠 너비로 축소시키는 경우가 있습니다. VBoxwidth="100%"를 주고, GridContainer를 감싼 항목에 FlexItemDatagrowFactor="1"을 지정해야 격자가 컨테이너 전체 너비를 받습니다.

<VBox width="100%">
  <f:GridContainer>
    <layoutData>
      <FlexItemData growFactor="1" />
    </layoutData>
    <!-- ... -->
  </f:GridContainer>
</VBox>

Q3. layoutChange 이벤트가 호출되지 않습니다.

GridContainer는 ResizeHandler로 크기 변화를 감지합니다. 부모 컨테이너가 디스플레이되지 않은 상태(visible=false, 탭 비활성)에서는 이벤트가 발생하지 않습니다. onAfterRendering 후 첫 이벤트를 받지 못한 경우라면 컨트롤러에서 직접 oGrid.getActiveLayoutSettings()로 현재 설정을 조회해 초기 상태를 잡아주는 것이 안전합니다.

Q4. 카드 사이 간격이 통일되지 않습니다.

gap은 격자 칸 사이 간격이지만, 카드 내부 padding(sapFCardContent CSS class의 기본 padding)과 합쳐져 시각적으로 더 커 보일 수 있습니다. 카드 헤더가 있는 경우와 없는 경우의 padding이 달라서 인접 카드가 어긋나 보이기도 합니다. 디자인 통일을 위해서는 Card 대신 Panel이나 VBox를 격자 아이템으로 쓰고 내부 padding을 CSS로 통제하는 방법도 고려합니다.

Q5. 카드를 드래그 앤 드롭으로 재배치하고 싶습니다.

GridContainer는 sap.f.dnd.GridDropInfo를 통한 격자 단위 DnD를 권장합니다. dragDropConfig aggregation에 GridDropInfo를 추가하고 drop 이벤트에서 모델 데이터를 재배치하면 됩니다. 일반 DropInfo는 격자 좌표를 정확히 계산하지 못하므로 사용하지 않는 것이 일반적입니다.

8. 응용 패턴 — DynamicSideContent와의 선택 기준

대시보드 화면을 만들 때 GridContainer만이 정답은 아닙니다. SAPUI5에는 비슷한 역할의 컨트롤이 여럿 있고 선택 기준이 다릅니다.

  • sap.ui.layout.DynamicSideContent: 메인 콘텐츠 + 보조 콘텐츠(필터 패널, 상세 정보 등) 2분할 화면. breakpoint별로 보조 콘텐츠를 우측/하단/숨김으로 전환. 대시보드보다는 마스터-디테일 보조 화면용
  • sap.f.FlexibleColumnLayout: 1/2/3 컬럼 네비게이션 패턴(목록 → 상세 → 하위 상세). 화면 라우팅과 결합된 페이지 전환용. 격자가 아닌 페이지 흐름 제어
  • sap.f.GridContainer: 같은 화면 안에서 다수의 카드/위젯을 격자로 배치. OVP, Launchpad, KPI 대시보드의 표준
  • sap.ui.layout.Grid(레거시): 12-컬럼 float 기반. 폼이나 단순 정렬에는 여전히 유효하지만 카드 배치에는 부적합

판단 기준을 단순화하면, "한 화면에 여러 카드/위젯이 격자로 놓이는가?"에 Yes면 GridContainer, "메인 영역과 사이드 영역으로 나뉘는가?"면 DynamicSideContent, "여러 페이지를 단계적으로 보여주는가?"면 FlexibleColumnLayout입니다. 세 컨트롤은 서로 배타적이지 않고 조합 가능합니다. 예를 들어 FlexibleColumnLayout의 가운데 컬럼 안에 GridContainer를 두어 카드 대시보드를 구성하는 것이 OVP의 전형적 구조입니다.

마지막으로 성능 측면 한 가지. GridContainer는 카드 수가 50개를 넘어가면 ResizeHandler 콜백과 카드별 렌더링이 누적되어 초기 로딩이 느려집니다. 카드 수가 많은 경우 sap.f.GridList로 가상화 격자를 쓰는 것이 일반적으로 권장됩니다. GridListList 기반이라 페이징/가상 스크롤이 가능하면서도 시각적으로는 격자처럼 보입니다.

이 정도면 실무에서 GridContainer로 대시보드를 만들 때 마주치는 대부분의 결정과 함정을 커버할 수 있습니다. 처음에는 snapToRowminRows의 상호작용이 직관적이지 않지만, 한 번 손에 익으면 HBox/VBox 중첩 지옥에서 벗어나 선언적이고 유지보수하기 좋은 레이아웃 코드를 작성할 수 있습니다.

댓글 0

아직 댓글이 없습니다.