Compose의 시맨틱

컴포지션은 앱의 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. '켜짐'과 '꺼짐' 상태의 스위치

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

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

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

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

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

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

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

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

이러한 컴포저블의 예로는 버튼이 있습니다. 여러 하위 노드가 포함되어 있을 수 있지만 단일 요소로 버튼을 추론해보겠습니다.

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

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

컴포저블과 수정자는 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를 사용하면 뷰 필터에서 선호하는 트리를 선택하여 병합된 시맨틱 트리와 병합되지 않은 시맨틱 트리를 모두 표시할 수 있습니다.

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

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

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

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

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

병합 동작

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

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

자체적으로 mergeDescendants = true로 설정한 하위 요소는 병합에 포함되지 않는다는 사실이 중요합니다. 예를 살펴보겠습니다.

그림 6. 이미지와 텍스트, 북마크 아이콘이 있는 목록 항목

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

그림 7. 행 노드 내의 목록에 여러 텍스트가 포함된 병합된 트리. 각 텍스트 컴포저블에 관한 별도의 노드가 포함된 병합되지 않은 트리

시맨틱 트리 조정

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