Semantics in Compose
구성(Composition)은 컴포저블을 실행하므로써 UI를 묘사하고 이를 생산합니다. 구성은 UI를 설명하는 컴포저블 들로 구성된 트리 구조입니다.
구성의 옆에는, 시멘틱 트리라고 불리우는 병렬 트리가 존재합니다. 이 트리는 접근성(Accessablilty) 서비스 및 테스팅(Testing) 프레임워크가 이해할 수 있는 방법으로 UI 를 설명합니다. 접근성 서비스는 어떤 요구가 있는 사용자에게 앱을 설명하기 위해 트리를 사용합니다. 테스팅 프레임워크는 앱과 상호작용하는 것에 대해 사실인지를 주장하기 위해(assertions) 트리를 사용합니다. 시멘틱 트리는 컴포저블이 어떻게 그려져야하는지에 대한 정보를 포함하지 않지만, 컴포저블의 시멘틱 의미(semantic meaning) 를 포함하고 있습니다. (* semantics 은 특정 언어로 프로그램을 실행할 때마다 컴퓨터가 수행하는 프로세스를 설명합니다.)
만약 컴포저블이 컴포즈 Foundation 계층의 modifier 나 머터리얼 라이브러리로 구성된 경우, 시멘틱트리는 자동적으로 채워지고 생성됩니다. 하지만 저수준의 커스텀 컴포저블을 추가한다면, 그 시멘틱은 수동으로 제공되도록 해야 합니다. 화면에 표시되는 요소의 의미가 올바르게 또는 완전히 나타내지 못할 경우 트리를 조정할 수 있습니다.
다음과 같은 커스텀 캘린더 컴포저블을 예로 생각해 보겠습니다.
이 예제는 캘린더 전체가 Layout
컴포저블을 사용하여 Canvas
에 직접 그리는 하나의 저수준(low-level) 컴포저블을 나타냅니다. (캘린더 내 사용자의 선택 행위에 대하여) 만약 접근성 서비스에 어떠한 작업 수행하지 않는다면, 달력 내 컴포저블 내용 및 사용자 선택에 대한 충분한 정보를 수신할 수 없습니다. 예를들어 사용자가 17이 포함된 날짜를 선택하여도, 접근성 프레임워크는 전체 캘린더에 대한 설명 정보만 받습니다. 이 경우, 토크백(=관계자들끼리만 커뮤니케이션 하는 서비스) 접근성 서비스에서 단순히 “Calendar” 이나 “April Calendar” 만 알려준다면 사용자는 몇일이 선택되었는지 궁금해 할 것입니다. 이 컴포저블을 좀 더 접근가능하도록 만드려면, 시멘틱 정보를 수동으로 추가하면 됩니다.
Semantics properties
시멘틱 의미(semantic meaning)가 있는 UI 트리 안의 모든 노드는, 시멘틱 트리 안에 병렬 노드가 있습니다. 시멘틱 트리 안의 노드는 컴포저블에 해당되는 의미를 전달하는 속성을 포함하고 있습니다. 예를들어, Text
컴포저블에는 text 시멘틱 속성을 가지고 있는데, 이는 그 컴포저블의 의미(meaing)이기 때문입니다. Icon
에는 contentDescription
속성(개발자가 설정한다면) 있는데 이는 아이콘의 의미를 텍스트로 나타냅니다. 컴포즈 Foundation 라이브러리 로 작성된 컴포저블 및 수식자(modifier)는 이미 관련된 속성이 탑재되어 있습니다. 선택적으로, semantics
와 clearAndSetSemantics
수식자를 이용하여 속성을 직접 설정하거나 재정의 할 수 있습니다. 예를들어, 토글 가능한 요소를 위해 상태 설명(state description)을 제공한다던가, 텍스트 컴포저블이 머리글처럼 고려되어야 할 경우, 사용자 접근에 대한 행동을 노드에 추가할 수 있습니다.
시멘틱 트리를 시각화하려면, Layout Inspector 도구나 테스트 내에서 printToLog()
메소드를 사용할 수 있습니다. 이 예제는 Logcat 에서 현재 시멘틱 트리를 출력합니다.
이 테스트의 출력은 아래와 같습니다 :
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
에 대하여 생각해 봅시다. 사용자에게는 아래처럼 표시됩니다.
이 요소의 의미(meaning) 를 설명하기 위해, 이렇게 이야기 할 수 있습니다: “이것은 전환 가능한 요소이며, 현재는 ‘On’ 상태를 가진 스위치입니다. 클릭을 통해 이 요소와 상호작용이 가능합니다.”
이것이 시멘틱 속성이 사용되는 이유입니다. 이 Switch 요소의 시멘틱 노드는 아래 속성을 포함하고있고, Layout Inspector 을 통해 그림과 같이 시각화됩니다.
Role 은 우리가 보고있는 요소의 유형을 나타냅니다. StateDescription 은 참조하는 상태가 “On” 임을 나타냅니다. 단순히 “On” 이라는 단어의 지역화된 버전으로 사용했지만, 상황에 따라 좀 더 구체적으로 만들 수 있습니다(예를들어, “Enabled”). ToggleableState
는 스위치의 현재 상태입니다. OnClick
은 이 요소와 상호작용 하는데 사용하는 메소드를 참조합니다. 시멘틱 속성 전체 목록을 확인하려면, SemanticsProperties
을 확인하세요. 그리고 사용자 접근에 대한 행동(Accessibility Actions) 전체 목록을 확인하려면, SemanticsActions
을 확인하세요.
앱에서 각 컴포저블에 시멘틱 속성을 추적한다면 많은 강력한 가능성을 가질 수 있습니다. 몇 가지 예입니다 :
- (음성지원기능) 톡백은 화면에 표시되는것을 소리내어 읽거나 사용자와 좀 더 자연스럽게 상호작용을 하기위해 프로퍼티를 사용합니다.
- 테스트 프레임워크에서는 프로퍼티를 이용하여 노드를 탐색하고, 상호작용하며, assertion 을 수행합니다. Switch 의 테스트 샘플은 아래처럼 가능합니다:
참조 : 앱을 개발하는 동안, 시멘틱 속성을 사용할 때 올바른 의미(meaning)를 전달하는데 주의를 기울여야 합니다. 여러 접근성 서비스들에 대해 좋은 결과를 얻을 수 있는지 검증합니다. 접근성에 영향을 미치는 시멘틱 속성은 완전히 테스트가 가능할 경우에만 추가하거나, 그렇지 않으면 컴포즈 Foundation 계층 라이브러리를 사용하세요.
Merged and unmerged Semantics tree
앞서 언급한 것 처럼, UI 트리의 각 컴포저블은 0개 이상의 시멘틱 의미를 가질 수 있습니다. 시멘틱 속성이 설정되지 않은 컴포저블은, 시멘틱 트리의 일부로 포함되지 않습니다. 이러한 방식으로 시멘틱 트리는 실제로 시멘틱 의미를 가진 노드만 포함합니다. 하지만, 때로는 화면에 표시되는 것이 정확한 의미를 전달하기에, 특정 노드의 하위 트리를 병합하거나 하나로 처리하는것도 유용합니다. 이렇게 하면 하위 노드를 각각 처리하는 대신 노드 집합 전체를 추론할 수 있습니다. 상식적으로, 트리 안의 각 노드는 접근성 서비스를 사용하는 요소에 초점을 두고 있습니다.
이러한 컴포저블의 예는 Button 입니다. 버튼은 여러 하위 노드들로 구성할 수 있으나, 단일 요소로 추론하고 싶습니다.
시멘틱 트리에서 버튼 하위 요소들의 속성은 병합되고, 버튼은 트리에서 단일 리프 노드(single leaf node)로 나타내어 집니다.
컴포저블 및 modifier 는 Modifier.semantics (mergeDescendants = true) {}
을 호출하여 하위 요소들의 시멘틱 속성을 병합하여 나타낼 수 있습니다. 이 속성을 true 로 설정하면 시멘틱 속성이 병합된다는 것을 나타낼 수 있습니다. Button
예제에서는 시멘틱 modifier 를 포함하는 clickable
modifier 를 내부적으로 사용합니다. 따라서 버튼의 자식 노드들은 병합됩니다. 컴포저블의 병합 행위 변경(change merging behavior)은 접근성 문서를 참조하세요.
Foundation 및 Material 컴포즈 라이브러리의 몇 가지 컴포저블 및 modifier 는 이러한 속성을 설정할 수 있습니다. 예를들어, clickable
이나 toggleable
modifier 은 자동적으로 하위 노드와 병합합니다. 또한ListItem
컴포저블도 하위 노드와 병합합니다.
Inspecting the trees
시멘틱 트리에 대해 이야기 할 때, 우리는 실제로 두 가지 다른 트리에 대하여 이야기하고 있습니다. 병합된 시멘틱 트리는(merged Semantics tree) mergeDescendants
가 true 로 설정되어 자식 노드들이 병합합니다. 그리고 병합되지 않은 시멘틱 트리는(unmerged Semantics tree) 모든 노드를 그대로 유지한 채 병합을 적용하지 않습니다. 접근성 서비스(Accessibility services)는 병합되지 않은 트리를 사용하고 mergeDescendants
속성을 고려하여 자체 병합 알고리즘을 적용합니다. 테스팅 프레임워크는 기본적으로 병합된 트리를 사용합니다.
printToLog()
방법을 사용하여 두 트리 모두 검사할 수 있습니다. 기본적으로, 이전 예시와 같이 병합된 트리가 기록됩니다. 병합되지 않은 트리를 출력하려면, onRoot()
함수의 useUnmergedTree 파라메터를 true 로 설정합니다.
composeTestRule.onRoot(useUnmergedTree = true).printToLog("MY TAG")
Layout Inspector 에서 병합된 시멘틱 트리와 병합되지 않은 시멘틱 트리를 필터를 이용하여 선호하는 방식으로 확인할 수 있습니다.
트리안의 각 노드에 대하여, Layout Inspector 는 병합된 시멘틱스와 시멘틱스 정보 모두 속성 패널에서 나타냅니다.
기본적으로, 테스트 프레임워크는 병합된 시멘틱 트리를 사용합니다. 따라서 표시되는 텍스트 매칭되는 버튼과 상호작용을 할 수 있습니다.
composeTestRule.onNodeWithText("Like").performClick()
또한 onRoot 에서 설정한 것 처럼, useUnmergedTree
파라미터를 true 로 설정하여 위 동작방식을 재정의 할 수 있습니다.
Merging behavior
컴포저블이 하위 요소들과 병합되어 표시될 때, 이 병합은 정확히 어떻게 일어날까요?
각각의 시멘틱 속성은 정의된 병합 전략이 있습니다. 예를들어, ContentDescription
속성은 모든 하위 요소의 ContentDescription 값을 리스트에 추가합니다. SemanticsProperties.kt
에서 mergePolicy
를 구현을 확인하여 시멘틱 속성의 병합 전략을 확인할 수 있습니다. 속성은 항상 부모나 자식의 값을 선택하고, 값을 리스트나 문자열로 병합하거나, 병합을 허용하지 않고 대신 예외를 발생시킨다던가, 또는 다른 커스텀 병합 전략을 선택할 수 있습니다.
중요한 것은 mergeDescendants = true
를 설정한 하위 항목의 경우에는 스스로 하위노드를 병합에 포함시키지 않는다는 것입니다. 예제를 확인해 보겠습니다.
선택 가능한 리스트 아이템이 있습니다. 사용자가 행 하나를 선택하면, 아티클 상세 화면으로 앱이 이동되고, 사용자는 아티클을 읽을 수 있습니다. 리스트 아이템 안에는 아티클을 즐겨찾기 할 수 있는 버튼이 있습니다. 이 경우 선택 가능 요소가 중첩되어 있고, 따라서 버튼은 병합된 트리에서 따로 표시됩니다. 그리고 행의 나머지 내용들은 병합됩니다:
Adapting the Semantics tree
앞서 설명한 것 처럼, 트리의 병합 행위를 재정의(override) 하거나 시멘틱 속성을 제거하거나 변경할 수 있습니다. 이는 커스텀 컴포넌트를 만들 때 특히 유용합니다. 올바른 속성과 병합 행위가 설정되어있지 않으면, 앱에 접근할 수 없으며 테스트는 예상과 다르게 동작할 수 있습니다. 시멘틱 트리를 적용하는 몇 가지 일반적인 사례는 접근성 문서를 읽어보세요. 테스트에 대해 좀 더 알고싶다면, 테스트 안내 문서를 확인해 보세요.