이 글이 답하는 질문
목록에서 상세 화면으로 넘어가는 것까지는 잘 됩니다. 그런데 같은 상세 화면을 "필터 조건 없이" 열어야 하는 요건이 생기면 갑자기 라우트가 안 잡힙니다. 주소창에 값 하나가 빠졌을 뿐인데 빈 화면이 뜨거나 notFound로 떨어집니다.
원인은 대부분 라우트 패턴에 있습니다. UI5 라우터는 패턴을 문자열로 매칭하기 때문에, 값이 없을 수도 있는 세그먼트를 필수처럼 적어두면 그 URL은 어떤 라우트에도 걸리지 않습니다. 이 글에서는 선택적으로 들어오는 파라미터를 다루는 세 가지 방법과, 각각을 언제 쓰는지 정리합니다.
- 라우터 패턴 문법에서 필수 파라미터와 선택 파라미터의 표기 차이
- 세그먼트를 통째로 생략 가능하게 만드는 방법
- 정렬·필터처럼 개수가 유동적인 값을 쿼리로 넘기는 방법
- 라우트를 아예 둘로 나누는 방법과 그 장단점
- 파라미터가 없을 때 컨트롤러에서 안전하게 처리하는 패턴
미리 알고 있으면 좋은 것
manifest.json에 routes와 targets를 정의해 화면을 이동시켜 본 경험이면 충분합니다. 라우터 인스턴스를 얻는 방법과 attachPatternMatched로 화면 진입 시점을 잡는 흐름을 알고 있다면 그대로 이어서 읽으시면 됩니다. 예제는 SAPUI5 1.120 기준이며, 패턴 문법 자체는 오래전부터 동일하게 유지되고 있어 하위 버전에서도 그대로 적용됩니다.
먼저 패턴 문법을 정확히 구분한다
혼동의 출발점은 표기법입니다. 라우터 패턴에서 콜론의 위치와 개수가 의미를 바꿉니다.
| 표기 | 의미 | 매칭 예 |
|---|---|---|
orders/{'{'}id{'}'} 형태의 고정 세그먼트 | 문자 그대로 일치해야 함 | orders/list |
:param | 필수 파라미터 — 값이 반드시 있어야 매칭 | orders/:orderId → orders/4500 |
:param: | 선택 파라미터 — 없어도 매칭 | orders/:orderId: → orders/ 또는 orders/4500 |
*rest | 나머지 경로 전체를 하나로 흡수 | orders/*all |
핵심은 양쪽을 콜론으로 감싸면 선택이라는 점입니다. :orderId와 :orderId:는 한 글자 차이지만 동작이 완전히 다릅니다. 라우트가 안 잡힌다는 문의의 상당수가 뒤쪽 콜론을 빠뜨린 경우입니다.
방법 1 — 세그먼트를 선택으로 선언한다
가장 단순한 방법입니다. 값이 없을 수 있는 세그먼트를 선택 파라미터로 바꿉니다.
{
"routing": {
"routes": [
{
"name": "orderDetail",
"pattern": "orders/:orderId:/:tab:",
"target": "orderDetail"
}
]
}
}
이렇게 하면 다음 URL이 모두 같은 라우트에 걸립니다.
#/orders— 두 값 모두 없음#/orders/4500001234— 주문번호만#/orders/4500001234/items— 주문번호와 탭
컨트롤러에서는 값이 없을 수 있다는 전제로 읽어야 합니다. 없는 파라미터는 undefined로 넘어오므로 기본값 처리를 함께 넣습니다.
onInit: function () {
this.getOwnerComponent().getRouter()
.getRoute("orderDetail")
.attachPatternMatched(this._onMatched, this);
},
_onMatched: function (oEvent) {
var oArgs = oEvent.getParameter("arguments");
var sOrder = oArgs.orderId; // 없을 수 있음
var sTab = oArgs.tab || "general"; // 기본 탭으로 폴백
if (!sOrder) {
// 목록 모드로 표시하고 상세 바인딩은 하지 않는다
this.getView().unbindElement();
this._showEmptyState();
return;
}
this.getView().bindElement({
path: "/Orders('" + encodeURIComponent(sOrder) + "')"
});
this._selectTab(sTab);
}
여기서 자주 나는 실수는 파라미터가 없는 경우에도 bindElement를 호출하는 것입니다. 경로가 /Orders('undefined')가 되어 백엔드에 잘못된 요청이 나가고, 콘솔에는 404가 찍힙니다. 위 코드처럼 값이 없으면 바인딩 자체를 건너뛰어야 합니다.
방법 2 — 개수가 유동적이면 쿼리로 넘긴다
정렬 기준, 필터 조건처럼 개수가 정해지지 않은 값은 세그먼트로 표현하기 어렵습니다. 선택 세그먼트를 여러 개 늘어놓으면 순서에 의존하게 되고, 중간 값만 빠지는 경우를 다루기 까다로워집니다. 이럴 때는 쿼리 파라미터를 씁니다.
{
"name": "orderList",
"pattern": "orders:?query:",
"target": "orderList"
}
:?query:는 물음표 뒤에 오는 값들을 객체로 묶어 전달합니다. 다음처럼 읽습니다.
_onListMatched: function (oEvent) {
var oQuery = oEvent.getParameter("arguments")["?query"] || {};
var sSortBy = oQuery.sortBy || "OrderDate";
var sStatus = oQuery.status; // 없으면 전체
var aFilters = [];
if (sStatus) {
aFilters.push(new Filter("Status", FilterOperator.EQ, sStatus));
}
this.byId("orderTable").getBinding("items")
.filter(aFilters)
.sort(new Sorter(sSortBy, false));
}
이 방식의 장점은 값이 늘어나도 패턴을 바꾸지 않아도 된다는 것입니다. #/orders?status=OPEN&sortBy=Amount처럼 조합이 자유롭고, 사용자가 URL을 복사해 공유하면 화면 상태가 그대로 재현됩니다. 반대로 주의할 점은 쿼리 값은 검증 없이 신뢰하면 안 된다는 것입니다. 외부에서 임의의 값이 들어올 수 있으므로 허용 목록과 대조한 뒤 사용해야 합니다.
방법 3 — 라우트를 둘로 나눈다
값의 유무에 따라 화면이 실질적으로 다르게 동작한다면, 하나의 라우트에 선택 파라미터를 넣기보다 라우트를 분리하는 편이 깔끔합니다.
"routes": [
{ "name": "orderList", "pattern": "orders", "target": "orderList" },
{ "name": "orderDetail", "pattern": "orders/{'{'}orderId{'}'}", "target": "orderDetail" }
]
실제 표기는 orders/:orderId처럼 필수 파라미터로 두고, 값이 없는 경우는 별도 라우트가 받습니다. 이렇게 하면 각 컨트롤러가 자기 상황만 다루면 되므로 조건 분기가 줄어듭니다. 목록과 상세가 서로 다른 뷰를 쓰는 구조라면 이 방식이 가장 읽기 쉽습니다.
다만 라우트가 늘어나면 관리 지점도 늘어납니다. 두 라우트가 같은 뷰를 공유한다면 attachPatternMatched를 두 번 붙여야 하고, 한쪽만 수정해 동작이 어긋나는 실수가 생깁니다. 뷰가 같다면 방법 1이, 뷰가 다르다면 방법 3이 유리합니다.
세 방법 선택 기준
| 상황 | 권장 | 이유 |
|---|---|---|
| 값 유무만 다르고 화면은 같다 | 방법 1 (선택 세그먼트) | 패턴 하나로 끝나고 분기가 단순 |
| 정렬·필터처럼 개수가 유동적 | 방법 2 (쿼리) | 값이 늘어도 패턴 변경 불필요 |
| 값 유무에 따라 화면이 다르다 | 방법 3 (라우트 분리) | 컨트롤러별 책임이 명확 |
| URL을 공유·북마크해야 한다 | 방법 2 우선 | 상태가 URL에 그대로 담김 |
순서 의존을 만들지 않는다
선택 세그먼트를 두 개 이상 연속으로 두면 함정이 생깁니다. orders/:a:/:b:에서 앞 값을 비우고 뒤 값만 넘기는 URL은 자연스럽게 표현되지 않습니다. #/orders//items 같은 형태는 의도대로 매칭되지 않거나, 매칭되더라도 빈 문자열이 전달돼 이후 로직이 흔들립니다.
그래서 선택 세그먼트는 가급적 하나만 두고, 둘 이상 필요하면 쿼리 방식으로 넘기는 편이 안전합니다. 세그먼트는 계층 구조(주문 → 품목)를 표현할 때 쓰고, 화면 상태(정렬·필터·탭)는 쿼리로 표현한다는 기준을 세워두면 패턴이 복잡해지지 않습니다.
notFound로 떨어질 때 확인할 것
라우트가 안 잡히면 대개 다음 순서로 좁혀집니다.
- 뒤쪽 콜론이 있는가 —
:orderId와:orderId:를 다시 확인합니다. 가장 흔한 원인입니다. - 더 앞선 라우트가 먼저 먹지 않는가 — 라우터는 정의 순서대로 매칭을 시도합니다. 광범위한 패턴(
*rest등)을 위에 두면 뒤쪽 라우트가 영영 실행되지 않습니다. - 값에 슬래시나 특수문자가 들어가지 않는가 — 세그먼트에
/가 포함되면 경계가 깨집니다. 인코딩해서 넘기고 읽을 때 디코딩해야 합니다. - 대소문자가 일치하는가 — 패턴 매칭은 문자 단위이므로
Orders와orders는 다릅니다.
디버깅할 때는 라우터에 매칭 실패 이벤트를 붙여 어떤 해시가 들어왔는지 로그로 확인하면 원인이 빨리 드러납니다.
this.getOwnerComponent().getRouter()
.attachBypassed(function (oEvent) {
Log.warning("라우트 매칭 실패: " + oEvent.getParameter("hash"));
});
딥링크가 문제를 드러내는 지점
선택 파라미터 설계가 실제로 시험대에 오르는 순간은 사용자가 앱 밖에서 들어올 때입니다. 알림 메일의 링크, 즐겨찾기, 다른 앱에서 넘겨준 주소가 여기에 해당합니다.
앱 안에서 버튼을 눌러 이동할 때는 이전 화면이 이미 모델을 채워둔 상태라 값이 하나 비어도 화면이 그럭저럭 그려집니다. 그래서 개발 중에는 문제가 안 보입니다. 반면 URL로 직접 진입하면 아무 상태도 없는 상태에서 라우트 매칭부터 시작하므로, 패턴이 조금이라도 어긋나면 곧바로 빈 화면이 됩니다.
테스트할 때는 반드시 다음 세 경로를 모두 확인해야 합니다. 첫째, 앱 내부 이동. 둘째, 해당 URL을 새 탭에 붙여넣어 진입. 셋째, 그 화면에서 새로고침. 세 번째가 특히 중요합니다. 새로고침은 컴포넌트를 다시 만들기 때문에 초기화 순서 문제까지 함께 드러납니다.
또 하나, 사용자가 주소를 손으로 고치는 경우도 고려해야 합니다. 존재하지 않는 주문번호나 허용되지 않은 탭 이름이 들어올 수 있으므로, 파라미터를 그대로 바인딩 경로에 넣기 전에 형식을 검증하고 실패 시 안내 화면으로 보내는 처리가 필요합니다. 검증 없이 그대로 넘기면 백엔드 오류 메시지가 사용자에게 그대로 노출됩니다.
Fiori Elements를 쓸 때는 다르다
지금까지의 내용은 프리스타일 UI5 앱 기준입니다. Fiori Elements로 만든 앱은 라우팅을 프레임워크가 생성하므로 패턴을 직접 손볼 일이 거의 없습니다. 목록·상세 구조와 그에 맞는 라우트가 어노테이션과 매니페스트 설정에서 자동으로 파생됩니다.
그래서 Fiori Elements 앱에서 "특정 필터가 걸린 상태로 링크를 공유하고 싶다"는 요건이 오면, 라우트 패턴을 고치는 대신 표준이 제공하는 상태 보존 기능을 먼저 검토하는 것이 맞습니다. 패턴을 임의로 수정하면 표준 동작과 충돌해 업그레이드 때 문제가 생길 수 있습니다.
정리하면 패턴을 직접 설계하는 것은 프리스타일 앱의 몫이고, Fiori Elements에서는 표준 범위 안에서 해결책을 찾는 편이 안전합니다. 두 방식이 한 프로젝트에 섞여 있다면 어느 앱이 어느 방식인지 먼저 확인하고 접근해야 불필요한 수정을 피할 수 있습니다.
운영 반영 전 점검 목록
- 선택 파라미터에 뒤쪽 콜론을 붙였는가
- 파라미터가 없을 때 bindElement를 건너뛰도록 처리했는가
- 기본값이 필요한 값에 폴백을 넣었는가(탭·정렬 기준 등)
- 쿼리로 받은 값을 허용 목록과 대조한 뒤 사용하는가
- 선택 세그먼트가 두 개 이상 연속되지 않는가
- 넓은 패턴이 좁은 패턴보다 먼저 정의돼 있지 않은가
- 딥링크로 직접 진입했을 때(새로고침 포함) 화면이 정상 복원되는가
마지막 항목은 특히 놓치기 쉽습니다. 앱 안에서 이동할 때는 이전 화면의 모델이 남아 있어 문제가 안 보이지만, URL로 직접 들어오면 그 상태가 없습니다. 새로고침 후에도 화면이 온전히 뜨는지 반드시 확인해야 합니다.
정리하면 선택 파라미터를 다루는 방법은 세 가지지만, 고르는 기준은 두 가지 질문으로 좁혀집니다. 값이 없을 때 화면이 달라지는가, 그리고 값의 개수가 정해져 있는가. 화면이 같고 개수가 고정이면 선택 세그먼트, 개수가 유동적이면 쿼리, 화면이 다르면 라우트 분리입니다.
패턴 설계는 한 번 정해두면 오래 가는 구조라, 초기에 기준을 세워두는 편이 유리합니다. 계층 관계는 세그먼트로, 화면 상태는 쿼리로 표현한다는 원칙만 지켜도 라우트가 복잡해지는 일은 거의 없습니다.
댓글 0
아직 댓글이 없습니다.