UI5

Formatter 없이 표현식만 쓰다 무너지는 3가지 #shorts #SAP #UI5

▶ YouTube에서 보기

이 글이 답하는 질문

화면에 값을 그대로 뿌리는 경우는 거의 없습니다. 상태 코드를 한글로 바꾸고, 금액에 통화를 붙이고, 조건에 따라 색을 달리해야 합니다. 이때 선택지가 둘 있습니다. XML 뷰 안에 표현식을 직접 쓰는 방법과, 자바스크립트 함수로 빼는 방법입니다.

둘 다 되기 때문에 팀마다 제각각으로 쓰다가, 나중에는 같은 화면 안에서도 어떤 필드는 뷰에 로직이 있고 어떤 필드는 컨트롤러에 있는 상태가 됩니다. 이 글에서는 두 방식의 동작 차이와, 실무에서 어느 쪽을 골라야 하는지 기준을 정리합니다.

  • Expression Binding과 Formatter가 각각 언제 실행되는지
  • 복잡도·재사용·테스트 세 축으로 나누는 선택 기준
  • 표현식을 남용했을 때 실제로 무너지는 지점
  • Formatter에서 자주 나는 실수(this 접근, 값 미도달)
  • 다국어와 통화가 얽힐 때의 처리 방향

미리 알고 있으면 좋은 것

XML 뷰에서 {property} 형태의 바인딩을 써본 경험이면 충분합니다. 모델 종류(JSON/OData)는 구분하지 않아도 되고, 예제는 SAPUI5 1.120 기준이지만 두 문법 모두 오래전부터 동일하게 동작합니다.

Expression Binding — 뷰 안에서 끝내는 방식

중괄호 안에 등호를 붙여 간단한 식을 직접 쓰는 문법입니다.

<ObjectStatus
  text="{= ${status} === 'A' ? '승인됨' : '대기' }"
  state="{= ${amount} > 1000000 ? 'Warning' : 'None' }" />

특징은 다음과 같습니다.

  • 파일을 오가지 않아도 됨 — 뷰만 보면 무엇이 표시되는지 바로 읽힙니다.
  • 간단한 비교·삼항 연산에 적합 — 값 하나를 두세 갈래로 나누는 정도.
  • 컨트롤러 코드가 늘지 않음 — 일회성 표시 규칙에 부담이 없습니다.

주의할 점은 표현식 안에서 할 수 있는 일이 제한적이라는 것입니다. 반복문이나 여러 단계 분기는 쓸 수 없고, 억지로 삼항 연산을 중첩하면 한 줄이 화면 너비를 넘어가면서 읽을 수 없게 됩니다.

Formatter — 함수로 빼는 방식

표시 규칙을 자바스크립트 함수로 정의하고 바인딩에서 참조합니다.

// model/formatter.js
sap.ui.define([], function () {
  "use strict";
  return {
    requestStatusText: function (sStatus, iAge) {
      if (!sStatus) { return ""; }
      if (sStatus === "A") { return "승인됨"; }
      if (sStatus === "R") { return "반려됨"; }
      // 대기 중인데 오래된 건은 따로 표시
      return iAge > 7 ? "대기(지연)" : "대기";
    },
  };
});
<!-- 뷰에서 참조 (여러 값을 함께 넘길 수 있다) -->
<Text text="{
  parts: ['status', 'ageInDays'],
  formatter: '.formatter.requestStatusText'
}" />
  • 분기와 예외 처리를 자유롭게 — 조건이 늘어나도 코드가 읽힙니다.
  • 여러 필드를 조합parts로 값을 함께 받아 판단할 수 있습니다.
  • 재사용과 테스트 — 여러 뷰에서 같은 함수를 쓰고, 단위 테스트로 검증할 수 있습니다.

선택 기준 세 가지

질문Expression BindingFormatter
분기가 몇 갈래인가2~3갈래까지그 이상, 또는 조건이 늘어날 여지가 있음
다른 화면에서도 쓰는가이 화면에서만두 곳 이상에서 동일 규칙
테스트가 필요한가불필요한 단순 표시금액·상태처럼 틀리면 문제가 되는 값

세 질문 중 하나라도 오른쪽에 해당하면 Formatter로 빼는 편이 안전합니다. 특히 세 번째가 중요합니다. 표현식은 단위 테스트를 붙일 수 없으므로, 금액 표시나 승인 상태처럼 틀렸을 때 업무에 영향을 주는 값은 함수로 두고 검증하는 것이 맞습니다.

표현식을 남용하면 생기는 일

가장 흔한 형태는 삼항 연산 중첩입니다.

<!-- 이런 코드가 실제로 자주 보인다 -->
<Text text="{= ${st} === 'A' ? '승인' : ${st} === 'R' ? '반려'
   : ${st} === 'P' ? (${age} > 7 ? '대기(지연)' : '대기') : '알 수 없음' }" />

이 시점이면 이미 함수로 뺐어야 할 로직입니다. 문제는 가독성만이 아닙니다.

  • 디버깅이 어렵다 — 표현식 중간에 로그를 남길 수 없어 어디서 갈렸는지 확인하기 힘듭니다.
  • 같은 규칙이 복사된다 — 다른 화면에 붙여 넣는 순간 두 곳을 따로 고쳐야 합니다.
  • 상태값이 늘면 전부 손봐야 한다 — 코드 한 곳이 아니라 뷰 여러 개를 뒤져야 합니다.

기준을 하나 정해두면 편합니다. 삼항 연산을 두 번 이상 겹치게 되면 그때가 Formatter로 옮길 시점입니다.

Formatter에서 자주 나는 실수

첫째, this 접근입니다. Formatter 함수 안에서 this는 상황에 따라 컨트롤이 될 수도 있고 아닐 수도 있습니다. 컨트롤러 인스턴스를 기대하고 this.getView()를 부르면 런타임에 실패합니다. 표시 규칙은 입력값만으로 결과가 정해지는 순수 함수로 작성하는 것이 안전합니다.

둘째, 값이 아직 없는 시점입니다. 바인딩은 데이터가 도착하기 전에도 한 번 실행됩니다. 이때 인자는 undefined이므로 방어 코드가 없으면 화면에 "undefined"가 그대로 찍히거나 예외가 납니다. 위 예제에서 if (!sStatus) return ""를 맨 앞에 둔 이유입니다.

셋째, parts 순서 착오입니다. parts에 나열한 순서대로 인자가 들어옵니다. 필드를 추가하면서 순서를 바꾸면 값이 뒤바뀐 채 표시되는데, 타입이 비슷하면 오류도 나지 않아 한참 뒤에 발견됩니다. 인자 이름을 필드명과 맞춰두면 실수를 줄일 수 있습니다.

다국어와 통화가 얽힐 때

표시 문자열이 다국어 대상이면 표현식 안에 한글을 직접 쓰는 방식은 곤란합니다. 리소스 번들에서 텍스트를 가져와야 하므로 Formatter 쪽이 자연스럽습니다.

statusText: function (sStatus) {
  var oBundle = this.getModel("i18n").getResourceBundle();
  return oBundle.getText("status." + (sStatus || "unknown"));
}

다만 이 경우에는 this가 필요하므로, 컨트롤러에서 formatter를 인스턴스에 바인딩해두고 쓰는 형태가 됩니다. 앞서 "순수 함수로 두라"는 원칙과 상충하는 지점인데, 실무에서는 i18n이 필요한 함수만 예외로 분리하고 나머지는 순수 함수로 유지하는 절충이 일반적입니다.

금액과 통화는 또 다릅니다. 직접 문자열을 조합하기보다 표준 타입(sap.ui.model.type.Currency 등)을 쓰는 편이 낫습니다. 통화별 소수 자릿수와 로케일별 구분기호가 자동으로 처리되기 때문입니다. 여기에 Formatter를 겹쳐 쓰면 오히려 형식이 어긋납니다.

테스트를 붙일 수 있다는 것의 의미

Formatter를 권하는 이유 중 가장 실질적인 것이 이 부분입니다. 표시 규칙은 눈으로 확인하기 쉬워 보이지만, 상태값이 예닐곱 개로 늘어나면 모든 조합을 화면에서 확인하기는 어렵습니다. 순수 함수로 빼두면 조합을 코드로 검증할 수 있습니다.

// webapp/test/unit/model/formatter.js
QUnit.test("요청 상태 표시", function (assert) {
  assert.strictEqual(formatter.requestStatusText("A", 0), "승인됨");
  assert.strictEqual(formatter.requestStatusText("R", 0), "반려됨");
  assert.strictEqual(formatter.requestStatusText("P", 3), "대기");
  assert.strictEqual(formatter.requestStatusText("P", 10), "대기(지연)");
  // 값이 아직 도착하지 않은 시점
  assert.strictEqual(formatter.requestStatusText(undefined, 0), "");
});

마지막 줄이 특히 중요합니다. 실무에서 나는 버그의 상당수가 "정상 값"이 아니라 값이 없는 순간에 발생합니다. 표현식으로 두면 이 케이스를 검증할 방법이 없고, 운영에서 화면에 undefined가 노출되고 나서야 발견됩니다.

테스트를 붙이려면 함수가 외부 상태에 의존하지 않아야 합니다. 앞서 this 접근을 피하라고 한 이유가 여기서 이어집니다. 입력만 받아 결과를 내면 테스트가 단순해지고, i18n처럼 외부 자원이 필요한 경우만 예외로 분리하면 됩니다.

팀 규칙으로 정해두면 좋은 것

두 방식 모두 유효하기 때문에, 개인 취향으로 두면 같은 프로젝트 안에서 스타일이 갈립니다. 코드 리뷰에서 매번 논쟁하지 않으려면 짧은 규칙 몇 줄을 합의해두는 편이 낫습니다. 예를 들면 다음과 같습니다.

  • 삼항 연산은 한 번까지만 뷰에 허용한다
  • 두 개 이상 필드를 조합하면 무조건 Formatter
  • 화면에 보이는 금액·날짜는 표준 타입을 먼저 검토한다
  • 다국어 텍스트는 뷰에 하드코딩하지 않는다
  • Formatter는 model/formatter.js 한 곳에 모은다

마지막 항목은 사소해 보이지만 효과가 큽니다. 표시 규칙이 컨트롤러 여기저기에 흩어지면 "이 값이 어디서 바뀌는지" 찾는 데 시간이 걸립니다. 한 파일에 모아두면 신규 인원도 규칙 전체를 한 번에 훑을 수 있습니다.

참고로 Fiori Elements 기반 앱이라면 이 논의 자체가 줄어듭니다. 표시 형식 상당 부분이 CDS 어노테이션으로 결정되기 때문입니다. 프리스타일 앱에서 뷰에 로직이 쌓이고 있다면, 그 규칙이 백엔드 어노테이션으로 내려갈 수 있는지 한 번 검토해보는 것도 방법입니다.

성능은 어느 쪽이 유리한가

결론부터 말하면 일반적인 화면에서는 유의미한 차이가 없습니다. 둘 다 바인딩 값이 바뀔 때 다시 계산되고, 계산 비용 자체는 대개 무시할 수준입니다.

차이가 드러나는 경우는 행이 매우 많은 테이블입니다. 수천 행을 그리면서 각 셀마다 함수 호출이 일어나면 누적 비용이 보이기 시작합니다. 이때는 방식 선택보다 애초에 화면에서 계산하지 않는 것이 정답에 가깝습니다. 백엔드 CDS 뷰에서 계산된 필드를 내려주면 프론트엔드는 표시만 하면 됩니다.

즉 성능을 이유로 표현식을 고르는 것은 대체로 근거가 약합니다. 판단은 앞서 정리한 복잡도·재사용·테스트 세 축으로 하는 편이 실용적입니다.

운영 반영 전 점검 목록

  • 삼항 연산이 두 번 이상 겹친 표현식이 남아 있지 않은가
  • 같은 표시 규칙이 여러 뷰에 복사돼 있지 않은가
  • Formatter 함수가 입력값만으로 결과를 내는가(this 의존 최소화)
  • 값이 없는 시점(undefined)에 대한 방어 코드가 있는가
  • parts 순서와 함수 인자 순서가 일치하는가
  • 다국어 대상 문자열이 표현식 안에 하드코딩돼 있지 않은가
  • 금액·날짜는 표준 타입을 우선 검토했는가
  • 대량 행 테이블에서 셀마다 계산하고 있지 않은가

마지막 항목은 성능 문의가 들어왔을 때 가장 먼저 확인할 지점입니다. 표시 로직을 어느 쪽에 두느냐를 고민하기 전에, 그 계산이 화면에서 이뤄져야 하는지부터 되짚어보면 문제가 단순해지는 경우가 많습니다.

정리하면 두 방식은 대체 관계가 아니라 역할이 다릅니다. 표현식은 "이 화면에서만 쓰는 한 줄짜리 표시 규칙"을 뷰 안에서 끝내기 위한 도구이고, Formatter는 "규칙이 자라날 것을 전제로" 분리해두는 도구입니다. 처음부터 모든 것을 함수로 빼면 파일만 늘어나고, 반대로 전부 뷰에 두면 시간이 지나며 손댈 수 없는 상태가 됩니다.

실무에서 가장 흔한 실패는 처음에 표현식으로 시작해 그대로 방치하는 경우입니다. 요건이 추가될 때마다 삼항을 하나씩 덧붙이다 보면 어느 순간 아무도 손대지 않으려는 코드가 됩니다. 조건이 하나 늘어날 때 "이제 옮길 시점인가"를 한 번씩 되묻는 습관만으로 대부분 예방됩니다.

댓글 0

아직 댓글이 없습니다.