안녕하세요, iOS 개발하는 루피입니다.
오늘은 SwiftUI의 근간을 이루는 핵심 개념인 뷰와 모디파이어에 대해 깊이 있게 알아보려고 합니다.
바로 시작합니다.
1. SwiftUI가 View에 Struct를 사용하는 이유
UIKit이나 AppKit을 사용해 보셨다면, 두 프레임워크에서는 뷰를 클래스로 구현한다는 것을 알고 계실 겁니다. 그러나 SwiftUI는 다른 접근 방식을 취합니다. SwiftUI에서는 뷰를 구조체로 구현하며, 여기에는 몇 가지 중요한 이유가 있습니다.
1) 성능적 이점
구조체는 클래스보다 단순하고 빠릅니다.
UIKit에서는 모든 뷰가 UIView라는 클래스에서 파생되며, 이 클래스는 배경색, 위치 제약조건, 렌더링 레이어 등 수많은 속성과 메서드를 포함하고 있습니다. 상속의 특성상 UIView의 모든 하위 클래스는 이러한 속성과 메서드를 모두 가져야 합니다. 반면, SwiftUI에서는 뷰가 단순한 구조체로, 생성하는데 비용이 거의 들지 않습니다.
단일 정수를 담는 구조체를 만들면, 그 구조체의 전체 크기는 그 하나의 정수뿐입니다. 부모 클래스나 조상 클래스에서 상속받은 추가 값이 없습니다. 구조체는 정확히 우리가 볼 수 있는 것만 포함하고 그 이상은 없습니다.
그리고 이러한 기술은 현대 iPhone의 성능을 고려하면, 1,000개나 심지어 100,000개의 SwiftUI 뷰를 생성하는 것은 매우 빠르게 처리하기 때문에 일도 아닌 거죠
2) 상태 관리의 명확성
구조체를 사용하는 더 중요한 이유는 상태 관리의 명확성입니다.
구조체는 기본적으로 불변성을 가지므로, 상태 변경이 명확하게 추적됩니다. 클래스는 자유롭게 값을 변경할 수 있어 코드가 더 복잡해질 수 있는 반면, 구조체를 사용하면 SwiftUI가 UI를 업데이트하기 위해 값이 언제 변경되었는지 명확하게 알 수 있습니다.
예를 들어, UIKit에서의 카운터 앱은 다음과 같이 구현할 수 있습니다.
class CounterViewController: UIViewController {
var count = 0 // 상태 let countLabel = UILabel() let incrementButton = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
*// 버튼 설정*
incrementButton.setTitle("증가", for: .normal)
incrementButton.addTarget(self, action: #selector(incrementCount), for: .touchUpInside)
*// 레이아웃 설정 코드...*
}
@objc func incrementCount() {
count += 1 *// 상태 변경*
countLabel.text = "\\(count)" *// UI 수동 업데이트*
}
}
반면 SwiftUI에서는
struct CounterView: View {
@State private var count = 0 // 상태 - @State로 명시적 표시
var body: some View {
VStack {
Text("\\(count)") *// UI는 상태에 자동으로 반응*
Button("증가") {
count += 1 *// 상태 변경*
}
}
}
}
SwiftUI에서는 @State로 명시적으로 "이것은 UI에 영향을 미치는 상태"라고 표시하고, 상태 변경 시 SwiftUI가 자동으로 UI를 업데이트합니다. 이는 상태 관리를 더 명확하고 예측 가능하게 만듭니다.
3) 메모리 관리의 효율성
SwiftUI의 뷰는 구조체로 값 타입의 특성을 가지므로, 실제 사용될 때 COW(Copy On Write)되어 메모리가 할당됩니다.
이는 메모리 효율성을 높이고, 참조 타입이 아니므로 메모리 누수가 발생할 가능성이 줄어듭니다.
클래스는 ARC를 통해 메모리 관리가 되지만, 참조 타입이므로 인스턴스 생성 시 메모리를 차지합니다. 사용되지 않는데도 실제 해제가 되지 않고 메모리 누수가 발생할 수 있습니다.
2. SwiftUI에서 some View의 역할
SwiftUI는 "불투명 반환 타입(opaque return types)"이라는 Swift의 강력한 기능을 활용합니다. some View는 "View 프로토콜을 준수하는 어떤 객체이지만, 정확히 어떤 타입인지는 말하고 싶지 않다"는 의미입니다.
1) 성능에 중요한 불투명 타입
some View를 사용하는 것은 성능에 중요합니다. SwiftUI는 우리가 보여주는 뷰들을 살펴보고 그것들이 어떻게 변하는지 이해해야 사용자 인터페이스를 올바르게 업데이트할 수 있습니다. SwiftUI가 이러한 추가 정보를 갖지 못한다면, 정확히 무엇이 변했는지 파악하는 것은 매우 느릴 것입니다.
2) View 프로토콜과 연관 타입
View 프로토콜에는 associated type이 붙어 있어, 정확히 어떤 종류의 뷰인지 명시해야 합니다.
따라서 다음과 같은 코드는 허용되지 않습니다.
struct ContentView: View {
var body: View { *// 오류!* Text("Hello, world!") }
}
대신 다음과 같이 작성해야 합니다
struct ContentView: View {
var body: some View { *// 정상* Text("Hello, world!") }
}
some View를 사용하면 복잡한 뷰 타입을 직접 작성하지 않고도 View 프로토콜을 준수하는 객체를 반환할 수 있습니다.
3. 모디파이어의 작동 방식
SwiftUI 뷰에 모디파이어를 적용할 때마다, 우리는 실제로 기존 뷰를 직접 수정하는 것이 아니라 변경사항이 적용된 새로운 뷰를 생성합니다.
1) 모디파이어 순서의 중요성
다음 코드를 살펴보겠습니다.
Button("Hello, world!") { *// 아무것도 하지 않음* }
.background(.red)
.frame(width: 200, height: 200)
이 코드는 가운데에 "Hello, world!"가 있고, 텍스트 주변에만 빨간색 배경이 있는 200x200 크기의 빈 정사각형을 생성합니다.
이는 모디파이어가 적용되는 순서 때문입니다.
모디파이어의 순서를 바꾸면 결과도 달라집니다.
Button("Hello, world!") { *// 아무것도 하지 않음* }
.frame(width: 200, height: 200)
.background(.red)
이제 200x200 크기의 빨간색 배경을 가진 버튼이 생성됩니다.
2) 모디파이어의 내부 구조
모디파이어가 적용될 때마다 SwiftUI는 ModifiedContent <Base, Modifier> 형태의 새로운 뷰를 생성합니다.
여러 모디파이어를 적용하면 이러한 구조가 중첩됩니다.
ModifiedContent<ModifiedContent<Button<Text>, _BackgroundStyleModifier<Color>>, _FrameLayout>
이 타입은 "배경색이 적용된 버튼에 프레임 레이아웃이 적용됨"을 의미합니다.
4. 환경 모디파이어와 일반 모디파이어
SwiftUI에는 환경 모디파이어와 일반 모디파이어. 이렇게 두 가지 유형의 모디파이어가 있습니다.
1) 환경 모디파이어
환경 모디파이어는 컨테이너에 적용될 때 그 안의 모든 자식 뷰에 영향을 미칩니다. 그러나 자식 뷰가 동일한 모디파이어를 재정의하면 자식의 버전이 우선시 됩니다.
VStack {
Text("Gryffindor")
.font(.largeTitle) *// 이 설정이 우선됨*
Text("Hufflepuff")
Text("Ravenclaw")
Text("Slytherin")
}
.font(.title) *// 환경 모디파이어*
여기서 font()는 환경 모디파이어이므로, "Gryffindor" 텍스트는. largeTitle을 사용하고 나머지는. title을 사용합니다.
2) 일반 모디파이어
일반 모디파이어는 자식 뷰에 의해 재정의되지 않고, 대신 누적됩니다.
VStack {
Text("Gryffindor")
.blur(radius: 0) *// 이 설정이 VStack의 blur에 추가됨*
Text("Hufflepuff")
Text("Ravenclaw")
Text("Slytherin")
}
.blur(radius: 5) *// 일반 모디파이어*
여기서 "Gryffindor" 텍스트는 여전히 흐릿하게 보입니다. blur()는 일반 모디파이어이므로, 자식의 설정이 부모의 설정을 대체하지 않고 추가됩니다.
5. 복잡한 뷰 관리하기
SwiftUI에서는 복잡한 뷰를 더 작은 뷰로 분리하여 관리할 수 있습니다.
1) 속성을 사용한 뷰 분리
뷰의 일부를 속성으로 분리할 수 있습니다.
struct ContentView: View {
let motto1 = Text("Draco dormiens")
let motto2 = Text("nunquam titillandus")
var body: some View {
VStack {
motto1
.foregroundStyle(.red)
motto2
.foregroundStyle(.blue)
}
}
}
2) 계산 속성으로 뷰 분리
계산 속성을 사용하여 더 복잡한 뷰를 분리할 수도 있습니다.
struct ContentView: View {
var body: some View {
VStack {
headerView
mainContent
footerView
}
}
var headerView: some View {
Text("Header")
.font(.largeTitle)
.padding()
}
var mainContent: some View {
Text("Main content here")
.padding()
}
var footerView: some View {
Text("Footer")
.font(.footnote)
.padding()
}
}
계산 속성에서 여러 뷰를 반환하려면 세 가지 방법이 있습니다.
// 스택 사용:
var spells: some View {
VStack {
Text("Lumos")
Text("Obliviate")
}
}
// Group 사용:
var spells: some View {
Group {
Text("Lumos")
Text("Obliviate")
}
}
// @ViewBuilder 속성 사용:
@ViewBuilder var spells: some View {
Text("Lumos")
Text("Obliviate")
}
3) 커스텀 뷰 생성
반복되는 UI 패턴은 새로운 뷰 타입으로 추출할 수 있습니다.
struct CapsuleText: View {
var text: String
var body: some View {
Text(text)
.font(.largeTitle)
.padding()
.foregroundStyle(.white)
.background(.blue)
.clipShape(.capsule)
}
}
// 사용 예시
struct ContentView: View {
var body: some View {
VStack(spacing: 10) {
CapsuleText(text: "First")
CapsuleText(text: "Second")
}
}
}
4) 커스텀 모디파이어 만들기
자주 사용하는 스타일이나 효과는 커스텀 모디파이어로 만들 수 있습니다.
struct Title: ViewModifier {
func body(content: Content) -> some View {
content
.font(.largeTitle)
.foregroundStyle(.white)
.padding()
.background(.blue)
.clipShape(.rect(cornerRadius: 10))
} }
// View 확장으로 사용하기 쉽게 만들기
extension View {
func titleStyle() -> some View { modifier(Title()) } }
// 사용 예시 Text("Hello World") .titleStyle()
커스텀 모디파이어는 단순히 기존 모디파이어를 조합하는 것 외에도, 새로운 뷰 구조를 만들 수도 있습니다.
struct Watermark: ViewModifier {
var text: String
func body(content: Content) -> some View {
ZStack(alignment: .bottomTrailing) {
content
Text(text)
.font(.caption)
.foregroundStyle(.white)
.padding(5)
.background(.black)
}
}
}
extension View {
func watermarked(with text: String) -> some View { modifier(Watermark(text: text)) } }
// 사용 예시
Color.blue
.frame(width: 300, height: 200)
.watermarked(with: "KIM Luffy")`
5) 커스텀 컨테이너 만들기
SwiftUI에서는 커스텀 컨테이너도 만들 수 있습니다.
예를 들어, 그리드 레이아웃을 구현하는 커스텀 컨테이너를 만들어 보겠습니다.
struct GridStack<Content: View>: View { let rows: Int let columns: Int @ViewBuilder let content: (Int, Int) -> Content
var body: some View {
VStack {
ForEach(0..<rows, id: \\.self) { row in
HStack {
ForEach(0..<columns, id: \\.self) { column in
content(row, column)
}
}
}
}
}
}
// 사용 예시
struct ContentView: View {
var body: some View {
GridStack(rows: 4, columns: 4) {
row, col in Image(systemName: "\(row * 4 + col).circle")
Text("R\(row) C\(col)")
}
}
}
오늘도 화이팅입니다!
https://www.hackingwithswift.com/books/ios-swiftui/views-and-modifiers-introduction
'iOS > SwiftUI' 카테고리의 다른 글
| [SwiftUI] Demystify SwiftUI (1/3) - Identity (0) | 2025.10.29 |
|---|---|
| [SwiftUI] some View (0) | 2025.05.15 |
| [SwiftUI] StateObject (0) | 2025.04.21 |
| [SwiftUI] Bindable (1) | 2025.04.21 |
| [SwiftUI] 상태 관리 - PropertyWrapper (2) (0) | 2025.04.17 |