[iOS/Swift] 컴파일러 최적화 관점에서 본 Opaque Type

SwiftUI로 View를 구성하다보면

`var body: some View { ... }` 라는 연산 프로퍼티 내에 뷰를 그리는 코드를 작성하게 된다.

 

여기서 body라는 프로퍼티의 타입인 `some View`는 대체 어떤 구조이며, 왜 이런식으로 사용하는지 궁금해졌다.

 

이번 글에서는 컴파일 최적화 관점에서의 Opaque Type을 알아보도록 하겠다.

 


Opaque Type이란?

SwiftUI의 body는 다음과 같이 구성되어있다.

var body: some View {
    Button("버튼") {
        let value = type(of: self.body)
        print(value)
    }
}

여기서 body의 타입을 확인해보면 `Button<Text>`임을 알 수 있는데,

그럼 body의 타입을 다음과 같이 지정해도 문제가 없다.

var body: Button<Text> {
    Button("버튼") {
        let value = type(of: self.body)
        print(value)
    }
}

 

그런데 이 Button의 foregroundColor를 바꾸고 싶어서 다음과 같이 모디파이어를 지정해주면 타입이 다음과 같이 바뀌게 된다.

var body: Button<Text> {
    Button("버튼") {
        let value = type(of: self.body)
        print(value)
    }
    .foregroundStyle(.yellow)
}

 

그럼 body의 타입을 이와 일치시켜주기 위해서 이 길고 긴 타입을 써주더라도

이후에 모디파이어를 추가하거나, 삭제하면 그에 맞게 또 타입을 수정해주어야 한다.

 

이렇게 복잡한 타입을 외부에 노출하지 않고, 단순히 View 프로토콜을 따르는 어떠한 뷰 라는 의미로

`some View`라는 타입으로 간단하게 써줄 수 있는데, 이게 Opaque Type - 불투명 타입 이라고 부르는 것이다.


Opaque Type의 특징

`some`이라는 키워드를 통해 `View`라는 프로토콜을 따르는 어떤 타입이라는건 명시했지만,

사실 내부적으로는 `ModifiedContent<Button<Text>, _ForegroundStyleModifier<Color>>` 이렇게 긴 타입이 숨겨져 있다는 것인데..

컴파일러는 이 타입을 어떻게 알고있는 것일까?

결론부터 말하자면, 컴파일러는 실제 타입을 알고 있으면서 숨기는 것이다.

Opaque Type을 역 제네릭 타입이라고도 부르는데 여기에 그 이유가 있다.

 

제네릭은 선언할 당시에는 어떤 타입이 들어올 지 모르지만, 런타임 시점 즉 실제로 실행할 때 어떤 타입인지 알게 되는데,

Opaque Type은 선언할 당시에 어떤 타입일지 알고 있다. 다만, 그 타입 이름을 명시하지 않아도 된다는 특징이 있는 것이다.

 

Opaque Type의 특징을 정리하면 다음과 같다.

Opaque Type: 함수나 프로퍼티의 반환 타입이 실제로 어떤 구체 타입인지 외부에 노출하지 않으면서도, 내부적으로는 특정 제약을 만족하는 ‘하나의 구체 타입’임을 보장

 

 

그렇다면 이 Opaque Type을 사용함에 있어 컴파일에서는 어떤 이점이 있는지 알아보자.


컴파일러 최적화 관점에서 본 Opaque Type의 이점

Opaque Type을 사용하면 컴파일러가 반환할 구체 타입을 정확히 알고 있기 때문에 여러 가지 최적화가 가능하다.

 

  • 정적 디스패치

먼저 정적 디스패치와 관련된 이점이다.

https://dev-voo.tistory.com/31

 

[iOS/Swift] WMO와 Method Dispatch - class에 final을 붙이는 진짜 이유

일반적으로 더이상 상속되지 않을 class 앞에 `final`이라는 키워드를 붙이곤 한다.그렇게 하면 이 class는 더이상 상속되지 않을걸 알기에, 시스템의 입장에서 재정의를 위한 일련의 과정을 수행하

dev-voo.tistory.com

지난 글에서 동적 디스패치와 정적 디스패치와 관련된 글을 쓴 적이 있는데, Opaque Type 역시 이와 관련지어볼 수 있다.

 

Opaque Type의 메서드가 있는경우 반환 타입은 컴파일 시점에 정해져 있다.

즉, 이 메서드의 호출은 동적 디스패치가 아닌 정적 디스패치로 호출되므로 성능상 이점을 가져갈 수 있다.

 

  • SwiftUI의 뷰를 그리는 방식

다음으로는, SwiftUI가 UI를 그리는 방식과도 연관이 있다.

SwiftUI는 UI에서 변경된 부분이 있을때 측정 최적화 매커니즘을 통해 어떤 부분이 같고, 다른지 구분하려고 한다.

그 중에 `Structural Identity 구조적 동일성`은 뷰의 구조와 위치를 기반으로 구분하여 diff 연산을 통해 필요한 부분만 업데이트를 함을 의미한다.

 

예를 들어 다음과 같은 코드는 구조적 동일성을 지키는 코드이다.

struct TamagochiView: View {
    
    @State var count = 100
    
    var body: some View {
        VStack {
            Text("밥알 갯수")
            Text("\(count)개")
            Text("밥알 먹이기")
                .wrapToButton {
                    count += 1
                }
        }
    }
}

여기서 count가 변경되면 `Text("\(count)개")`만 렌더링되고, 다른 뷰들은 렌더링이 안되는 효율적인 코드라고 할 수 있다.

 

아무튼, 이때 `some View`라는 Opaque Type을 사용하면 diff 연산에서의 이점을 얻을 수 있다.

먼저, Opaque Type으로 인해 컴파일 시점에 구체적 타입을 알고 있는 상태이기 때문에 SwiftUI가 뷰 계층구조를 비교할 때 정확한 타입 정보를 알 수 있다. 이는 diff 연산을 할때 더 효율적인 연산이 가능하다는 뜻이다.

또한 diff 연산을 할 때는 뷰 타입과 식별자를 사용하여 변경된 부분을 감지하는데, Opaque Type은 구체적인 타입 정보를 유지하므로 효율적인 비교가 가능하다.

 

사실 이런 이점은 `Any View`와 같은 `Type Erasure`와 비교를 해서 살펴보면 더 와닿을거라고 생각한다.

 

하지만,,, `Type Erasure`에 대한 이해가 아직 부족한 상태이므로, 추후에 `Opaque Type`과 `Type Erasure`를 비교하는 글을 별도로 써보려고 한다.

 

그래도 간단하게 비교하자면 `Type Erasure`는 `Opaque Type`과 다르게 런타임 시점에 타입을 완전히 특정하기엔 불가능하기 때문에 SwiftUI가 뷰 계층구조를 비교할 때 더 많은 작업이 필요하다.

그래서 공식 문서에서도 `AnyView`와 같은 `Type Erasure`의 사용보다 `some View`와 같은 `Opaque Type`의 사용을 더 권장하고 있다.

 

추후에 별도의 글로 더 자세히 정리해보겠다.


* 참고자료

https://www.hackingwithswift.com/books/ios-swiftui/why-does-swiftui-use-some-view-for-its-view-type#:~:text=First%2C%20using%20,again%20after%20every%20small%20change

 

https://mini-min-dev.tistory.com/319

 

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/opaquetypes/

 

https://ios-development.tistory.com/1286