UI5

CrossAppNavigation 파라미터 누락? — toExternal 안전 전달법 #shorts #SAP #Fiori

▶ YouTube에서 보기

📖 개요: Cross-App Navigation에서 파라미터가 왜 생명선인가

SAP Fiori Launchpad(FLP) 환경에서는 하나의 업무가 여러 앱을 넘나들며 완성됩니다. 판매 오더 목록 앱에서 특정 오더를 선택해 청구서 조회 앱으로 이동할 때, 오더 번호 같은 컨텍스트 파라미터가 누락되면 대상 앱은 빈 화면이나 초기 검색 화면만 보여주고, 사용자는 방금 보던 데이터를 다시 찾아야 합니다. 이 글은 CrossApplicationNavigation 서비스를 이용해 intent 기반으로 파라미터를 안전하게 전달하고 수신하는 방법을 실전 코드 중심으로 다룹니다.

  • intent(SemanticObject-Action) 기반 내비게이션의 동작 원리 이해
  • toExternal / hrefForExternal로 파라미터를 전달하는 3단계 구현
  • 수신 앱에서 startupParameters와 inner-app route를 처리하는 방법
  • URL 길이 제한을 넘는 대용량 컨텍스트를 sap-xapp-state로 넘기는 프로덕션 패턴

📚 미리 갖추고 있으면 좋은 배경

SAPUI5 컴포넌트 구조(Component.js, manifest.json)와 라우팅 설정 경험, FLP 타일과 Target Mapping의 관계에 대한 기본 이해가 필요합니다. JavaScript의 Promise/async-await 문법과 SAPUI5의 AMD 모듈 로딩(sap.ui.define)에 익숙하다면 예제를 그대로 따라올 수 있습니다. 고급 주제이므로 앱 하나를 FLP에 배포해 본 경험을 전제로 설명합니다.

🔧 환경 / 버전 / 준비 사항

이 글의 코드는 다음 환경을 기준으로 작성했습니다.

  • SAPUI5 1.120 LTS (1.71 이상이면 대부분 동일하게 동작. 단, 1.119부터는 후속 서비스인 sap.ushell.services.Navigation 사용이 권장됨)
  • SAP S/4HANA 2023 임베디드 FLP 또는 SAP BTP, SAP Build Work Zone standard edition
  • 발신·수신 앱 모두 FLP 콘텐츠(카탈로그/Target Mapping 또는 Work Zone 콘텐츠)로 등록되어 있어야 함
  • 수신 앱의 Target Mapping에 파라미터(예: SalesOrderId)가 허용되도록 설정. 일반적으로 "추가 파라미터 허용(allow additional parameters)" 옵션을 켜두는 것이 권장됩니다

로컬 개발 시에는 FLP 샌드박스(flpSandbox.html)에 두 앱을 함께 등록해야 cross-app 이동을 테스트할 수 있습니다. 단독 실행(index.html)에서는 ushell Container가 없어 서비스 호출이 실패한다는 점을 기억하세요.

💡 핵심 개념: intent, 파라미터, 그리고 두 개의 우편 시스템

Cross-App Navigation을 이해하는 가장 쉬운 비유는 우편 시스템입니다. 발신 앱은 편지를 직접 상대 앱에 건네지 않습니다. 대신 "SalesOrder를 display 해달라"는 intent(의도)를 봉투에 적어 FLP라는 우체국에 맡깁니다. 우체국(FLP Shell)은 Target Mapping이라는 주소록을 뒤져 그 intent를 처리할 수 있는 앱을 찾아 배달합니다. 발신 앱이 수신 앱의 URL이나 기술적 위치를 몰라도 되는 느슨한 결합이 핵심 가치입니다.

URL 해시 구조를 뜯어보면 파라미터가 어디에 실리는지 보입니다.

// #SemanticObject-action?param1=value1&param2=value2&/innerAppRoute
#SalesOrder-display?SalesOrderId=4500001234&CompanyCode=1000&/items/20
  • intent 파라미터(? 뒤): 수신 앱 컴포넌트의 startupParameters로 전달됩니다. 짧은 키 값 전달에 적합합니다.
  • inner-app route(&/ 뒤): 수신 앱 내부 라우터가 해석하는 경로입니다. 앱 안의 특정 화면(예: 오더의 20번 라인아이템)까지 딥링크할 때 사용하며, toExternalappSpecificRoute 속성이나 별도 inner-app route 추가 방식으로 실어 보냅니다.
  • sap-xapp-state: 필터 조건 수십 개, 선택 행 목록처럼 URL에 담기 힘든 대용량 컨텍스트는 앱 상태(AppState) 컨테이너에 저장하고 그 키만 URL에 실어 보냅니다. 소포는 창고에 맡기고 보관증만 봉투에 넣는 방식입니다.

또 하나 중요한 동작 원리는 파라미터 값이 항상 배열로 수신된다는 점입니다. 같은 키가 여러 번 올 수 있는 URL 특성 때문에 startupParameters.SalesOrderId["4500001234"] 형태입니다. 이를 문자열로 착각하는 것이 파라미터 누락 다음으로 흔한 사고 원인입니다.

💻 실전 코드 3단계

1단계 — 기본: toExternal로 오더 상세 화면 호출

판매 오더 목록 컨트롤러에서 행을 클릭하면 청구 조회 앱으로 이동하는 가장 단순한 형태입니다.

// SalesOrderList.controller.js
onOrderPress: async function (oEvent) {
    const oCtx = oEvent.getSource().getBindingContext();
    const oNavService = await sap.ushell.Container
        .getServiceAsync("CrossApplicationNavigation");

    oNavService.toExternal({
        target: {
            semanticObject: "SalesOrder",
            action: "display"
        },
        params: {
            SalesOrderId: oCtx.getProperty("OrderId"),
            CompanyCode: oCtx.getProperty("CompanyCode")
        }
    });
}

수신 앱의 Component.js(또는 첫 라우트 컨트롤러)에서는 다음과 같이 읽습니다.

// 수신 앱 Component.js - init()
const oCompData = this.getComponentData();
const aOrderId = oCompData?.startupParameters?.SalesOrderId || [];
if (aOrderId.length) {
    this.getModel("view").setProperty("/orderId", aOrderId[0]); // 배열 주의!
}

2단계 — 실무: 사전 검증, 에러 처리, 로깅, inner-app route

실무에서는 사용자 권한에 따라 대상 앱이 없을 수 있습니다. isNavigationSupported로 미리 확인하고, 실패 시 로그와 사용자 메시지를 남깁니다. 오더의 특정 아이템 화면까지 딥링크하려면 appSpecificRoute로 inner-app route를 함께 실어 보냅니다.

sap.ui.define([
    "sap/base/Log",
    "sap/m/MessageBox"
], function (Log, MessageBox) {
    "use strict";
    return {
        navToInvoiceItem: async function (sOrderId, sItemNo) {
            const oNavService = await sap.ushell.Container
                .getServiceAsync("CrossApplicationNavigation");

            const oNavTarget = {
                target: { semanticObject: "BillingDoc", action: "review" },
                params: { SalesOrderId: sOrderId },
                appSpecificRoute: "&/items/" + encodeURIComponent(sItemNo)
            };

            const [oResult] = await oNavService
                .isNavigationSupported([oNavTarget]);

            if (!oResult.supported) {
                Log.warning("Navigation blocked: BillingDoc-review",
                    "order=" + sOrderId, "btp.demo.salesorders");
                MessageBox.information(
                    "청구 조회 권한이 없거나 앱이 할당되지 않았습니다.");
                return;
            }
            oNavService.toExternal(oNavTarget);
        }
    };
});

링크를 화면에 노출해야 한다면 toExternal 대신 hrefForExternal로 href 문자열을 생성해 sap.m.Link에 바인딩하면, 사용자가 Ctrl+클릭으로 새 탭 열기를 할 수 있어 UX 측면에서 권장됩니다.

3단계 — 프로덕션: sap-xapp-state로 대용량 컨텍스트 전달

선택된 오더 50건의 키 목록이나 복잡한 필터 상태를 URL에 다 실으면 브라우저·게이트웨이의 URL 길이 제한에 걸립니다. FLP는 일정 길이를 넘는 파라미터를 sap-intent-param으로 자동 압축하기도 하지만, 처음부터 AppState를 쓰는 편이 안전합니다.

// 발신 앱: 대량 선택 컨텍스트를 AppState에 저장 후 이동
sendBulkContext: async function (aSelectedOrders) {
    const oNavService = await sap.ushell.Container
        .getServiceAsync("CrossApplicationNavigation");

    const oAppState = oNavService.createEmptyAppState(
        this.getOwnerComponent());
    oAppState.setData({
        selectedOrders: aSelectedOrders.map(o => o.OrderId),
        appliedFilters: { CompanyCode: "1000", Status: "OPEN" }
    });
    await oAppState.save();

    oNavService.toExternal({
        target: { semanticObject: "SalesOrder", action: "massReview" },
        params: { "sap-xapp-state": oAppState.getKey() }
    });
}
// 수신 앱: AppState 복원
const oState = await oNavService.getStartupAppState(this.getOwnerComponent());
const oData = oState.getData() || {};
const aOrders = oData.selectedOrders || [];

프로덕션 체크포인트 세 가지입니다. 첫째, 보안 — URL 파라미터는 브라우저 히스토리·북마크·서버 로그에 남으므로 개인정보나 인증 토큰은 절대 싣지 말고 AppState 또는 서버 측 조회로 대체합니다. 둘째, 성능getServiceAsync 결과를 컨트롤러 멤버로 캐싱해 반복 호출을 줄입니다. 셋째, 테스트 — QUnit에서 sap.ushell.Container를 스텁 처리하고 toExternal 호출 인자를 검증하는 단위 테스트를 두면 파라미터 이름 오타를 배포 전에 잡을 수 있습니다.

⚠️ 흔한 실수 / 트러블슈팅 FAQ

Q1. 수신 앱에서 startupParameters가 undefined입니다.

단독 실행(index.html)이거나 FLP를 거치지 않은 URL 직접 진입이면 getComponentData() 자체가 비어 있을 수 있습니다. 옵셔널 체이닝으로 방어 코드를 넣고, 파라미터가 없을 때의 폴백 화면(검색 화면 등)을 반드시 설계하세요. 또한 Target Mapping에서 추가 파라미터를 허용하지 않으면 FLP가 파라미터를 걸러내므로 매핑 설정을 먼저 확인해야 합니다.

Q2. 파라미터 값이 문자열이 아니라 이상한 형태로 옵니다.

startupParameters의 모든 값은 배열입니다. aOrderId[0]처럼 첫 요소를 꺼내야 하며, 숫자 비교가 필요하면 명시적으로 형 변환하세요. 파라미터 이름은 대소문자를 구분하므로 SalesOrderIdsalesorderid는 서로 다른 키입니다.

Q3. appSpecificRoute를 넘겼는데 상세 화면으로 안 들어갑니다.

수신 앱 manifest의 라우트 패턴과 &/ 뒤 문자열이 정확히 일치해야 합니다. 값에 특수문자가 있으면 encodeURIComponent 처리가 필요하고, 수신 앱 라우터가 initialize()되기 전에 해시를 소비하는 커스텀 코드가 있는지도 점검하세요.

Q4. 파라미터가 sap-intent-param으로 바뀌어 옵니다.

URL이 길어 FLP가 자동 압축한 경우입니다. 수신 측에서 서비스의 해시 확장 기능으로 복원하거나, 애초에 2~3단계처럼 sap-xapp-state 설계로 전환하는 것이 일반적으로 안전합니다.

🚀 더 나아가기: 이어서 살펴볼 주제

이 글의 패턴을 익혔다면 다음 주제로 확장해 보세요. SAPUI5 1.119+에서 권장되는 후속 서비스인 sap.ushell.services.Navigation(Promise 기반 API)으로의 마이그레이션, Fiori elements 앱에서 manifest 설정만으로 outbound 내비게이션을 선언하는 crossNavigation/outbounds, 수신 파라미터를 필터바에 자동 반영하는 SmartFilterBar 연계, 그리고 SAP Build Work Zone에서의 콘텐츠 채널 기반 Target 관리가 자연스러운 다음 학습 경로입니다.

📚 함께 보면 좋은 문서와 링크

댓글 0

아직 댓글이 없습니다.