SwiftUI — some View에서 some은 뭐고, 왜 필요할까?

minsson
10 min readMay 21, 2023

--

SwiftUI를 처음 공부할 때, 첫 프로젝트를 생성하자마자 조금 낯선 키워드를 볼 수 있었습니다. 바로 `some`입니다.

우선 SwiftUI에서 왜 some View를 쓰는지 다룬 후, some 키워드가 무엇인지 다뤄보겠습니다.

다만 some 키워드를 이해하기 위해서는 제네릭, 프로토콜에 대한 선행지식이 필요합니다.

SwiftUI에서는 왜 some View를 쓸까요?

우선 some View라는 게 우리에게 얼마나 소중한 것인지 알아보겠습니다.

가장 기본적인 형태의 SwiftUI 뷰 코드를 보겠습니다.

import SwiftUI

struct ContentView: View {
var body: some View { // 👈 some View가 뭘까요?
Text("Hello, World!")
}
}

여기서 `body`라는 프로퍼티는 연산 프로퍼티의 형태를 하고 있고, some View라는 게 리턴 타입으로 보입니다. 코드블럭 내부를 보면 return 키워드는 생략되었지만, Text 객체를 리턴함을 알 수 있습니다.

Text 객체를 반환하는데, 굳이 some View라는 뭔지 모를 타입을 반환할 이유가 있을까요? 아래처럼 Text 타입을 반환해봅시다.

struct SwiftUIView: View {
var body: Text {
Text("Hello, World!")
}
}

잘 돌아갑니다.

그럼 아래 코드처럼 VStack이면 어떻게 될까요? 리턴 타입을 뭐라고 하면 될까요? 정답은 좀 더 밑에 있습니다.

VStack {
Text("1")
Text("2")
Text("3")
}

바로 아래처럼 됩니다.

struct ContentView: View {
var body: VStack<TupleView<(Text, Text, Text)>> {
VStack {
Text("1")
Text("2")
Text("3")
}
}
}

오..! 🙉

`VStack<TupleView<(Text, Text, Text)>>` 입니다.
`some View`라는 수수한 타입을 보다가 이렇게 화려한 타입을 보니 멋지군요!

더 화려한 `Group<_ConditionalContent<Color, Text>>` 타입과 `VStack<TupleView<(Text, HStack<TupleView<(Text, Text)>>)>>` 타입도 보여드리겠습니다. 🙉

struct ContentView: View {
var body: Group<_ConditionalContent<Color, Text>> {
Group {
if true {
Color.yellow
} else {
Text("Hi")
}
}
}
}

struct ContentView: View {
var body: VStack<TupleView<(Text, HStack<TupleView<(Text, Text)>>)>> {
VStack {
Text("Hi")

HStack {
Text("I'm")
Text("Minsson")
}
}
}
}

진짜 이렇게 써야 한다면 어떻게 SwiftUI의 매력에 빠질 수 있었을까요? 위처럼 간단한 예시 코드로도 타입이 저렇게 복잡해지는 걸 볼 수 있습니다.

화면이 복잡한 앱을 개발한다면 뷰 코드 길이도 만만치 않을테니 타입만으로도 코드 몇 줄을 쓸 수 있을 겁니다. 그 와중에 모디파이어가 하나라도 추가 되거나, 심지어 스택의 순서가 바뀌거나 하면 저 타입을 어디서부터 손봐야 할지 막막해집니다.

그런데 우리는 some 키워드 덕분에 저 구체 타입들을 일일이 명시하지 않아도 되는 겁니다. 그저 some View 타입이라고만 쓰면 됩니다. 정확히 무슨 타입인지는 알 수 없지만(알고 싶지도 않지만 🙈), 아무튼 View 프로토콜을 준수하는 타입이라는 겁니다.

Opaque Types (some의 의미)

some은 기본적으로 Opaque type을 나타내는 키워드입니다. opaque이라는 단어는 불투명한, 불명확한, 이해하기 힘든 등으로 해석할 수 있습니다. 그래서 불명확 타입이라고 번역하기도 합니다.

프로퍼티나 서브스크립트의 선언, 아니면 함수의 반환 타입 위치에 프로토콜을 쓰면서 앞에 some 키워드를 붙이면, ‘정확히 뭔지는 불명확하지만, 아무튼 이 프로토콜을 준수하는 어떤 타입 중에 하나일 것은 분명하다’라는 뜻입니다.

즉, 내부에서 구체적인 타입을 정해서 내보내게 되는데, 밖에서는 정확히 어떤 타입인지 몰라도 쓸 수 있다는 겁니다.

WWDC 2019 SwiftUI Essentials에서 `SwiftUI에서 사용하는 some 키워드는 Swift 언어가 내부 리턴타입을 자동으로 추론해낼 수 있게 도와주는 switch 기능을 한다. (the some keyword that we use here is a switch feature that lets swift infer out inter return type automatically.)`라고 설명하기도 하고요.

이걸 SwiftUI와 밀접하게 생각해보면요,

View 구조체의 내부에 body라는 연산 프로퍼티가 있고, 그 연산 프로퍼티의 반환 타입이 some View입니다. 그래서 body 내부에는 구체적인 타입이 존재하지만 외부에서 사용할 때, 리턴 받을 때는 some View라는 불명확 타입으로 반환한다는 거죠.

그럼 왜 이걸 쓰는가를 생각해보면, 아주 큰 장점이 있습니다. some View가 없었다면 body에 들어가는 모든 뷰들의 타입을 모두 하나로 묶어, body 연산 프로퍼티의 리턴 타입이 무엇인지 일일이 명시해줘야 할 겁니다.

누구 입장에서 불명확하다는 것일까요?

이런 질문으로 다시 한번 생각해봅시다. 불명확, 불투명하다는 말이 있다면, 명확, 투명하다는 말도 있을 겁니다. 누구 입장에서, 무엇을 기준으로 하는 말일까요? 호출자(Caller) 입장에서 컴파일 타임 전에 구체적인 타입이 보이는지, 안 보이는지를 기준으로 합니다.

이런 함수가 있다고 가정해보겠습니다.

func sum(_ a: Int, _ b: Int) -> Int {
return a + b
}

위의 `sum(a:b:)` 함수를 호출해보겠습니다.

let number1: Int = 1
let number2: Int = 2

let sum: Int = sum(a: number1, b: number2) // 3

호출자 입장에서 파라미터로 무엇이 들어가는지, 그리고 리턴 타입이 무엇이 될지도 확인해볼 수 있습니다.

이번에는 제네릭 함수로 구현하고, 호출도 바로 해보겠습니다.

func sum<T: Numeric>(a: T, b: T) -> T {
return a + b
}

let number1: Int = 1
let number2: Int = 2

let sum: Int = sum(a: number1, b: number2) // 3

제네릭 함수에서는 함수를 구현할 때는 T가 정확히 무슨 타입이 될지 알 수 없고, 호출할 때 직접 Int 타입의 number1과 number2를 넣으며 T가 Int 타입이라는 걸 알게 되었습니다.

즉, 호출자가 T의 구체 타입이 Int라는 걸 알게 되므로, 호출자 입장에서 플레이스 홀더 T가 투명하고, 명확하다는 걸 알 수 있습니다.

이번에는 Opaque Type을 써서 호출자 입장에서 불투명, 불명확한 게 무엇인지 알아보겠습니다.

아래의 코드를 보면, Shape 프로토콜이 있고, 이를 채택한 Rectanlge과 Circle 타입이 있습니다.

protocol MyShape {
func describe() -> String
}

struct Rectangle: MyShape {
func describe() -> String {
return "I'm a Rectangle"
}
}

struct Circle: MyShape {
func describe() -> String {
return "I'm a Circle"
}
}

추가로 makeShape() 함수를 구현해보겠습니다.

func makeShape() -> some MyShape {
return Circle()
}

코드를 작성한 우리는 makeShape()의 구체적인 반환 타입이 Circle이라는 걸 압니다.

하지만 아래처럼 호출자, 즉, 코드를 호출하는 부분에서만 보면 Circle이 만들어질지, Rectangle이 만들어질지 정확한 타입을 알 수 없습니다. 호출자 입장에서 불명확합니다.

let shape = makeShape()
print(shape.describe())

위 코드를 컴파일하면 “I’m a Circle”이 프린트 될 겁니다. 즉, 컴파일 타임에서야 shape가 구체적으로 무슨 타입이었는지 알게 되는 겁니다.

Opaque 타입과 Protocol Type의 차이

위 설명을 읽으며, 누군가는 ‘저기서 some을 없애도 동일하게 동작할텐데, 무슨 의미가 있지?’라고 생각할 수 있습니다. 바로 제가 그랬거든요. 처음에 저런 예시 코드를 보고 some 없이 안될리가 없는데? 하고 코드로 쳐보니 역시 됐습니다.

그러니 아래 코드도 살펴보겠습니다. 컴파일 에러가 납니다.
`"Protocol ‘Collection’ can only be used as a generic constraint because it has Self or associated type requirements”`라고 합니다.

func someArray() -> Collection {
return [1, 2, 3]
}

즉, 프로토콜 정의부에 associatedType을 사용했거나, Self 타입을 사용하는 프로토콜이라면 타입 자체가 제네릭하게 되므로 반환 타입으로 사용할 수 없습니다.

위에서 우리가 정의했던 MyShape의 경우 이런 경우에 해당하지 않았으니 에러가 나지 않았던 것이죠.

따라서, 이런 경우에는 아래와 같이 some 키워드를 붙여주어야 합니다.

func someArray() -> some Collection {
return [1, 2, 3]
}

다시 한번 정리해보기

  • 반환 타입에 Opaque Types를 사용하면, 반환할 타입의 정확한 타입을 알려주지 않은 채로 Caller에게 반환하겠다는 것을 의미합니다.
  • 프로토콜을 반환 타입으로 정의해 줄 수도 있지만, 프로토콜 정의부에 associatedType을 사용했거나, Self 타입을 사용하는 프로토콜이라면 타입 전체가 제네릭하게 되므로 반환 타입으로 사용할 수 없습니다.
  • 만약 Opaque Types라는 개념이 없었다면 SwiftUI에서 View를 정의할 때 매번 구체타입을 작성하고, 유지보수할 때마다 구체타입을 수정해야 했을 겁니다.
  • 우리는 some View라고 씀으로써, 컴파일러에게 “구체적인 타입까지는 명시하지 않겠지만, 어쨌든 뷰 프로토콜을 따르게 잘 짜놨어. 그러니 구체적인 타입은 네가 컴파일 타입에 결정해줘~!”라고 말하는 거라고도 생각할 수 있습니다.

참고

--

--

No responses yet