Background
SwiftUI는 선언형 기반으로 구현하도록 설계되어 있다. 이 선언형의 관점을 이해하기 위해서는 함수형 프로그래밍에 대한 깊이있는 이해가 필요하다. Immutable type을 왜 쓰는지, Value type을 왜 사용하는지, 순수함수를 왜 쓰고 어떻게 비지니스 로직을 구현해야 하는지 등이다. 그리고 SwiftUI의 동작 체계에 대한 이해도 필요하다. View의 업데이트는 어떻게 이루어지는지, 이에 대한 최적화 작업은 어떻게 해야 하는지 등이다. SwiftUI를 가볍게만 사용하던지라 이에 대한 이해가 부족했고, 그러기에 이에 대해서 정리하려고 합니다.
SwiftUI가 iOS 13부터 지원했지만, 최근까지 버그도 많고 아직 기술 자체가 덜 성숙했던지라 어떤 변화가 생겼는지 정도만 관심을 가져왔다. 이제는 슬슬 SwiftUI가 프로젝트에 적용할만한 단계로 다가온 듯 하여, 보아만 왔던 부분들을 정리하고자 한다. SwiftU의 기초 개념들은 아래의 포스트에서 훑어보고 오는 것을 권한다.
https://maskkwon.tistory.com/300
[iOS] SwiftUI Foundation
ProtocolsApp 프로토콜 앱의 구조를 설정하기 위한 프로토콜. @main을 통해서 main() 메소드의 기본 구현을 통해 entry point를 제공한다. Scene 프로토콜을 구현하여 App의 body를 구현한다. 각 scene은 각각의
maskkwon.tistory.com
UIKit과 SwiftUI는 어떻게 다른가?
UIKit
UIKit은 각 view가 레퍼런스 타입의 클래스로 이루어져있다. 각 클래스는 UI에 표시되는 정보를 내부적으로 캡슐화하여 가지고 있다. 이 데이터들이 업데이트 되면 사용자는 이에 따른 View의 업데이트를 명령한다. 명령받은 UIKit 객체들은 OS에서 정의된 렌더링 싸이클을 수행하여 view를 업데이트 한다.
Event (Touch 등)
↓
setNeedsLayout() → layoutSubviews()
↓
setNeedsDisplay() → draw(_:)
↓
CALayer 트리 업데이트
↓
Core Animation → GPU Render → Display (V-Sync)
위의 렌더링 싸이클을 통해서 view들이 업데이트 된다. 각 view는 내부에 선언된 혹은 외부의 데이터를 참조하여 UI를 업데이트한다. 이는 데이터와는 별개로 수동적으로 업데이트가 이루어진다. 여기서 수동적이라는 것은 데이터의 변화가 생겨도 사용자가 직접 업데이트를 명령하지 않으면 업데이트가 이루어지지 않는다는 것이다. 이는 불필요한 업데이트가 호출되는 자원의 낭비를 예방하고, 명시적인 사용자의 명령에 의해 움직이기에 예외적인 동작 이상을 방지할 수 있다. 단지 유효한 데이터를 제때 보여지기 위해서 사용자가 개발하면서 각별히 신경을 써줘야한다. 그리고 각 스레드의 간섭에 의한 데이터의 무결성을 해칠 수 있는 상황 등에 대해서도 각별히 신경을 써야한다. 이를 위해서 우리는 데이터 동기화 및 비동기 동작 처리와 view의 생명주기 등을 공부했다. 그리고 각 view와 데이터의 관계를 보다 체계적으로 관리하기 위해서 아키텍처 등을 배우고 응용해왔다.
SwiftUI
SwiftUI는 비동기/병렬 처리 등이 대중화되고, 이에 대해서 좀더 편리하고 안정적인 개발 방법론 들이 대두되면서 등장했다. 이미 개발 방법에서 함수형과 선언형이 널리 사용되고 있다. 함수형을 이용해서 각 비지니스 로직을 함수 단위로 캡슐화하고, 이 무결한 로직들을 파이프라인으로 연결하여 구현한다. 생성된 데이터는 즉시 UI에 반영되도록 하여 데이터의 외부 참조 등으로 인한 side effect를 최소화한다. 앱의 구현 상으로 부득이하게 외부 참조 및 데이터의 보존이 필요한 경우가 발생하는데, 이 경우에 대해서 몇 가지의 데이터 저장 및 전달 방식 (State, Binding, ObservedObject, StateObject, EnvironmentObject 등)을 제공하도록 한다. 데이터의 변화는 함수형으로 이루어진 파이프라인을 통해서 처리되고, 선언적으로 체결된 UI에 렌더링 싸이클에 따라서 반영되도록 한다. UIKit과 비교하여 매우 즉각적이고 능동적인 방식이다.
State 변화 발생
↓
SwiftUI Diff Engine이 View 트리 비교
↓
변화된 부분만 새로 렌더링
↓
Core Animation으로 전달
↓
GPU Render → Display
위의 랜더링 싸이클에 따라서 데이터는 UI에 즉시 반영되도록 한다. SwiftUI는 데이터의 변화를 감지하고 기존 View 렌더 트리와 새로 구성될 View 렌더 트리의 차이를 Diffing 알고리즘을 통해서 비교하고, 차이가 발생한 부분만 다시 그리도록하여 불필요한 업데이트를 사전에 방지한다. Diffing 알고리즘은 각 트리를 DFS (깊이 우선 탐색)을 통하여 차이를 비교한다. 하지만 시스템적으로 사전 감지가 불가능한 부분, 즉 불필요한 렌더링은 사용자가 인식하고 예방해야 실제 성능적인 부분에서의 이슈를 방지할 수 있다.
State는 어떻게 관리되는가?
@State로 선언된 데이터는 어떻게 관리되는 지에 대해서 알아볼 필요가 있다. SwiftUI에서 View는 value type이다. 즉, View가 업데이트될 때에 기존의 view에서 데이터를 교체해서 다시 그리는 것이 아니라, view를 새로운 데이터를 기반으로 다시 생성한다. 그렇다면 새로운 view를 다시 생성할 때에 view 구조체에 담긴 state property도 초기화되는가? 결론부터 얘기하면 아니다. 우리는 @State라는 프로퍼티 래퍼로 감싼 property의 값을 view의 업데이트와 상관없이 지속적으로 사용할 수 있다. 매번 view 업데이트마다 새로 생성되는 view와 같이 초기화된다면 state property의 사용성이 떨어진다. 이를 위해서 SwiftUI는 state peropery를 별도의 공간에 두어 관리한다. view가 렌더 트리 상에서 제외되면 state property 또한 지워지겠지만, view가 렌더 트리에 존재하는 이상에는 view id와 연계되어 지속적으로 관리된다.
SwiftUI의 View는 왜 Value type인가?
위에서 SwiftUI에서 View는 value type이라고 했다. 일반적으로 객체지향부터 개발해오던 친구들은 객체를 재사용하는 것이 자원관리나 성능적으로 이점이 있는데 왜 다시 만들면서까지 value type을 고집하는 것인지 의문을 가질 수 있다. 또한 value type을 왜 쓰는 지에 대한 이해도 없이 작성한 코드가 만드는 어마무시한 악영향도 보왔었다. 이를 이해하고 쓰기 위해서 ‘자원 재사용의 효율성‘과 ‘예측 가능한 코드를 통한 런타임 안정성‘ 사이에서 균형을 잡을 필요가 있다.
SwiftUI의 얘기로 돌아가서, SwiftUI는 선언형이고 데이터의 변화를 감지한다. diffing 알고리즘에 의해서 view의 업데이트가 필요한 부분만 감지하여 view 업데이트를 수행한다고 했다. 여기서 diffing 알고리즘이 효율적으로 동작하기 위해서는 각 view는 불변이어야한다. 불변이 아니라면, diffing 단계에서 각 view에 어떤 정보가 담겼는지 체크해야하는 별도의 동작이 필요하다. view에 표시된 데이터가 A인 상태에서 불변이라면 이 친구는 계속 A만 표시된다. 하지만 view에 표시된 데이터가 A였다가 B로 바뀔 수 있는 가변성을 가졌다면, 현재의 view에서 A, B 혹은 C가 표시되고 있는지 등에 대한 확인 처리가 필요해진다. 이는 렌더링 시점에 불필요한 부하를 발생할 뿐만 아니라, 함수형과 선언형이라는 패러다임하고도 맞지 않다. 즉, '새 술은 새 부대에 담는다'라는 속담이 있듯이 새로운 데이터는 새로운 view에 담도록 한다. 자연스럽게 불변과 가변을 구분하면서 메모리 관리에서도 단순하고 편한 value type으로 손이 가게 된다. reference type은 어떤 데이터가 담겼을지도 예측하기 어렵고, 비동기 동작까지 처리하려면 reference count 관리 등으로 인한 메모리 누수의 가능성 또한 있다. 이렇게 view를 reference type으로 사용하면 위험성이 점점 예측 불가능한 상황으로 번지게 된다. OS 관점에서는 지속적으로 발생하는 데이터 변경과 그에 따른 비교와 업데이트를 위해서는 value type의 view를 차용하는 것이 효과적인 선택이었을 것으로 사료된다. 그렇다고 reference type이 꼭 나쁘다는 것이 아니라는 것만 기억하자 ㅎㅎ
'Programming > Mac & iOS' 카테고리의 다른 글
| [iOS] Core Video Foundation (0) | 2025.10.30 |
|---|---|
| [iOS] Accelerate computing with Metal (0) | 2025.10.22 |
| [iOS] Swift Concurrency Foundation (0) | 2025.10.11 |
| [iOS] SwiftUI Foundation (0) | 2025.10.11 |
| [iOS] 버전별 히스토리 관리 (0) | 2018.07.10 |