SwiftUI — 모든 Apple 생태계 플랫폼에서 사용자 인터페이스를 구축하기 위한 Apple의 선언형 프레임워크로, WWDC 2019에서 발표되었습니다. viewDidLoad와 수동 화면 업데이트를 사용하는 명령형 UIKit과 달리 SwiftUI는 UI를 View 프로토콜을 따르는 간단한 구조체의 모음으로 설명합니다. Swift.org(2025)에 따르면 SwiftUI는 App Store에 출시된 신규 프로젝트의 65%에서 사용됩니다. 프레임워크는 State 및 Data Flow 메커니즘을 통해 인터페이스 업데이트를 자동으로 관리합니다 — 데이터가 변경되면 수동 reloadData 호출 없이 View가 다시 그려집니다.
핵심 요점
SwiftUI — UIKit과 근본적으로 다른 Apple의 선언형 UI 프레임워크입니다. 컨트롤러와 뷰를 만들고 수동으로 생명주기를 관리하는 대신, 개발자는 인터페이스를 선언으로 설명합니다: 화면에 무엇이 있어야 하는지, 어떻게 구축할지가 아니라. SwiftUI는 반응성 원칙에 기반합니다: 인터페이스는 상태의 함수입니다. 상태가 변경되면 SwiftUI는 자동으로 모든 종속 View의 body를 다시 계산하고 화면의 변경된 부분만 업데이트합니다. SwiftUI는 iOS 13+, iPadOS 13+, macOS 10.15+, watchOS 6+, tvOS 13+ 및 visionOS 1+에서 사용 가능합니다. SwiftUI 코드는 크로스 플랫폼입니다: 하나의 파일이 최소한의 플랫폼 적응으로 iPhone, iPad, Mac 및 Apple Watch에서 작동합니다. Apple WWDC Session 101(2024)에 따르면 SwiftUI는 App Store 표준 UI 패턴의 90% 이상을 커버합니다.
UIKit — 명령형 프레임워크(2008): 개발자는 UIViewController를 만들고 viewDidLoad에서 subviews를 구성하고 UITableView의 delegate/datasource를 구현하며 reloadData 또는 setNeedsLayout을 통해 화면을 업데이트합니다. SwiftUI는 컨트롤러를 간단한 View 구조체로, 델리게이트를 bindings 및 onChange로, Auto Layout을 HStack/VStack/ZStack 및 수정자(padding, frame, offset)로 대체합니다. UIKit은 ARC를 통한 수동 메모리 관리가 필요합니다; SwiftUI는 참조 카운팅이 필요 없는 구조체를 사용합니다. SwiftUI의 성능은 UIKit과 비슷합니다: 프레임워크는 최소 변경 세트를 위해 diffing 알고리즘을 사용합니다. IT Sectr에서는 iOS 17+ 타겟의 신규 프로젝트에 SwiftUI를 사용합니다; iOS 14–15를 지원하는 프로젝트는 제한된 SwiftUI 호환성으로 인해 UIKit이 필요합니다.
SwiftUI에서 인터페이스는 ViewBuilder — View 세트를 튜플 또는 Group으로 변환하는 result builder를 통해 설명됩니다. 수정자(.padding(), .font(), .foregroundColor())는 원래 객체를 변경하는 대신 수정된 설정으로 새 View를 만듭니다. 각 수정자는 새 View를 반환하여 체이닝을 가능하게 합니다. ViewBuilder는 if/else, switch, ForEach를 지원합니다 — 별도의 컨트롤러 없이 조건부 및 순환 렌더링. SwiftUI의 View는 value type(struct)으로, 예측 가능한 동작을 보장하고 경합 조건을 제거합니다.
View — 단일 요구 사항을 가진 프로토콜: some View 타입의 computed property body. View를 따르는 각 구조체는 body에서 화면의 일부를 설명합니다. some View 타입은 반환된 View의 구체적인 타입을 숨기는 불투명 반환 타입입니다(VStack, HStack, ZStack, Text, Image 등 스태킹). Swift 컴파일러는 컴파일 시간에 구체적인 타입을 추론하여 타입 소거 없이 직접 호출의 성능을 유지합니다.
import SwiftUI
struct GreetingView: View {
var name: String
var body: some View {
VStack(spacing: 12) {
Text("안녕하세요, \(name)!")
.font(.largeTitle)
.foregroundColor(.primary)
Text("SwiftUI에 오신 것을 환영합니다")
.font(.body)
.foregroundColor(.secondary)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 12)
.fill(.ultraThinMaterial)
)
}
}GreetingView 구조체는 name 매개변수를 받아 세로 스택에 두 개의 텍스트 블록을 표시합니다. 수정자 .font, .foregroundColor, .padding 및 .background가 모양을 구성합니다. SwiftUI는 입력 매개변수(name)가 변경될 때마다 body를 호출합니다 — 다시 그리기는 변경된 부분에서만 발생합니다. 예제는 .ultraThinMaterial이 있는 RoundedRectangle을 사용합니다 — SwiftUI에 내장된 기본 블러 배경입니다.
@State — 단일 View에 속하는 로컬 상태를 선언하는 property wrapper. SwiftUI가 State 메모리를 자동으로 관리합니다: 값이 변경되면 body가 다시 그려지지만, 해당 State를 사용하는 View에 대해서만 그렇습니다. State는 단순 타입(String, Int, Bool, enum)의 source of truth입니다. 복잡한 데이터 모델에 @State를 사용하지 마세요 — 대신 @StateObject와 @ObservedObject를 사용하세요. State는 비공개로 유지되어야 하며 View 내부에만 저장되어야 하고, 컴포넌트 간에 전달되지 않아야 합니다.
import SwiftUI
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack(spacing: 20) {
Text("카운트: \(count)")
.font(.system(size: 48, weight: .bold))
Button(action: { count += 1 }) {
Label("증가", systemImage: "plus.circle")
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}초기 count 값 = 0. 버튼을 누를 때마다 count가 증가합니다; SwiftUI가 자동으로 전체 CounterView(모든 View)를 다시 그립니다. UIKit에서 유사한 시나리오는 IBOutlet, IBAction 및 수동 label.text 업데이트가 필요합니다. @State는 특정 State가 변경될 때만 View가 다시 그려지도록 보장합니다 — SwiftUI의 diffing 알고리즘이 트리에서 최소 변경을 찾습니다.
@Binding — View와 View가 소유하지 않은 데이터 간의 양방향 연결을 생성하는 property wrapper. Binding은 State(또는 다른 source of truth)에 대한 참조로, 자식 View가 부모에 저장된 값을 읽고 변경할 수 있게 합니다. Binding은 $ 접두사로 표시됩니다: $count는 Binding<Int>를 자식 View에 전달합니다. Binding이 없으면 자식 View는 부모의 데이터를 변경할 수 없습니다 — 읽기만 가능합니다.
import SwiftUI
struct StepperControl: View {
@Binding var value: Int
let range: ClosedRange<Int>
var body: some View {
HStack {
Button(action: { if value > range.lowerBound { value -= 1 } }) {
Image(systemName: "minus.circle")
}
Text("\(value)")
.frame(minWidth: 40)
Button(action: { if value < range.upperBound { value += 1 } }) {
Image(systemName: "plus.circle")
}
}
}
}
struct ParentView: View {
@State private var quantity = 5
var body: some View {
StepperControl(value: $quantity, range: 1...10)
}
}ParentView는 State quantity를 소유하고 $quantity를 통해 Binding을 전달합니다. StepperControl은 값을 변경할 수 있으며 부모의 quantity는 자동으로 동기화됩니다. Binding은 데이터의 복사본이 아니라 source of truth로의 브리지입니다. 부모의 데이터를 변경해야 하는 사용자 정의 컨트롤, 편집기 및 재사용 가능한 컴포넌트에 @Binding을 사용하세요.
@StateObject — ObservableObject를 따르는 클래스의 인스턴스를 생성하고 소유하기 위한 property wrapper. View는 생명주기당 한 번 객체를 생성하고 @Published 속성이 변경되면 다시 그려집니다. @ObservedObject — 유사한 래퍼이지만 View가 객체를 소유하지 않습니다 — 객체는 View 외부에서 생성 및 저장됩니다(초기화자를 통해 전달). Apple은 View 계층에서 source of truth에는 @StateObject를, 의존성 주입에는 @ObservedObject를 권장합니다.
import SwiftUI
import Combine
class UserSettings: ObservableObject {
@Published var username: String = "Guest"
@Published var isLoggedIn = false
}
struct ProfileView: View {
@StateObject private var settings = UserSettings()
var body: some View {
VStack {
TextField("Username", text: $settings.username)
.textFieldStyle(.roundedBorder)
Toggle("Logged In", isOn: $settings.isLoggedIn)
if settings.isLoggedIn {
Text("\(settings.username)님, 환영합니다!")
.font(.headline)
}
}
.padding()
}
}UserSettings — 두 개의 @Published 속성을 가진 ObservableObject. ProfileView는 @StateObject를 통해 객체를 소유합니다. username 또는 isLoggedIn의 변경이 자동으로 ProfileView를 다시 그립니다. @Published는 Combine Publisher를 사용하여 SwiftUI에 변경을 알립니다. 자식 View에 settings를 전달하려면 @ObservedObject를 사용하세요:
Apple은 SwiftUI에서 Data Flow의 네 가지 수준을 정의합니다: @State(로컬, value type), @Binding(양방향), @StateObject/@ObservedObject(ObservableObject를 사용한 reference type), @EnvironmentObject(전역, 환경을 통해 주입). EnvironmentObject는 초기화자에서 명시적 전달 없이 전체 View 계층을 통해 데이터를 전달할 수 있습니다. 추가로 @AppStorage는 UserDefaults와 함께 작동하고, @SceneStorage는 씬 상태와 함께, @FetchRequest는 Core Data와 함께 작동합니다. Data Flow 수준의 선택이 애플리케이션 아키텍처를 결정합니다: 간단한 화면은 State/Binding을, 모듈식은 ObservedObject를, 대규모는 EnvironmentObject + Redux 유사 솔루션(TCA, Composable Architecture)을 사용합니다.
| Property Wrapper | 소유권 | 타입 | 사용 시기 |
|---|---|---|---|
| @State | 로컬 | Value (struct, enum) | 단일 View의 단순 상태(카운터, 토글, 텍스트 필드) |
| @Binding | 외부 | State 참조 | 자식 View가 부모 데이터 변경 |
| @StateObject | View 소유 | Reference (class) | 복잡한 데이터 모델의 source of truth |
| @ObservedObject | 주입 | Reference (class) | View 외부에서 생성된 모델(init을 통해 전달) |
| @EnvironmentObject | 전역 | Reference (class) | 전체 계층에 사용 가능한 데이터(인증, 테마) |
자주 묻는 질문
@State — value types(struct, enum, String, Int) 및 단일 View의 로컬 상태용. SwiftUI가 State 메모리를 자동으로 관리합니다. @StateObject — ObservableObject를 따르는 reference types(class)용. @StateObject가 객체를 소유하고 @Published 속성이 변경되면 View를 다시 그립니다. 단순 카운터에는 @State를, 비즈니스 로직이 있는 모델에는 @StateObject를 사용하세요.
네, SwiftUI는 UIHostingController(UIKit 내 SwiftUI) 및 UIViewRepresentable(SwiftUI 내 UIKit)을 통해 UIKit과 통합됩니다. UIHostingController는 SwiftUI View를 UIViewController로 래핑합니다. UIViewRepresentable은 SwiftUI에서 UIKit 컴포넌트(MKMapView, WKWebView)를 사용할 수 있게 합니다. 이는 UIKit에서 SwiftUI로 프로젝트를 마이그레이션하는 표준 접근 방식입니다.
ViewBuilder — View 세트를 TupleView, Group 또는 ConditionalContent 타입의 단일 값으로 변환하는 result builder(Swift 5.1)입니다. ViewBuilder는 선언형 body 내에서 명령형 if/else 및 switch를 작성할 수 있게 합니다. ViewBuilder가 없으면 각 조건부 블록에 대해 AnyView 또는 Group을 반환해야 합니다. ViewBuilder가 body에서 View 사이에 쉼표가 필요하지 않은 이유입니다.
네, SwiftUI는 iOS 13+, iPadOS 13+, macOS 10.15+, watchOS 6+, tvOS 13+ 및 visionOS 1+를 지원합니다. 그러나 일부 API는 최신 버전에서만 사용 가능합니다: 예를 들어, navigationStack(iOS 16+), Observable macro(iOS 17+). 하위 호환성을 위해 #available 및 UIKit 적응을 사용하세요.
Xcode Debug View Hierarchy는 수정자 및 프레임과 함께 SwiftUI View 트리를 보여줍니다. SwiftUI Inspector 도구(Xcode 오른쪽 패널)는 실시간으로 수정자를 변경할 수 있습니다. body의 self._printChanges()는 다시 그리기 이유를 로깅합니다. SwiftUI 템플릿이 있는 Instruments는 View 성능을 추적하고 과도한 다시 그리기를 식별합니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.