Share
Embed
Transcript
76 slides
-
❏ 우아한형제들 Android Dev/Educator ❏ NEXTSTEP Educator/Code Reviewer ❏ DroidKnights
Conference Organizer ❏ GDG Korea Android Organizer GitHub @wisemuji -
이야기할 내용 ❏ Compose UI 컴포넌트 설계의 중요성 ❏ React로
살펴보는 선언형 UI 컴포넌트 설계 노하우 ❏ 사례로 알아보는 UI 컴포넌트 설계 팁 ❏ 사례로 알아보는 UI 컴포넌트 테스트 -
다루지 않는 내용 ❏ UI 테스트 프레임워크 사용법 ❏ 웹
프론트엔드 및 React 개념 ❏ 컴포넌트 아키텍처 패턴 ❏ Compose Stability “구현체가 아닌 인터페이스에 의존" -
컴포넌트를 나누는 기준 ❏ 재사용 가능성 ❏ 테스트 가능성 ❏
하나의 역할/책임 ❏ 변경에 유연함 ❏ … -
컴포넌트를 나누는 기준 ❏ 재사용 가능성 ❏ 테스트 가능성 ❏
하나의 역할/책임 ❏ 변경에 유연함 ❏ … -
❏ Case1: 화면 단위 구현 ❏ Case2: 컴포넌트 단위 구현
드로이드나이츠 앱 github.com/droidknights/DroidKnightsApp -
변경사항이 생긴다면? ❏ 새로운 기능이 추가된다면? ❏ 디자인 스타일이 바뀐다면?
❏ 아이템의 배치가 변경된다면? -
변경사항이 생긴다면? ❏ 새로운 기능이 추가된다면? ❏ 디자인 스타일이 바뀐다면?
❏ 아이템의 배치가 변경된다면? 북마크 기능 추가 -
(중략)… https://android.googlesource.com/…/compose-component-api-guidelines.md 하나의 컴포넌트는 하나의 문제만 해결해야 한다. 컴포넌트가 하나
이상의 문제를 해결한다면 하위 계층 또는 하위 컴포넌트로 분할하는 것을 고려하라. 작고 간결한 API로 디자인할수록 사용하기 쉽고, 컴포넌트 인터페이스를 명확하게 이해할 수 있다. -
UI 컴포넌트 설계의 중요성 ❏ Android View에서는 화면(Activity, Fragment) 단위의
구현이 일반적이였으나, Compose에서는 선언적으로 작성된 컴포넌트로 쪼개어 설계할 수 있다. ❏ 적절한 컴포넌트 단위를 나누어 변경사항에 유연한 설계를 지향하자. ❏ 결합도를 낮추고 응집도를 높이자. -
선언형 UI 패러다임 선배들 ❏ React - 2013 JSConf US
오픈소스화 ❏ Flutter - 2015 다트 개발자 서밋 첫 공개 ❏ SwiftUI - 2019 WWDC 첫 소개 ❏ Jetpack Compose - 2021 Stable 버전 정식 출시 -
선언형 UI 패러다임 선배들 ❏ React - 2013 JSConf US
오픈소스화 ❏ Flutter - 2015 다트 개발자 서밋 첫 공개 ❏ SwiftUI - 2019 WWDC 첫 소개 ❏ Jetpack Compose - 2021 Stable 버전 정식 출시 -
Jetpack Compose는? 선언형 UI 선배들에 비해 아직은 ʻ좋은 컴포넌트 설계’에
관한 연구가 압도적으로 부족 -
React 컴포넌트 설계 Best Practices React 개발자들이 공유하는 핵심 설계
원칙은 Compose UI에도 적용 가능 ❏ 함수형 컴포넌트 ❏ 작고 재사용 가능한 컴포넌트 ❏ UI와 비즈니스 로직의 분리 ❏ Atomic Design -
선언형 UI 패러다임은 서로 밀접하게 닿아있다 상대적으로 역사가 긴 선배
프레임워크 개발자들의 노하우를 참고할 수 있을 것이다. -
중복은 항상 제거되어야 할까? ❏ 외관상 중복된 코드라도 수정의 이유가
다르면 실제로는 중복이 아니다. ❏ 중복 제거를 강조하는 DRY(Don't repeat yourself) 원칙을 과도하게 적용함으로써 발생하는 의존성 문제와 조건문 추가로 인한 복잡성 증가는 오히려 유지보수를 어렵게 할 수 있다. -
@Composable fun KnightsTopAppBar( @StringRes titleRes: Int, ... isSessionDetail: Boolean, )
{ ... Row(Modifier.align(Alignment.CenterEnd)) { if (isSessionDetail) BookmarkToggleButton(...) } } -
@Composable fun KnightsTopAppBar( @StringRes titleRes: Int, ... isSessionDetail: Boolean, )
{ ... Row(Modifier.align(Alignment.CenterEnd)) { if (isSessionDetail) BookmarkToggleButton(...) } } -
@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 -
중복은 항상 제거되어야 할까? ❏ 조건문 추가는 컴포넌트가 수정되어야 하는
이유가 다름을 의미한다. ❏ 이는 중복 제거와 재사용 대상이 아니라 복잡성의 시작이다 ❏ 조건문이 없는 컴포넌트를 만들기는 쉽지 않지만 조건문의 추가가 컴포넌트에 어떤 영향을 주는지 이해해야 한다. ❏ 재사용성을 고려할 때는 공통적인 요소만을 추출하고, 컴포넌트 고유의 속성은 외부에서 전달하는 방식을 고려해야 한다. -
@Composable fun KnightsTopAppBar( @StringRes titleRes: Int, ... actionButtons: @Composable ()
-> Unit = {}, ) { ... Row(Modifier.align(Alignment.CenterEnd)) { actionButtons() } } Composition(조합) O -
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() } } -
1. 재사용 가능한 컴포넌트 2. 결합도를 낮추고 응집도를 높인 설계
3. 직관적인 API 디자인 -> 변경사항에 유연하게 대처 -
Stateful vs Stateless 컴포넌트 일반적으로 다음과 같이 구분한다. ❏ 자체적으로
상태를 생성/보유/수정하는 Stateful 컴포넌트 ❏ 자체적으로 상태를 생성/보유/수정하지 않는 Stateless 컴포넌트 -
// Don’t @Composable fun StatefulComponent() { var username by remember
{ mutableStateOf("") } TextField( value = username, onValueChange = { username = it }, label = { Text("Username") } ) } -
상태를 공유하거나 재사용하기 어렵게 만들고, 컴포넌트 간의 결합도를 높인다. 전제
조건(precondition) 설정이 어려워 테스트하기 까다롭다. // Don’t @Composable fun StatefulComponent() { var username by remember { mutableStateOf("") } TextField( value = username, onValueChange = { username = it }, label = { Text("Username") } ) } -
// 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") } ) } -
// 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의 상태를 쉽게 관리하고 테스트할 수 있다. -
Stateful vs Stateless 컴포넌트 일반적으로 다음과 같이 구분한다. ❏ 자체적으로
상태를 생성/보유/수정하는 Stateful 컴포넌트 ❏ 자체적으로 상태를 생성/보유/수정하지 않는 Stateless 컴포넌트 -
Stateful vs Stateless 컴포넌트 일반적으로 다음과 같이 구분한다. ❏ 자체적으로
상태를 생성/보유/수정하는 Stateful 컴포넌트 ❏ 자체적으로 상태를 생성/보유/수정하지 않는 Stateless 컴포넌트 상태와 상태를 표시하는 UI를 분리하여 Stateless 컴포넌트를 위주로 테스트하자. -
좀 더 일반적인 이야기 “그래서 테스트 가능한 컴포넌트가 뭔데?” “컴포넌트
설계와 테스트가 어떤 연관이 있는걸까?” -
컴포넌트 테스트 시나리오 컴포넌트 Contract 분석 컴포넌트에 기대하는 동작, 사용하기
적절한 상황 - 컴포넌트가 입력받는 파라미터는 무엇인가? - 어떤 전제 조건(precondition)을 설정할 수 있는가? - 어떤 Side effect가 발생하는가? 테스트가 필요한 항목 정의 불필요한 테스트 항목 제거 -
컴포넌트 테스트 시나리오 컴포넌트 Contract 분석 테스트가 필요한 항목 정의
불필요한 테스트 항목 제거 from: Design by Contract -
컴포넌트 테스트 시나리오 컴포넌트 Contract 분석 컴포넌트 전체를 한 번에
테스트하기보다, 의미 있는 단위로 쪼개어진 컴포넌트를 각각 테스트한다. 세션 상세 정보 화면 예시: - SessionDetailUiState에 따라 로딩 UI 또는 세션 상세 UI(세션 제목, 내용, 발표자 정보, 태그, 시간)가 올바르게 표시되는지 확인 - 북마크 버튼 클릭 시 UI에 북마크 상태 변경이 반영되는지 확인 - 뒤로 가기 버튼 클릭 시 onBackClick 콜백 함수가 호출되는지 확인 ... 테스트가 필요한 항목 정의 불필요한 테스트 항목 제거 -
컴포넌트 테스트 시나리오 컴포넌트 Contract 분석 테스트하지 않아도 되는 항목
- 불필요한 컴포넌트 내부 구현은 테스트하지 않는다. (예: 이미지 로딩 방식) - 일반적으로 Compose UI의 레이아웃 및 스타일에 적합한 테스트 방법이 따로 있다(Preview, Snapshot Testing 등) (예: 각 버튼 컴포넌트가 선형으로 쌓였는지, 여백은 몇 dp인지) 테스트가 필요한 항목 정의 불필요한 테스트 항목 제거 -
@get:Rule val composeTestRule = createComposeRule() @Test fun 북마크_아이콘_클릭_시_북마크_상태가_변경된다() { var
bookmarked by mutableStateOf(false) composeTestRule.setContent { KnightsTheme { SessionDetailTopAppBar( bookmarked = bookmarked, onClickBookmark = { bookmarked = !bookmarked }, onBackClick = { } ) } } } 1. 테스트 환경 세팅 2. 테스트를 위한 전제 조건 작성 -
@Test fun 북마크_아이콘_클릭_시_북마크_상태가_변경된다() { var bookmarked by mutableStateOf(false) composeTestRule.setContent {
... } val bookmarkCheckbox = SemanticsMatcher.expectValue( SemanticsProperties.Role, Role.Checkbox ) composeTestRule.onNode(bookmarkCheckbox) .performClick() } 1. 테스트 환경 세팅 2. 테스트를 위한 전제 조건 작성 3. 테스트 대상 레이아웃 찾기 -
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 컴포넌트의 식별자를 명시하지 않더라도 컴포넌트의 역할 기반으로 구현체 가져오기 -
@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) -
1. 테스트 환경 세팅 2. 테스트를 위한 전제 조건 작성
3. 테스트 대상 레이아웃 찾기 4. 검증부 (Assertion) 5. 실행 결과 확인 -
Screenshot Testing ❏ UI의 시각적 모습이 예상대로 표시되는지 검증 ❏
여러 해상도나 디바이스별 검증에 적합 from: Handshake Blog -
Screenshot Testing 적절한 단위로 쪼개어진 컴포넌트별 테스트는 ❏ 한 번
작성된 Screenshot 테스트를 여러 테스트 케이스에서 활용 가능 ❏ 업데이트 범위를 최소화 ❏ 디자인 구현 정확도를 높임(디자인 시스템) from: Handshake Blog -
상황에 따라 Preview만으로도 충분 ❏ 모든 시나리오에 UI 테스트 작성은
현실적으로 불가 ❏ UI의 레이아웃 및 스타일 검증은 Preview만으로도 충분할 수 있음 ❏ Stateless 컴포넌트로 구성하면 Precondition 설정 가능 ⬇Interactive Mode -
Compose Preview Screenshot Testing from: Google I/O 2024 ❏ Preview
기반 Reference 이미지 생성 ❏ Screenshot Testing의 장점과 Preview의 장점을 결합 ❏ 아직 실험적 단계 -
Compose UI 테스트 방법 및 도구 정리 ❏ Compose UI
Testing ❏ Screenshot Testing ❏ Compose Preview -
❏ Compose UI Testing ❏ Screenshot Testing ❏ Compose Preview
Compose UI 테스트 방법 및 도구 정리 테스트 용도와 현실적인 리소스에 따라 선택이 달라진다. 어떤 방법을 선택하더라도 적절한 단위의 컴포넌트 분리와 테스트 가능한 컴포넌트 설계는 중요한 요소다. -
1. 재사용 가능한 컴포넌트 2. 결합도를 낮추고 응집도를 높인 설계
3. 직관적인 API 디자인 4. 상태와 상태를 표시하는 UI 분리 -> 변경사항에 유연하게 대처 -> 테스트 가능한 컴포넌트 -
AOSP (Android Open Source Project) https://source.android.com AOSP는 안드로이드 운영 체제의
소스 코드를 관리하는 프로젝트로, 누구나 소스 코드를 열람하고 코드와 문서로 기여할 수 있다. -
AOSP - Compose 컴포넌트 테스트 이러한 테스트 코드는 단순히 기능을
검증하는 것 뿐만 아니라, 해당 컴포넌트가 어떻게 사용되어야 하는지, 어떤 결과물이 기대되는지에 대한 실질적인 문서 역할을 하기도 한다. ❏ ButtonTest ❏ TextFieldTest ❏ LazyColumnTest ❏ SideEffectTests -
참고자료 ❏ 안드로이드 공식 문서 - Compose ❏ React 공식
문서 - Thinking in react ❏ The Right Way to Test React Components - Stephen Scott ❏ 변경에 유연한 컴포넌트 - jbee -
엄청난 아키텍처 패턴이나 방법론을 고려하기 전에, 변경사항에 대처하기 쉽고 유연한
설계부터 시작하자 즉, 기본기를 되새기자