🤖 이 글은 Claude Code(AI)가 작성합니다. | 시리즈 목차 | 이전: [14편] Swift Concurrency 2부
SwiftUI란 무엇인가?
지금까지 Swift 언어 자체를 배웠다면, 이제부터는 그 언어를 사용해 실제 화면을 만드는 방법을 배울 차례입니다.
SwiftUI는 Apple이 2019년에 발표한 UI 프레임워크입니다. iOS, macOS, watchOS, tvOS에서 동작하는 앱의 화면을 Swift 코드로 선언적으로 작성할 수 있게 해줍니다.
그 이전에는 UIKit(iOS용)과 AppKit(macOS용)이라는 오래된 프레임워크를 사용했습니다. 두 프레임워크는 지금도 현역이지만, SwiftUI는 훨씬 적은 코드로 같은 화면을 만들 수 있어 빠르게 주류가 되었습니다.
명령형 vs 선언형
SwiftUI를 이해하는 핵심은 선언형 프로그래밍이 무엇인지 아는 것입니다.
명령형(Imperative) 방식 — UIKit의 방식
명령형은 “어떻게 해라”를 코드로 쓰는 방식입니다. 레스토랑 주방에서 요리사에게 “팬을 가져와 → 버터를 녹여 → 달걀을 깨 → 저어줘”처럼 단계를 하나씩 지시하는 것과 같습니다.
// UIKit (명령형) 예시
let label = UILabel()
label.text = "안녕하세요"
label.font = UIFont.systemFont(ofSize: 24)
label.textColor = .blue
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
텍스트 하나를 화면 중앙에 표시하는 데 이만큼의 코드가 필요합니다.
선언형(Declarative) 방식 — SwiftUI의 방식
선언형은 “무엇을 원하는가”를 코드로 쓰는 방식입니다. 레스토랑에서 “달걀 프라이 주세요”라고 주문하는 것과 같습니다. 어떻게 요리할지는 말하지 않아도 됩니다.
// SwiftUI (선언형) 예시
struct ContentView: View {
var body: some View {
Text("안녕하세요")
.font(.system(size: 24))
.foregroundColor(.blue)
}
}
같은 결과를 단 5줄로 표현했습니다. 더 중요한 것은 코드를 읽었을 때 화면의 모습이 바로 눈에 들어온다는 점입니다.
첫 SwiftUI 코드 뜯어보기
import SwiftUI
struct ContentView: View {
var body: some View {
VStack(spacing: 16) {
Text("안녕하세요!")
.font(.largeTitle)
.bold()
Text("SwiftUI입니다")
.font(.body)
.foregroundColor(.gray)
Button("눌러보세요") {
print("버튼 눌림!")
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}
한 줄씩 살펴보겠습니다.
struct ContentView: View
View 프로토콜을 채택한 구조체입니다. SwiftUI에서 모든 화면 요소는 View입니다. 클래스가 아닌 구조체를 사용하는 이유는 성능과 안전성 때문입니다(값 타입의 장점).
var body: some View
View 프로토콜이 요구하는 유일한 속성입니다. “이 뷰의 내용이 무엇인가”를 반환합니다. some View는 “어떤 View인지는 Swift가 알아서 추론하게 두겠다”는 의미입니다(불투명 반환 타입).
VStack, Text, Button
이것들이 뷰 빌더(ViewBuilder)로 만들어진 뷰 컴포넌트들입니다.
VStack: 자식 뷰들을 수직으로 배열HStack: 수평으로 배열ZStack: 겹쳐서 배열Text: 텍스트 표시Button: 탭/클릭 가능한 버튼
수정자(Modifier)
.font(), .bold(), .foregroundColor() 같은 것들이 수정자입니다. 각 수정자는 원래 뷰를 감싼 새 뷰를 반환합니다. 그래서 체이닝이 가능합니다.
Text("Hello")
.font(.title) // 폰트 적용한 새 뷰 반환
.bold() // 굵게 한 새 뷰 반환
.padding() // 여백 추가한 새 뷰 반환
.background(.yellow) // 배경색 추가한 새 뷰 반환
레이아웃 기본
Stack으로 배치하기
struct LayoutExample: View {
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Image(systemName: "person.circle")
.font(.title)
Text("홍길동")
.font(.headline)
Spacer() // 남은 공간을 채움
Text("온라인")
.foregroundColor(.green)
}
Text("안녕하세요! 반갑습니다.")
.font(.body)
.foregroundColor(.secondary)
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(12)
.padding(.horizontal)
}
}
Spacer()는 가용 공간을 최대한 차지하는 투명한 뷰입니다. HStack 안에 넣으면 요소들을 양끝으로 밀어냅니다.
ZStack으로 겹치기
struct OverlayExample: View {
var body: some View {
ZStack {
// 아래 레이어
RoundedRectangle(cornerRadius: 16)
.fill(Color.blue.opacity(0.2))
.frame(width: 200, height: 120)
// 위 레이어
VStack {
Text("😀")
.font(.largeTitle)
Text("SwiftUI")
.font(.headline)
}
}
}
}
리스트와 ForEach
같은 형태의 뷰를 여러 개 나열할 때는 ForEach를 사용합니다.
struct FruitList: View {
let fruits = ["🍎 사과", "🍊 오렌지", "🍋 레몬", "🍇 포도"]
var body: some View {
List {
ForEach(fruits, id: \.self) { fruit in
Text(fruit)
.padding(.vertical, 4)
}
}
}
}
id: \.self는 각 항목을 구분하는 식별자를 “값 자체”로 쓰겠다는 의미입니다. 실제 앱에서는 모델에 Identifiable 프로토콜을 채택해 사용합니다.
struct Item: Identifiable {
let id = UUID()
let name: String
let emoji: String
}
struct ItemList: View {
let items = [
Item(name: "사과", emoji: "🍎"),
Item(name: "오렌지", emoji: "🍊"),
Item(name: "레몬", emoji: "🍋")
]
var body: some View {
List(items) { item in
HStack {
Text(item.emoji)
.font(.title2)
Text(item.name)
.font(.body)
}
}
}
}
Identifiable을 채택하면 ForEach나 List에 id: 없이 바로 넘길 수 있습니다.
조건부 렌더링
SwiftUI에서 if 문은 뷰 내부에서 그대로 사용할 수 있습니다.
struct ToggleView: View {
var isLoggedIn: Bool
var body: some View {
VStack {
if isLoggedIn {
Text("환영합니다!")
.foregroundColor(.green)
Button("로그아웃") { }
} else {
Text("로그인이 필요합니다")
.foregroundColor(.red)
Button("로그인") { }
.buttonStyle(.borderedProminent)
}
}
.padding()
}
}
이것이 선언형의 장점입니다. 명령형에서는 뷰를 숨기고 보이게 하려면 isHidden = true/false를 직접 조작해야 했지만, SwiftUI에서는 그냥 “로그인 상태면 이걸 보여줘”라고 선언하면 됩니다.
다른 프레임워크와 비교
| 특성 | SwiftUI | React (웹) | Flutter |
|---|---|---|---|
| 패러다임 | 선언형 | 선언형 | 선언형 |
| 언어 | Swift | JavaScript/JSX | Dart |
| 상태관리 | @State, @Observable | useState, Redux | setState, Provider |
| 레이아웃 | VStack/HStack/ZStack | Flexbox/Grid | Column/Row/Stack |
| 재사용 단위 | View(구조체) | Component(함수) | Widget(클래스) |
세 프레임워크 모두 비슷한 철학을 공유합니다. React를 아는 웹 개발자라면 SwiftUI의 개념이 낯설지 않을 것입니다.
SwiftUI가 UI를 업데이트하는 방식
SwiftUI의 핵심 동작 원리는 간단합니다.
상태(State)가 바뀌면 → 뷰를 다시 계산한다
개발자가 직접 “이 레이블 텍스트를 바꿔라”고 명령하지 않아도 됩니다. 상태 변수만 바꾸면 SwiftUI가 알아서 필요한 부분만 재렌더링합니다.
struct CounterView: View {
@State private var count = 0 // 상태 변수
var body: some View {
VStack(spacing: 20) {
Text("카운트: \(count)")
.font(.largeTitle)
Button("증가") {
count += 1 // 상태 변경 → 뷰 자동 업데이트
}
.buttonStyle(.borderedProminent)
}
}
}
count가 바뀌면 SwiftUI는 body를 다시 실행해 새 뷰를 계산합니다. 실제 화면에서 달라진 부분(여기서는 Text)만 효율적으로 업데이트합니다. @State에 대한 자세한 내용은 다음 편에서 다룹니다.
미리보기(Preview)
Xcode에서 SwiftUI 파일을 열면 오른쪽에 캔버스가 나타납니다. 코드를 작성하는 즉시 결과를 볼 수 있습니다. 이를 가능하게 하는 것이 #Preview 매크로입니다.
struct ContentView: View {
var body: some View {
Text("Hello, SwiftUI!")
}
}
#Preview {
ContentView()
}
#Preview 블록 안에 원하는 뷰를 넣으면 Xcode 캔버스에서 실시간으로 미리볼 수 있습니다. 앱을 실행하지 않아도 되므로 개발 속도가 크게 향상됩니다.
여러 상태를 동시에 미리볼 수도 있습니다.
#Preview("로그인 상태") {
ToggleView(isLoggedIn: true)
}
#Preview("미로그인 상태") {
ToggleView(isLoggedIn: false)
}
핵심 요약
- SwiftUI는 선언형 UI 프레임워크 — “무엇”을 보여줄지 선언하면 된다
- View 프로토콜을 채택한 구조체가 UI의 기본 단위
- body 속성에 뷰 계층을 반환한다
- 수정자(Modifier)로 뷰에 스타일과 동작을 추가한다
- VStack/HStack/ZStack으로 레이아웃을 구성한다
- 상태가 바뀌면 뷰가 자동으로 재계산된다
- #Preview로 Xcode에서 실시간 미리보기가 가능하다
다음 편에서는 SwiftUI의 핵심인 상태 관리를 본격적으로 배웁니다. @State와 @Binding으로 뷰 간에 데이터를 주고받는 방법을 알아봅니다.
🤖 Generated with Claude Code