SwiftUI를 사용하다보면 some View라는 반환 타입을 많이 보게된다. body의 반환 타입도 그렇고 뷰를 분리해서 따로 변수로 만들 때도 타입을 some View를 사용해서 반환하게 된다.
나는 그 동안 some이 호출 시점에 타입이 확정되어 고정된 타입으로 사용할 수 있게 해주는 키워드라고 생각했다. 그러다가 하나의 함수에서 분기 처리에 따라 여러 종류의 뷰 타입을 리턴할 수 있는 함수를 만들 때 단순히 some View로 반환해주면 리턴 타입에서 문제가 없을 거라고 생각했다. 해당 코드는 아래와 같다.
private func destinationView(type: ViewType) -> some View {
switch type {
case ViewType.original:
AsyncImageView(imageViewModel: imageViewModel)
case ViewType.first:
ImageScrollView(imageViewModel: imageViewModel)
case ViewType.second:
ResizedImageView(imageViewModel: imageViewModel)
case ViewType.third:
ResizeInBgView(imageViewModel: imageViewModel)
case ViewType.fourth:
ThumbnailView(imageViewModel: imageViewModel)
}
}
하지만 위와 같이 작성하면 리턴 타입을 확정할 수 없다는 컴파일 에러가 발생한다. 나는 그 동안 some의 고정 타입이 호출 시점에서 확정된다고 생각했기에 오류가 나는 원인을 알 수 없었다. 알고보니 some의 타입은 컴파일 과정에서 확정이 된다고 한다. 그래서 위와 같은 경우에는 컴파일 과정에서 고정 타입을 알 수 없기 때문에 오류가 나는 것이다.
(위의 경우에는 @ViewBuilder
를 붙여주는 것으로 해결할 수 있다.)
문제를 해결하고 보니 몇 가지 의문점이 생겼다.
- 컴파일 과정에서 구체 타입으로 고정된다면 애초에 반환 타입을 구체 타입으로 설정하는 것과 무엇이 다른지?
- 자주 보이는 any와 some은 어떤 관계인지?
그래서 해당 궁금증을 해결하기 위해 WWDC22 영상과 다른 블로그 글들을 보면서 공부해봤다.
📄 컨텐츠
some과 any는 제너릭과 프로토콜을 사용하다보면 자주 보게되는 친구들이다. 둘 다 추상화된 타입을 사용할 때 적용하는 키워드들이다.
간단하게 표현하자면 some은 컴파일 과정에서 단일 고정 타입을 요구하고 any는 모든 타입을 수용할 수 있는 추상화된 형식이다. some은 타입을 제한시키는 느낌이고 any는 반대로 추상화시켜서 사용할 수 있는 느낌이다. 아래의 예시로 간단하게 확인해보자
protocol Shape {
var angleCount: Int { get }
}
struct CircleShape: Shape {
var angleCount: Int = 0
}
struct TriangleShape: Shape {
var angleCount: Int = 3
}
struct RectangleShape: Shape {
var angleCount: Int = 4
}
//anyShapes는 오류가 나지 않지만 someShape는 컴파일 에러가 발생한다.
var anyShapes: [any Shape] = [CircleShape(), TriangleShape(), RectangleShape()]
var someShapes: [some Shape] = [CircleShape(), TriangleShape(), RectangleShape()]
위의 코드는 원, 삼각형, 사각형을 Shape
라는 프로토콜을 이용해 추상화하고 이를 두 개의 배열에 담고 있다. 하나는 any Shape
를 받고 하나는 some Shape
를 받는다. 다음과 같은 상황에서 any는 정상적으로 동작하지만 some의 경우 컴파일 에러가 발생한다. 이는 some Shape
가 하나의 단일 타입을 추론해야 하는데 Shape를 채택하는 여러 타입들이 배열에 있어 단일 타입을 추론하지 못해 생기는 에러이다.
추가적으로 WWDC22에 나왔던 예시를 보도록 하자
아래의 코드에 대해 간단히 설명을 하자면 농장에서 동물들에게 적절한 먹이를 주기 위해 추상화가 되어 있는 코드이다. Farm에 있는 feed 메서드는 특정 동물(some Animal) 에게 적절한 먹이를 찾아 동물이 먹을 수 있도록 하는 코드이다. feedAll은 모든 동물(any Animal)들에게 적절한 먹이를 주는 feed 메서드를 적용해주는 메서드이다.
모든 동물들이 다 같은 음식만 먹는다면 feed라는 구체적인 메서드 없이 feedAll에서 배열을 순회하면서 animal의 eat 메서드를 호출해주기만 해도 괜찮을 것이다.
하지만 각 동물들은 associatedtype Feed: AnimalFeed
코드를 통해 먹는 먹이(Feed
)가 다르기 때문에 [any Animal]
배열을 순회하면서 eat 메서드를 호출하는데 한계가 있다. 따라서 고정 타입을 요구하는 some Animal을 파라미터로 받는 feed 메서드를 생성해 모든 동물들이 적절한 먹이를 찾을 수 있도록 구현한 것이다.
예시코드
struct Farm {
func feed(_ animal: some Animal) {
let crop = type(of: animal).Feed.grow()
let produce = crop.harvest()
animal.eat(produce)
}
func feedAll(_ animals: [any Animal]) {
for animal in animals {
feed(animal)
}
}
}
protocol AnimalFeed {
associatedtype CropType: Crop where CropType.Feed == Self
static func grow() -> CropType
}
protocol Crop {
associatedtype Feed: AnimalFeed where Feed.CropType == Self
func harvest() -> Feed
}
protocol Animal {
associatedtype Feed: AnimalFeed
func eat(_ food: Feed)
}
// MARK: Cow
struct Cow: Animal {
func eat(_ food: Hay) {}
}
struct Hay: AnimalFeed {
static func grow() -> Alfalfa { Alfalfa() }
}
struct Alfalfa: Crop {
func harvest() -> Hay { Hay() }
}
Type erasure
WWDC 영상을 확인하니 타입을 지운다는 표현을 하고 있었다.
이는 any 키워드가 구체 타입의 특징을 지우고 채택한 프로토콜의 특징(메서드, 속성)만을 남긴다는 의미였다.
그리고 WWDC에서는 상자를 통해서 any 키워드를 표현했다. Animal이라는 상자(any Animal)에 구체 타입을 넣어 구체적인 특징을 지워서 사용한다. 하지만 특정 시점에서는 구체 타입이 필요한 순간이 온다. 영상에서는 이 과정을 언박싱이라고 묘사하고 있다. any Animal로 사용된 부분을 some 키워드를 사용해 구체타입으로 변환해주는 것이다.
Type erase라는 어디서 봤나 했더니 Combine을 사용할 때 봤다. 흔히 Subject를 AnyPublisher로 변환해줄 때 eraseToAnyPublisher를 사용하게 된다. 이를 사용하게 되면 AnyPublisher로 추상화하여 반환할 수 있게된다. 처음 사용할 때 왜 erase라는 동사를 사용하나 했더니 드디어 의문이 풀렸다.
some을 사용하는 이유
그렇다면 첫번째 의문이었던 구체 타입을 리턴하지 않고 some View를 통해 리턴하는 것은 어떤 이점이 있을까?
- some View를 리턴하게 되면 해당 메서드 또는 변수의 변경이 유연하다.
- 이는 SwiftUI의 body 변수를 보면 알 수 있다. body에는 Text()가 올 수도 있고 Image()가 올 수도 있다. 하지만 우리는 일일이 해당 변수의 타입을 변경하지 않아도 유연하게 코드를 변경할 수 있다. 리턴 타입을 some View로 해두고 구현해두면 이후에 내부 뷰가 다른 뷰 타입으로 바뀌더라도 외부에서 해당 변수에 접근할 때 일관된 방식으로 접근할 수 있다. 이에 따라 외부에서 추가적인 변경 사항이 적어진다.
- 반환하는 구체 타입이 외부에 노출되지 않는다
- 반환 타입을 some View로 지정하면, 내부 구현에서 어떤 구체적인 타입을 반환하는지 외부에 노출하지 않을 수 있다. 이는 API 설계 시 내부 구현을 감추면서도 “이 메서드는 View를 반환한다”는 정보를 명확히 제공할 수 있어, 확장성과 안정성을 높인다.
- 런타임 오버헤드 최소화
- any 키워드를 사용하는 경우 런타임에 타입 정보가 필요하므로 성능 저하가 발생할 수 있다. 그 반면에 some 키워드는 컴파일 타임에 반환 타입을 확정하므로, 런타임에서 타입 추론이나 동적 디스패치를 처리하는 오버헤드를 줄일 수 있다.
'iOS > Swift' 카테고리의 다른 글
SwiftUI Widget (0) | 2025.01.12 |
---|---|
Swift의 다양한 이미지 - UIImage, CGImage, CIImage (0) | 2025.01.03 |