Upgrade to Pro — share decks privately, control downloads, hide ads and more …

DroidKnights 2024 - Compose UI 컴포넌트 설계와 테스트

DroidKnights 2024 - Compose UI 컴포넌트 설계와 테스트

Suhyeon Kim

June 11, 2024
Tweet

More Decks by Suhyeon Kim

Other Decks in Programming

Transcript

  1. ❏ 우아한형제들 Android Dev/Educator ❏ NEXTSTEP Educator/Code Reviewer ❏ DroidKnights

    Conference Organizer ❏ GDG Korea Android Organizer GitHub @wisemuji
  2. 이야기할 내용 ❏ Compose UI 컴포넌트 설계의 중요성 ❏ React로

    살펴보는 선언형 UI 컴포넌트 설계 노하우 ❏ 사례로 알아보는 UI 컴포넌트 설계 팁 ❏ 사례로 알아보는 UI 컴포넌트 테스트
  3. 다루지 않는 내용 ❏ UI 테스트 프레임워크 사용법 ❏ 웹

    프론트엔드 및 React 개념 ❏ 컴포넌트 아키텍처 패턴 ❏ Compose Stability “구현체가 아닌 인터페이스에 의존"
  4. 컴포넌트를 나누는 기준 ❏ 재사용 가능성 ❏ 테스트 가능성 ❏

    하나의 역할/책임 ❏ 변경에 유연함 ❏ …
  5. 컴포넌트를 나누는 기준 ❏ 재사용 가능성 ❏ 테스트 가능성 ❏

    하나의 역할/책임 ❏ 변경에 유연함 ❏ …
  6. ❏ Case1: 화면 단위 구현 ❏ Case2: 컴포넌트 단위 구현

    드로이드나이츠 앱 github.com/droidknights/DroidKnightsApp
  7. (중략)… https://android.googlesource.com/…/compose-component-api-guidelines.md 하나의 컴포넌트는 하나의 문제만 해결해야 한다. 컴포넌트가 하나

    이상의 문제를 해결한다면 하위 계층 또는 하위 컴포넌트로 분할하는 것을 고려하라. 작고 간결한 API로 디자인할수록 사용하기 쉽고, 컴포넌트 인터페이스를 명확하게 이해할 수 있다.
  8. UI 컴포넌트 설계의 중요성 ❏ Android View에서는 화면(Activity, Fragment) 단위의

    구현이 일반적이였으나, Compose에서는 선언적으로 작성된 컴포넌트로 쪼개어 설계할 수 있다. ❏ 적절한 컴포넌트 단위를 나누어 변경사항에 유연한 설계를 지향하자. ❏ 결합도를 낮추고 응집도를 높이자.
  9. 선언형 UI 패러다임 선배들 ❏ React - 2013 JSConf US

    오픈소스화 ❏ Flutter - 2015 다트 개발자 서밋 첫 공개 ❏ SwiftUI - 2019 WWDC 첫 소개 ❏ Jetpack Compose - 2021 Stable 버전 정식 출시
  10. 선언형 UI 패러다임 선배들 ❏ React - 2013 JSConf US

    오픈소스화 ❏ Flutter - 2015 다트 개발자 서밋 첫 공개 ❏ SwiftUI - 2019 WWDC 첫 소개 ❏ Jetpack Compose - 2021 Stable 버전 정식 출시
  11. React 컴포넌트 설계 Best Practices React 개발자들이 공유하는 핵심 설계

    원칙은 Compose UI에도 적용 가능 ❏ 함수형 컴포넌트 ❏ 작고 재사용 가능한 컴포넌트 ❏ UI와 비즈니스 로직의 분리 ❏ Atomic Design
  12. 선언형 UI 패러다임은 서로 밀접하게 닿아있다 상대적으로 역사가 긴 선배

    프레임워크 개발자들의 노하우를 참고할 수 있을 것이다.
  13. 중복은 항상 제거되어야 할까? ❏ 외관상 중복된 코드라도 수정의 이유가

    다르면 실제로는 중복이 아니다. ❏ 중복 제거를 강조하는 DRY(Don't repeat yourself) 원칙을 과도하게 적용함으로써 발생하는 의존성 문제와 조건문 추가로 인한 복잡성 증가는 오히려 유지보수를 어렵게 할 수 있다.
  14. @Composable fun KnightsTopAppBar( @StringRes titleRes: Int, ... isSessionDetail: Boolean, )

    { ... Row(Modifier.align(Alignment.CenterEnd)) { if (isSessionDetail) BookmarkToggleButton(...) } }
  15. @Composable fun KnightsTopAppBar( @StringRes titleRes: Int, ... isSessionDetail: Boolean, )

    { ... Row(Modifier.align(Alignment.CenterEnd)) { if (isSessionDetail) BookmarkToggleButton(...) } }
  16. @Composable fun KnightsTopAppBar( @StringRes titleRes: Int, ... isSessionDetail: Boolean, isTimetable:

    Boolean, isSomethingElse: Boolean, ) { ... Row(Modifier.align(Alignment.CenterEnd)) { if (isSessionDetail) BookmarkToggleButton(...) if (isTimetable) EditModeButton(...) if (isSomethingElse) SomethingElseButton(...) } } 컴포넌트끼리 강하게 결합, 직관적이지 않은 API 인터페이스 X
  17. 중복은 항상 제거되어야 할까? ❏ 조건문 추가는 컴포넌트가 수정되어야 하는

    이유가 다름을 의미한다. ❏ 이는 중복 제거와 재사용 대상이 아니라 복잡성의 시작이다 ❏ 조건문이 없는 컴포넌트를 만들기는 쉽지 않지만 조건문의 추가가 컴포넌트에 어떤 영향을 주는지 이해해야 한다. ❏ 재사용성을 고려할 때는 공통적인 요소만을 추출하고, 컴포넌트 고유의 속성은 외부에서 전달하는 방식을 고려해야 한다.
  18. @Composable fun KnightsTopAppBar( @StringRes titleRes: Int, ... actionButtons: @Composable ()

    -> Unit = {}, ) { ... Row(Modifier.align(Alignment.CenterEnd)) { actionButtons() } } Composition(조합) O
  19. enum class TopAppBarNavigationType { Back, Close } @Composable fun KnightsTopAppBar(

    @StringRes titleRes: Int, ... navigationType: TopAppBarNavigationType, actionButtons: @Composable () -> Unit = {}, ) { ... if (navigationType == TopAppBarNavigationType.Back) { icon( Modifier.align(Alignment.CenterStart), Icons.AutoMirrored.Filled.ArrowBack ) } Row(Modifier.align(Alignment.CenterEnd)) { actionButtons() } }
  20. 1. 재사용 가능한 컴포넌트 2. 결합도를 낮추고 응집도를 높인 설계

    3. 직관적인 API 디자인 -> 변경사항에 유연하게 대처
  21. Stateful vs Stateless 컴포넌트 일반적으로 다음과 같이 구분한다. ❏ 자체적으로

    상태를 생성/보유/수정하는 Stateful 컴포넌트 ❏ 자체적으로 상태를 생성/보유/수정하지 않는 Stateless 컴포넌트
  22. // Don’t @Composable fun StatefulComponent() { var username by remember

    { mutableStateOf("") } TextField( value = username, onValueChange = { username = it }, label = { Text("Username") } ) }
  23. 상태를 공유하거나 재사용하기 어렵게 만들고, 컴포넌트 간의 결합도를 높인다. 전제

    조건(precondition) 설정이 어려워 테스트하기 까다롭다. // Don’t @Composable fun StatefulComponent() { var username by remember { mutableStateOf("") } TextField( value = username, onValueChange = { username = it }, label = { Text("Username") } ) }
  24. // Do @Composable fun StatefulComponent() { var username by remember

    { mutableStateOf("") } StatelessComponent(username, onChange = { username = it }) } @Composable fun StatelessComponent(username: String, onChange: (String) -> Unit) { TextField( value = username, onValueChange = onChange, label = { Text("Username") } ) }
  25. // Do @Composable fun StatefulComponent() { var username by remember

    { mutableStateOf("") } StatelessComponent(username, onChange = { username = it }) } @Composable fun StatelessComponent(username: String, onChange: (String) -> Unit) { TextField( value = username, onValueChange = onChange, label = { Text("Username") } ) } State Hoisting 상태를 상위 컴포넌트로 옮겨 여러 컴포넌트에서 공유하고, UI의 상태를 쉽게 관리하고 테스트할 수 있다.
  26. Stateful vs Stateless 컴포넌트 일반적으로 다음과 같이 구분한다. ❏ 자체적으로

    상태를 생성/보유/수정하는 Stateful 컴포넌트 ❏ 자체적으로 상태를 생성/보유/수정하지 않는 Stateless 컴포넌트
  27. Stateful vs Stateless 컴포넌트 일반적으로 다음과 같이 구분한다. ❏ 자체적으로

    상태를 생성/보유/수정하는 Stateful 컴포넌트 ❏ 자체적으로 상태를 생성/보유/수정하지 않는 Stateless 컴포넌트 상태와 상태를 표시하는 UI를 분리하여 Stateless 컴포넌트를 위주로 테스트하자.
  28. 컴포넌트 테스트 시나리오 컴포넌트 Contract 분석 컴포넌트에 기대하는 동작, 사용하기

    적절한 상황 - 컴포넌트가 입력받는 파라미터는 무엇인가? - 어떤 전제 조건(precondition)을 설정할 수 있는가? - 어떤 Side effect가 발생하는가? 테스트가 필요한 항목 정의 불필요한 테스트 항목 제거
  29. 컴포넌트 테스트 시나리오 컴포넌트 Contract 분석 컴포넌트 전체를 한 번에

    테스트하기보다, 의미 있는 단위로 쪼개어진 컴포넌트를 각각 테스트한다. 세션 상세 정보 화면 예시: - SessionDetailUiState에 따라 로딩 UI 또는 세션 상세 UI(세션 제목, 내용, 발표자 정보, 태그, 시간)가 올바르게 표시되는지 확인 - 북마크 버튼 클릭 시 UI에 북마크 상태 변경이 반영되는지 확인 - 뒤로 가기 버튼 클릭 시 onBackClick 콜백 함수가 호출되는지 확인 ... 테스트가 필요한 항목 정의 불필요한 테스트 항목 제거
  30. 컴포넌트 테스트 시나리오 컴포넌트 Contract 분석 테스트하지 않아도 되는 항목

    - 불필요한 컴포넌트 내부 구현은 테스트하지 않는다. (예: 이미지 로딩 방식) - 일반적으로 Compose UI의 레이아웃 및 스타일에 적합한 테스트 방법이 따로 있다(Preview, Snapshot Testing 등) (예: 각 버튼 컴포넌트가 선형으로 쌓였는지, 여백은 몇 dp인지) 테스트가 필요한 항목 정의 불필요한 테스트 항목 제거
  31. @get:Rule val composeTestRule = createComposeRule() @Test fun 북마크_아이콘_클릭_시_북마크_상태가_변경된다() { var

    bookmarked by mutableStateOf(false) composeTestRule.setContent { KnightsTheme { SessionDetailTopAppBar( bookmarked = bookmarked, onClickBookmark = { bookmarked = !bookmarked }, onBackClick = { } ) } } } 1. 테스트 환경 세팅 2. 테스트를 위한 전제 조건 작성
  32. @Test fun 북마크_아이콘_클릭_시_북마크_상태가_변경된다() { var bookmarked by mutableStateOf(false) composeTestRule.setContent {

    ... } val bookmarkCheckbox = SemanticsMatcher.expectValue( SemanticsProperties.Role, Role.Checkbox ) composeTestRule.onNode(bookmarkCheckbox) .performClick() } 1. 테스트 환경 세팅 2. 테스트를 위한 전제 조건 작성 3. 테스트 대상 레이아웃 찾기
  33. 1. 테스트 환경 세팅 2. 테스트를 위한 전제 조건 작성

    3. 테스트 대상 레이아웃 찾기 @Test fun 북마크_아이콘_클릭_시_북마크_상태가_변경된다() { var bookmarked by mutableStateOf(false) composeTestRule.setContent { ... } val bookmarkCheckbox = SemanticsMatcher.expectValue( SemanticsProperties.Role, Role.Checkbox ) composeTestRule.onNode(bookmarkCheckbox) .performClick() } Layout Inspector -> Semantics Role 컴포넌트의 식별자를 명시하지 않더라도 컴포넌트의 역할 기반으로 구현체 가져오기
  34. @Test fun 북마크_아이콘_클릭_시_북마크_상태가_변경된다() { var bookmarked by mutableStateOf(false) composeTestRule.setContent {

    ... } val bookmarkCheckbox = SemanticsMatcher.expectValue( SemanticsProperties.Role, Role.Checkbox ) composeTestRule.onNode(bookmarkCheckbox) .performClick() .assertIsOn() assert(bookmarked) } 1. 테스트 환경 세팅 2. 테스트를 위한 전제 조건 작성 3. 테스트 대상 레이아웃 찾기 4. 검증부 (Assertion)
  35. 1. 테스트 환경 세팅 2. 테스트를 위한 전제 조건 작성

    3. 테스트 대상 레이아웃 찾기 4. 검증부 (Assertion) 5. 실행 결과 확인
  36. Screenshot Testing ❏ UI의 시각적 모습이 예상대로 표시되는지 검증 ❏

    여러 해상도나 디바이스별 검증에 적합 from: Handshake Blog
  37. Screenshot Testing 적절한 단위로 쪼개어진 컴포넌트별 테스트는 ❏ 한 번

    작성된 Screenshot 테스트를 여러 테스트 케이스에서 활용 가능 ❏ 업데이트 범위를 최소화 ❏ 디자인 구현 정확도를 높임(디자인 시스템) from: Handshake Blog
  38. 상황에 따라 Preview만으로도 충분 ❏ 모든 시나리오에 UI 테스트 작성은

    현실적으로 불가 ❏ UI의 레이아웃 및 스타일 검증은 Preview만으로도 충분할 수 있음 ❏ Stateless 컴포넌트로 구성하면 Precondition 설정 가능 ⬇Interactive Mode
  39. Compose Preview Screenshot Testing from: Google I/O 2024 ❏ Preview

    기반 Reference 이미지 생성 ❏ Screenshot Testing의 장점과 Preview의 장점을 결합 ❏ 아직 실험적 단계
  40. Compose UI 테스트 방법 및 도구 정리 ❏ Compose UI

    Testing ❏ Screenshot Testing ❏ Compose Preview
  41. ❏ Compose UI Testing ❏ Screenshot Testing ❏ Compose Preview

    Compose UI 테스트 방법 및 도구 정리 테스트 용도와 현실적인 리소스에 따라 선택이 달라진다. 어떤 방법을 선택하더라도 적절한 단위의 컴포넌트 분리와 테스트 가능한 컴포넌트 설계는 중요한 요소다.
  42. 1. 재사용 가능한 컴포넌트 2. 결합도를 낮추고 응집도를 높인 설계

    3. 직관적인 API 디자인 4. 상태와 상태를 표시하는 UI 분리 -> 변경사항에 유연하게 대처 -> 테스트 가능한 컴포넌트
  43. AOSP (Android Open Source Project) https://source.android.com AOSP는 안드로이드 운영 체제의

    소스 코드를 관리하는 프로젝트로, 누구나 소스 코드를 열람하고 코드와 문서로 기여할 수 있다.
  44. AOSP - Compose 컴포넌트 테스트 이러한 테스트 코드는 단순히 기능을

    검증하는 것 뿐만 아니라, 해당 컴포넌트가 어떻게 사용되어야 하는지, 어떤 결과물이 기대되는지에 대한 실질적인 문서 역할을 하기도 한다. ❏ ButtonTest ❏ TextFieldTest ❏ LazyColumnTest ❏ SideEffectTests
  45. 참고자료 ❏ 안드로이드 공식 문서 - Compose ❏ React 공식

    문서 - Thinking in react ❏ The Right Way to Test React Components - Stephen Scott ❏ 변경에 유연한 컴포넌트 - jbee