개요 및 이 글의 목표
SAPUI5 애플리케이션을 만들 때 시각·청각·운동 제약을 가진 사용자가 동일한 기능을 사용할 수 있도록 보장하는 작업이 접근성(Accessibility, 줄여서 a11y)입니다. 화면에 보이는 픽셀만 잘 그리는 것으로는 부족합니다. 스크린 리더, 키보드 내비게이션, 고대비 모드 같은 보조 기술이 컴포넌트의 의미를 읽어낼 수 있어야 비로소 사용 가능한 화면이 됩니다. 이 글에서는 UI5의 AccessibilityKit(AccKit)과 ARIA 속성, 그리고 OPA5 매처를 활용해 접근성을 코드 레벨에서 검증하는 흐름을 단계적으로 다룹니다.
- AccKit으로 ARIA role과 label 누락을 자동으로 검사할 수 있습니다.
- writeAccessibilityState API로 커스텀 컨트롤에 ARIA 속성을 직접 부여할 수 있습니다.
- OPA5 Accessibility Matcher를 통해 회귀 테스트에 a11y 검증을 통합할 수 있습니다.
- WCAG 2.1 AA 수준에 부합하는 결과를 만들어내는 체크리스트를 갖출 수 있습니다.
이 글을 읽기 전에 알면 좋은 것
SAPUI5의 컨트롤 라이프사이클(init, onBeforeRendering, renderer), sap.ui.core.Control 상속, XML View 작성법을 익혔다면 무리 없이 따라올 수 있습니다. HTML의 role, aria-label, aria-describedby 속성에 대한 기초 개념과 OPA5 기반 통합 테스트의 흐름(arrangement-action-assertion)도 함께 알고 있으면 코드 해석이 빨라집니다.
환경 및 준비물
다음 환경을 기준으로 코드를 검증했습니다. 다른 버전에서도 대체로 호환되지만, AccKit의 일부 API는 1.120 이후에 안정화되었으므로 가급적 최신 LTS를 권장합니다.
- SAPUI5 / OpenUI5 1.120.x 이상 (LTS 기준)
- Node.js 18 LTS, UI5 Tooling 3.x
- OPA5 (sap.ui.test 네임스페이스) 내장 모듈
- 크롬 또는 엣지 최신판 + axe DevTools 확장(보조 검증용)
- NVDA(Windows) 또는 VoiceOver(macOS) 스크린 리더 - 수동 확인용
프로젝트 루트에서 ui5 serve로 개발 서버를 띄우고, ui5 test 또는 Karma 러너로 OPA5를 실행할 수 있도록 karma-ui5 패키지를 설치해두면 자동화 단계가 매끄러워집니다.
핵심 개념 정리
UI5에서 접근성은 세 개의 레이어로 나누어 이해하면 명확합니다. 첫째는 시맨틱 레이어로, 컨트롤이 렌더링되는 DOM에 적절한 HTML 의미와 ARIA role이 부여되어야 합니다. 둘째는 레이블 레이어로, 입력 필드와 버튼이 무엇을 의미하는지 보조 기술이 읽을 수 있는 텍스트가 연결돼야 합니다. 셋째는 상태 레이어로, 확장/축소·선택·진행 중 같은 동적 상태가 ARIA 속성으로 전달돼야 합니다.
이 세 레이어를 손쉽게 다루도록 UI5가 제공하는 도구가 AccessibilityKit(AccKit)과 RenderManager.writeAccessibilityState() 메서드입니다. AccKit은 일종의 "도서관의 점자 표기 점검원"이라고 비유할 수 있습니다. 책(컴포넌트)이 시각 장애 독자에게도 닿을 수 있는지 입구에서 한 번 검사해주는 역할입니다. writeAccessibilityState는 책마다 점자 라벨을 붙이는 작업 자체에 해당합니다.
구조를 도식으로 표현하면 다음과 같습니다.
// 접근성 처리 흐름
// [Control] --(render)--> [RenderManager.writeAccessibilityState]
// |
// v
// [DOM with role/aria-*]
// |
// v
// [Screen Reader] <-- [AccKit 검사] <-- [OPA5 Matcher]
핵심은 "선언은 컨트롤에서, 검증은 테스트에서"라는 분업 원칙입니다. 컨트롤이 자기 책임 영역의 ARIA 속성을 정확히 출력하도록 만들고, 테스트는 그 결과가 회귀 없이 유지되는지를 감시합니다. 두 축이 동시에 움직여야 a11y는 일회성 점검이 아니라 지속 가능한 품질 지표가 됩니다.
실전 코드 1단계 - AccKit으로 ARIA 역할과 레이블 검사하기
첫 번째 단계에서는 가벼운 폼 화면을 만들고 AccKit 스타일의 검사 헬퍼를 적용해 누락된 ARIA 속성을 찾아냅니다. 다음 XML View는 도서 신청 폼을 표현한 예시입니다.
<mvc:View
xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc"
xmlns:f="sap.ui.layout.form"
controllerName="acme.bookreq.controller.RequestForm">
<Page id="requestPage" title="도서 신청">
<content>
<f:SimpleForm id="bookRequestForm" editable="true">
<Label text="도서명" labelFor="bookTitleInput" />
<Input id="bookTitleInput" placeholder="예: 접근성 디자인 패턴" />
<Label text="신청 사유" labelFor="reasonArea" />
<TextArea id="reasonArea" rows="4" />
<Button id="submitReqBtn" text="신청" press=".onSubmitRequest" />
</f:SimpleForm>
</content>
</Page>
</mvc:View>
컨트롤러에서 AccKit 검사 헬퍼를 호출해 렌더링 직후 ARIA 속성이 제대로 출력됐는지 확인합니다.
sap.ui.define([
"sap/ui/core/mvc/Controller"
], function (Controller) {
"use strict";
return Controller.extend("acme.bookreq.controller.RequestForm", {
onAfterRendering: function () {
this._runAccKitInspection();
},
_runAccKitInspection: function () {
var oRoot = this.getView().getDomRef();
if (!oRoot) { return; }
var aFindings = [];
oRoot.querySelectorAll("input, textarea, button").forEach(function (el) {
var sLabel = el.getAttribute("aria-label")
|| el.getAttribute("aria-labelledby");
if (!sLabel) {
aFindings.push({ node: el, issue: "missing-label" });
}
if (el.tagName === "BUTTON" && !el.getAttribute("type")) {
aFindings.push({ node: el, issue: "implicit-button-type" });
}
});
if (aFindings.length) {
console.warn("[AccKit] 접근성 결함 감지", aFindings);
}
},
onSubmitRequest: function () {
// 신청 로직
}
});
});
이 단계의 핵심은 "DOM이 그려진 직후 즉시 점검"하는 사이클을 도입한다는 점입니다. 개발 모드에서 콘솔에 경고가 뜨면 디자이너와 개발자가 같은 시점에 결함을 인지할 수 있어, 늦은 단계의 수정 비용을 크게 줄여줍니다.
실전 코드 2단계 - writeAccessibilityState로 ARIA 속성 직접 설정
커스텀 컨트롤을 만들 때는 표준 컨트롤이 자동으로 처리해주던 ARIA 속성을 개발자가 명시적으로 출력해야 합니다. 다음은 카드 형식으로 책 정보를 보여주는 BookSummaryTile 컨트롤입니다. 단순히 박스를 그리는 데 그치지 않고, 펼침/접힘 상태와 강조 영역을 ARIA로 전달합니다.
sap.ui.define([
"sap/ui/core/Control",
"sap/base/Log"
], function (Control, Log) {
"use strict";
return Control.extend("acme.bookreq.control.BookSummaryTile", {
metadata: {
properties: {
title: { type: "string", defaultValue: "" },
author: { type: "string", defaultValue: "" },
expanded: { type: "boolean", defaultValue: false }
},
events: {
toggle: {}
}
},
renderer: {
apiVersion: 2,
render: function (oRm, oTile) {
oRm.openStart("section", oTile);
oRm.class("acmeBookTile");
// 접근성 상태를 한 번에 출력
oRm.accessibilityState(oTile, {
role: "button",
expanded: oTile.getExpanded(),
label: oTile.getTitle() + " " + oTile.getAuthor(),
describedby: oTile.getId() + "-desc"
});
oRm.attr("tabindex", "0");
oRm.openEnd();
oRm.openStart("h3").openEnd();
oRm.text(oTile.getTitle());
oRm.close("h3");
oRm.openStart("span", oTile.getId() + "-desc").openEnd();
oRm.text("저자: " + oTile.getAuthor());
oRm.close("span");
oRm.close("section");
}
},
onAfterRendering: function () {
try {
var oDom = this.getDomRef();
oDom.addEventListener("keydown", this._onKey.bind(this));
oDom.addEventListener("click", this._onClick.bind(this));
} catch (e) {
Log.error("BookSummaryTile 이벤트 바인딩 실패", e);
}
},
_onKey: function (oEvent) {
if (oEvent.key === "Enter" || oEvent.key === " ") {
oEvent.preventDefault();
this._toggleState();
}
},
_onClick: function () {
this._toggleState();
},
_toggleState: function () {
this.setExpanded(!this.getExpanded());
this.fireToggle({ expanded: this.getExpanded() });
Log.info("BookSummaryTile 토글", { id: this.getId(), expanded: this.getExpanded() });
}
});
});
여기서 oRm.accessibilityState()는 내부적으로 writeAccessibilityState 로직과 동일한 처리를 하며, role·label·describedby·expanded 같은 속성을 일관되게 직렬화합니다. 키보드 핸들러를 함께 등록한 것도 의도적입니다. 시각 사용자에게는 마우스, 그 외 사용자에게는 키보드라는 입력 경로를 같은 컴포넌트에서 보장하는 것이 a11y의 출발점이기 때문입니다. try/catch와 Log 모듈로 운영 환경에서 진단 흔적을 남기는 점도 실무에서 중요합니다.
실전 코드 3단계 - OPA5 Accessibility Matcher로 자동화 테스트
접근성은 한 번 맞춘 후에도 리팩터링 한 번에 깨지기 쉽습니다. 그래서 OPA5에 커스텀 매처를 정의하고, 회귀 테스트 파이프라인에 묶어두는 방식이 권장됩니다. 다음 예시는 위에서 만든 BookSummaryTile의 ARIA 속성이 유지되는지 검사합니다.
sap.ui.define([
"sap/ui/test/Opa5",
"sap/ui/test/opaQunit",
"sap/ui/test/matchers/Matcher",
"sap/ui/test/matchers/PropertyStrictEquals"
], function (Opa5, opaTest, Matcher, PropertyStrictEquals) {
"use strict";
var AriaAttributeMatcher = Matcher.extend("acme.test.matcher.AriaAttribute", {
metadata: {
properties: {
attribute: { type: "string" },
expected: { type: "string" }
}
},
isMatching: function (oControl) {
var oDom = oControl.getDomRef();
if (!oDom) { return false; }
var sActual = oDom.getAttribute(this.getAttribute());
var bMatch = sActual === this.getExpected();
if (!bMatch) {
Opa5.getJQuery().sap.log.warning(
"ARIA 불일치: " + this.getAttribute()
+ " 기대=" + this.getExpected()
+ " 실제=" + sActual
);
}
return bMatch;
}
});
Opa5.extendConfig({
autoWait: true,
timeout: 20
});
opaTest("도서 타일의 접근성 속성이 올바르게 노출된다", function (Given, When, Then) {
Given.iStartMyAppInAFrame("test-resources/acme/bookreq/index.html");
When.waitFor({
controlType: "acme.bookreq.control.BookSummaryTile",
matchers: new PropertyStrictEquals({ name: "title", value: "접근성 디자인 패턴" }),
success: function (aTiles) {
Opa5.assert.ok(aTiles.length === 1, "타일 1개 검색 성공");
}
});
Then.waitFor({
controlType: "acme.bookreq.control.BookSummaryTile",
matchers: [
new AriaAttributeMatcher({ attribute: "role", expected: "button" }),
new AriaAttributeMatcher({ attribute: "aria-expanded", expected: "false" }),
new AriaAttributeMatcher({ attribute: "tabindex", expected: "0" })
],
success: function () {
Opa5.assert.ok(true, "ARIA 속성 일관성 유지");
},
errorMessage: "타일의 ARIA 속성이 회귀되었습니다"
});
Then.iTeardownMyApp();
});
});
여기서 주목할 점은 매처를 속성 단위로 분리한 설계입니다. role, aria-expanded, tabindex 각각이 독립 검사 단위가 되므로 실패 메시지만 보고도 어느 부분이 깨졌는지 즉시 파악할 수 있습니다. CI 환경에서는 이 OPA5 슈트를 PR 게이트에 포함시키고, 실패 시 머지를 차단하는 방식으로 운영하면 일반적으로 안정적인 a11y 품질을 유지할 수 있습니다.
성능과 보안 측면에서 한 가지 더 챙길 부분은, 매처 내부에서 무거운 DOM 탐색을 반복하지 않도록 캐시를 두는 것과, aria-label 등에 사용자가 입력한 값을 그대로 노출할 때 XSS 방어를 위해 UI5의 자동 인코딩 경로(예: oRm.text())를 거치는 것입니다.
자주 마주치는 실수와 트러블슈팅 FAQ
실무에서 반복적으로 발견되는 패턴을 정리하면 다음과 같습니다. 작은 실수 하나가 스크린 리더 사용자에게는 "버튼인데 이름이 없는 정체불명의 요소"로 해석되곤 합니다.
- Label-Input 연결 누락:
Label에labelFor속성을 빠뜨리면 클릭 영역 확장도, ARIA 연결도 작동하지 않습니다. 반드시 입력 컨트롤의 id를 지정해주세요. - 아이콘 전용 버튼:
IconTabBar나 아이콘만 있는 버튼은 시각적으로는 의미가 분명하지만 보조 기술에는 침묵합니다.tooltip이나ariaLabelledBy로 텍스트 의미를 부여해야 합니다. - 동적 영역 갱신 미알림: 비동기 로딩 결과를 화면에 표시할 때
aria-live를 부여하지 않으면 스크린 리더가 변경을 알아채지 못합니다. 알림성 영역에는polite또는assertive를 상황에 맞게 지정하세요.
FAQ 1. AccKit과 axe-core 중 어느 쪽을 써야 하나요? 두 도구는 경쟁이 아니라 보완 관계입니다. AccKit은 UI5 컨트롤이 가진 메타데이터를 인지해 false positive를 줄여주며, axe-core는 일반 HTML/ARIA 규칙 전반을 폭넓게 검사합니다. 개발 단계에서는 AccKit, 릴리스 직전 회귀 점검에서는 axe-core를 결합하는 흐름이 권장됩니다.
FAQ 2. writeAccessibilityState가 출력한 속성이 DOM에 보이지 않습니다. 대부분 RenderManager의 apiVersion: 2 또는 4에서 openStart/openEnd 사이에 호출했는지 확인해야 합니다. openEnd 이후에 호출하면 속성이 무시됩니다. 또한 컨트롤 자체의 writeAccessibilityState 훅 메서드가 부모 클래스에서 falsy를 반환하도록 오버라이드돼 있지는 않은지 점검하세요.
FAQ 3. OPA5 매처가 항상 실패합니다. 비동기 렌더링과 폴링 타이밍을 의심하세요. autoWait: true가 켜져 있어도 커스텀 매처는 첫 폴링 시점에 DOM이 아직 미완성일 수 있습니다. waitFor의 pollingInterval을 늘리거나 visible: true를 명시해 렌더 완료를 보장하면 안정성이 일반적으로 개선됩니다.
이어서 살펴보면 좋은 주제들
이번 글은 컨트롤·렌더링·테스트의 3단 구조에 집중했습니다. 이후에는 SAP Fiori Design Guidelines의 접근성 패턴, sap.ui.core.InvisibleText를 활용한 숨김 레이블 전략, sap.ui.core.theming의 고대비 테마 적용, 그리고 BTP Build Work Zone에 배포한 앱에 대한 외부 감사 도구 연계 흐름을 차례로 학습하면 a11y 역량을 한 단계 더 끌어올릴 수 있습니다. 키보드 트랩 방지와 포커스 관리 패턴(sap.ui.core.Focusable) 역시 후속 학습 후보로 적합합니다.
더 깊이 읽어볼 만한 자료
댓글 0
아직 댓글이 없습니다.