[SwiftUI] SwiftUI의 View는 왜 Struct (Value Type)로 설계된 것일까?

2025. 5. 7. 16:28UIKit, SwiftUI, H.I.G

이 글을 읽기 전에 알고 있어야 하는 개념, 즉 모른다면 참고해서 보면 좋을 글을 아래에 첨부합니다😊

 

[Swift] 구조체(Struct)와 클래스(Class) 완전 정복하기: 기본 개념부터 프로퍼티, 인스턴스, 상속까지

이번 글에서는 구조체(Struct)와 클래스(Class)에 대해 아주 자세하게 다뤄보려 한다. 처음 Swift를 배우는 입장도 아닌데, 이제 와서 이 내용을 포스팅하는 이유가 뭐냐고 물어본다면... 음... 몇 번

mini-min-dev.tistory.com

 

[iOS] 내가 보려고 정리하는 개발 용어 사전 (3) - 명령형 프로그래밍(Imperative Programming) vs 선언형

현재 이 글을 쓰고 있는 2024년 기준, iOS 개발을 배우고 싶다고 마음을 먹게 되면 선택할 수 있는 옵션은 두 가지.명령형 프로그래밍 기반의 UIKit와 선언형 프로그래밍 기반의 SwiftUI가 있다.처음

mini-min-dev.tistory.com


UIKit과 SwiftUI의 여러 차이점이 있지만,
처음 View를 만들 때 가장 눈에 띄게 들어왔던 차이점은 UIKit의 UIView는 참조 타입인 클래스 (class)로, SwiftUI의 View는 값 타입인 구조체 (struct)로 선언한다는 점이었습니다.

왜 애플의 대표적인 UI 프레임워크 두 개가 각각 화면을 생성하는 방법이 다른 걸까요?

오늘은 이 질문에 대한 답을 찾아 떠나보도록 하겠습니다!

왼쪽이 UIKit에서 선언한 UIView, 오른쪽이 SwiftUI에서 선언한 View


우선 Apple은 공식문서에서부터 구조체 (struct)와 클래스 (class) 중 하나를 선택해야하는 상황이라면,
구조체 (struct)를 우선 기본값으로 사용하는 것을 권장하고 있습니다. -> Prefer structs by default!

*Objective-C와의 상호 운용성 (Interoperability)이 필요하거나, 객체의 고유성 (Identity)이 중요한 경우에 클래스를 사용하라고 언급하고 있습니다.
**클래스의 고유한 특징인 상속 (Inheritance) 마저도 구조체에서 구현 가능한 프로토콜 채택 방식으로 상속 관계를 구현하도록 애플이 권장하고 있습니다. (If you’re building an inheritance relationship from scratch, prefer protocol inheritance.)

 

Choosing Between Structures and Classes | Apple Developer Documentation

Decide how to store data and model behavior.

developer.apple.com


아직 본격적으로 그 이유를 설명하지는 않았지만,
애플이 클래스보다 구조체를 권장하는 이유가 SwiftUI에서 View를 구조체로 선언하는 이유와 아마 동일하지 않을까해요!
감히 추측해 보면... 너무도 당연하게 성능적으로 구조체가 클래스에 비해 우월하다는 이유가 있겠죠...?

그렇다면 UIKit은 "어쩔 수 없이" 클래스를 사용할 수밖에 없었던 모종의 이유가 있었던 것일거구요. 이야기를 본격적으로 이어가볼게요!

 

 

1️⃣ Composable한 SwiftUI의 선언형 컴포넌트 스타일 (Supports small, single-purpose components)

UIKit에서는 하나의 커스텀 뷰를 만들기 위해 UIView라는 상위 클래스를 상속받아 만들게 됩니다.

상위 UIView는 뷰의 속성을 지정하기 위한 다양한 프로퍼티들을 이미 갖고 있었죠. (alpha, backgroundColor, frame과 같은)
우리는 이 상위 UIView를 채택했기 때문에 이 모든 저장된 프로퍼티를 함께 상속받아 수정해가며 뷰를 직접 커스텀할 수 있었습니다.
즉, 객체지향프로그래밍 (OOP)의 가장 기본 원리인 상속 (Inheritance) 기반 방식이었던 것이죠.
이 상속 방식에서는 상위 객체에 담긴 많은 프로퍼티, 메서드 등이 기본적으로 함께 따라오게 되는, 무겁고 큰 크기의 하위 객체가 생성될 수밖에 없는 문제가 있었습니다.


이를 해결하기 위해 SwiftUI는 프로토콜 (protocol)조합 (Composition)의 방식을 가져갑니다.

UIView처럼 모든 공통 속성 (opacity, background)을 View가 직접 갖고 있는 것이 아니라, 각각의 Modifier로 분리해서 표현을 하게 되는 것이죠. View에는 더 이상 공통의 큰 저장소가 필요 없습니다.
각각의 한 Modifier는 자신의 역할 (예를 들어, 투명도나 배경색 같은)만 수행하는 View가 되고, (SRP도 지키고 있는 셈이네요😊)
전체 View 트리 구조는 이 Modifier View (= Components)의 중첩되는 계층으로 표현할 수 있게 됩니다. -> 훨씬 가볍고 최적화가 이루어진 형태이죠!

"Each concrete type of view is just an encapsulation of some other view in its body property, and the inputs required to create that view are its own properties."
-> SwiftUI에서 모든 View 하나하나는 거대한 View hierarchy에서 하나의 노드에 불과, 입력을 받아 출력 UI를 구성하는 함수형 구조를 나타낸다는 의미라고 이해하면 되겠습니다 ^__^

왼쪽이 상속 기반 UIView, 오른쪽이 Protocol과 Modifier 계층 기반 SwiftUI의 View

 

 

2️⃣ 성능적 이점 - struct는 할당되지 않고, 포인터를 갖지 않음 (Not allocated, no pointers)

값 타입 (Value Type)인 구조체 (struct)는 스택(Stack)에 직접 저장됩니다.
즉, View 인스턴스를 생성할 때 별도의 힙(Heap) 메모리에 할당되지 않는다는 의미죠.

🧐 스택 (Stack)과 힙 (Heap) 메모리 비용의 차이
- 스택 (Stack) : 메모리 할당과 해제가 단순하고 빠르다. 메모리 풀에서 추가적인 공간을 요청하지 않고, 단순하게 포인터만 움직이는 방식
- 힙 (Heap) : 할당 알고리즘과, 메모리가 산발적으로 배치 (non-contiguous)되기 때문에 상대적으로 느리고 복잡함. 동적 크기의 메모리를 저장하는 공간 -> 그러기에 객체 해제 시기가 명확하지 않고, 오버헤드가 불가피하게 발생한다.


또한 구조체는 포인터를 사용하지 않기 때문에 메모리 관리 오버헤드가 없고, ARC에 의한 참조 카운팅(reference counting)이 수행되지 않습니다.
*여기서의 메모리 오버헤드 (Overhead)란 프로그램이 본래 필요한 메모리 외에, 부가적으로 사용되는 메모리를 의미합니다.
**포인터를 사용하지 않으니 -> ARC에 의한 참조 카운팅이 수행되지 않고 -> 인스턴스 생성/소멸 시마다 발생하는 참조 카운트 관리 비용이 들지 않게 되는 것이죠. -> 쉽게 말해 복잡한 일을 수행하지 않아도 된다는 것!

각각의 View별로 메모리 주소를 할당받는 UIView에 비해, 고유한 메모리 주소를 갖지 않는 SwiftUI의 View


이 모든 것은 위에서 말했던 View 자체가 무겁지 않고, 크기가 작게 유지되기에 가능한 것입니다.

 

 

3️⃣ 효율적인 메모리 표현 방식 (Efficient memory representation)

우선, 값 타입이기 때문에 레이스 컨디션 (Race Condition) 문제로부터 SwiftUI View는 자유롭다고 볼 수 있습니다.

구조체 값 (data)은 복사를 할 때 독립적으로 생성되므로,
하나의 복사본을 변경하더라도 다른 복사본에 끼치는 영향이 없다는 것이죠. 멀티 스레드 환경에서도 예상치 못한 부작용 (Side effect)이 없을 겁니다!
실제로 참조 타입 (Reference Type)에서 발생하는 Data Race 문제가 Swift 6의 주요 개선사항인 것을 생각하면,
최신 프레임워크 SwiftUI에서도 이 문제를 근본적으로 개선할 수 있는 방법을 생각한 것이 바로 struct 사용이었을 것입니다.


그리고 앞에서 봤던 내용과 이어집니다.
UIView가 상속 방식을 채택하면서 담기게 되는 공통적인 속성을 SwiftUI의 View에서는 경량화된 방식으로 분리했기 때문에, 각 View struct는 자신에게 필요한 정보만 저장해 불필요한 저장공간을 갖지 않습니다.
-> 즉, 뷰 하나하나의 메모리 footprint가 매우 가볍게 되는 것이죠.

 

 

☑️ 결론!

Combine도 그렇고, TCA도 그렇고, 오늘 딥하게 살펴본 SwiftUI도 그렇고..
최신 애플 (공식/비공식적인) 프레임워크의 주요 특징은 하나하나의 작은 요소들을 조합 (compose)함으로써 커다란 하나를 만들어나간다는 느낌입니다.

실제로 그렇게 구성을 했을 때,
각 객체는 하나의 책임만을 져야한다는 SRP (단일 책임 원칙)이나 / 사용하지 않는 것에 의존해서는 안된다는 ISP (인터페이스 분리 원칙) 등의 SOLID 원칙에도 부합하는 코드를 짜게 된다는 느낌이 확실히 드는 것 같고요.

클래스 (class)던, 구조체 (struct)던 사실 실제로 제가 개발을 하면서 "와 성능차이가 난다!" 라고 느낀적은 없습니다.
*그리고 class를 사용할 때도 더 이상 상속이 발생하지 않는다면 final 키워드를 붙여 클래스의 성능을 높이는 노력도 습관적으로 하기도 했구요.
그럼에도 불구하고,
애플은 SwiftUI의 View를 조금 더 가볍게 만들고 싶었으며, (상속이 아니라 프로토콜이라는 새로운 패러다임을 직접 적용하며)
추가적으로 발생하는 사이드 이펙트, 선언적 프로그래밍에 적합한 방식으로 View를 struct (Value type)로 선택하는 결정을 하게 된 것으로 보였습니다. 이상 미니의 재밌는 공부였습니다 😊