SwiftUI는 2019년 iOS 13과 함께 공개된 Apple의 선언형 UI 프레임워크다. 화면을 «어떻게 바꿀지»의 절차가 아니라 «지금 상태에서 어떤 모습이어야 하는지»를 적으면, 프레임워크가 상태 변화와 화면 갱신을 잇는다. 이 편의에는 조건이 붙는다. 어떤 API를 쓸 수 있는지는 최소 배포 버전이 정하고, 어떤 뷰가 다시 그려지는지는 상태를 담는 방식이 정한다. 이 글은 Xcode 27(iOS 27 SDK) 시점의 Apple 공식 문서를 기준으로, 처음 시작할 때 정해야 할 것과 자주 걸리는 지점을 정리했다.
선언형 UI의 기본 구조
View 프로토콜과 body
SwiftUI의 화면 요소는 View 프로토콜을 따르는 구조체다. 클래스가 아니라 값 타입이고, 필요한 것은 body 계산 프로퍼티 하나뿐이다. body에는 결과 빌더가 붙어 있어 여러 뷰를 나열하면 하나의 합성 타입으로 묶인다. 중첩이 깊어질수록 이 타입이 길어지고 타입 검사 부담도 커지므로, body 하나에 수십 줄을 몰아넣으면 빌드가 느려지거나 «타입 추론에 실패했다»는 오류를 만나기 쉽다. 대응은 단순하다. 뷰를 잘게 나누고, 반복되는 묶음은 별도 타입으로 뺀다.
import SwiftUI
@main
struct CounterApp: App {
var body: some Scene {
WindowGroup {
CounterView()
}
}
}
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack(spacing: 12) {
Text("눌린 횟수: \(count)")
Button("하나 더") { count += 1 }
}
.padding()
}
}
앱 진입점과 Scene
iOS 14부터 App 프로토콜과 @main으로 앱 진입점 전체를 SwiftUI로 쓸 수 있다. 위 예제처럼 WindowGroup 같은 Scene이 최상위 그릇이 되고, 그 안에 루트 뷰를 넣는다. iOS 13 시절 방식은 UIHostingController를 UIKit 앱 델리게이트에 얹는 형태였다. 지금 App Store는 배포 대상 iOS 13 이상을 요구하므로(아래 3절), 새 프로젝트라면 처음부터 App 프로토콜로 시작하면 된다.
레이아웃 협상 규칙
SwiftUI 레이아웃은 세 단계로 움직인다. 부모가 크기를 제안하고, 자식이 그 제안 안에서 자기 크기를 스스로 정하고, 부모가 그 결과를 배치한다. 자식이 부모의 제안을 꼭 따르지는 않는다는 점이 핵심이다. frame 수정자를 걸었는데 뜻대로 안 되는 경우 대부분은 이 순서를 오해한 결과다. 제약을 선언하고 해결기에 맡기는 오토레이아웃과는 출발점이 다르다.
상태 관리 — 누가 값을 소유하는가
상태 관련 실수는 대부분 «이 값을 누가 소유하는가»를 잘못 정한 데서 나온다. 값을 직접 가진 뷰는 @State, 위에서 받은 값을 읽고 쓰기만 하는 뷰는 @Binding을 쓴다. 참조 타입(모델 클래스)을 다룰 때는 iOS 17부터 들어온 Observation 방식이 기본이 됐다.
Observation 방식 — @Observable
Apple 문서는 iOS 17·iPadOS 17·macOS 14·tvOS 17·watchOS 10부터 SwiftUI가 Observation을 지원한다고 적는다. 모델 클래스에 @Observable 매크로를 붙이면 ObservableObject·@Published 없이 일반 프로퍼티만으로 변화가 추적된다. 문서 설명대로 SwiftUI는 body가 직접 읽은 프로퍼티가 바뀔 때만 그 뷰를 갱신하고, 읽지 않은 프로퍼티가 바뀌어도 갱신하지 않는다. 화면 하나가 모델의 모든 변화에 다시 그려지던 문제가 여기서 줄어든다.
import Observation
@Observable
final class CartModel {
var items: [String] = []
var coupon = ""
}
struct CartScreen: View {
@State private var cart = CartModel() // 이 뷰가 소유
var body: some View {
CouponField(cart: cart)
.environment(cart) // 아래 뷰들에 전달
}
}
struct CouponField: View {
@Bindable var cart: CartModel // 바인딩이 필요할 때만
var body: some View {
TextField("쿠폰", text: $cart.coupon)
}
}
struct CartCount: View {
@Environment(CartModel.self) private var cart
var body: some View { Text("\(cart.items.count)개") }
}
예전 방식과의 대응
| 예전(ObservableObject) | Observation 방식 | 문서 설명 |
|---|---|---|
@StateObject | @State | 모델 타입을 바꾼 뒤 StateObject를 State로 바꾼다 |
@ObservedObject | 일반 프로퍼티 · 바인딩이 필요하면 @Bindable | Observation에서는 이 래퍼가 필요 없다 |
@EnvironmentObject | @Environment(타입.self) + .environment(모델) | Environment 래퍼를 쓴다 |
iOS 16 이하를 지원해야 한다면 예전 방식을 써야 한다. 이때 가장 흔한 실수는 객체를 만드는 자리에 @ObservedObject를 쓰는 것이다. @StateObject와 달리 뷰가 다시 평가될 때 인스턴스를 지켜 주지 않아 상태가 처음으로 돌아간다. 화면이 이유 없이 초기화된다면 이 자리부터 본다. @EnvironmentObject는 주입하지 않은 채 접근하면 실행 중에 멈추므로, 미리보기나 시험용 뷰에서 주입을 빠뜨리지 않았는지도 확인한다.
시작 전 확인할 제약 — 버전·도구·배포
최소 배포 버전이 곧 API 범위
SwiftUI를 배울 때 가장 자주 부딪히는 벽은 문법이 아니라 가용성이다. 튜토리얼 코드가 그대로 빌드되지 않는다면 대개 그 API의 최소 버전이 프로젝트 설정보다 높기 때문이다. 주요 API가 들어온 시점은 다음과 같다(Apple 문서의 가용성 표시 기준).
| 기능 | 최소 iOS 버전 | 비고 |
|---|---|---|
| 기본 뷰(List, TabView, NavigationView) | iOS 13 | NavigationView는 지금 사용 중단 표시 |
| App 프로토콜 기반 수명 주기, @StateObject | iOS 14 | iOS 13은 UIHostingController 필요 |
| .task 수정자, .searchable | iOS 15 | async/await 연동 |
| NavigationStack, NavigationSplitView, Layout 프로토콜, Swift Charts | iOS 16 | 내비게이션 모델 바뀜 |
| @Observable(Observation), SwiftData | iOS 17 | 상태·저장 방식 바뀜 |
내비게이션은 특히 조심한다. Apple 문서는 NavigationView에 사용 중단(deprecated) 표시를 달고 NavigationStack과 NavigationSplitView를 쓰라고 안내한다. iOS 15 이하까지 지원하면 신형 API를 쓸 수 없어 if #available로 두 경로를 갈라 관리하고 각각 따로 확인해야 한다. 지원 범위를 한 단계 낮추는 결정이 코드량과 확인 범위를 함께 늘린다는 뜻이다.
개발 환경 — Xcode 27(2026-10-30 확인)
| Xcode | 필요한 macOS | iOS SDK | Swift |
|---|---|---|---|
| Xcode 27.2 RC | macOS Tahoe 26.6 이상 | iOS 27.2 | Swift 6.4 컴파일러 |
| Xcode 27.1 | macOS Tahoe 26.6 이상 | iOS 27.1 | Swift 6.4 컴파일러 |
| Xcode 27 | macOS Tahoe 26.6 이상 | iOS 27 | Swift 6.4 컴파일러 |
Xcode는 macOS에서만 돌고, 미리보기와 시뮬레이터도 여기에 묶여 있다. 쓸 수 있는 SDK와 시뮬레이터 런타임은 설치한 Xcode 판이 정하므로, 새 OS의 API를 쓰려면 Xcode부터 올린다. Apple 지원 페이지 기준으로 Xcode 27 계열은 Swift 6·5·4.2·4 언어 모드를 모두 지원하므로, 기존 프로젝트를 올릴 때 언어 모드를 한꺼번에 바꿀 필요는 없다.
App Store에 올리려면
- 빌드 SDK — Apple 개발자 뉴스의 요건 공지 기준으로 2026-04-28부터 App Store Connect에 올리는 앱은 Xcode 26 이상, iOS 26 SDK 이상에서 빌드해야 한다.
- 배포 대상 — 2026-09-09부터 iOS·iPadOS 앱은 iOS 13 이상을 대상으로 해야 한다.
- 멤버십 — Apple Developer Program은 멤버십 1년에 99 USD(지역 통화 가능)이고, 요건을 갖춘 비영리 단체·교육 기관·정부 기관은 면제를 신청할 수 있다. 등록 전 개발자 사이트에서 현재 금액과 조건을 다시 확인한다.
스토어 등록 뒤 검색 노출과 상세 페이지를 다듬는 일은 앱 스토어 최적화(ASO) 전략 가이드에 따로 정리했다.
2027 릴리스에서 바뀐 것
Apple의 «What's new in SwiftUI» 페이지는 Xcode 27과 함께 나온 2027 릴리스의 변화를 아래처럼 묶는다. 페이지에는 OS 판이 따로 적혀 있지 않으므로, 쓰기 전에 각 심볼 문서의 가용성 표시를 확인한다.
| 갈래 | 바뀐 것 |
|---|---|
| 상태 | State가 매크로가 되어, @State에 담은 클래스가 뷰 수명 동안 한 번만 늦게 초기화된다 |
| 빌드 | ViewBuilder가 ContentBuilder로 공개되고, 빌드 시간이 나아졌다고 설명 |
| 이미지 | AsyncImage가 기본으로 표준 HTTP 캐시를 쓰고, asyncImageURLSession으로 세부 조정 |
| 툴바 | visibilityPriority·toolbarOverflowMenu·topBarPinnedTrailing · 스크롤 때 내비게이션 바를 접는 toolbarMinimizeBehavior |
| 목록 상호작용 | List·LazyVGrid 등에서 쓰는 순서 바꾸기 API(watchOS 첫 지원) · 아무 ScrollView에서 밀어서 동작을 쓰는 swipeActionsContainer |
| 알림 창 | 확인 대화상자와 알림이 시트처럼 항목 바인딩을 받음 |
| 문서 앱 | WritableDocument·ReadableDocument로 비동기·점진 저장과 진행률 보고 |
입문 단계에서 체감이 큰 것은 첫 줄이다. 예전에는 @State에 클래스를 넣으면 뷰가 다시 만들어질 때마다 초기화 코드가 불리는 점을 신경 써야 했는데, 새 매크로는 이를 뷰 수명당 한 번으로 묶는다. 다만 배포 대상이 그보다 낮다면 예전 동작을 기준으로 코드를 짠다.
UIKit 상호운용과 미리보기
UIKit을 감싸고, SwiftUI를 얹는다
SwiftUI가 모든 것을 대신하지는 않는다. 세밀한 텍스트 편집 제어, 일부 카메라·미디어 화면, 오래된 외부 UI 라이브러리는 여전히 UIKit 기반인 경우가 많다. UIViewRepresentable·UIViewControllerRepresentable로 UIKit 부품을 감싸 SwiftUI 안에 넣고, 반대로 UIHostingController로 SwiftUI 뷰를 UIKit 화면에 넣는다. 두 방향 통로가 공식 API로 있으니, 기존 앱을 한 번에 갈아엎지 않고 화면 단위로 옮길 수 있다.
미리보기가 멈추는 자리
Xcode 미리보기는 앱 전체를 실행하지 않고 해당 뷰만 따로 빌드해 그린다. 그래서 뷰가 앱 실행 때만 있는 값에 기대면 빌드는 되는데 미리보기만 실패한다. 네트워크 응답, 로그인 세션, 전역 싱글턴이 대표적이다. 뷰의 입력을 파라미터로 빼서 미리보기용 더미 데이터를 넣을 수 있게 하면 풀리고, 그 과정에서 뷰가 시험하기 쉬운 모양으로 정리된다.
미리보기가 성공해도 실기기와 다른 지점이 남는다. 글자 크기 조절(Dynamic Type), 다크 모드, 화면 회전, 저사양 기기의 스크롤은 미리보기가 보장하지 않는다. 특히 글자를 키우면 고정 높이를 준 레이아웃이 잘린다. 출시 전에 글자 크기 최대, 다크 모드, 좁은 화면 세 조합을 한 번씩 훑는 것을 기본 점검으로 둔다.
초반에 자주 걸리는 지점
ForEach의 식별자를 그릴 때마다 새로 만들면(예: 그 자리에서 UUID 생성) 애니메이션과 내부 상태가 유지되지 않는다.Identifiable을 따르게 하거나 안정적인 키를 준다..task수정자 안의 작업은 뷰가 사라질 때 취소된다. 화면을 떠난 뒤에도 끝나야 하는 저장 작업은 모델 쪽으로 옮긴다.- 목록 행마다 무거운 계산을
body안에서 하면 스크롤이 버벅인다. 계산은 모델에서 미리 해 두고 뷰는 결과만 읽게 한다.
SwiftUI, UIKit, 크로스플랫폼 비교
| 항목 | SwiftUI | UIKit | 크로스플랫폼 계열 |
|---|---|---|---|
| 대상 플랫폼 | iOS·iPadOS·macOS·watchOS·tvOS·visionOS | iOS·iPadOS 중심(Mac Catalyst) | Apple 플랫폼 + Android 등 |
| 언어 | Swift | Swift·Objective-C | Dart·JavaScript·Kotlin 등 |
| UI 작성 방식 | 선언형, 상태 기반 | 명령형, 뷰 계층 직접 조작 | 대체로 선언형 |
| 새 OS 기능 대응 | OS SDK와 함께 제공 | OS SDK와 함께 제공 | 브리지·플러그인 지원을 기다림 |
| 개발 환경 | macOS + Xcode 필수 | macOS + Xcode 필수 | iOS 빌드·배포 단계에 macOS + Xcode 필요 |
고르는 기준
- 지원할 최소 iOS 버전 — 첫 번째 기준이다. iOS 17 이상만 지원하면 Observation과 SwiftData를 전제로 설계할 수 있고, iOS 15까지 내려가면 내비게이션과 상태 관리 모두 예전 방식에 맞춰야 한다. 기능 목록보다 배포 대상을 먼저 정한다.
- 화면의 성격 — 목록·폼·설정처럼 상태에 따라 구성이 바뀌는 화면은 SwiftUI가 코드를 크게 줄인다. 픽셀 단위 그리기, 복잡한 제스처 조합, 큰 컬렉션의 세밀한 스크롤 제어가 핵심이면 UIKit 감싸기를 처음부터 계획에 넣는다.
- Android 동시 지원 — SwiftUI는 Apple 플랫폼 전용이다. Android 쪽 선언형 UI는 Jetpack Compose 입문 가이드에서 같은 관점으로 정리했고, 한 코드로 두 플랫폼을 덮어야 한다면 React Native Expo 앱 개발 입문 같은 크로스플랫폼 쪽이 먼저 검토 대상이다.
- 기존 UIKit 자산 — 코드가 많다면 전면 재작성보다
UIHostingController로 화면 단위 도입이 현실적이다. 두 프레임워크가 함께 있는 구성은 공식적으로 지원된다.
정리 — 시작 순서
SwiftUI 학습의 출발점은 문법이 아니라 두 가지 결정이다. 지원할 최소 iOS 버전, 그리고 각 상태를 누가 소유하는지. 이 둘이 정해지면 나머지 API 선택은 대체로 따라온다.
| 순서 | 할 일 | 확인 |
|---|---|---|
| 1 | 배포 대상 iOS 버전 정하기(App Store 하한 iOS 13) | 쓰려는 API의 가용성 표시 |
| 2 | Xcode 최신판 설치(macOS Tahoe 26.6 이상) | 빌드 SDK가 스토어 요건(iOS 26 SDK 이상)을 넘는지 |
| 3 | 상태 소유 정하기 — iOS 17 이상이면 @Observable + @State | body가 읽는 프로퍼티만 갱신되는지 |
| 4 | 내비게이션은 NavigationStack·NavigationSplitView | NavigationView를 새로 쓰지 않았는지 |
| 5 | UIKit이 필요한 부분만 감싸기 | Representable 범위가 화면 일부에 머무는지 |
| 6 | 글자 크기 최대·다크 모드·좁은 화면 점검 | 잘리는 레이아웃이 없는지 |
버전별 가용성과 동작 세부는 해마다 바뀌므로, 판단 근거는 Apple 개발자 문서의 해당 심볼 페이지에 표시된 가용성 정보에서 확인한다.
자주 묻는 것
SwiftUI를 배우려면 어떤 iOS 버전을 기준으로 잡아야 하나요?
새 프로젝트라면 iOS 17 이상을 배포 대상으로 잡으면 @Observable과 SwiftData, NavigationStack을 분기 없이 쓸 수 있습니다. App Store는 2026년 9월 9일부터 iOS 13 이상을 대상으로 하도록 요구하므로 그보다 낮출 수는 없습니다.
@StateObject와 @ObservedObject는 이제 안 쓰나요?
iOS 17 이상이면 Apple 문서는 모델에 @Observable을 붙이고 StateObject 자리는 State로, 바인딩이 필요한 ObservedObject 자리는 Bindable로 바꾸라고 안내합니다. iOS 16 이하를 지원해야 하면 예전 방식을 계속 씁니다.
NavigationView를 계속 써도 되나요?
Apple 문서에서 NavigationView에는 사용 중단 표시가 붙어 있고 NavigationStack과 NavigationSplitView를 쓰라고 안내합니다. 새로 만드는 화면은 두 신형 API로 시작하는 편이 낫습니다.
Windows PC로도 SwiftUI 앱을 만들 수 있나요?
Xcode는 macOS에서만 실행되고, 지금 Xcode 27은 macOS Tahoe 26.6 이상이 필요합니다. App Store에 올리려면 Xcode 26 이상과 iOS 26 SDK 이상에서 빌드해야 하므로 Mac이 필요합니다.
참고 자료
- Apple Developer Documentation. Migrating from the Observable Object protocol to the Observable macro — iOS 17 등 지원 시점, StateObject→State, ObservedObject→Bindable, EnvironmentObject→Environment, body가 읽은 프로퍼티만 갱신(2026-10-30 확인). developer.apple.com
- Apple Developer Documentation. NavigationView — iOS 13 도입, 사용 중단 표시, NavigationStack·NavigationSplitView 안내(2026-10-30 확인). developer.apple.com
- Apple Developer. Xcode support — Xcode 27·27.1·27.2 RC, macOS Tahoe 26.6 이상, iOS 27 SDK, Swift 6.4, 언어 모드(2026-10-30 확인). developer.apple.com
- Apple Developer News. Upcoming requirements — 2026-04-28 Xcode 26·iOS 26 SDK 빌드, 2026-09-09 iOS 13 이상 대상(2026-10-30 확인). developer.apple.com
- Apple Developer. What's new in SwiftUI — 2027 릴리스: State 매크로, ContentBuilder, AsyncImage 캐시, 툴바·순서 바꾸기·문서 API(2026-10-30 확인). developer.apple.com
- Apple Developer Program. What's included — 멤버십 1년 99 USD·면제 대상(2026-10-30 확인). developer.apple.com