Compose의 시맨틱

컴포지션은 앱의 UI를 설명하며 컴포저블을 실행하여 생성됩니다. 컴포지션은 UI를 설명하는 컴포저블로 구성된 트리 구조입니다.

컴포지션 옆에는 시맨틱 트리라는 병렬 트리가 있습니다. 이 트리는 접근성 서비스와 테스트 프레임워크에서 이해할 수 있는 대체 방식으로 UI를 설명합니다. 접근성 서비스는 이 트리를 사용하여 특정 요구사항이 있는 사용자에게 앱을 설명합니다. 테스트 프레임워크는 트리를 사용하여 앱과 상호작용하고 앱에 관한 어설션을 만듭니다. 시맨틱 트리에는 컴포저블을 그리는 방법에 관한 정보는 포함되어 있지 않지만 컴포저블의 시맨틱 의미에 관한 정보는 포함되어 있습니다.

일반적인 UI 계층 구조 및 시맨틱 트리
그림 1. 일반적인 UI 계층 구조 및 시맨틱 트리

앱이 Compose 기초 및 Material 라이브러리의 컴포저블과 수정자로 구성되어 있다면 시맨틱 트리가 자동으로 채워지고 생성됩니다. 그러나 맞춤 하위 수준 컴포저블을 추가할 때는 시맨틱을 수동으로 제공해야 합니다. 트리가 화면에 있는 요소의 의미를 올바르게 또는 완전히 나타내지 못하는 경우도 있을 수 있으며 이 경우 트리를 조정할 수 있습니다.

예를 들어 다음 맞춤 캘린더 컴포저블을 생각해보세요.

선택 가능한 날짜 요소가 있는 맞춤 캘린더 컴포저블
그림 2. 선택 가능한 날짜 요소가 있는 맞춤 캘린더 컴포저블

이 예에서 전체 캘린더는 Layout 컴포저블을 사용하고 Canvas에 직접 그리는 단일 하위 수준 컴포저블로 구현됩니다. 다른 작업을 하지 않으면 접근성 서비스에서 컴포저블의 콘텐츠와 캘린더 내에서 사용자가 선택한 항목에 관한 정보를 충분히 받지 못합니다. 예를 들어 사용자가 17이 포함된 날짜를 클릭하면 접근성 프레임워크는 전체 캘린더 컨트롤에 관한 설명 정보만 수신합니다. 이 경우 TalkBack 접근성 서비스는 '캘린더' 또는 약간 더 나은 '4월 캘린더'만 알리며 사용자는 어떤 날짜가 선택되었는지 알 수 없습니다. 이 컴포저블의 접근성을 높이려면 시맨틱 정보를 수동으로 추가해야 합니다.

시맨틱 속성

시맨틱 의미가 있는 UI 트리의 모든 노드에는 시맨틱 트리에 병렬 노드가 있습니다. 시맨틱 트리의 노드에는 상응하는 컴포저블의 의미를 전달하는 속성이 포함되어 있습니다. 예를 들어 Text 컴포저블에는 시맨틱 속성 text가 포함되어 있습니다. 이 컴포저블의 의미이기 때문입니다. Icon에는 텍스트로 Icon의 의미를 전달하는 contentDescription 속성 (개발자가 설정한 경우)이 포함되어 있습니다. Compose 기초 라이브러리를 기반으로 빌드된 컴포저블과 수정자는 이미 관련 속성을 설정했습니다. 원하는 경우 semanticsclearAndSetSemantics 수정자를 사용하여 속성을 직접 설정하거나 재정의합니다. 예를 들어 노드에 맞춤 접근성 작업을 추가하거나 전환 가능한 요소의 대체 상태 설명을 제공하거나 특정 텍스트 컴포저블이 제목으로 간주되어야 한다고 표시합니다.

시맨틱 트리를 시각화하려면 Layout Inspector 도구를 사용하거나 테스트 내에서 printToLog() 메서드를 사용합니다. 그러면 Logcat 내 현재 시맨틱 트리가 출력됩니다.

class MyComposeTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun MyTest() {
        // Start the app
        composeTestRule.setContent {
            MyTheme {
                Text("Hello world!")
            }
        }
        // Log the full semantics tree
        composeTestRule.onRoot().printToLog("MY TAG")
    }
}

이 테스트는 다음과 같이 출력됩니다.

    Printing with useUnmergedTree = 'false'
    Node #1 at (l=0.0, t=63.0, r=221.0, b=120.0)px
     |-Node #2 at (l=0.0, t=63.0, r=221.0, b=120.0)px
       Text = '[Hello world!]'
       Actions = [GetTextLayoutResult]

시맨틱 속성이 컴포저블의 의미를 전달하는 방식을 고려하세요. Switch을 고려하세요. 사용자에게는 다음과 같이 표시됩니다.

그림 3. '켜짐' 및 '꺼짐' 상태인 스위치

이 요소의 의미를 설명하려면 다음과 같이 말할 수 있습니다. "이것은 '켜짐' 상태의 전환 가능한 요소인 스위치입니다. 클릭하면 상호작용할 수 있습니다.'

이것이 시맨틱 속성이 사용되는 정확한 용도입니다. 이 Switch 요소의 시맨틱 노드에는 Layout Inspector로 시각화된 다음 속성이 포함됩니다.

Switch 컴포저블의 시맨틱 속성을 보여주는 Layout Inspector
그림 4. Switch 컴포저블의 시맨틱 속성을 보여주는 Layout Inspector

Role는 요소의 유형을 나타냅니다. StateDescription는 '켜짐' 상태를 참조하는 방법을 설명합니다. 기본적으로 'On'이라는 단어의 현지화된 버전이지만 컨텍스트에 따라 좀 더 구체적으로 표현될 수 있습니다 (예: '사용 설정됨'). ToggleableState는 스위치의 현재 상태입니다. OnClick 속성은 이 요소와 상호작용하는 데 사용되는 메서드를 참조합니다. 시맨틱 속성의 전체 목록은 SemanticsProperties 객체를 확인하세요. 가능한 접근성 작업의 전체 목록은 SemanticsActions 객체를 확인하세요.

앱에서 각 컴포저블의 시맨틱 속성을 추적하면 여러 강력한 가능성이 열립니다. 예를 들면 다음과 같습니다.

  • TalkBack은 속성을 사용하여 화면에 표시된 내용을 소리 내 읽고 사용자가 원활하게 상호작용할 수 있도록 합니다. 스위치 컴포저블의 경우 TalkBack에 '켜짐, 스위치, 두 번 탭하여 전환'이라고 표시될 수 있습니다. 사용자는 화면을 두 번 탭하여 스위치를 끄면 됩니다.
  • 테스트 프레임워크에서는 속성을 사용하여 노드를 찾아 상호작용하고 어설션을 만듭니다. Switch의 샘플 테스트는 다음과 같을 수 있습니다.
    val mySwitch = SemanticsMatcher.expectValue(
        SemanticsProperties.Role, Role.Switch
    )
    composeTestRule.onNode(mySwitch)
        .performClick()
        .assertIsOff()

병합된 시맨틱 트리와 병합되지 않은 시맨틱 트리

앞서 언급했듯이 UI 트리의 각 컴포저블에는 시맨틱 속성이 0개 이상 설정되어 있을 수 있습니다. 컴포저블에 시맨틱 속성이 설정되어 있지 않으면 컴포저블은 시맨틱 트리의 일부로 포함되지 않습니다. 따라서 시맨틱 트리에는 실제로 시맨틱 의미가 포함된 노드만 포함됩니다. 그러나 때로는 화면에 표시되는 내용의 정확한 의미를 전달하기 위해 노드의 특정 하위 트리를 병합하여 하나로 처리하는 것도 유용합니다. 이렇게 하면 각 하위 노드를 개별적으로 처리하는 대신 전체 노드 집합을 추론할 수 있습니다. 일반적으로 이 트리의 각 노드는 접근성 서비스를 사용할 때 포커스 가능 요소를 나타냅니다.

이러한 컴포저블의 예는 Button입니다. 여러 하위 노드가 포함되어 있더라도 버튼을 단일 요소로 추론할 수 있습니다.

Button(onClick = { /*TODO*/ }) {
    Icon(
        imageVector = Icons.Filled.Favorite,
        contentDescription = null
    )
    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
    Text("Like")
}

시맨틱 트리에서는 버튼의 하위 요소 속성이 병합되고 버튼은 트리에 단일 리프 노드로 표시됩니다.

병합된 단일 리프 시맨틱 표현
그림 5. 병합된 단일 리프 시맨틱 표현

컴포저블과 수정자는 Modifier.semantics (mergeDescendants = true) {}를 호출하여 하위 요소의 시맨틱 속성을 병합하려고 함을 나타낼 수 있습니다. 이 속성을 true로 설정하면 시맨틱 속성을 병합해야 함을 나타냅니다. Button 예에서 Button 컴포저블은 이 semantics 수정자가 포함된 clickable 수정자를 내부적으로 사용합니다. 따라서 버튼의 하위 노드가 병합됩니다. 컴포저블에서 병합 동작을 변경해야 하는 경우에 관한 자세한 내용은 접근성 문서를 참고하세요.

Foundation 및 Material Compose 라이브러리의 여러 수정자와 컴포저블에는 이 속성이 설정되어 있습니다. 예를 들어 clickabletoggleable 수정자는 자동으로 하위 요소를 병합합니다. ListItem 컴포저블도 하위 요소를 병합합니다.

트리 검사

시맨틱 트리는 사실 서로 다른 두 개의 트리입니다. 병합된 시맨틱 트리가 있습니다. 이 트리는 mergeDescendantstrue로 설정되면 하위 노드를 병합합니다. 병합되지 않은 시맨틱 트리도 있는데, 이 트리는 병합을 적용하지 않지만 모든 노드를 그대로 유지합니다. 접근성 서비스는 병합되지 않은 트리를 사용하고 mergeDescendants 속성을 고려하여 자체 병합 알고리즘을 적용합니다. 테스트 프레임워크는 기본적으로 병합된 트리를 사용합니다.

printToLog() 메서드로 두 트리를 모두 검사할 수 있습니다. 기본적으로 이전 예와 같이 병합된 트리가 로깅됩니다. 대신 병합되지 않은 트리를 출력하려면 onRoot() 매처의 useUnmergedTree 매개변수를 true로 설정하세요.

composeTestRule.onRoot(useUnmergedTree = true).printToLog("MY TAG")

Layout Inspector를 사용하면 뷰 필터에서 선호하는 시맨틱 트리를 선택하여 병합된 시맨틱 트리와 병합되지 않은 시맨틱 트리를 모두 표시할 수 있습니다.

병합된 시맨틱 트리와 병합되지 않은 시맨틱 트리를 모두 표시할 수 있는 Layout Inspector 뷰 옵션
그림 6. 병합된 시맨틱 트리와 병합되지 않은 시맨틱 트리를 모두 표시할 수 있는 Layout Inspector 뷰 옵션

트리의 각 노드의 경우 Layout Inspector에는 병합된 시맨틱과 속성 패널에서 이 노드에 설정된 시맨틱이 모두 표시됩니다.

시맨틱 속성 병합 및 설정
그림 7. 시맨틱 속성이 병합되고 설정되었습니다.

기본적으로 테스트 프레임워크의 매처는 병합된 시맨틱 트리를 사용합니다. 따라서 내부에 표시된 텍스트를 일치시켜 Button와 상호작용할 수 있습니다.

composeTestRule.onNodeWithText("Like").performClick()

onRoot 매처와 마찬가지로 매처의 useUnmergedTree 매개변수를 true로 설정하여 이 동작을 재정의합니다.

병합 동작

컴포저블이 하위 요소를 병합해야 한다고 나타낼 때 이 병합은 정확히 어떻게 발생하나요?

각 시맨틱 속성에는 정의된 병합 전략이 있습니다. 예를 들어 ContentDescription 속성은 모든 하위 ContentDescription 값을 목록에 추가합니다. SemanticsProperties.kt에서 mergePolicy 구현을 확인하여 시맨틱 속성의 병합 전략을 확인합니다. 속성은 상위 또는 하위 값을 취하고, 값을 목록이나 문자열로 병합하거나, 병합을 전혀 허용하지 않고 대신 예외를 발생시키거나, 다른 맞춤 병합 전략을 사용할 수 있습니다.

자체적으로 mergeDescendants = true를 설정한 하위 요소는 병합에 포함되지 않는다는 점에 유의해야 합니다. 다음 예를 살펴봅시다.

이미지, 일부 텍스트, 북마크 아이콘이 포함된 목록 항목
그림 8. 이미지, 텍스트, 북마크 아이콘이 포함된 목록 항목

다음은 클릭 가능한 목록 항목입니다. 사용자가 행을 누르면 앱이 도움말 세부정보 페이지로 이동하고 여기서 사용자는 도움말을 읽을 수 있습니다. 목록 항목 내에는 기사를 북마크하는 버튼이 있습니다. 이 버튼은 중첩된 클릭 가능한 요소를 형성하므로 버튼이 병합된 트리에서 별도로 표시됩니다. 행의 나머지 콘텐츠는 병합됩니다.

행 노드 내의 목록에 여러 텍스트가 포함된 병합된 트리. 각 텍스트 컴포저블에 관한 별도의 노드가 포함된 병합되지 않은 트리
그림 9. 행 노드 내의 목록에 여러 텍스트가 포함된 병합된 트리. 병합되지 않은 트리에는 각 Text 컴포저블에 관한 별도의 노드가 포함되어 있습니다.

시맨틱 트리 조정

앞서 언급했듯이 특정 시맨틱 속성을 재정의 또는 삭제하거나 트리의 병합 동작을 변경할 수 있습니다. 이는 자체 맞춤 구성요소를 만들 때 특히 중요합니다. 올바른 속성과 병합 동작을 설정하지 않으면 앱에 액세스하지 못할 수 있으며 테스트가 예상과 다르게 작동할 수 있습니다. 시맨틱 트리를 조정해야 하는 일반적인 사용 사례에 관한 자세한 내용은 접근성 문서를 참고하세요. 테스트에 관해 자세히 알아보려면 테스트 가이드를 확인하세요.

추가 리소스