SAPUI5 Routing & Navigation 완전 가이드 — Router, Target, manifest.json 설정부터 딥링크까지

Moderator · 조회 4
SAPUI5 Routing 개요 manifest.json routing 구조 navTo & History API

SAPUI5 Routing & Navigation 완전 가이드 — Router, Target, manifest.json 설정부터 딥링크까지

1. 개요 및 라우팅의 필요성

SAPUI5 애플리케이션은 SPA(Single Page Application) 구조로 동작합니다. 페이지 전체를 새로 불러오지 않고 하나의 HTML 페이지 안에서 뷰를 교체하는 방식이기 때문에, URL과 화면 상태를 연결해 주는 라우팅(Routing) 메커니즘이 필수적입니다. 라우팅이 없으면 브라우저의 뒤로가기 버튼이 작동하지 않고, 특정 화면을 URL로 직접 공유(딥링크)할 수도 없습니다.

SAPUI5의 라우팅 시스템은 URL 해시(#) 변화를 감지하여 해당하는 Target(뷰)을 자동으로 로드합니다. manifest.json에 선언적으로 경로를 정의하고, Router가 런타임에 해시 변경을 처리하는 구조입니다.

이 가이드를 통해 다음을 학습할 수 있습니다.

2. manifest.json routing 섹션 완전 분석

SAPUI5 라우팅의 핵심은 manifest.jsonsap.ui5.routing 섹션입니다. 이 섹션은 크게 config, routes, targets 세 부분으로 구성됩니다.

config — 전역 라우팅 설정

모든 라우트와 타겟에 공통으로 적용되는 기본값을 정의합니다.

routes — 경로 배열

각 라우트는 URL 패턴과 타겟을 연결합니다. pattern이 빈 문자열("")이면 기본 홈 화면을 의미합니다. 패턴 내 {paramName}은 필수 파라미터, :paramName:은 선택(optional) 파라미터입니다.

targets — 뷰 매핑

타겟은 실제로 어떤 뷰를 로드할지 정의합니다. name은 뷰 파일명, level은 화면 깊이를 나타내어 전환 방향을 결정합니다(숫자가 클수록 하위 화면).

아래는 홈, 제품 목록, 제품 상세, Not Found를 포함한 전체 routing 설정 예시입니다.

{
  "sap.ui5": {
    "routing": {
      "config": {
        "routerClass": "sap.m.routing.Router",
        "type": "View",
        "viewType": "XML",
        "path": "com.example.app.view",
        "controlId": "app",
        "controlAggregation": "pages",
        "transition": "slide",
        "bypassed": {
          "target": "notFound"
        }
      },
      "routes": [
        {
          "pattern": "",
          "name": "home",
          "target": "home"
        },
        {
          "pattern": "product/{productId}",
          "name": "product",
          "target": "product"
        },
        {
          "pattern": "product/{productId}/:tab:",
          "name": "productDetail",
          "target": "productDetail"
        }
      ],
      "targets": {
        "home": {
          "id": "home",
          "name": "Home",
          "level": 0
        },
        "product": {
          "id": "product",
          "name": "Product",
          "level": 1
        },
        "productDetail": {
          "id": "productDetail",
          "name": "ProductDetail",
          "level": 2
        },
        "notFound": {
          "id": "notFound",
          "name": "NotFound"
        }
      }
    }
  }
}

위 설정에서 product/{productId} 패턴은 #/product/123 형태의 URL에 매칭됩니다. :tab:은 선택 파라미터이므로 #/product/123/info처럼 있어도 되고 없어도 됩니다.

3. Router 초기화 — Component.js 패턴

SAPUI5에서 라우터는 Component.jsinit 메서드에서 초기화합니다. metadatamanifest: "json"을 선언하면 manifest.json의 routing 설정을 자동으로 읽어들입니다. 그 후 this.getRouter().initialize()를 호출하면 라우터가 해시 체인저(HashChanger)에 연결되어 URL 변화를 감지하기 시작합니다.

sap.ui.define([
  "sap/ui/core/UIComponent",
  "sap/ui/Device",
  "./model/models"
], function (UIComponent, Device, models) {
  "use strict";

  return UIComponent.extend("com.example.app.Component", {
    metadata: {
      manifest: "json"
    },

    init: function () {
      // 부모 클래스의 init을 먼저 호출
      UIComponent.prototype.init.apply(this, arguments);

      // 디바이스 모델 설정
      this.setModel(models.createDeviceModel(), "device");

      // Router 초기화 — manifest.json의 routing 설정을 기반으로 동작 시작
      this.getRouter().initialize();
    }
  });
});

initialize() 메서드는 선택적으로 bIgnoreInitialHash 파라미터를 받습니다. 기본값은 false이며, 이 경우 페이지 최초 로드 시 현재 URL 해시를 파싱하여 해당 라우트를 즉시 실행합니다. 이것이 딥링크가 자동으로 작동하는 원리입니다.

4. navTo로 화면 이동하기

프로그래밍 방식으로 화면을 전환할 때는 Router의 navTo 메서드를 사용합니다. 첫 번째 인자는 라우트 이름, 두 번째는 파라미터 객체입니다.

// 기본 네비게이션 — 필수 파라미터
onProductPress: function (oEvent) {
  var sProductId = oEvent.getSource()
    .getBindingContext()
    .getProperty("ProductID");

  this.getOwnerComponent().getRouter().navTo("product", {
    productId: sProductId
  });
},

// 선택 파라미터 포함 네비게이션
onProductTabPress: function (sProductId, sTab) {
  this.getOwnerComponent().getRouter().navTo("productDetail", {
    productId: sProductId,
    tab: sTab        // 선택 파라미터 — 생략 가능
  });
},

// replace 모드 — 히스토리에 새 항목을 추가하지 않고 현재 항목을 교체
onRedirect: function () {
  this.getOwnerComponent().getRouter().navTo("home", {}, true);
  // 세 번째 인자 true = bReplace
}

navTo의 시그니처navTo(sName, oParameters, oComponentTargetInfo, bReplace)입니다. 파라미터 값에 포함된 특수 문자(; , / ? : @ & = + $ 등)는 자동으로 URI 인코딩됩니다. navTo는 체이닝을 지원하여 this를 반환합니다.

주의: 필수 파라미터를 누락하면 라우트 패턴이 완성되지 않아 네비게이션이 실패합니다. 콘솔에 별도 에러가 출력되지 않는 경우도 있으므로, navTo 호출 전 파라미터 값이 유효한지 반드시 확인하세요.

5. Route Matched 이벤트 처리 — attachPatternMatched

라우트가 매칭되었을 때 실행할 로직은 attachPatternMatched로 등록합니다. 일반적으로 타겟 뷰의 컨트롤러 onInit에서 이벤트 핸들러를 바인딩합니다.

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

  return Controller.extend("com.example.app.controller.Product", {
    onInit: function () {
      var oRouter = this.getOwnerComponent().getRouter();
      // 이 뷰에 해당하는 라우트에만 이벤트 등록
      oRouter.getRoute("product")
        .attachPatternMatched(this._onRouteMatched, this);
    },

    _onRouteMatched: function (oEvent) {
      var sProductId = oEvent.getParameter("arguments").productId;

      // OData 엔티티 바인딩
      this.getView().bindElement({
        path: "/Products('" + sProductId + "')",
        model: "catalog",
        events: {
          dataRequested: function () {
            // 로딩 표시
          },
          dataReceived: function (oData) {
            // 데이터가 없으면 Not Found로 이동
            if (!oData.getParameter("data")) {
              this.getOwnerComponent().getRouter()
                .getTargets().display("notFound");
            }
          }.bind(this)
        }
      });
    }
  });
});

attachPatternMatchedattachRouteMatched의 차이를 이해하는 것이 중요합니다.

대부분의 경우 attachPatternMatched를 사용하는 것이 권장됩니다. 불필요한 중복 실행을 방지할 수 있기 때문입니다.

6. History API로 뒤로가기 구현 — 딥링크 포함

SPA에서 뒤로가기 버튼을 구현할 때, 단순히 window.history.go(-1)만 사용하면 딥링크로 직접 진입한 경우 앱 바깥으로 나가버리는 문제가 발생합니다. 이를 해결하기 위해 SAPUI5의 History 유틸리티를 활용합니다.

sap.ui.define([
  "sap/ui/core/mvc/Controller",
  "sap/ui/core/routing/History"
], function (Controller, History) {
  "use strict";

  return Controller.extend("com.example.app.controller.Product", {
    onNavBack: function () {
      var oHistory = History.getInstance();
      var sPreviousHash = oHistory.getPreviousHash();

      if (sPreviousHash !== undefined) {
        // 이전 해시가 있으면 브라우저 히스토리로 뒤로가기
        window.history.go(-1);
      } else {
        // 딥링크로 직접 진입한 경우 — 홈으로 이동 (replace 모드)
        this.getOwnerComponent().getRouter().navTo("home", {}, true);
      }
    }
  });
});

핵심 로직은 getPreviousHash()의 반환값입니다. 앱 내에서 네비게이션한 이력이 있으면 이전 해시 값이 존재하고, 외부에서 URL을 직접 입력하여 진입한 경우에는 undefined를 반환합니다. 이 분기를 통해 딥링크 시나리오를 안전하게 처리할 수 있습니다.

navTo의 세 번째 인자를 true로 설정하면 replace 모드로 동작하여 히스토리 스택에 새 항목을 추가하지 않습니다. 홈으로 이동 후 다시 뒤로가기를 누르면 앱 바깥으로 나가게 되므로, 이것이 일반적으로 기대하는 동작입니다.

7. 중첩 라우팅 및 마스터-디테일 패턴

SAP Fiori의 대표적인 UX 패턴인 마스터-디테일(Master-Detail)은 화면을 좌우로 분할하여 좌측에 목록, 우측에 상세 내용을 동시에 표시합니다. 이를 구현하려면 하나의 라우트에 다중 타겟을 지정합니다.

{
  "routes": [
    {
      "pattern": "",
      "name": "master",
      "target": ["master", "defaultDetail"]
    },
    {
      "pattern": "order/{orderId}",
      "name": "orderDetail",
      "target": ["master", "orderDetail"]
    }
  ],
  "targets": {
    "master": {
      "name": "Master",
      "controlAggregation": "masterPages",
      "controlId": "splitContainer"
    },
    "defaultDetail": {
      "name": "DefaultDetail",
      "controlAggregation": "detailPages",
      "controlId": "splitContainer"
    },
    "orderDetail": {
      "name": "OrderDetail",
      "controlAggregation": "detailPages",
      "controlId": "splitContainer"
    }
  }
}

위 설정에서 target을 배열로 지정하면 두 개의 타겟이 동시에 로드됩니다. SplitContainermasterPagesdetailPages 애그리게이션에 각각 뷰가 배치됩니다. 라우트가 변경되어도 master 타겟은 동일하므로 좌측 목록은 유지되고 우측 상세만 교체됩니다.

모바일 환경에서는 SplitContainer가 자동으로 단일 컬럼 모드로 전환되므로, 별도의 반응형 처리 코드가 필요 없는 것이 장점입니다.

8. 실무 팁 및 주의사항

Not Found (404) 처리

manifest.json의 config.bypassed.target에 Not Found 타겟을 지정하면, 등록된 어떤 패턴에도 매칭되지 않는 URL 접근 시 자동으로 해당 뷰가 표시됩니다. 추가로, OData 요청 후 데이터가 없는 경우에도 프로그래밍 방식으로 getTargets().display("notFound")를 호출하여 처리하는 것이 좋습니다.

파라미터 검증 및 보안

성능 최적화

자주 발생하는 실수

참고 자료