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://mini-min-dev.tistory.com/319
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/opaquetypes/
'iOS > Swift' 카테고리의 다른 글
| [iOS/Swift] Mock과 Stup은 다르다고? - Test Double (0) | 2025.04.03 |
|---|---|
| [iOS/Swift] Swift의 UnitTest 살펴보기 (0) | 2025.03.31 |
| [iOS/Swift] Kingfisher를 쓰지 않고 이미지 캐싱 구현하기 - NSCache (0) | 2025.03.25 |
| [iOS/Swift] Combine에서 cancellables가 inout파라미터인 이유 (0) | 2025.03.23 |
| [iOS/Swift] DI와 DIP는 다르다! - 의존성 주입 알아보기 (1) | 2025.03.18 |