서론
SwiftUI를 학습하면서 Swift 기본기의 중요성을 크게 느꼈습니다.
SwiftUI는 구현이 매우 편리하지만, 학습 과정은 그만큼 쉽지 않다고 생각합니다.
이번 포스팅에서는 View 프로토콜을 Swift 기반으로 단계적으로 이해해 나갔던 제 학습 과정을 공유하고자 합니다.
1단계: View 프로토콜 정의
가장 먼저 공식문서가 말하는 View 프로토콜을 봅시다.
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol View {
associatedtype Body : View
@ViewBuilder var body: Self.Body { get }
}
View 프로토콜은 body computed 프로퍼티를 가지는 타입입니다.
View를 정의할 수 있는 인터페이스를 제공합니다.
struct MyView: View {
var body: some View {
// 여기에 우리가 원하는 View 구현
}
}
View를 채택한 구조체를 작성하면 위와 같은 형태로 자동 완성이 됩니다.
우리는 body 안에 원하는 구현을 하면 됩니다.
2단계: some 반환에 의문 갖기
자동 완성 코드에서 흥미로웠던 점 중 하나는 'some' 키워드의 반환이었습니다.
UIKit을 사용할 때는 some을 자주 활용하지 않았는데, SwiftUI에서는 기본적으로 사용되는 점이 흥미로웠고 그 이유가 궁금했습니다.
some 키워드는 복잡한 타입을 단순화하는 역할을 합니다.
"에티오피아 원두로 만든 신맛이 강한 아이스 아메리카노"를 "some 커피" 타입으로 반환하면, 이 타입을 사용하는 곳에서는 단순히 "커피"로 인식하게 됩니다. 즉, 사용하는 곳에서 불필요한 정보를 제거하고 필수적인 타입 정보만 유지하는 것이 핵심입니다.
struct MyView: View {
var body: some View {
VStack {
Text("Hello, World!")
.font(.title)
Text("Glad to meet you.")
}
}
}
이 시점에서 한 가지 의문이 생겼습니다.
VStack과 Text도 View 프로토콜을 채택하는 구조체인데, 과연 여기서 생략할 타입이 있을까요?
UILabel 클래스를 생성하면 UILabel 타입이 되는 것처럼, VStack이나 Text도 이미 충분히 단순한 타입이 아닐까 하는 의문이 들었습니다.
@frozen
struct VStack<Content> where Content : View
VStack의 공식 문서를 살펴보니 한 가지 특이한 점을 발견했습니다. View 프로토콜을 채택한 구조체가 맞지만, 제네릭 타입을 가지고 있었습니다.
// Text
@frozen
struct Text
// font 모디파이어
nonisolated
func font(_ font: Font?) -> some View
또한 Text의 font 모디파이어를 확인해보니 이 역시 some View를 반환하고 있었습니다.
구체적으로 어떤 타입을 반환하는지는 아직 파악하지 못했지만, VStack과 Text.font가 단순히 VStack 타입이나 Text 타입이 아니라는 것을 알 수 있었습니다.
3단계: 반환 타입 직접 확인하기
의문을 해결하기 위해 프로젝트를 생성하고 print문으로 타입을 찍어봤습니다. (참고: 실제 실행 결과와 Preview 실행 결과가 다를 수 있습니다.)
VStack이 Text를 감싸는 구조기 때문에 Text부터 살펴봤습니다.
var text: some View {
Text("Hello")
}
// AnyView(Text(storage: SwiftUI.Text.Storage.anyTextStorage(<LocalizedTextStorage: 0x00006000021259f0>: "Hello"), modifiers: []))
Text는 제네릭 타입이 아님에도 다양한 속성을 가지고 있었습니다.
특히 LocalizedTextStorage라는 속성으로 "Hello" 문자열을 저장하고 있는 점이 흥미로웠습니다.
현재는 어떤 모디파이어도 적용하지 않았기 때문에 modifiers가 빈 배열로 출력되고 있습니다.
Text에 font를 적용하면 아래 배열로 설정됩니다.
Text("Hello")
.font(.title)
// [SwiftUI.Text.Modifier.font(Optional(SwiftUI.Font(provider: SwiftUI.(unknown context at $1d30c2c94).FontBox<SwiftUI.Font.(unknown context at $1d30d5738).TextStyleProvider>)))]
앞서 살펴본 것처럼 font 모디파이어가 some View를 반환한다는 점에서 예상했던 대로
font도 특정한 하나의 타입을 가지고 있음을 확인할 수 있었습니다.
다음은 Text를 지니는 VStack의 타입을 볼게요.
이번에는 길이가 길어서 적절히 개행을 추가했습니다. (땡큐 Claude)
var vstack: some View {
VStack {
Text("Hello")
.font(.title)
}
}
AnyView(
VStack<Text>(
_tree: SwiftUI._VariadicView.Tree
SwiftUI._VStackLayout,
SwiftUI.Text
>(
root: SwiftUI._VStackLayout(
alignment: SwiftUI.HorizontalAlignment(
key: SwiftUI.AlignmentKey(bits: 2)
),
spacing: nil
),
content: SwiftUI.Text(
storage: SwiftUI.Text.Storage.anyTextStorage(
<LocalizedTextStorage: 0x0000600002119130>: "Hello"
),
modifiers: [
SwiftUI.Text.Modifier.font(
Optional(
SwiftUI.Font(
provider: SwiftUI.FontBox
SwiftUI.Font.TextStyleProvider
>
)
)
)
]
)
)
)
)
Text 타입이 VStack의 Content 타입으로 설정되었고, 각종 레이아웃과 정렬 속성이 자동으로 추가되었습니다.
content로 우리가 작성한 Text가 들어갔네요.
4단계: some 키워드 사용 이유
우리는 3단계 작업으로 SwiftUI의 타입이 내부적으로 복잡하게 정의되어 있음을 확인했습니다.
그렇다면 사용하는 곳에서 이런 복잡한 정보가 필요한지 고민해봐야 합니다.
우리는 "뭐 마셔?"라는 질문에 "커피 마신다"라고 말하지, 에티오피아 어쩌구 저쩌구를 말하지 않습니다.
하지만 "무슨 맛이 나는 커피야?"라는 질문에는 "신맛이 강한 커피"라고 대답하고,
"무슨 원두로 만든 커피야?"라는 질문에는 "에티오피아 원두로 만든 커피"라고 대답합니다.
SwiftUI의 반환 타입도 이와 같은 원리입니다.
View라는 것만 확인해도 되는가, Text라는 게 반드시 필요한가를 고민하고 반환 타입을 결정하면 됩니다.
Text의 고유한 모디파이어가 필요하다면 Text를 반환하고, 아니라면 View를 반환하면 됩니다.
body의 기본 반환 타입이 some View인 것도 Window에서 View를 렌더링하는 데 필요한 정보가 View에 모두 들어있기 때문에 더 구체적인 타입은 필요 없기 때문입니다.
마무리
some 키워드의 개념을 통해 SwiftUI의 자동 완성 코드에 의문을 가지고 그 원인을 파악했습니다.
SwiftUI가 class가 아닌 struct를 선택한 이유를 깊이 살펴보는 것도 흥미로울 것 같습니다.
WWDC 내용을 바탕으로 SwiftUI의 View가 업데이트되는 과정에서 구조체가 언제 생성되고 해제되는지 이해한다면, 자연스럽게 struct의 특징과도 연결 지을 수 있을 것입니다.
이처럼 SwiftUI를 학습하는 데 Swift 기본기는 매우 중요한 역할을 합니다.
Swift 기본기가 없었다면 SwiftUI의 원리를 이해하는 데 훨씬 더 많은 시간이 필요했을 것입니다.
따라서 새로운 기술 스택을 학습하기 전에 언어를 확실히 다지고 진행하는 것이, 당장은 느리게 보이더라도 오히려 더 빠른 길이 될 것이라고 생각합니다.
저도, 그리고 모두 화이팅입니다!
감사합니다.
아직은 초보 개발자입니다.
더 효율적인 코드 훈수 환영합니다!
공감과 댓글 부탁드립니다.