스크린리더 포커스 순서와 읽기 라벨 누락을 자동 검사하는 게임 UI 접근성 테스트

게임 UI의 스크린리더 포커스 순서와 읽기 라벨 누락을 빌드 단계에서 자동 검출하는 방법을 정리합니다. Unity UI 테스트 코드와 규칙 설계, CI 실패 기준, 예외 처리까지 실무 절차로 설명합니다.

✓ 복사 완료!
챕터자동화 테스트 & 회귀 검증
난이도중급
상태● 검토 완료
대상 엔진
Engine Agnostic
최종 업데이트
스크린리더 포커스 순서와 읽기 라벨 누락을 자동 검사하는 게임 UI 접근성 테스트

핵심 요약

스크린리더 접근성 테스트의 핵심은 UI 트리에서 포커스 가능한 요소를 수집한 뒤 예상 포커스 순서와 실제 순서를 비교하고 각 요소에 접근성 라벨이 있는지 검증하는 것입니다. 빌드 전 자동화 테스트에서 focusOrder, accessibleName, focusable, enabled 같은 명시적 속성을 검사하면 메뉴 개편으로 생기는 누락을 CI 단계에서 차단할 수 있습니다.

이 글에서는 엔진에 종속되지 않는 검사 원칙과 Unity C# 기반의 테스트 예제를 함께 다룹니다. 실제 스크린리더 음성 출력은 운영체제와 TTS 엔진에 따라 달라지므로 자동 검사는 구조적 결함을 빠르게 찾고 최종 음성 검수는 별도로 수행해야 합니다.

왜 게임 UI에서 스크린리더 포커스 순서가 깨지는가?

게임 UI는 버튼을 배치한 순서와 사용자가 탐색하는 순서가 항상 같지 않습니다. 런타임에 생성되는 보상 목록, 비활성화된 메뉴, 팝업 우선순위, 컨트롤러 내비게이션 설정이 서로 다른 기준을 사용하면 시각적으로는 자연스러운 화면도 스크린리더에서는 예측하기 어려운 순서로 읽힐 수 있습니다.

특히 다음 변경에서 문제가 자주 발생합니다.

변경 유형발생 가능한 접근성 결함자동 검사 방법
버튼 추가포커스 순번 또는 라벨 누락포커스 가능한 요소의 속성 검사
로컬라이징 변경번역 키는 있지만 접근성 이름이 비어 있음최종 문자열 기준의 공백 검사
팝업 추가배경 UI까지 포커스됨활성 포커스 루트와 모달 범위 비교
동적 목록 생성아이템 순서가 데이터 순서와 다름안정적인 식별자와 기대 순서 비교
비활성화 처리disabled 요소가 포커스 대상에 포함됨탐색 가능 조건 재계산
프리팹 병합중복 순번으로 탐색 순서가 불명확함순번 중복 및 누락 검사

스크린리더의 읽기 순서는 단순히 화면 좌표를 위에서 아래로 훑는 방식이 아닙니다. 게임 접근성 계층이 제공하는 포커스 가능 요소와 접근성 이름을 기준으로 탐색하므로 UI 제작 단계에서 이 정보를 명시적으로 관리해야 합니다.

게임 UI의 포커스 순서와 접근성 라벨을 검사하는 테스트 화면

자동 접근성 테스트는 무엇을 검증해야 하는가?

자동화 테스트의 검사 대상을 시각적 배치와 의미 정보로 나누면 규칙을 설계하기 쉽습니다.

1. 포커스 가능 요소 검사

포커스 가능 요소는 실제 입력으로 도달할 수 있는 활성 UI 컨트롤입니다. 일반적으로 다음 조건을 만족해야 합니다.

  • 현재 활성화된 화면 또는 모달의 자식이어야 합니다.
  • focusable == true여야 합니다.
  • 상호작용 가능한 요소라면 enabled == true여야 합니다.
  • 동일한 포커스 그룹 안에서 유일한 순번을 가져야 합니다.
  • 숨겨진 요소나 화면 뒤에 가려진 요소는 탐색 대상에서 제외해야 합니다.

2. 읽기 라벨 검사

읽기 라벨은 스크린리더가 컨트롤을 설명할 때 사용하는 접근성 이름입니다. 화면에 보이는 텍스트를 자동으로 가져오는 방식은 아이콘 버튼, 숫자만 표시되는 슬롯, 이미지로 된 탭에서 쉽게 실패합니다.

라벨 검사에서는 다음을 확인합니다.

규칙통과 기준
필수 라벨포커스 가능한 요소의 accessibleName이 null 또는 공백이 아님
의미 있는 라벨Button, Object, Image 같은 타입명만 단독으로 사용하지 않음
동적 값수량, 상태, 잠금 여부 등 사용에 필요한 정보가 포함됨
번역 가능성하드코딩 문장 대신 로컬라이징 키 또는 접근성 전용 리소스를 사용함
중복 방지같은 그룹에서 서로 다른 컨트롤이 같은 라벨을 무의미하게 공유하지 않음

예를 들어 아이콘만 있는 설정 버튼의 시각 텍스트가 비어 있더라도 접근성 이름은 설정이어야 합니다. 반대로 버튼이라는 라벨만 있으면 컨트롤의 목적을 설명하지 못하므로 테스트에서 실패시키는 편이 안전합니다.

예상 포커스 순서는 어떻게 표현할까?

가장 관리하기 쉬운 방법은 UI 트리에 순번을 암묵적으로 맡기지 않고 테스트 가능한 데이터로 선언하는 것입니다. 순서는 화면 전체의 전역 숫자보다 포커스 그룹과 그룹 내부 순번의 조합으로 관리하는 것이 좋습니다.

screen: inventory
focusGroup: main
items:
  - id: inventory-tab
    focusOrder: 10
  - id: item-slot-01
    focusOrder: 20
  - id: item-slot-02
    focusOrder: 30
  - id: close-button
    focusOrder: 90

이 구조에서는 중간에 아이템 슬롯을 추가해도 기존 요소의 순서를 전부 다시 매기지 않아도 됩니다. 테스트는 focusOrder를 오름차순으로 정렬했을 때 실제 탐색 순서와 일치하는지 비교합니다.

flowchart TD
    A[UI 씬 또는 프리팹 로드] --> B[활성 포커스 루트 선택]
    B --> C[포커스 가능 요소 수집]
    C --> D[라벨과 식별자 검사]
    C --> E[포커스 순번 정렬]
    E --> F[기대 순서와 비교]
    D --> G{검사 통과?}
    F --> G
    G -->|예| H[테스트 통과 및 리포트 저장]
    G -->|아니오| I[빌드 실패와 결함 목록 출력]

Unity에서 포커스 순서와 라벨 누락을 어떻게 검사할까?

아래 예제는 특정 접근성 플러그인의 API를 전제로 하지 않고 프로젝트의 접근성 메타데이터를 읽어 검사하는 형태입니다. 실제 프로젝트에서는 AccessibleNode를 UGUI 컴포넌트, UI Toolkit의 커스텀 속성, 또는 별도 JSON 메타데이터와 연결할 수 있습니다.

using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using UnityEngine;

public sealed class AccessibleNode : MonoBehaviour
{
    public bool focusable = true;
    public bool enabledForAccessibility = true;
    public string focusGroup = "main";
    public int focusOrder;
    public string accessibleName;
}

public sealed class AccessibilityRulesTests
{
    [Test]
    public void InventoryScreen_ShouldHaveValidFocusOrderAndLabels()
    {
        var nodes = Resources.FindObjectsOfTypeAll<AccessibleNode>()
            .Where(node => node.gameObject.scene.IsValid())
            .Where(node => node.focusable && node.enabledForAccessibility)
            .Where(node => node.gameObject.activeInHierarchy)
            .ToList();

        Assert.That(nodes, Is.Not.Empty, "활성 포커스 대상이 없습니다.");

        var missingLabels = nodes
            .Where(node => string.IsNullOrWhiteSpace(node.accessibleName))
            .Select(node => node.name)
            .ToList();

        Assert.That(
            missingLabels,
            Is.Empty,
            "읽기 라벨 누락: " + string.Join(", ", missingLabels));

        var invalidLabels = nodes
            .Where(node => IsGenericLabel(node.accessibleName))
            .Select(node => $"{node.name}={node.accessibleName}")
            .ToList();

        Assert.That(
            invalidLabels,
            Is.Empty,
            "의미 없는 읽기 라벨: " + string.Join(", ", invalidLabels));

        foreach (var group in nodes.GroupBy(node => node.focusGroup))
        {
            var duplicateOrders = group
                .GroupBy(node => node.focusOrder)
                .Where(orderGroup => orderGroup.Count() > 1)
                .Select(orderGroup =>
                    $"{orderGroup.Key}({string.Join(", ", orderGroup.Select(node => node.name))})")
                .ToList();

            Assert.That(
                duplicateOrders,
                Is.Empty,
                $"포커스 순번 중복: 그룹={group.Key}, " +
                string.Join(", ", duplicateOrders));

            var hasInvalidOrder = group.Any(node => node.focusOrder < 0);
            Assert.That(
                hasInvalidOrder,
                Is.False,
                $"음수 포커스 순번: 그룹={group.Key}");
        }
    }

    private static bool IsGenericLabel(string label)
    {
        var normalized = label.Trim().ToLowerInvariant();
        return normalized is "button" or "object" or "image" or "ui element";
    }
}

이 테스트는 최소한의 구조적 오류를 찾는 예제입니다. 실제 제품에서는 화면별 테스트 픽스처를 두고 Resources.FindObjectsOfTypeAll 대신 테스트 대상 루트에서 노드를 순회하는 편이 안정적입니다. 씬 전체를 검색하면 테스트용 오브젝트나 다른 화면의 요소가 섞일 수 있기 때문입니다.

실제 탐색 순서와 기대 순서를 어떻게 비교할까?

포커스 순서를 검증하려면 기대 목록과 런타임 탐색 결과가 같은 식별자 배열인지 비교해야 합니다. 두 목록의 길이만 비교하면 요소가 바뀌어도 발견하지 못하므로 식별자와 순서를 모두 비교해야 합니다.

[Test]
public void PauseMenu_ShouldMatchExpectedFocusSequence()
{
    var expected = new[]
    {
        "resume-button",
        "options-button",
        "restart-button",
        "quit-button"
    };

    var actual = AccessibilityNavigator
        .GetFocusableNodes("pause-menu")
        .OrderBy(node => node.focusOrder)
        .Select(node => node.id)
        .ToArray();

    Assert.That(
        actual,
        Is.EqualTo(expected),
        "일치하지 않는 포커스 순서: " + string.Join(" -> ", actual));
}

동적 목록에서는 배열의 인덱스 대신 안정적인 ID를 사용해야 합니다. 상품 이름이나 화면에 표시되는 숫자는 로컬라이징과 데이터 변경에 따라 바뀔 수 있으므로 테스트 식별자로 적합하지 않습니다.

모달과 팝업의 포커스 범위는 어떻게 검사할까?

모달이 열렸을 때 배경 화면의 버튼까지 읽히면 사용자는 현재 어떤 화면을 조작하는지 알기 어렵습니다. 따라서 모달 상태에서는 다음과 같은 포커스 트랩 규칙을 적용합니다.

  1. 활성 모달 루트를 찾습니다.
  2. 모달의 자손 중 포커스 가능한 요소만 수집합니다.
  3. 배경 루트에 속한 포커스 요소가 결과에 포함되지 않았는지 확인합니다.
  4. 모달이 닫히면 이전 포커스 또는 정의된 복귀 요소로 돌아가는지 검사합니다.
[Test]
public void ConfirmDialog_ShouldTrapFocusInsideModal()
{
    var focusable = AccessibilityNavigator
        .GetFocusableNodes("confirm-dialog")
        .Where(node => node.isVisible)
        .ToList();

    Assert.That(focusable.Select(node => node.rootId),
        Is.All.EqualTo("confirm-dialog"));

    Assert.That(focusable.Select(node => node.id),
        Does.Contain("confirm-button"));

    Assert.That(focusable.Select(node => node.id),
        Does.Contain("cancel-button"));
}

여기서 isVisible은 단순한 GameObject.activeInHierarchy보다 프로젝트 UI 레이어의 표시 상태를 반영해야 합니다. 캔버스 그룹 알파가 0이거나 입력 차단 레이어가 적용된 경우를 별도로 고려해야 합니다.

CI에서 접근성 테스트를 어떤 기준으로 실패시킬까?

모든 접근성 결함을 같은 강도로 처리하면 초기 도입 단계에서 팀의 부담이 커질 수 있습니다. 빌드 차단 규칙과 경고 규칙을 분리하면 점진적으로 품질 기준을 높일 수 있습니다.

심각도예시CI 처리
Blocker포커스 가능한 버튼의 라벨 누락즉시 빌드 실패
Critical모달 외부 요소가 포커스됨즉시 빌드 실패
Major포커스 순번 중복 또는 기대 순서 불일치PR 단계 실패
Minor라벨 문장이 권장 용어와 다름경고와 리포트 기록
Known exception접근성상 의도적으로 제외한 장식 요소만료일이 있는 예외 목록으로 관리

예외는 테스트 코드를 우회하는 주석보다 데이터로 관리하는 편이 좋습니다. 예외 ID, 담당 팀, 사유, 등록일, 만료일을 함께 기록하면 임시 예외가 영구적인 누락으로 굳어지는 것을 막을 수 있습니다.

{
  "id": "decorative-background-001",
  "rule": "missing-accessible-name",
  "reason": "입력과 탐색 대상이 아닌 장식용 이미지",
  "owner": "ui-team",
  "expiresAt": "2026-12-31"
}

빌드 로그에는 실패한 화면, 오브젝트 경로, 식별자, 검사 규칙을 함께 출력해야 합니다. 라벨 누락만 출력하면 QA가 원인을 찾기 위해 씬을 다시 검색해야 하므로 수정 속도가 떨어집니다.

스크린리더 자동 검사는 무엇을 대체하지 못하는가?

자동 검사는 구조적 누락과 순서 오류를 빠르게 찾지만 음성 품질 전체를 보장하지는 않습니다. 다음 항목은 실제 운영체제 스크린리더와 입력 장치로 별도 확인해야 합니다.

  • 라벨이 문맥에 맞게 자연스럽게 읽히는지
  • 수량과 상태 정보가 지나치게 반복되지 않는지
  • 포커스 이동 시 사용자가 현재 위치를 알 수 있는지
  • 화면 전환 뒤 포커스가 합리적인 위치로 이동하는지
  • 컨트롤러, 키보드, 터치 입력이 같은 의미 구조를 유지하는지
  • 한국어와 영어 등 언어별 발음과 숫자 읽기가 적절한지

자동 검사와 수동 검수의 역할은 다음처럼 나누는 것이 효율적입니다.

검사 방식잘 찾는 문제적합한 시점
정적 규칙 검사라벨 누락, 중복 순번, 잘못된 메타데이터커밋과 PR
런타임 UI 테스트모달 범위, 동적 목록, 화면 전환빌드 파이프라인
실제 스크린리더 검수발화 품질, 문맥, 사용성릴리스 후보와 주요 패치

CI 테스트 리포트에서 화면별 접근성 실패 항목을 확인하는 예시

게임 UI 접근성 테스트 도입 절차 3단계

1단계: 접근성 메타데이터 표준화

먼저 포커스 가능한 컨트롤에 공통 컴포넌트나 데이터 스키마를 적용합니다. 최소 필드는 id, focusGroup, focusOrder, focusable, accessibleName, enabledForAccessibility입니다.

이 단계에서는 모든 화면을 한 번에 수정하기보다 메인 메뉴, 일시정지 메뉴, 인벤토리처럼 사용 빈도가 높은 화면부터 시작하는 것이 현실적입니다.

2단계: 규칙 검사와 런타임 검사를 분리

프리팹이나 데이터만 확인하는 정적 검사는 빠르게 실행하고 동적 목록과 모달처럼 런타임 상태가 필요한 검사는 플레이 모드 테스트로 분리합니다. 두 검사를 하나의 거대한 테스트로 만들면 실패 원인을 파악하기 어렵습니다.

3단계: CI 품질 게이트 연결

PR 빌드에서 Blocker와 Critical 규칙을 실패 처리하고 테스트 리포트를 아티팩트로 저장합니다. 릴리스 브랜치에서는 전체 화면 스모크 테스트와 지원 플랫폼별 스크린리더 수동 검수를 함께 실행합니다.

자주 묻는 질문 (FAQ)

스크린리더 포커스 순서를 화면 좌표로 자동 계산해도 되나요?

보조 기준으로는 사용할 수 있지만 권장하지 않습니다. 가로·세로 배치만으로는 탭 순서, 모달 우선순위, 컨트롤러 탐색 규칙을 표현하기 어렵습니다. 명시적인 focusOrder와 포커스 그룹을 기본값으로 두는 편이 안전합니다.

읽기 라벨이 화면의 텍스트와 항상 같아야 하나요?

아닙니다. 화면 텍스트가 아이콘, 축약어, 숫자만 제공한다면 스크린리더용 라벨은 사용 목적과 상태를 더 분명하게 설명해야 합니다. 다만 시각 정보와 음성 정보가 서로 모순되지 않도록 로컬라이징 리소스와 함께 관리해야 합니다.

자동 테스트만 통과하면 접근성 QA가 끝나나요?

아닙니다. 자동 테스트는 누락과 구조 오류를 보장하는 품질 게이트입니다. 실제 스크린리더에서 발화 문맥, 포커스 복귀, 언어별 읽기 품질을 확인하는 수동 검수가 반드시 필요합니다.

정리

게임 UI 접근성은 릴리스 직전의 수동 점검 항목이 아니라 빌드 품질의 일부로 관리해야 합니다. 포커스 순서와 읽기 라벨을 명시적인 메타데이터로 만들고 화면별 노드 수집·규칙 검사·기대 순서 비교를 자동화하면 메뉴 변경과 패치 과정에서 발생하는 접근성 회귀를 빠르게 발견할 수 있습니다.

가장 먼저 적용할 기준은 간단합니다. 포커스 가능한 요소에는 의미 있는 접근성 이름이 있어야 하고 활성 포커스 그룹 안에서는 순서가 유일하고 예측 가능해야 합니다. 이 두 규칙을 CI에 연결하는 것만으로도 접근성 결함이 사용자에게 도달하는 시점을 크게 앞당길 수 있습니다.