Swift

[Swift] SwiftUI에서 some View를 사용하는 이유 | any VS some

하이D:) 2024. 11. 14. 16:56

 

왜 SwiftUI에서는 some View를 사용해서 뷰를 그려요?

 

some 키워드가 Opaque Type에서 사용되고 Opaque Type에 대한 개념도 어느 정도 알고 있지만, 어느 날 문득 왜 SwiftUI에서는 any View도 아니고 some View로 뷰에 대한 타입을 리턴 받아 사용하는 걸까 궁금했다.

 

 

 

 

이렇게 some 키워드로 나타내는 타입을 Opaque Type이라고 하는데, 불명확 타입이라고도 불리며, 역제네릭 타입으로도 불린다.

 

왜 불명확 혹은 역제네릭 타입으로 불리는지 Opaque Type에 대해 살펴보고, 왜 SwiftUI에서 뷰를 그릴 때는 some View 처럼 Opaque Type을 사용하는지에 대해서도 알아보자!

 

 

 

 

📍Generic 

왜 Opaque Type 알아본다고 했는데 갑자기 Generic인가 싶지만 

 

 

Opaque Type은 직역하면 '불투명 타입'이지만 '역제네릭 타입'으로도 불린다.

즉, Opaque Type을 알기 위해서는 제네릭을 간단하게 먼저 살펴보면 좋을 것 같다!

 

 

 

 

▶️ 제네릭을 사용하는 이유

제네릭 을 사용하는 이유에 대해 생각해 보면 유연하고 재사용성 높은 코드를 작성할 수 있기 때문일 것이다. 

 

제네릭으로 구현한 기능과 타입은 특정 타입에 종속되지 않아 다양한 타입에 유연하게 대응할 수 있으며, 타입 안정성까지 유지할 수 있는 swift의 강력한 기능이다.

 

 

 

 

▶️  제네릭의 특징

 

제네릭의 가장 큰 특징을 살펴보면,

  • 구현부에서는 타입을 숨기기 때문에 구체 타입을 알지 못하지만, ( 이렇기 때문에 유연한 타입 사용이 가능한 것 ) 
  • 호출부에서는, 즉, 제네릭 타입이나 함수 등 제네릭으로 구현되어 있는 것을 사용할 때에는 타입 파라미터 ( = Placeholder = <T> ) 구체 타입을 정해주게 된다. 

 

// 구현부에서는 구체 타입을 알지 못함
struct Stack<T> {
    var items : [T] = []
}

// 호출부에서 placeholder(타입파라미터)를 추론함으로써 구체 타입 추론 가능
var StringStack = Stack<String>() //Stack<String> 으로 구체 타입 추론
var IntStack = Stack<Int>() //Stack<Int> 으로 구체 타입 추론

 

 

 

 

 

 

 

📍Opaque Type

 

그럼 '역제네릭 타입'이라고도 불리는 Opaque Type은 어떨까?

 

불투명 타입(Opaque Type)은 주요 특징이 제네릭과 반대되기 때문에 '역제네릭 타입'이라고 불리는 것이다.

 

 

 

 

▶️ Opaque Type의 특징 

  • 구현부에서는 구체적으로 어떤 게 리턴되는지 알지만,
  • 호출부에서는 어떤 구체 타입이 리턴되는지 모른다.

특징적인 부분만 제네릭과 비교해서 딱 봐도 왜 '역제네릭 타입' 이라고 불리는지 알겠죠?

// 구현부에서는 어떤게 리턴되는지 알지만
func returnSomeString() -> some Equatable {
	return "Hello" //⭐️ Equatable을 준수하는 String 타입 반환
}
func returnSomeInt() -> some Equatable {
	return 10 //⭐️ Equatable을 준수하는 Int 타입 반환
}


//호출부에서는 어떤 타입이 리턴되는지 모른다 
returnSomeString()
returnSomeInt()

 

 

 

 

 

▶️ some View는 어떨까?

 

SwiftUI에서 뷰를 그릴 때 사용하는 body 연산프로퍼티의 경우에 항상 some View를 반환하는데 이 경우에는 어떻게 동작하고 있는 걸까??

 

 

아래 코드처럼 body 연산 프로퍼티의 값이 리턴되기 직전에 프린트를 찍어보면 우리가 뷰를 만들면서는 본 적 없는 타입에 대한 정보가 찍히는 것을 확인할 수 있다. 

struct TestView : View {
    
    var body: some View {

    
        let view = VStack{
            Text("-")
                .foregroundStyle(.green)
            Text("--")
                .foregroundStyle(.blue)
        } .padding()
        
        print(type(of: view))
        //구현부에서는 어떤 타입이 리턴되는지 알고 있음
        
        return view
        
    }
}

 

 

이처럼 some 키워드로 설정한 Opaque Type(불투명 타입)은 구현 내부적으로는 구체 타입을 알고 있지만, 외부에서 이 타입으로 정의된 프로퍼티나 리턴값을 사용할 때는 some View와 같은 불투명 타입으로만 알고 사용하게 된다.

 

 

 

 

 

 

 

 

📍any VS some

그럼 이제 헷갈리는 게 some키워드와 any 키워드!!

 

Existential Type을 좀 더 명시적으로 나타내는 any 키워드에 대해서는 저번 블로그 글에서 다뤘으니 이를 바탕으로 생각해 보자!
[Swift] any 키워드는 왜 있는걸까? Existential Type은 뭔데?

 

any와 some... Existential Type과 Opaque Type..

얼핏 생각하면 프로토콜에 any,some과 같은 키워드를 더함으로써 구체 타입을 사용하지 않는다는 점이 비슷해 보이는데 어떤 차이가 있을지 자세히 살펴보자!

 

 

 

 

▶️ any 키워드 (Existential Type) VS some 키워드 (Opaque Type)

 

차이점을 볼 때는 역시 공통점부터 보는 게 공부할 때 더 명확하고 좋은 듯하다!

 

공통점

  • any, some 둘 다 프로토콜을 반환타입으로 사용할 때 사용한 수 있는 키워드이다.
  • 반환 타입의 구체적인 구현을 외부(호출부)에 노출하지 않고 구현부 내부에서만 알고 있다. 
  • 호출하는 코드 입장에서 반환된 값의 구체적인 타입 대신, 프로토콜의 요구 사항만 사용하도록 강제시킬 수 있다.

차이점

  • some
    • 정적( 하나의) 구체 타입만 반환 허용하기 때문에 컴파일 타임에 고정된 하나의 구체타입을 결정하게 된다.
    • 반환된 타입은 항상 동일한 구체타입임을 보장하기 때문에 예측 가능한 프로그래밍이 가능하다
    • 단, 외부에서는 구체적인 타입정보를 알 수 없다.
    • 메서드 사용 시, Static Dispatch가 가능 -> 성능 최적화가 가능
  • any
    • 동적( 조건별 다양한 ) 구체 타입 반환 허용
    • 반환된 타입이 프로토콜 요구사항을 충족하는 여러 다른 구체타입일 수 있다.
    • 단, 동일한 프로토콜을 준수하기 때문에 프로토콜의 메서드/속성 사용이 가능하다
    • 런타임에 여러 구체 타입 중 하나를 알 수 있다. 
    • 메서드 사용 시, Dynamic Dispatch성능상 비용이 있을 수 있다.

 

 

 

 

▶️ 예제 코드

 

protocol Shape {
    func area() -> Double
}

struct Circle: Shape {
    let radius: Double
    func area() -> Double { return Double.pi * radius * radius }
}

struct Square: Shape {
    let side: Double
    func area() -> Double { return side * side }
}


//some
func makeSomeShape() -> some Shape {
    return Circle(radius: 5) // Circle 타입이 고정
}

let shape1 = makeSomeShape()
print(shape1.area()) // 항상 Circle 관련 메서드 호출


//any
func makeAnyShape(isCircle: Bool) -> any Shape {
    if isCircle {
        return Circle(radius: 5) // Circle 타입 반환
    } else {
        return Square(side: 4) // Square 타입 반환
    }
}

let shape2: any Shape = makeAnyShape(isCircle: true)
print(shape2.area()) // Circle 또는 Square의 area 호출 가능

 

 

이 코드에서 확인할 수 있듯이 any Shape을 반환하는 함수에서는 Shape을 준수하는 어떤 구체 타입이든 반환할 수 있다.

 

 

 

반면, 아래 코드처럼 some Shape을 반환하는 makeSomeShape 함수에서 여러 타입을 반환하려고 하면 이런 에러가 뜰 것이다.

🚨Function declares an opaque return type 'some Shape', but the return statements in its body do not have matching underlying types

//some
func makeSomeShape(isCircle: Bool) -> some Shape { //🚨🚨🚨🚨
    if isCircle {
        return Circle(radius: 5) // Circle 타입 반환
    } else {
        return Square(side: 4) // Square 타입 반환
    }
}

 

 

 

 

 

 

 

📍any와 some 각각 언제 사용하면 좋을까?

 

위에서 살펴본 차이를 바탕으로 각각 키워드를 언제 사용하면 좋을지에 대해 생각해 보자면

 

 

  • some
    • some 키워드는 타입을 "제한"하는 역할만 할 뿐, 구현할 때는 정적(한 가지의) 구체타입만 허용되기 때문에 컴파일 타임에 타입 안정성이 중요하거나 성능 최적화가 필요할 때 사용하면 좋으며, 
    • 반환받아 사용하는 쪽에서는 굳이 구체적인 타입을 몰라도 되는 경우에 사용하면 좋다.
    • e.g. SwiftUI의 some View처럼 구체적이고 고정된 타입을 반환하는 UI 구성요소. SwiftUI에서 body 속성은 특정 유형의 뷰를 반환하지만, body 속성을 사용하는 코드는 특정 유형이 무엇인지 알 필요가 없습니다.

 

  • any
    • underlying type information, 즉, 대체될 수 있는 구체 타입에 대한 정보를 지워서 여러 가지 구체타입에 대해 리턴할 수 있기 때문에 다양한 타입을 유연하게 처리해야 하는 경우에 사용하면 좋다.
    • e.g. collection 프로토콜을 준수하는 여러 객체들을 처리한다고 할 때 연관 타입(associatedtype)인 Element가 어떤 타입이든 구체 타입이 될 수 있음.

 

 

즉, Existential Type(any 키워드)는 Opaque Type(some 키워드)와 상호 보완적으로 사용될 수 있는데, 유연성이 필요하면 any, 타입 안정성과 성능 최적화가 중요하다면 some을 선택하는 것이 좋다. 

 

 

 

 

 

▶️  애플에서 제안하는 any, some의 사용

wwdc에서 any와 some에 대해 설명할 때에 각각을 언제 사용하면 좋은 지도 언급하는데

 

https://developer.apple.com/videos/play/wwdc2022/110352/?time=1572

 

Embrace Swift generics - WWDC22 - Videos - Apple Developer

Generics are a fundamental tool for writing abstract code in Swift. Learn how you can identify opportunities for abstraction as your code...

developer.apple.com

 

 

In general, write "some" by default, and change "some" to "any" when you know you need to store arbitrary values.

기본적으로는 "some"을 사용하고, 여러 임의 타입에 대한 처리가 필요한 경우에만 "any"로 바꾸라고 이야기한다.

 

 

With this approach, you'll only pay the cost of type erasure and its semantic limitations when you need the storage flexibility it provides.

이렇게 하면 저장 유연성이 필요할 때만 type erasure에 대한 비용을 지불하게 된다고 한다.

* the cost of "type erasure"  :  any를 사용했을 때 컴파일 타임에는 구체타입에 대한 정보를 지우고 런타임에 구체 타입을 알게 되기 때문에 이에 대한 비용을 의미하는 것.

 

 

 

 

 

 

 

 

 

📍그럼 왜 SwiftUI는 "some" View 를 사용할까?

잊지 말자! 나는 "왜 SwiftUI가 some View를 사용하는가"가 궁금해서 이 공부를 시작한 것..!!!!

 

 

 

SwiftUI는 뷰의 구조나 modifier등에 따라 다양한 타입이 리턴된다! ( 위에서 프린트 찍어봤던 것처럼! ) 

하지만 뷰가 복잡해질수록 body 프로퍼티의 타입이 매우 복잡해질 수 있는데,  시스템상에서 이 body에서 리턴 받은 값을 사용해서 뷰를 그릴 때 굳이 이 복잡한 구체타입을 알 필요가 없다.

 

 

https://developer.apple.com/videos/play/wwdc2022/110352/?time=855

 

Embrace Swift generics - WWDC22 - Videos - Apple Developer

Generics are a fundamental tool for writing abstract code in Swift. Learn how you can identify opportunities for abstraction as your code...

developer.apple.com

 

이 WWDC 영상에서도 SwiftUI가 some을 사용하는 이유에 대해 언급한다. 

In a SwiftUI view, the body property returns some specific type of view, but code that uses the body property doesn't need to know what the specific type is.

 

 

 

 

이처럼 body 프로퍼티 내부에서는 구체적인 타입으로 인지하고 있지만,  외부에서 호출해서 사용할 때는 구체적인 타입을 숨기고 싶기 때문에! some이라는 키워드를 사용하는 것이다. 그리고 컴파일타임에 하나의 구체타입만을 보장하는 some의 특징으로 인해 컴파일 타임의 타입 안정성의 장점any 대비 성능상 이점을 가질 수 있다.

 

 

 

 

 

 

 

 

📍요약

 

some키워드, Opaque Type 특징

  • some 키워드로 나타내는 타입을 Opaque Type이라고 하는데, 불명확 타입이라고도 불리며, 역제네릭 타입으로도 불린다.
  • 구현부에서는 구체적으로 어떤 게 리턴되는지 알지만, 호출부에서는 어떤 구체 타입이 리턴되는지 모른다.
  • 정적(하나의) 구체 타입만 반환 허용하기 때문에 컴파일 타임에 고정된 하나의 구체타입을 결정하게 된다.
  • 메서드가 Static Dispatch로 동작하며 성능 최적화가 가능
  • -> 컴파일 타임에 타입 안정성이 중요하거나 성능 최적화가 필요할 때 사용된다.

 

SwiftUI에서 "some" View를 사용하는 이유

  • 뷰를 반환하는 연산 프로퍼티(e.g. body연산프로퍼티) 내부에서는 구체적인 타입으로 인지하고 있지만,  해당 프로퍼티를 호출해서 사용할 때는 구체적인 타입을 알 필요가 없다.
  • 컴파일타임에 하나의 구체타입만을 보장하는 some의 특징 때문에 컴파일 타임의 타입 안정성의 장점을 가질 수 있고,
  • type erasure의 비용이 드는 any와 비교했을 때 성능상 이점을 가질 수 있다.