UI5

Splitter 모른다고? — UI5 드래그 리사이즈 패널 완성 #shorts #SAP #UI5

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

SAPUI5 애플리케이션을 만들다 보면 화면을 좌/우 또는 상/하로 나누고, 사용자가 경계선을 드래그해서 영역 크기를 조절할 수 있게 만들어야 하는 요구사항을 자주 만나게 됩니다. 마스터-디테일 패턴, 코드 에디터와 미리보기, 트리 네비게이션과 콘텐츠 영역 같은 UX가 대표적입니다. 이때 divwidth: 50%을 박는 식으로 직접 계산해서 구현할 수도 있지만, 리사이즈, 키보드 접근성, 최소/최대 크기 제한, 반응형 처리까지 손으로 작성하면 코드가 빠르게 비대해집니다.

이 글에서는 sap.ui.layout.Splitter 컨트롤을 사용해 분할 레이아웃을 선언적으로 만드는 방법을 다룹니다. 다루는 범위는 다음과 같습니다.

  • 직접 계산 방식과 Splitter 컨트롤의 차이
  • XML 뷰에서 ContentArea 2개 이상으로 수평/수직 분할 선언
  • SplitterLayoutDatasize, minSize, resizable 속성 활용
  • 컨트롤러에서 getContentAreas()로 동적 크기 변경 및 resize 이벤트 처리
  • Splitter 중첩을 통한 IDE 스타일 3-페인 레이아웃 구성
  • height 설정 누락, overflow 충돌, 최소 크기 합계 초과 같은 흔한 함정
  • 접근성과 DynamicSideContent와의 선택 기준

SAPUI5 1.120 LTS 기준으로 작성되었으며, 1.71 이상의 대부분 버전에서 동일하게 동작합니다.

2. 핵심 개념 — Splitter와 ContentArea의 동작 원리

Splitter는 자식 컨트롤들을 일렬로 배치하고 그 사이에 드래그 가능한 스플리터 바(splitter bar)를 끼워 넣는 레이아웃 컨트롤입니다. 기본 구조는 다음 세 가지로 이해하면 충분합니다.

  • Splitter 컨테이너: 분할 방향(orientation: Horizontal/Vertical)을 결정하는 부모. 여기서 Horizontal은 가로로 나란히, Vertical은 세로로 쌓는다는 의미입니다.
  • ContentArea: Splitter의 contentAreas 어그리게이션에 들어가는 자식. 일반적인 UI5 컨트롤(Panel, List, Table 등)이면 무엇이든 가능합니다.
  • SplitterLayoutData: 각 ContentArea에 붙는 메타데이터. 초기 크기, 최소 크기, 사용자가 그 경계를 드래그할 수 있는지 여부를 지정합니다.

비유하자면, Splitter는 고무줄로 묶인 책장입니다. 책장(ContentArea) 사이에 손잡이(splitter bar)가 있고, 사용자가 손잡이를 잡아당기면 인접한 두 책장의 크기가 반비례로 늘었다 줄었다 합니다. 다만 각 책장은 "이 정도 이하로는 절대 줄일 수 없어요"라는 최소 크기를 가질 수 있고, "나는 고정"이라고 선언하면 손잡이를 움직여도 크기가 바뀌지 않습니다.

크기 지정 방식은 두 가지입니다.

  • 고정 크기: 200px처럼 픽셀 값. 윈도우 크기가 바뀌어도 그대로 유지됩니다.
  • 가변 크기: auto. 남은 공간을 다른 auto ContentArea들과 비율로 나눠 가집니다.

중요한 점은 Splitter가 부모의 높이/너비를 기준으로 자식 크기를 계산한다는 사실입니다. 부모가 높이를 가지지 못하면(예: 일반 Page 콘텐츠 안에 들어가면) Splitter는 0px 높이로 렌더링되어 화면에 아무것도 보이지 않게 됩니다. 이게 초보자가 가장 많이 빠지는 함정입니다.

3. 1단계: 기본 Splitter 선언 (수평 분할)

가장 단순한 수평 분할부터 시작합니다. 왼쪽에 마스터 리스트, 오른쪽에 디테일 패널을 두는 전형적인 패턴입니다.

<mvc:View
    xmlns="sap.m"
    xmlns:mvc="sap.ui.core.mvc"
    xmlns:layout="sap.ui.layout"
    controllerName="myapp.controller.Main"
    height="100%">

  <Page title="Splitter 기본 예제" enableScrolling="false">
    <content>
      <layout:Splitter id="rootSplitter" height="100%" orientation="Horizontal">

        <Panel headerText="마스터" height="100%">
          <List items="{/products}">
            <StandardListItem title="{name}" description="{category}" />
          </List>
        </Panel>

        <Panel headerText="디테일" height="100%">
          <Text text="오른쪽 영역에 상세 내용이 표시됩니다." />
        </Panel>

      </layout:Splitter>
    </content>
  </Page>
</mvc:View>

주목할 부분은 세 가지입니다. 첫째, mvc:Viewheight="100%"PageenableScrolling="false", Splitterheight="100%"까지 높이가 위에서부터 끊김 없이 전달되어야 합니다. 둘째, orientation="Horizontal"은 자식들을 가로로 늘어놓는다는 의미입니다. 셋째, SplitterLayoutData를 명시하지 않으면 두 ContentArea는 50:50으로 동등하게 공간을 나눕니다.

이 상태에서 브라우저를 열면 두 패널 사이에 얇은 회색 바가 보이고, 마우스로 드래그하면 좌우 크기가 즉시 변경됩니다. 별도의 자바스크립트 한 줄도 필요 없습니다.

4. 2단계: SplitterLayoutData로 크기 및 리사이즈 제어

실무에서는 "왼쪽 네비게이션은 240px 고정, 사용자가 줄여도 180px 이하로는 못 줄이게" 같은 요구가 들어옵니다. 이때 SplitterLayoutData가 필요합니다.

<layout:Splitter id="rootSplitter" height="100%" orientation="Horizontal">

  <Panel headerText="네비게이션" height="100%">
    <layoutData>
      <layout:SplitterLayoutData
          size="240px"
          minSize="180"
          resizable="true" />
    </layoutData>
    <List items="{/menu}">
      <StandardListItem title="{label}" type="Active" />
    </List>
  </Panel>

  <Panel headerText="콘텐츠" height="100%">
    <layoutData>
      <layout:SplitterLayoutData size="auto" minSize="320" />
    </layoutData>
    <Text text="네비게이션 메뉴에서 항목을 선택하세요." />
  </Panel>

  <Panel headerText="속성" height="100%">
    <layoutData>
      <layout:SplitterLayoutData
          size="280px"
          minSize="200"
          resizable="false" />
    </layoutData>
    <VBox>
      <Label text="ID" />
      <Text text="P-00123" />
    </VBox>
  </Panel>

</layout:Splitter>

속성 의미를 정리하면 다음과 같습니다.

  • size: 초기 크기. "240px"처럼 픽셀 또는 "auto". auto는 다른 auto 영역과 남은 공간을 균등 분배합니다.
  • minSize: 최소 크기(픽셀). 사용자가 드래그해도 이 값 아래로는 줄어들지 않습니다. 단위 없이 숫자만 입력합니다.
  • resizable: false로 두면 이 ContentArea와 인접한 splitter bar의 드래그가 비활성화됩니다. 위 예제에서 "속성" 패널은 항상 280px로 고정됩니다.

유의할 점은 resizable이 영역 자체가 아니라 인접한 바에 영향을 준다는 것입니다. 따라서 가운데 영역의 resizable을 끄면 양쪽 바가 모두 영향을 받는 식의 미묘한 동작이 일어날 수 있으니, 가능하면 끝쪽 영역에 resizable="false"를 거는 것이 일반적으로 권장됩니다.

5. 3단계: 컨트롤러에서 동적 제어

버튼을 눌러 사이드 패널을 토글하거나, 사용자의 마지막 분할 크기를 저장해두었다가 복원하는 등 자바스크립트로 Splitter를 다뤄야 할 때가 있습니다. 핵심 API는 getContentAreas()resize 이벤트입니다.

sap.ui.define([
  "sap/ui/core/mvc/Controller",
  "sap/ui/layout/SplitterLayoutData",
  "sap/base/Log"
], function (Controller, SplitterLayoutData, Log) {
  "use strict";

  return Controller.extend("myapp.controller.Main", {

    onInit: function () {
      var oSplitter = this.byId("rootSplitter");

      // resize 이벤트로 사용자의 드래그 결과를 감지
      oSplitter.attachResize(function (oEvent) {
        var aNewSizes = oEvent.getParameter("newSizes");
        Log.info("새 크기 배열: " + aNewSizes.join(", "));
        // 예: localStorage에 저장
        try {
          localStorage.setItem("splitterSizes", JSON.stringify(aNewSizes));
        } catch (e) {
          Log.warning("로컬 저장 실패", e);
        }
      });

      this._restoreSizes();
    },

    _restoreSizes: function () {
      var sSaved = localStorage.getItem("splitterSizes");
      if (!sSaved) {
        return;
      }
      try {
        var aSizes = JSON.parse(sSaved);
        var aAreas = this.byId("rootSplitter").getContentAreas();
        aAreas.forEach(function (oArea, i) {
          var oLD = oArea.getLayoutData();
          if (!oLD) {
            oLD = new SplitterLayoutData();
            oArea.setLayoutData(oLD);
          }
          if (aSizes[i] != null) {
            oLD.setSize(aSizes[i] + "px");
          }
        });
      } catch (e) {
        Log.error("Splitter 크기 복원 중 오류", e);
      }
    },

    onToggleNav: function () {
      var aAreas = this.byId("rootSplitter").getContentAreas();
      var oNavLD = aAreas[0].getLayoutData();
      var sCurrent = oNavLD.getSize();
      oNavLD.setSize(sCurrent === "0px" ? "240px" : "0px");
    }

  });
});


코드 흐름을 따라가 봅시다. onInit에서 Splitter를 가져온 뒤 attachResize로 사용자가 바를 드래그할 때마다 발생하는 이벤트를 구독합니다. 이벤트 파라미터 newSizes는 각 ContentArea의 현재 픽셀 크기를 담은 배열입니다. 이걸 localStorage에 저장해두면 다음 방문 시 _restoreSizes()가 복원할 수 있습니다.

주의할 점은 setSize에 항상 단위가 포함된 문자열("240px" 또는 "auto")을 넘겨야 한다는 것입니다. 숫자만 넘기면 무시되거나 예기치 않게 동작할 수 있습니다. 또한 localStorage 접근은 사파리 사생활 보호 모드 등에서 예외를 던질 수 있으므로 try/catch로 감싸는 것이 안전합니다.

토글 함수 onToggleNav는 첫 번째 영역의 크기를 0px와 240px 사이에서 전환합니다. 0px로 줄여도 DOM에서 제거되지 않으므로 토글 성능이 양호한 편입니다.

6. 4단계: Splitter 중첩으로 복잡한 레이아웃

IDE 같은 화면(좌측 파일 트리, 우측 상단 에디터, 우측 하단 콘솔)을 만들려면 Splitter를 중첩합니다. 바깥 Splitter는 수평, 안쪽 Splitter는 수직으로 두는 방식입니다.

<layout:Splitter id="outerSplitter" height="100%" orientation="Horizontal">

  <Panel headerText="Explorer" height="100%">
    <layoutData>
      <layout:SplitterLayoutData size="260px" minSize="200" />
    </layoutData>
    <Tree items="{/files}">
      <StandardTreeItem title="{name}" />
    </Tree>
  </Panel>

  <layout:Splitter id="innerSplitter" height="100%" orientation="Vertical">
    <layoutData>
      <layout:SplitterLayoutData size="auto" minSize="400" />
    </layoutData>

    <Panel headerText="Editor" height="100%">
      <layoutData>
        <layout:SplitterLayoutData size="auto" minSize="200" />
      </layoutData>
      <TextArea value="// 코드 영역" width="100%" height="100%" growing="false" />
    </Panel>

    <Panel headerText="Console" height="100%">
      <layoutData>
        <layout:SplitterLayoutData size="180px" minSize="100" />
      </layoutData>
      <Text text="> build 성공" />
    </Panel>

  </layout:Splitter>

</layout:Splitter>

중첩의 핵심은 안쪽 Splitter 자체에도 SplitterLayoutData를 붙일 수 있다는 점입니다. 바깥 Horizontal Splitter 입장에서 안쪽 Vertical Splitter는 그저 또 하나의 ContentArea일 뿐입니다. 그래서 위 예제에서 안쪽 Splitter가 size="auto" minSize="400"을 가져 가로 방향으로 남은 공간을 모두 차지하도록 했습니다.

중첩 단계가 3단 이상으로 깊어지면 사용자가 어떤 바를 잡아야 어디가 늘어나는지 직관적으로 이해하기 어려워집니다. 일반적으로 2단 중첩까지가 UX적으로 무난하며, 그 이상이 필요하다면 일부 영역은 탭으로 묶거나 IconTabBar로 대체하는 편이 권장됩니다.

7. 자주 겪는 함정과 FAQ

Q1. Splitter가 화면에 안 보입니다. 가장 흔한 원인은 부모의 높이가 0이라는 것입니다. Splitter는 부모의 크기에 의존하므로, Viewheight="100%", PageenableScrolling="false", 그리고 콘텐츠 영역까지 높이가 끊김 없이 전달되어야 합니다. Page 안에 직접 넣을 때 스크롤이 켜져 있으면 내부 콘텐츠는 자연스러운 높이가 0이 되어 Splitter가 보이지 않게 됩니다.

Q2. minSize 합계가 컨테이너보다 커지면? Splitter는 일단 minSize를 우선적으로 보장하려 시도합니다. 결과적으로 컨테이너 바깥으로 콘텐츠가 넘쳐 가로 스크롤바가 생기거나 일부 영역이 겹쳐 보일 수 있습니다. 반응형 화면을 만들려면 모든 ContentArea의 minSize 합이 예상되는 최소 뷰포트보다 충분히 작도록 설계하거나, 좁은 화면에서는 Splitter 대신 다른 레이아웃으로 전환하는 것이 권장됩니다.

Q3. ContentArea 내부에서 스크롤이 이상하게 동작합니다. Splitter는 자식 컨트롤에 명시적 높이를 강제하지 않습니다. ListTable 같은 컨트롤을 안에 넣을 때 스크롤이 영역 밖으로 새는 느낌이 든다면, ScrollContainer로 감싸고 height="100%"를 주거나, 컨트롤 자체에 height를 명시하는 방식으로 해결할 수 있습니다. overflow: hidden 같은 CSS 트릭을 강제하는 것은 키보드 포커스 이동을 깨뜨릴 수 있어 비권장입니다.

Q4. resize 이벤트가 너무 자주 발생합니다. 드래그 중에는 매 프레임 가까이 이벤트가 발생할 수 있습니다. localStorage 저장이나 백엔드 동기화를 매번 수행하면 성능 문제가 생깁니다. setTimeout으로 디바운스를 거는 것이 일반적인 해법입니다.

Q5. 키보드로도 분할 크기를 바꿀 수 있나요? Splitter bar에는 포커스가 갈 수 있고, 화살표 키로 단계적으로 크기를 조절할 수 있도록 설계되어 있습니다. 따라서 tabindex를 임의로 손대거나 CSS로 splitter bar를 숨기면 접근성이 크게 저하됩니다.

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

분할 레이아웃이 필요할 때마다 무조건 Splitter를 쓰는 것은 좋은 선택이 아닙니다. SAPUI5는 비슷한 영역의 컨트롤을 여러 개 제공하며, 사용 목적에 따라 갈라집니다.

  • sap.ui.layout.Splitter: 사용자가 직접 크기를 조절해야 하는 IDE, 비교 화면, 마스터-디테일 편집기에 적합. 영역은 일반적으로 2~3개.
  • sap.ui.layout.ResponsiveSplitter: Splitter와 비슷하지만 화면 너비에 따라 일부 영역을 탭으로 접는 기능을 내장. 모바일/태블릿 대응이 필요한 경우 권장.
  • sap.ui.layout.DynamicSideContent: 메인 콘텐츠 옆에 부차적인 사이드 정보를 보여주는 패턴. 사용자가 크기를 조절하지 않고, 단지 화면 폭에 따라 사이드가 보이거나 숨겨지는 것이 핵심. Fiori 가이드라인에서 "메인 + 보조 정보" 패턴의 표준으로 자주 권장됩니다.
  • sap.f.FlexibleColumnLayout: 1~3개 컬럼이 미리 정의된 비율로 펼쳐지는 Fiori 표준 네비게이션 레이아웃. 사용자 드래그가 아니라 라우팅과 상태 전환이 중심.

선택 기준을 한 줄로 요약하면 다음과 같습니다. "사용자가 마우스로 직접 크기를 조절해야 하는가?"의 답이 Yes라면 Splitter 계열, No이고 화면 크기에 따라 자동 배치만 하면 된다면 DynamicSideContent나 FlexibleColumnLayout이 일반적으로 더 적합합니다.

접근성 측면에서는 Splitter 사용 시 각 ContentArea의 의미를 스크린리더가 이해할 수 있도록 PanelheaderText 또는 aria-label을 적절히 부여하고, splitter bar에 임의로 CSS를 덮어쓰지 않는 것을 권장합니다. 키보드 사용자가 화살표 키로 영역을 조절할 수 있도록 자동 적용된 ARIA 속성을 그대로 유지하는 편이 안전합니다.

다음 단계로는 ResponsiveSplitterPaneContainer 구조, Splitter와 라우팅의 결합(왼쪽 마스터에서 항목 선택 시 오른쪽 디테일 라우팅), 그리고 Splitter 크기 상태를 사용자 설정과 함께 백엔드 프로파일에 저장하는 패턴까지 확장해보면 실무 수준의 분할 레이아웃을 자유롭게 다룰 수 있게 됩니다.

댓글 0

아직 댓글이 없습니다.