모바일 UI 개발의 새로운 혁명
Jetpack Compose와 SwiftUI를 이용한 새로운 UI 제작
op.gg 세미나에서 발표했던 내용을 기록용으로 남긴다. 원래 유튜브에 올릴려고 했는데 첫 발표라 너무 긴장해서 그런지 녹화를 못 했다…
안녕하세요 저는 모바일 UI 개발의 새로운 혁명 이라는 주제로 발표할
프로그래밍 방송을 하는 스트리머이자, 좋은 개발자가 되기 위해 항상 노력하는 지성빈 입니다. 안드로이드 개발을 시작한지 7년정도 되었고, iOS 개발은 5개월 정도 되었습니다.
발표 주제처럼 왜 새로운 UI 개발의 혁명이 필요할까요? 우선 지금까지의 UI 개발은 사용자 인터페이스의 상태를 항상 추적하고 연결해주어야 합니다. 또한 모두 명령형 프로그래밍으로 이루어져 있습니다. 저는 이 명령형 프로그래밍이라는 점에서 매우 큰 불편함을 느꼈습니다.
안드로이드에서의 UI 개발로 예시를 들어보겠습니다. 우선 안드로이드에서 커스텀뷰를 만들기 위해서 총 3가지의 작업이 필요합니다. 먼저 커스텀뷰에서 쓰일 속성들을 선언해주어야 합니다. 속성 선언 후, 이제 커스텀뷰가 어떻게 보일지를 작성해주어야 되겠죠? 이제 작성해준 뷰가 앞써 만든 속성들과 어울려 어떻게 동작할지를 설계해주어야 합니다. 커스텀뷰를 만드는 과정들이 너무 번거롭습니다! 안드로이드 개발자분들은 이러한 과정들이 얼마나 귀찮은지 아실겁니다.
또한, 버튼은 텍스트뷰를 상속하여 만들어졌기에 버튼의 텍스트가 선택되는 불참사가 발생할 수 있습니다.
그리고, 안드로이드 모든 뷰의 부모 클래스인 View.java의 코드라인이 3만줄을 향해 가고 있습니다. 부모 클래스의 줄 수가 너무 길어짐에 의해, 레거시 코드들이 감당이 안되고 유지보수가 힘들어지는 상황이 발생합니다.
이러한 단점들을 극복하기 위해 선언형 프로그래밍으로 UI 개발을 하는 여러가지 방법들이 탄생합니다. ios에서는 핀터레스트가 만든 AsyncDisplayKit에서 탄생한 Texture와,안드로이드에서는 Kotlin의 Anko, Facebook의 Litho, Airbnb의 Epoxy가 대표적으로 사용되었습니다.
또한 모바일 개발자분들이라면 한 번쯤은 접해보셨을법한 Google의 Flutter 또한 이를 극복하기 위한 노력으로 조명받았습니다. 하지만 플러터는 이 발표의 목적보다는 크로스 플렛폼 개발 위주로 사용되므로 더 이상 다루지 않겠습니다.
그렇다면, 이 4가지 방법들을 쓰면 UI 개발이 좋아질까요? 아쉽게도 4가지 모두 3rd-party 라이브러리 입니다. 따라서 개발 환경 안에서 프리뷰가 불가능하고, 오히려 앱 자체의 용량만 늘어납니다. 그리고 hot-reload 또한 지원되지 않습니다.
이러한 문제점들이 날뛰는 가운데, 드디어 안드로이드와 iOS에서 공식적으로 지원하는 선언형 UI 개발 프레임워크의 등장으로 인해 모바일 UI의 새로운 혁명이 시작됩니다.
iOS 먼저 알아봅시다. iOS 개발자 분들이라면 SwiftUI를 한 번쯤은 들어보셨을겁니다. SwiftUI는 WWDC 2019에서 처음 공개되었습니다. SwiftUI는 이름부터 나와있드시 Swift이기 때문에 Objective-C로는 사용이 불가능합니다. 애플은 이 SwiftUI를 “고급 앱 경험 및 도구”, “향상된 손쉬운 사용”, “macOS에서 향상된 SwiftUI”, “상세표시형 Retina 디스플레이 지원”, 그리고 “iPadOS용 위젯”. 이 5가지를 중심으로 소개하고 있습니다.
안드로이드는 어떨까요? 안드로이드에서는 Jetpack Compose라는 이름으로 등장하였습니다. 정식으로 나온지 몇달 되지 않아, 생소하거나 처음 들어보시는 안드로이드 개발자 분들도 계실겁니다. 이 Jetpack Compose는 작년 4월 mavenCentral에 처음으로 릴리즈되면서 등장합니다. 이로부터 2달 후인, 6월에 유튜브에 소개 영상이 올라가면서 세상에 조금씩 알려지기 시작합니다. 이렇게 관심을 받으면서 개발되다가, 올해 6월에 마지막 개발 버전이 릴리즈되고, 저번달 말에 정식 버전이 릴리즈되었습니다.
Compose는 정식으로 나온지 한달밖에 안되었지만, 기존 방식보다 UI 개발이 워낙 쉬워져서 구글 플레이스토어와 트위터, 또한 저희 팀 앱이 이 Compose를 사용하여 개발되고 있습니다.
그렇다면 SwiftUI와 Jetpack Compose를 사용하면 무엇이 좋을까요? 반복적으로 말했듯이, 선언형 프로그래밍으로 개발되므로 알고리즘 명시가 사라져 코드가 매우 짧아집니다. 또한 사용자 인터페이스의 상태를 별도로 추적해 줄 필요가 없고, Compose의 경우 웹에서만 보던 hot-reload가 가능합니다. 마지막으로 기존의 XML과 스토리보드에 비해, 매우 빠른 시간안에 UI 제작이 가능해집니다.
이제 충분히 이론을 알아봤으니, 직접 써봅시다. SwiftUI부터 시작합니다. View를 상속받는 구조체를 만든 후, 이 구조체 안에 some View 타입인 body 변수의 연산 프로퍼티를 사용함으로써 SwiftUI를 시작할 수 있습니다. 이때 some이라는 새로운 키워드가 등장합니다. 간단하게 설명하자면, some은 불투명한 타입으로 만들어주는 역할을 해줍니다. SwiftUI에 대해 더 알아본 후 some에 대해 자세히 알아보도록 하겠습니다.
SwiftUI의 생명주기는 어떻게 제어할 수 있을까요? 정답은 onAppear 메서드와 onDisappear 메서드에 있습니다. 각각 뷰가 보여졌다가, 사라질때 호출됩니다.
이제 상태 시스템에 대해 알아봅시다. 상태 시스템이란 무엇일까요? 상태는 State라고 불리며, State는 SwiftUI에서 값의 변화를 감지할 수 있는 property wrapper 입니다. 이 State를 사용함으로써, 사용자 인터페이스의 상태를 항상 추적해줄 필요가 없어집니다.
그렇다면 State는 어떻게 만들 수 있을까요? state로 쓸 변수에 state annotation을 붙이면 해당 변수가 state로 처리되게 됩니다. state로 처리된 변수의 값에 변경이 일어날 경우, 자동으로 해당 변수가 쓰인 부분이 바뀐 변수값으로 다시 로드가 이뤄집니다.
다음으로 Jetpack Compose의 사용법에 대해 알아봅시다. Compose를 설명하기 앞써, 기본 용어에 대해 먼저 설명하겠습니다. Compose를 사용하기 위해선, composable annotation이 필요합니다. 이 composable annotation이 붙어있는 부분만 compose를 사용할 수가 있고, 이렇게 사용할 수 있는 부분을 composable이라고 부릅니다. 이 composable들을 이용하여 UI가 그려지는 과정을 composition 이라고 부릅니다.
Compose를 사용하러면 기존처럼 AppCompatActivity가 아닌, ComponentAcitivty를 상속받아 클래스를 생성해야 합니다. ComponentActivity의 onCreate에서의 setContent 함수는 기본적으로 composable 상태이기 때문에, 이 함수를 사용하여 초기 composition을 해줄 수 있습니다. 이때 composable이 호출되는 공간을 call site라고 부릅니다. 또한 함수에 직접 composable annotation을 붙여, 새로운 composable을 만들어줄 수 있습니다.
compose의 생명주기는 어떻게 될까요? composition이 처음 이루어지는 초기-composition 상태,상태가 변경되어 새로운 UI를 다시 그려주는 re-composition 상태,그리고 더 이상 composition이 발생할 수 없는 상태. 이렇게 총 3가지의 상태로 이루어져 있습니다.
Compose에서의 상태는 무엇일까요? SwiftUI의 state와 마찬가지로, compose 또한 상태를 state라고 부르며, state는 composable에서 값의 변화를 감지할 수 있는 value holder 입니다. Compose 또한, state를 사용함으로써 사용자 인터페이스의 상태를 항상 추적해줄 필요가 없어집니다.
Compose에서 state는 다음과 같이 mutableStateOf라는 메서드로 만들어줄 수 있습니다. 이렇게 만들어진 state의 값이 변경될때마다 이 state를 사용중인 composable에서 re-composition이 진행됩니다.
re-composition은 composable을 다시 호출하는 방식으로 진행됩니다. 근데 만약, composable 함수 안에 선언한 state가 변경되어 re-composition이 진행되면,함수가 다시 호출되면서 해당 state 변수가 초깃값으로 초기화가 진행 되면서, 바뀐 state값이 사라지게 됩니다. 이럴때 remember라는 키워드를 사용하여 re-composition시 값이 사라지는걸 막아줄 수 있습니다. remember를 통해 값를 감싸주게 되면, re-composition이 진행되기 전에 값을 미리 기억해 뒀다가,re-composition이 완료되면, 미리 기억했던 값을 다시 불러오게 됩니다.
안드로이드에서는 엑티비티가 onStop 될 때 onSaveInstanceState 메서드가 호출되어 값을 저장하고, onCreate의 savedInstanceState 인자로 저장했던 값을 불러올 수 있습니다. 이를 compose에서도 rememberSaveable을 이용하여 사용할 수 있습니다. rememberSaveable을 이용하여 값을 감싸게 되면, 해당 값을 자동으로 번들에 저장했다가, 나중에 앱이 다시 활성화 될 때 번들로 부터 값을 자동으로 불러옵니다. 이를 이용하여 화면이 회전됐을때와 같이 생기는 데이터 유실을 막아줄 수 있습니다.
이제 간단한 레이아웃을 알아봅시다. 세로 레이아웃은 SwiftUI에서는 VStack, Compose에서는 Column으로 사용할 수 있습니다. 이때, 배치 사진을 보면 SwiftUI는 가운데에 배치된 반면, Compose는 상단에 배치되었습니다. 왜그럴까요?
이는 내부를 보면 원인을 알아볼 수 있습니다. SwiftUI는 기본적으로 전체 화면 영역에서 가운데에 배치되게 설정돼 있습니다. 하지만 Compose는 요소가 차지하는 영역에서 상단 왼쪽에 배치되게 설정돼 있습니다. 이러한 차이 때문에 같은 세로 배치에서 차이점이 보이게 됩니다.
가로 레이아웃은, SwiftUI는 HStack, compose는 Row로 사용할 수 있습니다.
마지막으로 중첩 레이아웃 입니다. SwiftUI는 ZStack, Compose는 Box로 사용할 수 있습니다. 이때 가로, 세로 레이아웃과는 다르게, Compose도 배치가 가운데에 돼있는걸 보실 수 있습니다. 이는 Box에 modifier 속성을 이용하여, 전체 화면 영역을 사용하게 해주고, 가운데로 정렬을 해주었기 때문입니다. 모든 Composable에는 modifier라는 속성이 있습니다. Modifier은 해당 composable이 화면에 어떻게 보일지를 정해주는 역할을 합니다.
이제 아이템들에 대해 알아봅시다. 우선 기본적인 텍스트 먼저 시작합니다. 눈치 채신분들도 계시겠지만, SwiftUI와 Compose 모두 Text로 사용할 수 있습니다. 이 텍스트에 폰트 색상과 크기 등등 커스터마이징을 해주려면 어떻게 해야 할까요?
SwiftUI 먼저 내부 구현을 보도록 하겠습니다. SwiftUI는 모두 초깃값밖에 설정을 못 해주는 구조체로 구현돼 있습니다. 따라서 SwiftUI에서 아이템들을 커스텀마이징 해주려면
Method-Chaining 을 이용해야 합니다. foregroundColor 메서드로 화면에 보일 색깔을 정해줄 수 있습니다. font 메서드로는 화면에 표시될 텍스트의 폰트에 대한 여러가지 옵션을 줄 수 있습니다. 이 예시 코드에서는 텍스트를 파란색으로 보이게 하고, 폰트 옵션으로 크기만 30으로 설정해 주었습니다.
이때 foregroundColor에는 무조건 Color와, font에는 무조건 Font밖에 못 들어가기에, 이러한 부분들을 생략해줄 수 있습니다.
이제 Compose를 알아볼 차례입니다. Compose는 모든 구현이 default argument가 사용된 함수로 이루어져 있습니다. 따라서 Compose는 코틀린만 사용 가능하다는 사실과, 함수의 인자값으로 사용과 동시에 커스터마이징을 해줄 수 있다는 사실을 알 수 있습니다.
텍스트의 색상과 크기를 변경하여 사용해보겠습니다. 코틀린의 named argument 속성을 이용하여, 원하는 인자값만 설정해 줄 수 있습니다. 이 예시 코드에서는 SwiftUI와 동일하게, 텍스트를 파란색으로 보이게 하고, 폰트 옵션으로 크기만 30으로 설정해 주었습니다.
다음은 이미지 입니다. SwiftUI에서 이미지는 이미지 구조체에 Xcode의 에셋 카탈로그에 추가한 이미지 이름 그대로 사용하여 사용할 수 있습니다. 간단하게 opgg의 로고를 출력해 보았습니다.
이제 이미지를 변형해 보겠습니다. resizable 메서드를 이용하여, 크기를 변경 가능하게 만들어주고,뷰의 크기를 정해주는 역할을 하는 frame 메서드를 사용하여 이미지의 크기를 300으로 맞춰주었습니다. 또한 colorMultiply 메서드를 이용하여 전체적으로 보라색을 더해 주었습니다. 마지막으로 Circle 모양으로 clipShape 메서드를 추가해, 원형으로 만들어 주었습니다. 따라서 다음과 같은 이미지가 탄생하게 됩니다. 이미지를 완전 원형으로 만드는게 아닌, 테두리만 둥글게도 할 수 있을까요?
cornerRadius 메서드에 원형 강도를 설정해주어 테두리만 둥글게 만들어줄 수 있습니다.
Compose에서의 이미지는 어떨까요? Compose 또한 Image라는 함수를 사용하여 이미지를 보여줄 수 있습니다. painter에 drawable에 추가한 이미지를 painterResource 함수로 불러와 보일 이미지를 설정해줄 수 있고,contentDescription 또한 설정해줄 수 있습니다.
이미지를 변경해보겠습니다. Modifier을 이용하여, size 메서드로 사이즈를 300으로 맞춰주고, clip 메서드로 CircleShape를 적용하여 모양을 원형으로 설정하였습니다. 여기에 ColorFilter로 파란색 틴트를 설정하여, 이미지를 파란색으로 바꿔보았습니다.
Compose 또한, 전체를 원형으로 만드는게 아닌, RoundedCornerShape 함수 안에 원형 강도를 설정하여 테두리만 둥글게 설정해 줄 수 있습니다.
다음은 버튼입니다. SwiftUI에서 버튼은 Button 구조체를 사용하여 만들어 줄 수 있습니다. action 인자에 버튼이 눌렀을 때 작동할 코드를 설정해줄 수 있습니다. 이때 Button은 그냥 클릭될 수 있는 요소만을 나타내지, 어떻게 보일지는 설정되있지 않습니다. 따라서 Button의 연산 프로퍼티에 버튼으로 보여질 뷰들을 작성해줘야 합니다. 이 예시의 실행 화면을 봤을 때 이게 텍스트인지 버튼인지 구분하기 어렵습니다.
따라서 버튼을 버튼처럼 만들어주는 작업이 필요합니다. 텍스트에 패딩 메서드로 상하좌우 패딩을 넣어주고, 오버레이 메서드로 모서리가 둥근 사각형 도형에 테두리와 그림자 메서드를 더해주어, 모서리가 둥근 테두리 및 그림자를 설정해주었습니다. 또한 foregroundColor 메서드로 모든 색깔을 회색으로 통일 시켜, 최종적으로 버튼을 버튼처럼 만들어 보았습니다.
Compose에서도 버튼을 Button 함수로 만들어 줄 수 있습니다. onClick 인자로 버튼이 눌렀을 때 작동할 코드를 써주고,Button 함수의 content 고차함수 인자로 버튼이 어떻게 보일지를 설정할 수 있습니다. 이때 composable 함수에서 모든 content 인자는 default argument가 아니기 때문에, named argument 속성 없이 그냥 바로 사용해줄 수 있습니다. 또한 compose의 버튼은 따로 버튼처럼 만들어주지 않아도, 자동으로 content가 버튼 테마에 맞게 보여집니다.
텍스트필드에 대해 알아보겠습니다. 텍스트필드는, 직접 텍스트을 키보드로 입력할 수 있는 뷰를 뜻하고,이를 사용하려면 SwiftUI와 Compose 모두 State가 꼭 필요합니다. SwiftUI의 경우, TextField 구조체를 placeholder로 보일 텍스트와 함께 사용해 만들어 줄 수 있고,text 인자에 들어간 state 변수의 값으로 텍스트를 보여줍니다. 이때 state 변수를 사용하면서 $ 기호가 붙었습니다. 이 $ 기호는 state 변수를 프로퍼티 래퍼 자체로 받아서, 래퍼 안에 있는 WrapperValue 자체를 변경할 수 있게 만들어줍니다. 따라서 텍스트 입력이 변경될 때마다 이 field 변수값이 변경되어, 바뀐 새로운 텍스트로 표시되게 됩니다.
그냥 TextField만 있으니 이게 Text인지 TextField인지 구분이 힘들거 같아, TextField의 스타일을 모서리가 둥근 테두리로 설정해 보았습니다. 이런식으로 textFieldStyle 메서드를 사용하여 텍스트필드의 스타일을 변경해줄 수 있습니다.
Compose에서도 동일하게 TextField 함수로 사용할 수 있습니다. 이때 함수의 value 인자로 보여질 텍스트를 갖는 TextFieldValue 변수를 넣어줘야 합니다.따라서 TextFieldValue를 갖는 state 변수를 만들어 주고, compose의 state는 value holder 이므로, 값을 가져오기 위해 value getter를 사용하여 value 인자에 넣어줍니다. onValueChange 인자는 텍스트가 입력될 때 마다, 고차함수의 인자로 새로 바뀐 TextFieldValue 값을 갖고 호출됩니다. 기존 TextFieldValue를 새로운 TextFieldValue로 바꿔주는 로직을 onValueChange안에 작성해 줍니다. 이때 state의 값을 변경해주기 위해 value setter를 사용해줍니다.
TextField 함수를 OutlinedTextField로 변경하여, 텍스트필드의 스타일을 바꿔줄 수 있습니다. 또한 onValueChange 로직에 입력받은 텍스트가 영어일때만 값을 바꿔주는 if문을 추가하여 영어만 입력 가능하게 구현할 수도 있습니다. 이때 예시 코드를 보시면, state 변수가 델리게이트 패턴으로 선언되었습니다. 따라서 state에 접근할 때 getter와 setter 없이 접근이 가능해 집니다.
이제 마지막으로 반복되는 뷰들을 표시하는 방법에 대해 알아보겠습니다. SwiftUI에서는 LazyVStack으로 반복되는 뷰들을 세로로 표시할 수 있습니다. LazyHStack을 하게 되면 가로로 표시가 가능합니다. LazyVStack에 SwiftUI에서 뷰 정렬을 담당해 주는 alignment 인자로 center를 주고, 안에 1부터 50까지 표시하는 Text를 넣어주었습니다. 또한 Text에 padding 메서드를 더해주어 사이 간격을 띄어주었습니다. 하지만 이대로 하면 스크롤이되지 않습니다. 따라서 전체를 ScrollView로 감싸주어 스크롤이 가능하게 해주었습니다.
여기에 sticky header 또한 넣어줄 수 있습니다. some View 타입의 변수에 헤더로 보일 뷰를 작성해줍니다. 그리고 LazyVStack 안에 헤더가 들어갈 부분에 Section을 열어주고, header 인자로 아까 만들어줬던 헤더로 보일 뷰 변수를 넣어줍니다. 이제 LazyVStack의 pinnedViews 인자로 sectionHeaders 값을 넣어주면 sticky header가 보여지게 됩니다. 여기에 각 뷰 사이 공간을 띄어주기 위해 뷰에 padding 메서드를 쓰는 방법 대신에, LazyVStack의 spacing 인자를 이용할 수도 있습니다.
Compose에서는 LazyColumn으로 반복되는 아이템들을 세로로 표시할 수 있습니다. LazyRow로 하면 아이템들을 가로로 표시하게 됩니다. LazyColumn 함수 인자에 modifier로 전체 화면 영역을 사용하게 해주고, horizontal alignment로 center값을 설정해주어, 가로에서 가운데 정렬을 해주었습니다. LazyColumn 안에 items 함수를 이용하여, items 인자에 반복될 아이템들의 데이터값을 넘겨줄 수 있고, content 인자로 각각 아이템들의 데이터값을 고차함수의 인자로 받으면서, 아이템들이 어떻게 보일지를 설정할 수 있습니다. 이 예시 코드에서는, 0부터 49까지 담긴 배열을 넘겨주었습니다. items 함수의 content 값으로 아이템의 데이터값을 보여주는 텍스트로 설정해주었고, 세로 패딩을 30만큼 주었습니다. 따라서 오른쪽 사진처럼 작동하게 됩니다. Compose는 SwiftUI 와 달리, 별도 ScrollView를 설정해주지 않아도 스크롤이 가능합니다.
Compose또한 스티키 헤더(sticky header) 를 설정해줄 수 있습니다. LazyColumn 안에 stickyHeader 함수를 이용하여 간단하게 만들어줄 수 있습니다. 또한 아이템에 padding을 붙여주는 방법 대신에, LazyColumn의 vertical arrangement 인자를 spacedBy로 설정해 아이템 사이 간격을 설정해줄 수 있습니다.
Compose는 아이템들을 그릴 때 instance 처리를 어떻게 해줄까요? re-composition이 이뤄질 때 각 call site에 위치한 composable에 변동이 없다면 이전에 그렸던 아이템의 인스턴스를 재사용 하여 다시 그려줍니다. 변동이 있다면 해당 call site에 위치한 composable만 다시 그려줍니다. 하지만 이 예시 코드와 같이 call site가 동일한 경우라면 어떡할까요? 이럴땐 아이템이 배치되는 위치에 따라 instance를 구분합니다.
이 사진에서 같은 색상은 같은 instance를 뜻합니다. 아까와 같은 코드에서 아이템이 마지막에 추가된다면기존 아이템들의 배치 순서가 동일하여 마지막으로 추가된 아이템만 instance를 새로 만들고, 기존 아이템들을 instance를 재사용 해줍니다.
하지만 새로운 아이템이 상단이나 중간에 추가됐을 경우엔, 아이템의 배치 순서가 완전히 달라져 다 새로운 instance를 만들게 됩니다.
이럴때 items 함수에 key 인자를 추가해줌으로써, instance를 구분하는 키를 추가해줄 수 있습니다. key 고차함수의 인자값으로 해당 아이템의 데이터값이 들어옵니다.
이렇게해서 key값을 설정해주게 되면, 새로운 아이템이 상단이나 중간에 추가됐을 경우에도, 각 instance를 배치 순서가 아닌 key로 구분하기에, 같은 key의 아이템은 instance를 재사용하여 그려지게 됩니다.
이제 애니메이션에 대해 알아보겠습니다. SwiftUI에서 사이즈 변경 애니메이션은 state 상태에 맞게 사이즈를 설정해주는 역할을 하는 frame 메서드을 작성해주고, animation 메서드를 추가하는것으로 구현할 수 있습니다.
Compose에서는 state 상태에 맞게 사이즈를 설정해주는 size와, animateContentSize 메서드를 modifier에 추가해주는것으로 구현할 수 있습니다.
Visible 애니메이션도 쉽게 구현이 가능합니다. SwiftUI에서는, visible을 관리할 state를 만들어주고, visible이 true일때만 뷰를 보여주게 if문을 작성합니다. SwiftUI에서 withAnimation는, 해당 스코프에서 이뤄지는 변경으로 영향을 받는 뷰에 애니메이션을 적용시켜줍니다. 따라서 visible state를 toggle하는 부분을 withAnimation으로 감싸주면, visible 애니메이션이 구현됩니다.
Compose에서는 훨씬 간단하게 구현이 가능합니다. AnimatedVisibility 함수를 이용하여, visible인자에 visible 상태를 관리할 state 변수를 넣어주고, content에 아이템을 넣어주면 구현이 끝납니다.
이제 이전에 언급했던 some에 대해 자세하게 알아봅시다. WWDC 2019에서 some 키워드는 리턴 타입을 자동으로 그리고 빠르게 추론할 수 있는 스위치 기능이라고 설명했습니다. 따라서 이 some 키워드를 사용하여, 불투명한 타입으로 선언하여 유연하고 강력하게 SwiftUI 코드를 작성할 수 있게 됩니다. 이 예시 코드를 보면, some 키워드를 사용하지 않을시 VStack TupleView Text Text 처럼 타입을 구체적으로 선언해주어야 합니다. 만약 뷰를 다르게 구성하고 더 많은 서브 뷰가 생긴다면 이것의 타입은 점점 더 복잡해 질 것입니다. 하지만 이 코드에 some 키워드를 사용하여 View를 선언하고 컴파일 타임에 구체적인 타입을 결정하도록 하여 이를 쉽게 작성할 수 있습니다.
마지막으로 Jetpack Compose에서 Composable을 직접 만들어보겠습니다. 이 사진처럼 FancyBottomBar라고 부를 바텀바를 만들어 보도록 하겠습니다.
우선 바텀바의 색깔 정보들이 담길 FancyColors 인터페이스와 인터페이스 구현체를 만들어 줍시다. 저는 간단하게 메인 색, 바의 배경색, 인디케이터의 배경색 이정도로 나눠 봤습니다.
다음으로 바텀바의 옵션을 설정해줄 FancyOptions 인터페이스와 인터페이스 구현체를 만들어보겠습니다. 바의 텍스트 폰트, 인디케이터 높이, 바의 높이, 아이콘과 텍스트 사이의 패딩 정도로 만들어봤습니다.
다음으로 바텀 바에 들어갈 아이템들의 데이터 클래스를 만들어 줍니다. 저는 텍스트와, 아이콘 그리고 아이템들을 구분할 아이디 정도로 구성 해봤습니다.
이제 메인 함수를 만들어 봅시다. FancyBottomBar라는 composable 함수를 만들어 주고, 인자로 modifier, fancyColors, fancyOptions, items, onItemChanged 고차함수를 넣어 주었습니다. 각각 composable의 modifier, 바의 색깔과 옵션, 바에 들어갈 아이템들, 바의 아이템이 선택되었을때 호출될 함수로 구성돼 있습니다. 그리고 현재 선택된 바 아이템의 아이디를 저장해줄 fancyItemState 변수를 만들어 줬습니다. 바텀바는 아이템들이 가로로 쌓이므로 Row로 레이아웃을 열어주고,modifier 로 인자의 modifier을 가져와 가로 꽉 채워주고, 높이를 fancyOptions에서 받아오고, 배경색도 fancyColors에서 가져오게 설정 해주었습니다.
이제 아이템들이 배치되는 로직을 작성해 봅시다. 인자로 받은 items 사이즈만큼 반복문을 돌려 아이템들을 그려줍니다. 현재 item의 아이디가 선택된 아이디와 동일하다면 메인 색을 주고, 그렇지 않다면 인디케이터 배경 색을 주게 아이템들의 색깔을 정해줄 fancyItemColor 변수를 만들어주었습니다. animateColorAsState로 색깔을 설정 해주게 되면 색깔이 바뀔때마다 애니메이션이 적용되고 색깔 정보가 담긴 state가 바뀌게 됩니다. 인디케이터, 아이콘, 텍스트가 담길 Box 레이아웃을 열어주었고,modifier로 가로 weight를 1만큼 주고, clickable로 온클릭 리스너를 설정해 주었습니다.
마지막으로 인디케이터와 아이콘, 텍스트를 그려줄 차례입니다. 아까 만든 Box 레이아웃 안에 차례대로 넣어줍니다. Spacer라는 새로운 composable이 등장하였습니다. Spacer은 특별한 기능 없이 공간만 차지해주는 역할을 합니다. 이 Spacer로 인디케이터를 만들어주기 위해, modifier로 가로 전체 채워주고, fancyOption에서 높이, 아까 만들어준 fancyItemColor 변수로 색깔을 정해주었습니다. 다음으로, Column으로 전체를 꽉 채우고, 정가운데에 배치되게 열어주었고,안에 만약 아이콘이 null이 아니라면 fancyItemColor 색깔로 아이콘을 그려주고,텍스트가 빈 값이 아니라면 fancyItemColor 색깔과, fancyOptions의 font-family로 폰트를 지정해서 그려주게 작성하였습니다. 또한 위 아이콘과 너무 가깝지 않게 fancyOptions의 titleTopPadding 값 만큼 패딩도 넣어주었습니다
이렇게 해서 FancyBottomBar가 완성되었습니다. 전체 코드와 라이브러리는 이 주소에서 확인하실 수 있습니다.
이렇게 해서 만들어진 composable 함수가 그려지는 원리는 무엇일까요? Composable 함수들이 실행되면서 AndroidComposeView로 바뀌게 됩니다. 이렇게 생성된 AndroidComposeView가 compose에 맞게 재설계된 Canvas로 화면에 그려지게 됩니다.
이렇게 SwiftUI와 Jetpack Compose에 대해 알아보았습니다. 둘이 차이점은 무엇이 있을까요? 우선 SwiftUI는 모두 구조체로 구현돼 있습니다. 하지만 Compose는 모두 함수로 구현돼 있습니다. 또한 hot-reload는 Jetpack Compose만 지원합니다. SwiftUI는 구글에 비해 폐쇠적인 애플에 의해 만들어져서 오픈소스와 커뮤니티가 발달돼 있지 않습니다. 하지만 Jetpack Compose는 구글에 의해 만들어져서 오픈소스와 커뮤니티가 발달돼 있습니다. 또한 개발 로드맵도 공개하고 있습니다.
이상으로 마치겠습니다. 감사합니다.