[Swift 입문] 15편 — SwiftUI 소개: 선언형 프로그래밍 패러다임

🤖 이 글은 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을 채택하면 ForEachListid: 없이 바로 넘길 수 있습니다.


조건부 렌더링

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

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 항목은 *(으)로 표시합니다