UI5

라우트 늘어날수록 이렇게 정리해요? — UI5 Route Group #shorts #SAP #UI5

▶ YouTube에서 보기

개요

SAPUI5/OpenUI5 애플리케이션의 규모가 커지면 manifest.jsonrouting 섹션도 빠르게 비대해집니다. 특히 모듈별로 /products/list, /products/detail/{id}, /products/edit/{id}처럼 동일한 URL 접두어를 공유하는 라우트가 십수 개씩 늘어나면, 같은 prefix를 라우트마다 반복 입력하느라 유지보수 부담이 커집니다. Route Group은 이러한 중복을 줄이고 라우팅 구조를 모듈화하기 위해 도입된 패턴으로, 공통 URL 접두어를 한 번만 선언하고 자식 라우트는 나머지 패턴만 정의하도록 만들어 줍니다.

  • 중복되는 URL 접두어 제거 및 manifest 간소화
  • 모듈 단위로 라우트 묶음을 시각적으로 구분
  • 마이그레이션 시 prefix 한 곳만 수정하면 그룹 전체 적용
  • 기존 navTo 호출 코드 변경 없이 적용 가능

사전에 알고 있어야 할 것

본 문서는 SAPUI5 1.108 LTS 이상 또는 OpenUI5 최신 안정 버전을 사용하는 중급 개발자를 대상으로 합니다. manifest.jsonsap.ui5.routing 섹션 구조, Router/Targets 개념, UIComponentinit()에서 Router.initialize()를 호출하는 흐름, 그리고 컨트롤러에서 this.getOwnerComponent().getRouter().navTo("routeName", { ... })를 호출해 본 경험이 있어야 내용을 원활하게 따라갈 수 있습니다.

환경 / 버전 / 준비물

아래 환경을 기준으로 작성되었습니다. 실제 프로젝트의 SAPUI5 버전에 따라 일부 속성 이름이나 동작 방식에 차이가 있을 수 있으니, 사내 가이드와 함께 검토하시는 것을 권장합니다.

  • SAPUI5: 1.120 LTS 권장(1.108 이상에서 동작 확인 일반적)
  • OpenUI5: 동일 버전대
  • 개발 IDE: SAP Business Application Studio 또는 VS Code + Fiori Tools
  • 런타임: Node.js 18+, @ui5/cli 3.x
  • 프로젝트 구조: webapp/manifest.json, webapp/Component.js, webapp/controller/*.controller.js
  • 참고: 본 패턴은 manifest 기반 라우팅을 사용하는 모든 Fiori freestyle 앱에서 활용 가능합니다. Fiori Elements는 자체 navigation 메타데이터를 사용하므로 별도 고려가 필요합니다.

핵심 개념

기존 SAPUI5 라우팅은 routes 배열에 모든 라우트를 평탄(flat)하게 나열합니다. 이 방식은 라우트가 적을 때는 명료하지만, 도메인별로 라우트가 늘어나기 시작하면 pattern 문자열의 접두어가 끝없이 반복되는 문제가 발생합니다. 예컨대 products/list, products/detail/{id}, products/edit/{id}, products/history/{id}가 있고 orders/list, orders/detail/{id}가 또 있다고 가정하면, manifest에서 동일한 prefix를 6번 입력해야 합니다.

Route Group은 이 구조를 트리 형태로 재구성합니다. 일종의 네임스페이스 폴더라고 비유할 수 있는데, 폴더(routeGroups)에 공통 prefix를 선언해 두고 그 안에 속한 라우트는 폴더 내부의 상대 경로만 갖는 셈입니다. 운영체제 파일시스템에서 /var/log/라는 디렉터리 안에 nginx.log, app.log를 두는 것과 같은 직관입니다.

[기존 flat routes]
  routes:
    - pattern: products/list
    - pattern: products/detail/{id}
    - pattern: products/edit/{id}
    - pattern: orders/list
    - pattern: orders/detail/{id}

[Route Group 도입 후]
  routeGroups:
    - name: products, prefix: products
        routes: list, detail/{id}, edit/{id}
    - name: orders, prefix: orders
        routes: list, detail/{id}

여기서 중요한 점은 라우트 이름(name)과 타깃(target)은 변하지 않는다는 것입니다. URL pattern만 자동으로 합성되므로 컨트롤러의 navTo 호출, 매치 이벤트 핸들러 등 자바스크립트 측 코드는 그대로 유지됩니다. 이는 기존 프로젝트에 점진적으로 도입하기 매우 유리한 특성입니다.

실전 코드

1단계 — routeGroups 선언

가장 먼저 manifest.jsonsap.ui5.routing 섹션에 routeGroups 배열을 추가합니다. 각 그룹은 name(그룹 식별자)과 prefix(공통 URL 접두어) 두 속성을 가집니다. 그리고 기존 routes 배열의 각 라우트에는 group 속성을 추가해 어느 그룹에 속하는지 지정합니다.

{
  "sap.ui5": {
    "routing": {
      "config": {
        "routerClass": "sap.m.routing.Router",
        "viewType": "XML",
        "viewPath": "com.example.app.view",
        "controlId": "app",
        "controlAggregation": "pages",
        "async": true
      },
      "routeGroups": [
        { "name": "products", "prefix": "products" },
        { "name": "orders",   "prefix": "orders"   }
      ],
      "routes": [
        {
          "name": "productList",
          "pattern": "list",
          "group": "products",
          "target": "productList"
        },
        {
          "name": "productDetail",
          "pattern": "detail/{id}",
          "group": "products",
          "target": "productDetail"
        },
        {
          "name": "productEdit",
          "pattern": "edit/{id}",
          "group": "products",
          "target": "productEdit"
        },
        {
          "name": "orderList",
          "pattern": "list",
          "group": "orders",
          "target": "orderList"
        }
      ],
      "targets": {
        "productList":   { "viewName": "ProductList",   "viewLevel": 1 },
        "productDetail": { "viewName": "ProductDetail", "viewLevel": 2 },
        "productEdit":   { "viewName": "ProductEdit",   "viewLevel": 3 },
        "orderList":     { "viewName": "OrderList",     "viewLevel": 1 }
      }
    }
  }
}

위와 같이 선언하면 productList 라우트의 실제 매칭 URL은 #/products/list, productDetail#/products/detail/42 형태가 됩니다. orderList는 같은 list pattern이지만 다른 그룹에 속하므로 #/orders/list로 매칭되어 충돌하지 않습니다.

2단계 — 자동 prefix 합성과 에러 처리

실무에서는 bypassed 라우트(매칭 실패 시 fallback)와 함께 사용할 일이 많습니다. 또한 일부 라우트는 어느 그룹에도 속하지 않는 전역 라우트일 수 있으니 그 경우 group 속성을 생략하면 기존처럼 flat 라우트로 동작합니다. 라우팅 라이프사이클 이벤트에 로깅을 추가해 잘못된 그룹/패턴 조합을 빠르게 탐지하는 것이 좋습니다.

// webapp/Component.js
sap.ui.define([
  "sap/ui/core/UIComponent",
  "sap/base/Log"
], function (UIComponent, Log) {
  "use strict";

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

    init: function () {
      UIComponent.prototype.init.apply(this, arguments);

      const oRouter = this.getRouter();

      // 매칭 실패(bypassed) 처리
      oRouter.attachBypassed((oEvent) => {
        const sHash = oEvent.getParameter("hash");
        Log.warning("[Routing] No route matched for hash: " + sHash,
          null, "com.example.app.Component");
        oRouter.getTargets().display("notFound");
      });

      // 모든 라우트 매칭 시 디버그 로그 (그룹/실제 패턴 추적용)
      oRouter.attachRouteMatched((oEvent) => {
        const sName = oEvent.getParameter("name");
        const oCfg  = oEvent.getParameter("config") || {};
        Log.debug(
          `[Routing] matched name=${sName} group=${oCfg.group || "-"} ` +
          `pattern=${oCfg.pattern}`,
          null, "com.example.app.Component"
        );
      });

      oRouter.initialize();
    }
  });
});

로그 출력에서 group=products pattern=detail/{id}와 같이 보이면 정상이며, 실제 매칭 URL은 products/detail/{id}로 합성되어 동작합니다. 만약 콘솔에 bypassed 경고가 자주 찍힌다면 prefix와 자식 pattern 사이에 슬래시가 중복되었거나, group 이름과 routeGroups[].name이 어긋났을 가능성이 큽니다.

3단계 — navTo 호출과 충돌 회피

컨트롤러에서 라우트로 이동할 때는 그룹 이름을 명시하지 않고 라우트 이름만 사용합니다. Route Group은 URL pattern을 합성하는 데 그치므로 자바스크립트 API 표면은 동일합니다. 기존 프로젝트의 navTo 호출을 수정할 필요가 거의 없다는 것이 장점입니다.

// webapp/controller/ProductList.controller.js
sap.ui.define([
  "sap/ui/core/mvc/Controller",
  "sap/base/Log"
], function (Controller, Log) {
  "use strict";

  return Controller.extend("com.example.app.controller.ProductList", {

    onOpenDetail: function (oEvent) {
      const oCtx = oEvent.getSource().getBindingContext();
      const sId  = oCtx.getProperty("ProductId");

      try {
        // 그룹 이름 없이 라우트 이름만 사용
        this.getOwnerComponent().getRouter().navTo("productDetail", {
          id: encodeURIComponent(sId)
        });
      } catch (e) {
        Log.error("navTo failed: " + e.message, e,
          "com.example.app.controller.ProductList");
        sap.m.MessageToast.show("페이지 이동에 실패했습니다.");
      }
    },

    onEdit: function (oEvent) {
      const sId = oEvent.getSource().data("productId");
      this.getOwnerComponent().getRouter().navTo("productEdit", { id: sId });
    }
  });
});

여러 그룹을 사용할 때 가장 흔한 함정은 라우트 이름 충돌입니다. 그룹 단위로 네임스페이스가 분리되는 것은 URL pattern에 한정되며, name은 전역적으로 유일해야 합니다. 따라서 products 그룹의 listorders 그룹의 list를 만들 때 라우트 name은 각각 productList, orderList처럼 그룹명을 접두어로 붙여 충돌을 회피하는 네이밍 컨벤션을 정해두는 것을 권장합니다.

// 권장 네이밍 컨벤션
// {그룹명}{역할}  — 예: productList, productDetail, orderList, orderDetail
// 또는 점 표기   — 예: "product.list", "order.list"

const ROUTE = Object.freeze({
  PRODUCT_LIST:   "productList",
  PRODUCT_DETAIL: "productDetail",
  ORDER_LIST:     "orderList"
});

this.getOwnerComponent().getRouter().navTo(ROUTE.PRODUCT_DETAIL, { id: sId });

실전 활용 시나리오

실제 중대형 Fiori freestyle 앱에서는 도메인별 모듈 라우트가 수십 개에 달합니다. 예를 들어 영업관리 앱이라면 customers, quotations, orders, invoices, reports 다섯 개 도메인이 있을 수 있고, 각 도메인마다 list/detail/edit/history 라우트가 필요합니다. Route Group으로 묶으면 manifest가 모듈 단위로 시각적으로 구분되며, 신규 라우트 추가 시 어느 그룹에 넣을지만 결정하면 되므로 PR 리뷰도 쉬워집니다. 또한 도메인 prefix를 한 번에 변경(예: orderssales-orders)해야 할 때 routeGroupsprefix 한 줄만 수정하면 그룹 내 모든 라우트가 새 URL로 동작합니다.

흔한 실수 / 트러블슈팅

도입 초기에 자주 마주치는 문제와 해결 방법을 FAQ 형식으로 정리합니다.

  • Q1. 자식 라우트 pattern 앞에 슬래시를 붙여야 하나요?
    A. 일반적으로 prefix와 자식 pattern은 슬래시로 자동 결합되므로 자식 쪽에서는 슬래시 없이 시작하는 것을 권장합니다. "prefix": "products"에 자식 "pattern": "/list"로 두면 products//list처럼 슬래시가 중복돼 매칭이 실패할 수 있습니다.
  • Q2. 같은 라우트 이름을 두 그룹에서 써도 되나요?
    A. 권장하지 않습니다. 라우트 name은 라우터 전역에서 유일해야 하므로 productList, orderList처럼 그룹 접두어를 포함한 네이밍 컨벤션을 정해두는 것이 안전합니다.
  • Q3. 중첩 그룹(group 안의 group)도 지원되나요?
    A. 다층 중첩은 일반적으로 단일 레벨 grouping을 가정하므로 공식 동작을 보장하기 어렵습니다. 매우 깊은 계층이 필요하다면 prefix에 슬래시를 포함시켜 "prefix": "admin/products"처럼 표현하는 우회 방법을 사용하는 것이 안전합니다.
  • Q4. 기존 flat 구조에서 마이그레이션할 때 무엇을 주의해야 하나요?
    A. 한 번에 모두 옮기지 말고 도메인 하나씩 점진적으로 옮긴 후 라우팅 회귀 테스트(QUnit/OPA5)를 돌리는 것을 권장합니다. 또한 외부에서 공유된 deep link URL이 바뀌지 않도록 prefix를 기존 URL과 동일하게 유지해야 합니다.
  • Q5. bypassed 경고만 뜨고 화면이 비어요.
    A. 대부분 group 속성의 오타이거나 routeGroups에 선언되지 않은 그룹명을 참조한 경우입니다. 2단계의 디버그 로그로 실제 합성된 pattern을 확인해 보세요.

다음 단계 / 관련 주제

Route Group을 도입했다면 다음 주제로 학습을 확장해 보시기를 권장합니다. (1) sap.m.routing.Targetsparent 속성을 활용한 중첩 뷰 라우팅, (2) RouteMatchedHandler와 함께 사용하는 비동기 라우팅, (3) UIComponent.targetsConfig를 통한 동적 타깃 생성, (4) Fiori Launchpad 환경에서의 cross app navigation(CrossApplicationNavigation 서비스)과 manifest 라우팅의 결합, (5) OPA5로 라우팅 시나리오 테스트 자동화. 각 주제 모두 Route Group으로 모듈화된 manifest 위에서 한층 깔끔하게 구현할 수 있습니다.

Route Group 도입 체크리스트

  • SAPUI5/OpenUI5 버전이 routeGroups를 지원하는지 확인했는가
  • 도메인별 prefix 후보를 정리하고 기존 deep link URL과 일치하는가
  • 라우트 name 충돌 방지용 네이밍 컨벤션을 팀이 합의했는가
  • 마이그레이션은 도메인 단위로 점진적으로 진행하고 있는가
  • bypassed 핸들러와 디버그 로그가 활성화되어 있는가
  • OPA5/QUnit 라우팅 테스트가 그룹 도입 후에도 통과하는가

참고 자료

댓글 0

아직 댓글이 없습니다.