최근에 개인 프로젝트를 해볼까하여, React Native를 대신해서 앱을 개발할 도구들을 탐색해보고 있었다. React Native는 회사에서 너무 많이 쓰고 있으니, 신선한 자극도 느껴보고 새로운 도구도 배워보고 할 심산이었다. 그래서 Flutter 를 해볼까, 아니면 이 기회에 나에게는 일종의 신비로운 판타지 세계와도 같았던 애플 개발 생태계에 입문해볼까를 고민했다. Flutter는 React Native 개발자로써 당연히 끌릴만한 프레임워크이긴했다. 하지만 개인적으로 "노는" 것이니, 좀 더 신선하고, 신비롭고, 새로운 세계가 아무래도 더 끌리는 것 아니겠는가. 그래서 결국 SwiftUI를 해보기로 했다.
사실 SwiftUI로 뭘 만들어야지 라고 생각하기까지 Flutter 도 건드려보고, 다시 그냥 익숙한걸로 하자 해서 React Native도 했다가, SwiftUI의 애플 튜토리얼도 했다가.. 하면서 몇주간 방황을 좀 했다. 주말마다 한두시간씩 하다보니, 뭘 해도 손에 잘 붙지가 않더라. 그러다 이번주에 An Introduction to SwiftUI for React Developers 라는 글을 보게 되었는데, 이 짧고 간단한 글을 보는 순간 갑자기 SwiftUI가 할만해보이는 마법을 느꼈다.
프레임워크라는건 기본적으로 동작을 안에 숨긴다. 소위 main
함수라는 것을 포함하고 있으면 프레임워크, 없으면 라이브러리라고 하지 않던가. 그런 관점에서 React는 라이브러리이지만, Creact React App(CRA)과 React Native는 프레임워크이다. 소프트웨어 구동을 위한 아주 기본적인 데이터의 흐름과 동작, 엔트리포인트를 숨기기 때문에 프레임워크를 처음 공부하는건 생각보다 진입장벽이 좀 있는 편이다. 이럴 때는 내가 이미 익숙한 프레임워크 대비 차이점을 중심으로 탐구하는게 도움이 된다. 그래서 저 짧은 글이 나에게 큰 도움이 된 것같다.
SwiftUI는 뭔가 애플답게(?) 굉장히 헤비한 프레임워크이고, 정말 많은 부분을 프레임워크 안으로 감춘다. 온갖 decorator(@)들이 난무하고, 이를 붙이면 자동으로 뭔가가 된다 (난 그래서 프로그래밍 언어 측면에서도 decorator 문법을 싫어한다). 그럼에도 수년전에 잠깐 경험했던 Objective-C와 Storyboard, 그리고 Xcode에서 드래그 앤 드랍으로 UI와 변수를 바인딩하는 프로그래밍에 비하면 훨씬 직관적으로 변하긴 했지만, 그럼에도 여전히 (Java의 Spring이 떠오르는) 묵직한 프레임워크다.
SwiftUI의 묵직한 느낌은 SwiftUI가 왠만한 컴포넌트들을 모두 갖추고 있다는데서 오기도 한다. 일단 3rd party 라이브러리 검색부터 시작하는 React Native 와는 달리, SwiftUI는 대부분 이미 구조체로 구현된 컴포넌트들을 이용한다. 심지어 내비게이션과 2,400여개의 아이콘들(SF Symbols)까지 구현되어 있으니, 말 다했다.
View
SwiftUI의 UI 컴포넌트들은 (각종 레이아웃용 컴포넌트들까지 포함해서) 모두 View
라는 프로토콜(Swift의 인터페이스같은 요소이다)을 구현한 구조체(struct
)들이다. 과거 UIKit과 AppKit 시절에는 각종 상속이 난무하는 클래스였는데, SwiftUI로 오면서 모두 구조체로 바뀌었다고 한다 (Why does SwiftUI use structs for views?).
SwiftUI의 UI 컴포넌트들은 View
들을 입력을 받아 대부분이 View
구조체를 반환하도록 되어 있다 (What is Content in SwiftUI?).
struct TimeDurationText: View {
...
var body: some View {
HStack(alignment:.firstTextBaseline, spacing: 0) {
Text("\(hours)")
.bold()
Text("시간")
.font(.system(.caption))
.bold()
.foregroundColor(.gray)
.padding(.trailing, 5)
Text("\(minutes)")
.bold()
Text("분")
.font(.system(.caption))
.bold()
.foregroundColor(.gray)
}
}
}
위 코드에서 보면, HStack
이라는 UI 컴포넌트는 구조체이고, 위 코드에서 쓰인 부분은 그 구조체의 init
함수이다. HStack
이라는 구조체의 정의를 보면 아래와 같다. 위 코드에서 HStack(...
코드는 사실 HStack
의 생성자 함수, 즉, init
함수를 부른 것이다.
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen public struct HStack<Content> : View where Content : View {
/// Creates a horizontal stack with the given spacing and vertical alignment.
///
/// - Parameters:
/// - alignment: The guide for aligning the subviews in this stack. This
/// guide has the same vertical screen coordinate for every child view.
/// - spacing: The distance between adjacent subviews, or `nil` if you
/// want the stack to choose a default distance for each pair of
/// subviews.
/// - content: A view builder that creates the content of this stack.
@inlinable public init(alignment: VerticalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)
/// The type of view representing the body of this view.
///
/// When you create a custom view, Swift infers this type from your
/// implementation of the required `body` property.
public typealias Body = Never
}
이런식으로, SwiftUI의 UI 컴포넌트들은 View
타입 구조체를 반환하게 되어 있고, 이 구조체의 함수들인 font
, bold
, foregroundColor
등을 체인형태로 호출하여 스타일을 정의하게 된다. 굳이 유사점을 찾으라면, 개인적으로 이 View
타입은 흡사 div
같이 느껴졌다.
체인형태로 호출되는 스타일 함수들은 호출 순서도 중요한게 재밌다. 가령, 웹의 경우 margin
과 padding
이 나누어져있지만, SwiftUI에는 padding
만 존재한다. 그렇다면 둘은 어떻게 구분하는가? 답은 padding
을 2번 호출하면된다. 즉, padding
을 호출하면 일단 padding이 생긴다. 그리고 border
등을 이용하여 경계선을 그리고, 뒤이어 padding
을 또 호출하면, 그 경계선 너머에 padding이 생긴다.
VStack {
ForEach(modelData.categories.indices){ index in
CategoryRow(...)
}
}
.padding() // CategoryRow 밖에 space가 생김 -> padding
.background(Color.white) // padding까지 포함해서 배경색 지정
.cornerRadius(10.0) // padding까지 포함된 지역에 radius 지정
.padding() // radius 지정된 경계선 밖에 padding을 지정 (배경색, radius 영향 안 받음 -> margin)
여러 강의 영상들을 보니, 이러한 조합으로 무궁무진한 UI 모양을 그려낼 수 있는 점이 재밌었다.
또, 웹과 React Native 에서 레이아웃은 스타일의 영역이었다. 하지만 SwiftUI에서는 레이아웃도 View
이다. 위의 코드들에서 나왔던 VStack
, HStack
모두 레이아웃 컴포넌트다. 웹으로 치면 div
에 적당히 flex-row
또는 flex-col
을 지정한 것과 같다. 이렇게 보니, React Native가 flex
레이아웃을 기본 레이아웃으로 사용한 것이 이해가 된다.
SwiftUI의 데이터 관리는 의외로 React와 비슷하다. State가 있고, Props도 있다. Props는 구조체의 맴버 변수들과 같다. 별다른 정의가 없으면, Swift는 자동으로 public
변수들에 대한 생성자 함수를 만들어준다. 예를 들어, 다음과 같다.
struct CategoryRow: View {
var category: Category
var isLast: Bool
...
}
struct CategoriesList: View {
...
var body: some View {
...
CategoryRow(category: category, isLast: false)
}
}
위와 같이, public으로 선언된 맴버 변수에 대한 생성자 함수는 따로 정의해주지 않아도 자동으로 정의된다.(여담으로, Swift는 언어 자체에 축약이 상당히 많다.) React의 props 자체가 사실 생성자 함수의 입력값과 같다는 걸 생각해보면, 바로 이 부분이 React 개발자들에게는 props와 같다.
그 외에 State의 경우 @State
를 붙이고, 이 변수의 변경 사항에 따라 변화하려면 $
를 통해 바인딩한다. 아래 코드를 참고하자.
struct ContentView: View {
@State private var selected: Tab = .summary
...
var body: some View {
TabView(selection: $selected) {
...
}
}
...
}
이 외에 Redux 등을 통해 관리할 수 있는 글로벌 데이터는 @EnvironmentObject
등을 통해 관리할 수 있다는데, 아직 이 부분은 명확하지 않다. 난 하여간 @
가 싫다.😞 향후 공부해나가면서 살펴보겠다.
WWDC의 SwiftUI 발표때 '우와' 했던 부분은 바로 이 프리뷰 기능일 것이다. 사실 React Native를 비롯해서 웹 개발 등에서는 핫로드가 너무 당연한 기능이긴하지만, 이를 네이티브에서 지원한다는건 주목할 만한 일이었던 것 같다.
써본 결과, 첫 느낌은 의외로 "무겁다" 이다. 맥북의 베터리 소모가 극심하고, 생각만큼 부드럽지 않다. Visual Studio Code + iOS Simulator가 체감상으로는 훨씬 부드럽고 빠르다.
그럼에도 파일 하단에 자동 생성되는 PreviewProvider
구조체를 이용해서 원하는 데이터를 다양하게 넣어어보며 테스트할 수 있고, 이게 크게는 페이지 단위에서 버튼과 같은 작은 단위까지 격리해서 실시간으로 테스트해볼 수 있다는 점은 아주 좋다. 일단 어떻게든 페이지에 그려놓고 테스트해야 하는 React Native에 비해 편한 부분이라 하겠다.
ContentView.swift
파일같이 루트 파일에서 프리뷰 구조체를 적당히 셋팅한다면, Simulator 를 키지 않고도 다양한 경로를 테스트해볼 수 있을 것 같아, 프리뷰의 활용 가능성은 매우 무궁무진해보인다.
XCode를 제대로 코딩용으로 써본건 처음이다. React Native를 하게 되면 XCode는 자주 키게 되긴하지만, 그렇다고 거기서 코딩을 하진 않았다. 나의 메인 개발 에디터는 (자주 바꾸기는 하지만) Visual Studio Code(VSCode)와 JetBrains 이다. 이 관점에서 XCode는 상당히 이질적인 도구였다.
일단, XCode는 일반적인 코드 에디터로 보이지 않는다. 그야말로 iOS, macOS 앱 개발이라는 명확한 목표에 맞춰진 개발도구다. 그래서 온갖 속성 창들, Info.plist
등의 설정 파일들을 잘 정리해서 보여주는 예쁜 UI 등이 어색하다. 모든게 오직 '앱' 개발에 맞춰진 느낌이다.
Color Scheme 은 아주 좋다. 기본 Dark 테마를 쓰는데, 별달리 다른 테마가 생각나지 않을 정도로 개인적으로는 색 배열이 괜찮다고 느꼈다.
반면, 단축키는 좀 이상한데, 특히 "줄 삭제"와 같은 아주 기본적인 기능에 대한 단축키가 디폴트값으로 지정이 안되어 있는게 신기했다. 그리고 별다른 자동 Formatter(React로 치면 prettier 등과 같은)가 없이 매번 ^i
를 눌러줘야 하는건 매우 불편했다.
Swift Packages를 통해 외부 디펜던시 관리를 도구 안에서 아주 손쉽게 해주는건 너무 좋았다. 이 외부 디펜던시 추가는 그냥 GitHub 저장소를 추가해주면 버전부터 다 알아서 관리해준다. 업데이트, 리셋, 버전 리졸브 등 모두 도구 안에서 해결되는 경험은 아주 좋았다. 여전히 CocoaPod 가 de facto standard 인 것 같긴한데, Swift Packages가 주류로 자리잡길 바란다.
장점이 많지만, 워낙 이질적인 도구라 적응에는 좀 시간이 걸릴 것 같다. 무엇보다, 은근히 버그가 많고(가끔 함수 정의로 연결되는 링크나 Syntax highlight이 먹통이 된다) 무겁다. 내 맥북이 2-3년 밖에 안된 맥북프로 15" 인데, 이렇게 무거우면 도대체 어느정도 맥을 갖춰야 스무스하게 동작할지 의문이다.
너가 뭘 좋아할지 몰라 다 준비해봤어
약간 이런 느낌을 받았다. 구조체도 있고, 클래스도 있다. 함수만 단독으로 쓸 수도 있다. 상속도 있다. 온갖 @
들이 넘쳐난다. 함수 클로져도 있고, 함수를 호출하는데도 온갖 축약 문법이 난무한다. 그야말로 각종 언어들에서 보던 Syntactic sugar 들을 모두 보는 느낌이다. 이는 언어의 진입장벽을 높이는데 매우 큰 몫을 한다. 물론 쉽고 단순하게 쓸수도 있다. 하지만 참고할 수 있는 (이미 고인물(?)들에 의해 작성된) 각종 레퍼런스들이 그렇게 쓰여져 있지 않다. 그래서 초반에 코드를 읽는데만도 많은 시간이 소요되었다.
ForEach(modelData.categories.indices){ index in
CategoryRow(category: modelData.categories[index], isLast: index == modelData.categories.count - 1)
}
위의 ForEach
함수는 참 이해하는데 오래 걸렸다. 도대체 마지막 브라켓은 뭐고, in
이라는 지시어는 또 뭔지..
여전히 Swift는 어렵다. 너무 많은 규칙이 존재한다. 새삼 Go 언어가 얼마나 단순한 문법을 갖고 있는지 느껴진다. Java 도 원래는 그랬다. Java가 초반에 실제로 추구했던 목표는 아주 얇은 언어 스팩을 갖는 것이었다 (현재는 아니지만). 여담이지만, 그래서 난 Go 언어에 제너릭이 들어오는것, JavaScript에 클래스 문법이 추가되고 #
으로 public/private 을 조절하는것에 아주아주 반대한다.
Swift는 고수가 되면 될 수록 간결하고, (고수들 사이에서) 읽기 쉬우며, 아름다운 코드를 짜도록 도와줄 것 같다. 하지만, 아름다운만큼 거기까지 가기엔 진입장벽이 있다는 것을 염두에 두어야 할 것 같다.
정확히는 이게 1주차 공부이다. 개인적으로 연재글이 되길 바라지만, 장담은 할 수 없으니, 일단은 1부, 2부 등과 같은 넘버링은 붙이지 않았다. 그래도 일단 이 신비로운 애플 개발 생태계에 대한 첫발은 내딛인것 같다. 하고 싶은 것이 많다. 특히, CloudKit
을 Firebase처럼 사용해서(가능한지 모르겠다) iOS, macOS를 관통하는 Universal App을 만들고픈 욕구가 있다.
하지만 일단, 이번 WWDC 때는 Swift 섹션을 흥미롭게 볼 수 있는 개발자가 되어보도록 하자.
잘읽고갑니다