UI5

UI5 Router 없이 화면 이동? Routing 3단계 #shorts #SAP #UI5

▶ YouTube에서 보기

📖 개요: 이 글에서 다루는 것

SAP UI5 초급 개발자가 가장 자주 저지르는 실수 중 하나는 addPage()to() 호출로 화면을 직접 밀어 넣는 방식입니다. 이렇게 만들면 브라우저 뒤로 가기 버튼이 동작하지 않고, URL을 복사해 공유해도 항상 첫 화면만 열립니다. 이 글은 SAPUI5의 표준 내비게이션 메커니즘인 Router를 3단계로 정리한 실전 예제입니다.

  • manifest.json에 routes / targets를 선언적으로 설정하는 방법
  • Router 인스턴스를 얻어 navTo()로 화면을 전환하는 방법
  • 파라미터 전달과 Hash 기반 딥링크로 상세 화면을 직접 여는 방법
  • patternMatched 이벤트에서 데이터를 바인딩하는 실무 패턴

📚 미리 알아두면 좋은 것

XML View와 Controller의 기본 구조, Component 기반 앱(즉 Component.jsmanifest.json이 있는 프로젝트)의 개념을 알고 있으면 충분합니다. OData 지식은 필수가 아니지만, 예제에서 판매오더(SalesOrder) 목록/상세 시나리오를 사용하므로 바인딩 문법({path})을 읽을 수 있으면 이해가 빠릅니다.

🔧 환경 / 버전 / 준비물

이 예제는 다음 환경을 기준으로 작성했습니다.

  • SAPUI5 1.120 LTS (OpenUI5 동일 버전에서도 동작, 1.71 이상이면 문법 차이 거의 없음)
  • 라이브러리: sap.m, sap.ui.core.routing
  • 도구: SAP Business Application Studio 또는 VS Code + UI5 CLI(ui5 serve)
  • 라우터 클래스: sap.m.routing.Routersap.ui.core.routing.Router를 확장해 모바일 전환 애니메이션과 viewLevel 기반 슬라이드 방향을 자동 처리하므로 sap.m 앱에서는 일반적으로 이 클래스를 권장합니다.

프로젝트 뼈대는 ui5 init 또는 BAS의 Basic 템플릿으로 생성했다고 가정합니다.

💡 핵심 개념: Router는 "주소 → 화면" 번역기

Router를 이해하는 가장 쉬운 비유는 우편 배달 시스템입니다. URL의 해시(#/orders/4711)는 편지봉투에 적힌 주소이고, Router는 그 주소를 읽고 어떤 View(집)에 배달할지 결정하는 집배원입니다. 구성 요소는 세 가지입니다.

  • Route: 주소 형식의 정의. pattern(예: orders/{orderId})이 해시와 매칭되면 해당 Route가 활성화됩니다.
  • Target: 실제로 표시할 View의 정의. 어떤 View를 어느 컨테이너(controlId)의 어느 영역(controlAggregation)에 넣을지 지정합니다. Route와 Target을 분리한 이유는 하나의 화면을 여러 Route에서 재사용하기 위해서입니다.
  • Hash: # 뒤의 문자열. 해시가 바뀌어도 브라우저는 페이지를 다시 로드하지 않으므로 SPA(Single Page Application) 내비게이션에 적합하고, 브라우저 히스토리에는 기록이 남아 뒤로 가기가 자연스럽게 동작합니다.

동작 흐름을 도식으로 표현하면 다음과 같습니다.

사용자 클릭 → navTo("route", 파라미터) → Router가 pattern으로 해시 생성 → URL 해시 변경 → 해시 변경 감지 → 매칭된 Route의 Target 로드 → View 표시 + patternMatched 이벤트 발생

핵심은 화면 전환의 진입점이 항상 "해시 변경"이라는 점입니다. 버튼 클릭이든, URL 직접 입력이든, 뒤로 가기든 모두 같은 경로를 타기 때문에 딥링크가 공짜로 따라옵니다. 반대로 NavContainer.to()를 직접 호출하면 이 흐름을 우회하므로 히스토리와 URL이 화면 상태와 어긋나게 됩니다.

💻 실전 코드 3단계: 판매오더 목록 → 상세

1단계 — manifest.json에 라우팅 선언하기

시나리오는 판매오더 목록(OrderList)에서 항목을 누르면 상세(OrderDetail)로 이동하는 앱입니다. manifest.jsonsap.ui5 섹션에 다음을 추가합니다.

{
  "sap.ui5": {
    "routing": {
      "config": {
        "routerClass": "sap.m.routing.Router",
        "type": "View",
        "viewType": "XML",
        "path": "com.acme.orders.view",
        "controlId": "app",
        "controlAggregation": "pages",
        "transition": "slide"
      },
      "routes": [
        { "name": "orderList",   "pattern": "",                  "target": "orderList" },
        { "name": "orderDetail", "pattern": "orders/{orderId}",  "target": "orderDetail" }
      ],
      "targets": {
        "orderList":   { "name": "OrderList",   "viewLevel": 1 },
        "orderDetail": { "name": "OrderDetail", "viewLevel": 2 }
      }
    }
  }
}

포인트를 정리하면: pattern: ""은 해시가 비어 있을 때(앱 최초 진입) 매칭되는 기본 Route이고, {orderId}필수 파라미터입니다. controlId: "app"은 루트 View에 있는 <App id="app"/>을 가리키며, 로드된 View는 이 컨트롤의 pages 집합에 삽입됩니다. Router는 Component.jsinit()에서 한 번만 초기화합니다.

// Component.js
init: function () {
  UIComponent.prototype.init.apply(this, arguments);
  this.getRouter().initialize(); // 이 호출이 없으면 어떤 Route도 동작하지 않음
}

2단계 — navTo로 이동하고 patternMatched로 수신하기

목록 화면에서 항목 클릭 시 오더 번호를 실어 상세로 이동합니다. 실무에서는 키가 없는 항목이 섞여 들어올 수 있으므로 방어 코드와 로깅을 함께 넣는 것이 좋습니다.

// OrderList.controller.js
onOrderPress: function (oEvent) {
  var oCtx = oEvent.getSource().getBindingContext("orders");
  var sOrderId = oCtx && oCtx.getProperty("OrderID");

  if (!sOrderId) {
    Log.error("OrderID 없음 — 내비게이션 중단", oCtx ? oCtx.getPath() : "no context");
    MessageToast.show("오더 정보를 읽을 수 없습니다.");
    return;
  }
  this.getOwnerComponent().getRouter().navTo("orderDetail", {
    orderId: sOrderId
  });
}

상세 컨트롤러는 onInit에서 자신의 Route에 patternMatched 핸들러를 등록하고, 전달받은 파라미터로 데이터를 바인딩합니다.

// OrderDetail.controller.js
onInit: function () {
  this.getOwnerComponent().getRouter()
    .getRoute("orderDetail")
    .attachPatternMatched(this._onMatched, this);
},

_onMatched: function (oEvent) {
  var sOrderId = oEvent.getParameter("arguments").orderId;
  this.getView().bindElement({
    path: "/SalesOrders(+ sOrderId +)",
    model: "orders",
    events: {
      dataRequested: function () { Log.info("오더 상세 요청: " + sOrderId); }
    }
  });
}

onInit은 View 생성 시 한 번만 실행되지만 patternMatched해시가 매칭될 때마다 실행됩니다. 딥링크로 진입하든 목록에서 이동하든 데이터 로직이 이 한 곳으로 모이는 것이 이 패턴의 가치입니다.

3단계 — 프로덕션 대비: notFound, 히스토리 없는 이동, 존재 검증

사용자가 #/orders/999999처럼 존재하지 않는 키로 딥링크 진입할 수 있습니다. 먼저 매칭되지 않는 해시 전체를 받는 안전망을 manifest에 추가합니다.

"config": { "...": "...", "bypassed": { "target": "notFound" } },
"targets": {
  "notFound": { "name": "NotFound", "transition": "show" }
}

바인딩 결과가 비어 있는 경우(패턴은 맞지만 데이터가 없는 경우)는 bindElementchange 이벤트에서 잡아 대체 화면으로 보냅니다. 이때 replace 옵션을 true로 주면 잘못된 해시가 브라우저 히스토리에 남지 않아, 뒤로 가기 시 오류 화면이 다시 나타나는 문제를 막을 수 있습니다.

events: {
  change: function () {
    var oCtx = this.getView().getBindingContext("orders");
    if (!oCtx || !oCtx.getObject()) {
      this.getOwnerComponent().getRouter()
        .getTargets().display("notFound");
    }
  }.bind(this)
}

보안 관점에서 한 가지 더: URL 파라미터는 사용자가 임의로 조작할 수 있는 값이므로, 상세 데이터 접근 권한은 반드시 백엔드(OData 서비스)의 권한 체크에 맡겨야 합니다. 프론트엔드 라우팅은 화면 흐름만 담당한다고 생각하는 것이 안전합니다.

⚠️ 흔한 실수와 트러블슈팅

Q1. navTo를 호출해도 화면이 전혀 바뀌지 않아요.
가장 흔한 원인은 Component.js에서 getRouter().initialize()를 호출하지 않은 경우입니다. 두 번째로 흔한 원인은 controlId가 루트 View의 App 컨트롤 id와 다르거나, 루트 View가 아직 로드되기 전에 라우팅이 시작되어 컨테이너를 찾지 못하는 경우입니다. 콘솔에 "Control with ID ... could not be found" 경고가 있는지 확인하세요.

Q2. 상세 화면에 이전 오더 데이터가 잠깐 보입니다.
View 인스턴스는 재사용되기 때문에 새 데이터가 오기 전까지 이전 바인딩이 남아 있습니다. _onMatched 첫 줄에서 this.getView().unbindElement("orders")를 호출하거나 busy 상태를 걸어 로딩 중임을 표시하는 것이 일반적입니다.

Q3. 파라미터에 슬래시가 들어간 키를 넘기면 라우팅이 깨집니다.
해시 패턴에서 /는 세그먼트 구분자입니다. 키에 특수문자가 올 수 있다면 encodeURIComponent()로 인코딩해서 navTo에 넘기고, 수신 측에서 decodeURIComponent()로 복원하세요.

Q4. 뒤로 가기 버튼 구현은요?
sap.ui.core.routing.History.getInstance().getPreviousHash()undefined이면(딥링크 직접 진입) navTo("orderList", {}, true)로 목록으로 보내고, 값이 있으면 window.history.go(-1)을 쓰는 이중 분기가 표준 패턴입니다.

🚀 이후에 확장해볼 주제

이 예제의 구조가 손에 익었다면 다음 주제로 확장해보세요. 선택 파라미터(orders/{orderId}/:tab:)와 쿼리 파라미터({?query})로 화면 상태를 URL에 보존하는 기법, FlexibleColumnLayout과 라우팅을 결합한 마스터-디테일 레이아웃, 중첩 컴포넌트 라우팅, 그리고 Fiori Launchpad 환경에서의 크로스 앱 내비게이션(CrossApplicationNavigation 서비스)이 자연스러운 다음 순서입니다.

댓글 0

아직 댓글이 없습니다.