UI5

Carousel로 이미지 슬라이드? — UI5 완성 #shorts #SAP #UI5

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

SAPUI5로 화면 좌우 슬라이드 전환이 필요한 UI를 만들 때, 많은 개발자들이 처음에는 setTimeout이나 CSS transition을 직접 조합해서 애니메이션을 구현하려 시도합니다. 하지만 SAPUI5에는 이런 슬라이드 패턴을 위해 설계된 sap.m.Carousel 컨트롤이 이미 준비되어 있습니다. 본 글에서는 직접 애니메이션을 구현하는 방식과 Carousel 컨트롤 활용을 비교하고, CarouselPage 자식 구조부터 데이터 바인딩 기반 동적 페이지 생성, pageChanged 이벤트 처리, 그리고 GenericTile·ImageContent와 조합해 대시보드형 슬라이더를 구성하는 방법까지 단계별로 다룹니다.

  • 직접 구현 대비 Carousel 컨트롤이 갖는 이점 파악
  • XML 뷰에서 Carousel 구조를 선언적으로 구성하는 패턴
  • JSONModel 바인딩으로 페이지 수가 가변적인 UI 만들기
  • loop·autoPlay·height 같은 자주 놓치는 속성 다루기
  • 접근성(aria-label, 키보드 내비게이션) 고려사항

2. 핵심 개념 — Carousel과 CarouselPage 동작 원리

먼저 직접 구현 방식과 Carousel 컨트롤의 차이를 짚어봅니다. 직접 슬라이드 애니메이션을 만들 경우 개발자는 다음을 모두 책임져야 합니다.

  • 현재 인덱스 상태 관리(currentIndex)
  • 좌우 전환 시 transform: translateX(...) 계산
  • 드래그(스와이프) 제스처 감지와 임계값 처리
  • 마지막 페이지에서 다시 처음으로 돌아가는 순환 로직
  • 화살표 버튼, 페이지 인디케이터(dot) UI
  • 화면 리사이즈와 모바일 터치 이벤트 분기

반면 sap.m.Carousel은 이 모든 기본 동작을 컨트롤 내부에 캡슐화합니다. 개발자는 "어떤 페이지들이 들어가야 하는지"만 선언하면 됩니다. 비유하자면 직접 구현은 시계 부품을 일일이 조립하는 것이고, Carousel은 이미 작동하는 시계에 다이얼만 갈아 끼우는 셈입니다.

Carousel의 핵심 구조는 단순합니다. sap.m.Carousel 컨테이너 안에 여러 개의 페이지(자식 컨트롤)를 두고, Carousel이 한 번에 하나씩 보여주는 것입니다. SAPUI5에서는 자식 위치를 pages aggregation으로 정의하며, 이 자리에는 sap.m.Image, sap.m.Page, sap.f.Card, sap.m.VBox 같은 일반 컨트롤이 그대로 들어갈 수 있습니다. 즉, "CarouselPage"는 별도 클래스라기보다는 Carousel의 한 슬라이드를 담당하는 자식 컨트롤 위치를 가리키는 개념적 명칭으로 이해하면 좋습니다.

Carousel이 내부적으로 제공하는 기본 동작은 다음과 같습니다.

  • 좌우 화살표 버튼(showPageIndicator, arrowsPlacement로 제어)
  • 마우스 드래그·터치 스와이프 인식
  • loop 속성을 통한 무한 순환
  • activePage 속성으로 현재 페이지 식별
  • pageChanged 이벤트로 페이지 전환 감지

3. 1단계: 기본 Carousel 선언 (이미지 슬라이드)

가장 단순한 형태로 이미지 세 장을 좌우로 넘기는 Carousel을 만들어보겠습니다. XML 뷰만으로 선언이 끝납니다.

<mvc:View
    controllerName="demo.controller.Main"
    xmlns="sap.m"
    xmlns:mvc="sap.ui.core.mvc"
    displayBlock="true">
    <Page title="Carousel 기본 예제">
        <content>
            <Carousel
                id="basicCarousel"
                height="320px"
                width="100%"
                loop="true"
                showPageIndicator="true">
                <pages>
                    <Image src="./img/slide1.jpg" densityAware="false" />
                    <Image src="./img/slide2.jpg" densityAware="false" />
                    <Image src="./img/slide3.jpg" densityAware="false" />
                </pages>
            </Carousel>
        </content>
    </Page>
</mvc:View>

여기서 주목할 포인트는 세 가지입니다. 첫째, height를 명시적으로 지정해야 슬라이드 영역이 화면에 표시됩니다. 둘째, loop="true"를 주면 마지막 슬라이드에서 다시 첫 번째로 자연스럽게 돌아갑니다. 셋째, showPageIndicator는 하단의 점(dot) 인디케이터 표시 여부입니다. 이 세 줄을 작성하는 것만으로 드래그, 화살표 클릭, 키보드 좌우 방향키 내비게이션이 모두 자동으로 동작합니다.

4. 2단계: 데이터 바인딩으로 동적 페이지 생성

실무에서는 슬라이드 개수가 고정되어 있지 않은 경우가 대부분입니다. 백엔드에서 받은 상품 목록, 공지사항, KPI 카드 등을 슬라이드로 보여주려면 pages aggregation을 데이터 바인딩해야 합니다.

// controller/Main.controller.js
sap.ui.define([
    "sap/ui/core/mvc/Controller",
    "sap/ui/model/json/JSONModel"
], function (Controller, JSONModel) {
    "use strict";

    return Controller.extend("demo.controller.Main", {
        onInit: function () {
            var oData = {
                slides: [
                    { title: "신제품 출시", image: "./img/p1.jpg", desc: "5월 신상" },
                    { title: "여름 세일",   image: "./img/p2.jpg", desc: "최대 40%" },
                    { title: "회원 혜택",   image: "./img/p3.jpg", desc: "포인트 2배" }
                ]
            };
            this.getView().setModel(new JSONModel(oData));
        }
    });
});

XML 뷰에서는 pages aggregation에 items="{/slides}" 형태로 바인딩합니다. SAPUI5에서는 aggregation 바인딩 시 템플릿 컨트롤을 하나만 선언하면 데이터 개수만큼 자동으로 복제됩니다.

<Carousel
    height="360px"
    width="100%"
    loop="true"
    pages="{/slides}">
    <VBox
        alignItems="Center"
        justifyContent="Center"
        height="100%"
        class="sapUiSmallMargin">
        <Image
            src="{image}"
            width="240px"
            densityAware="false" />
        <Title text="{title}" level="H3" />
        <Text text="{desc}" />
    </VBox>
</Carousel>

모델의 slides 배열에 항목을 추가하거나 제거하면 Carousel이 자동으로 재구성됩니다. 백엔드에서 OData로 받은 EntitySet을 그대로 바인딩하는 패턴도 동일합니다.

5. 3단계: pageChanged 이벤트 핸들링

현재 어떤 페이지가 보이는지를 컨트롤러에서 알아야 할 때가 있습니다. 예를 들어 헤더 텍스트를 페이지마다 다르게 보여주거나, 특정 슬라이드에 도달했을 때 분석 이벤트를 발생시키는 경우입니다. 이때 pageChanged 이벤트를 사용합니다.

<Carousel
    id="dynCarousel"
    pages="{/slides}"
    pageChanged="onPageChanged"
    height="320px">
    <!-- 템플릿 생략 -->
</Carousel>
<Text id="statusText" text="" class="sapUiSmallMargin" />
onPageChanged: function (oEvent) {
    var sNewPageId  = oEvent.getParameter("newActivePageId");
    var sOldPageId  = oEvent.getParameter("oldActivePageId");
    var oCarousel   = oEvent.getSource();
    var aPages      = oCarousel.getPages();
    var iIndex      = aPages.findIndex(function (p) {
        return p.getId() === sNewPageId;
    });

    // 모델에서 현재 슬라이드 데이터 가져오기
    var oContext = aPages[iIndex].getBindingContext();
    var oSlide   = oContext ? oContext.getObject() : null;

    this.byId("statusText").setText(
        (iIndex + 1) + " / " + aPages.length + " - " + (oSlide ? oSlide.title : "")
    );

    // 로깅
    sap.base.Log.info("Carousel 전환: " + sOldPageId + " -> " + sNewPageId);
}

newActivePageIdoldActivePageId는 페이지 컨트롤의 ID만 알려주기 때문에, 인덱스나 모델 데이터가 필요하면 위처럼 getPages()로 배열을 받아 위치를 찾고 getBindingContext()로 원본 객체에 접근하는 패턴이 일반적으로 권장됩니다.

6. 4단계: Card/GenericTile과 조합한 대시보드 슬라이더

Carousel은 이미지뿐만 아니라 어떤 컨트롤이든 슬라이드 콘텐츠로 받을 수 있다는 점에서 강력합니다. 대시보드형 UI에서는 sap.m.GenericTile이나 sap.f.Card를 그룹으로 묶어 슬라이드 단위로 보여주는 패턴이 자주 사용됩니다.

<Carousel
    height="220px"
    width="100%"
    loop="true"
    pages="{/kpiGroups}">
    <HBox justifyContent="SpaceAround" alignItems="Center" height="100%">
        <GenericTile
            header="{title1}"
            subheader="{subtitle1}"
            press="onTilePress">
            <TileContent>
                <ImageContent src="sap-icon://line-chart" />
            </TileContent>
        </GenericTile>
        <GenericTile
            header="{title2}"
            subheader="{subtitle2}">
            <TileContent>
                <NumericContent value="{value2}" valueColor="Good" />
            </TileContent>
        </GenericTile>
    </HBox>
</Carousel>

각 슬라이드(kpiGroups 항목)는 두 개의 KPI 타일을 담고 있고, Carousel이 그룹 단위로 좌우 전환을 처리합니다. ImageContent로 아이콘을 표시하거나 NumericContent로 수치를 강조하는 식의 조합은 Fiori 대시보드에서 흔히 보이는 패턴입니다.

여기에 자동 재생을 추가하려면 다음과 같이 컨트롤러에서 타이머를 만드는 방식이 일반적입니다. (Carousel 자체에는 단순한 autoPlay 속성이 노출되지 않을 수 있으므로 버전을 확인하시기 바랍니다.)

onInit: function () {
    // ... 모델 바인딩
    this._iAutoPlayId = setInterval(function () {
        var oCarousel = this.byId("dashCarousel");
        var aPages    = oCarousel.getPages();
        var sActiveId = oCarousel.getActivePage();
        var iIdx      = aPages.findIndex(function (p) { return p.getId() === sActiveId; });
        var iNext     = (iIdx + 1) % aPages.length;
        oCarousel.setActivePage(aPages[iNext]);
    }.bind(this), 5000);
},

onExit: function () {
    if (this._iAutoPlayId) {
        clearInterval(this._iAutoPlayId);
    }
}

onExit에서 반드시 인터벌을 해제해야 메모리 누수와 뷰 파괴 후 예외가 발생하지 않습니다.

7. 자주 겪는 함정과 FAQ

Q1. Carousel을 선언했는데 화면에 아무것도 보이지 않습니다.
가장 흔한 원인은 height가 지정되지 않은 경우입니다. Carousel은 부모 컨테이너의 높이를 자동 추정하지 않으므로 height="320px"처럼 명시적으로 주거나, 부모를 FlexBox로 만들고 height="100%"를 부여해야 합니다. sap.m.Page 안에 직접 넣을 때도 content 영역이 기본적으로 자식 높이에 맞춰지므로 Carousel 높이가 0이 되기 쉽습니다.

Q2. pages="{/slides}"로 바인딩했더니 슬라이드가 하나만 나옵니다.
바인딩 경로가 배열을 가리키고 있는지 확인하시기 바랍니다. JSONModel 루트가 { slides: [...] } 구조라면 {/slides}가 맞고, OData V2에서는 {/ProductSet} 같이 EntitySet 경로가 되어야 합니다. 또한 템플릿으로 둘 이상의 컨트롤을 선언하면 첫 번째만 템플릿으로 인식되므로 항상 단일 루트 컨트롤(VBox, HBox 등으로 감싸기)을 사용하는 것이 권장됩니다.

Q3. 자동 재생을 켰는데 사용자가 직접 드래그하면 다시 자동으로 넘어가 불편합니다.
사용자 상호작용이 감지되면 일정 시간 자동 재생을 일시 정지하는 패턴이 일반적입니다. pageChanged 이벤트의 oEvent.getParameter("reason") 등을 활용해 트리거 원인을 구분하고, 사용자 액션이라면 clearInterval 후 일정 시간 뒤 재개하도록 구현합니다. 또한 접근성 관점에서 자동 재생은 시각·인지 장애 사용자에게 부담이 될 수 있으므로 일시정지 버튼을 함께 제공하는 것이 좋습니다.

Q4. 화살표 버튼을 모바일에서는 숨기고 싶습니다.
arrowsPlacementContent 또는 PageIndicator로 변경하고, 디바이스 모델(sap.ui.Device)을 활용해 showPageIndicator나 화살표 표시 여부를 분기 처리하는 방법이 권장됩니다.

8. 응용 패턴 — loop·autoPlay 및 접근성 고려사항

마지막으로 실제 프로덕션에서 챙겨야 할 항목들을 정리합니다.

  • loop 사용 시 인디케이터 혼동: 무한 순환을 사용하면 "현재 몇 번째인지"가 시각적으로 모호해질 수 있습니다. 사용자에게 페이지 번호를 텍스트로 함께 노출(3 / 7)하는 것이 권장됩니다.
  • 접근성(aria): 각 슬라이드 자식 컨트롤에 tooltip이나 ariaLabelledBy를 지정해 스크린 리더가 내용을 안내할 수 있도록 합니다. Carousel 컨테이너 자체에도 tooltip="상품 슬라이드"처럼 의미를 부여하는 것이 좋습니다.
  • 키보드 내비게이션: Carousel은 좌우 방향키로 전환을 지원하지만, 자동 재생이 켜진 상태에서는 포커스가 잡혔을 때 자동 재생을 멈추는 것이 사용자 경험상 권장됩니다.
  • 성능: 슬라이드가 수십 개 이상인 경우, 각 슬라이드 안에 무거운 컨트롤(차트, 큰 이미지)을 넣으면 초기 렌더링이 느려질 수 있습니다. 이미지의 경우 lazy loading 전략이나 첫 번째 슬라이드만 즉시 렌더하고 나머지는 pageChanged 시 데이터를 로드하는 패턴을 검토해보시기 바랍니다.
  • 테스트: OPA5에서 Carousel 전환은 iPressArrowNext 같은 커스텀 액션이나 setActivePage 직접 호출로 시뮬레이션할 수 있습니다.

정리하면, sap.m.Carousel은 직접 슬라이드 애니메이션을 구현할 때 들어가는 상태 관리·제스처·접근성 부담을 컨트롤 차원에서 해결해 줍니다. 정적인 이미지 슬라이드에서 시작해 데이터 바인딩 기반 동적 페이지, pageChanged 이벤트 후크, GenericTile/ImageContent 조합 대시보드까지 동일한 패턴이 확장되므로, 한 번 익혀두면 다양한 Fiori 화면에서 재사용할 수 있습니다.

댓글 0

아직 댓글이 없습니다.