SwiftUI: iOS๋ฅผ ์œ„ํ•œ ๋ชจ๋˜ ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌ์ถ• ๊ฐ€์ด๋“œ

SwiftUI๋กœ ๋ชจ๋˜ UI๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ด…๋‹ˆ๋‹ค. ์„ ์–ธ์  ๊ตฌ๋ฌธ, ์ปดํฌ๋„ŒํŠธ, ์• ๋‹ˆ๋ฉ”์ด์…˜, iOS 18 ๋ชจ๋ฒ” ์‚ฌ๋ก€๋ฅผ ์ข…ํ•ฉ์ ์œผ๋กœ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

SwiftUI๋ฅผ ํ™œ์šฉํ•œ ๋ชจ๋˜ iOS ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌ์ถ• ๊ฐ€์ด๋“œ

SwiftUI๋Š” Apple ํ”Œ๋žซํผ์˜ ์ธํ„ฐํŽ˜์ด์Šค ๊ฐœ๋ฐœ ๋ฐฉ์‹์„ ๊ทผ๋ณธ์ ์œผ๋กœ ๋ฐ”๊พธ์—ˆ์Šต๋‹ˆ๋‹ค. ์„ ์–ธ์  ๊ตฌ๋ฌธ๊ณผ ๋„ค์ดํ‹ฐ๋ธŒ ํ†ตํ•ฉ์„ ํ†ตํ•ด ์ด์ „๋ณด๋‹ค ํ›จ์”ฌ ์ ์€ ์ฝ”๋“œ๋กœ ์„ธ๋ จ๋œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. iOS 18์—์„œ๋Š” ์„ฑ๋Šฅ์˜ ๋Œ€ํญ์ ์ธ ํ–ฅ์ƒ๊ณผ ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์ด ๋„์ž…๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

Why SwiftUI in 2026?

SwiftUI๋Š” iOS 18์—์„œ ์™„์ „ํ•œ ์„ฑ์ˆ™๊ธฐ์— ์ ‘์–ด๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ๋ Œ๋”๋ง ์ตœ์ ํ™”, ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ ๊ฐœ์„ , UIKit ํ†ตํ•ฉ ๊ฐ„์†Œํ™”๊ฐ€ ์ด๋ฃจ์–ด์กŒ์œผ๋ฉฐ, ์‹ ๊ทœ iOS ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐœ๋ฐœ์˜ ํ‘œ์ค€์œผ๋กœ ์ž๋ฆฌ ์žก์•˜์Šต๋‹ˆ๋‹ค.

์„ ์–ธ์  ํŒจ๋Ÿฌ๋‹ค์ž„ ์ดํ•ดํ•˜๊ธฐ

์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์ „์— SwiftUI์˜ ๋ณธ์งˆ์ ์ธ ํŠน์ง•์„ ํŒŒ์•…ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. UIKit(๊ธฐ์กด ํ”„๋ ˆ์ž„์›Œํฌ)์—์„œ๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋‹จ๊ณ„๋ณ„๋กœ ๊ตฌ์ถ•ํ•˜๋„๋ก ์ง€์‹œํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค. "๋ ˆ์ด๋ธ”์„ ์ƒ์„ฑํ•˜๊ณ , ์—ฌ๊ธฐ์— ๋ฐฐ์น˜ํ•˜๊ณ , ์‚ฌ์šฉ์ž๊ฐ€ ํด๋ฆญํ•˜๋ฉด ์ƒ‰์ƒ์„ ๋ณ€๊ฒฝํ•˜๋ผ"๋Š” ์‹์ž…๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ๋ช…๋ นํ˜• ํŒจ๋Ÿฌ๋‹ค์ž„์ž…๋‹ˆ๋‹ค.

SwiftUI๋Š” ๋‹ค๋ฅธ ์ ‘๊ทผ ๋ฐฉ์‹์„ ์ทจํ•ฉ๋‹ˆ๋‹ค. ํ‘œ์‹œํ•˜๊ณ ์ž ํ•˜๋Š” ๋‚ด์šฉ์„ ๊ธฐ์ˆ ํ•˜๋ฉด ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ๋‚˜๋จธ์ง€๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๋‚ด๋น„๊ฒŒ์ด์…˜์—์„œ ๊ฒฝ๋กœ๋ฅผ ํ•˜๋‚˜์”ฉ ์•ˆ๋‚ดํ•˜๋Š” ๊ฒƒ(๋ช…๋ นํ˜•)๊ณผ ๋ชฉ์ ์ง€๋งŒ ๋งํ•˜๋Š” ๊ฒƒ(์„ ์–ธํ˜•)์˜ ์ฐจ์ด์™€ ์œ ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

์ฒซ ๋ฒˆ์งธ View ๋งŒ๋“ค๊ธฐ

SwiftUI์—์„œ ๋ชจ๋“  ์ธํ„ฐํŽ˜์ด์Šค ์š”์†Œ๋Š” View์ž…๋‹ˆ๋‹ค. View๋Š” ํ™”๋ฉด์— ํ‘œ์‹œ๋  ๋‚ด์šฉ์„ ๊ธฐ์ˆ ํ•˜๋Š” struct์ž…๋‹ˆ๋‹ค. ์ œ๋ชฉ, ๋ถ€์ œ๋ชฉ, ๋ฒ„ํŠผ์ด ํฌํ•จ๋œ ์ฒซ ๋ฒˆ์งธ ํ™”๋ฉด์„ ๋งŒ๋“ค์–ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

ContentView.swiftswift
import SwiftUI

// Each screen is a struct that implements the View protocol
struct ContentView: View {

    // The "body" property describes what the view displays
    // "some View" means "a type of View, but Swift figures it out automatically"
    var body: some View {

        // VStack = "Vertical Stack": stacks elements vertically
        // spacing: 20 = 20 points of space between each element
        VStack(spacing: 20) {

            // A simple text with style modifiers
            Text("Welcome to SharpSkill")
                .font(.largeTitle)      // Large title font
                .fontWeight(.bold)      // Bold text

            Text("Prepare for your iOS interviews")
                .font(.subheadline)     // Smaller font
                .foregroundColor(.secondary)  // Gray secondary color

            // Button with an action (closure) and a label
            Button("Get Started") {
                print("Button tapped!")
            }
            .buttonStyle(.borderedProminent)  // Filled blue button style
        }
        .padding()  // Adds margins around the VStack
    }
}

์ด ๊ตฌ์กฐ๋ฅผ ์‚ดํŽด๋ณด๋ฉด, ํ‘œ์‹œํ•  ์š”์†Œ(ํ…์ŠคํŠธ, ๋ฒ„ํŠผ)๋ฅผ ์„ ์–ธํ•˜๊ณ , ์„ธ๋กœ๋กœ ๋ฐฐ์น˜ํ•˜๊ณ (VStack), ๋ชจ๋””ํŒŒ์ด์–ด(.font(), .padding())๋กœ ์Šคํƒ€์ผ์„ ์ ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•ต์‹ฌ ํฌ์ธํŠธ: SwiftUI์—์„œ๋Š” UI ๊ฐ์ฒด๋ฅผ "์ƒ์„ฑ"ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ์›ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ธฐ์ˆ ํ•ฉ๋‹ˆ๋‹ค. ์‹ค์ œ ์š”์†Œ์˜ ์ƒ์„ฑ, ๊ฐฑ์‹ , ์†Œ๋ฉธ์€ SwiftUI๊ฐ€ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค.

๋ชจ๋””ํŒŒ์ด์–ด: View ๋ณ€ํ™˜ํ•˜๊ธฐ

๋ชจ๋””ํŒŒ์ด์–ด๋Š” View ๋’ค์— ์ฒด์ด๋‹ํ•˜์—ฌ ๋ณ€ํ™˜์„ ์ ์šฉํ•˜๋Š” ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. Instagram ํ•„ํ„ฐ๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ์ ์šฉํ•˜๋Š” ๊ฒƒ๊ณผ ๋น„์Šทํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์ˆœ์„œ๊ฐ€ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ๋ชจ๋””ํŒŒ์ด์–ด๊ฐ€ ์ด์ „ View๋ฅผ ๊ฐ์‹ธ๋Š” ์ƒˆ๋กœ์šด View๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

์ˆœ์„œ๊ฐ€ ์™œ ์ค‘์š”ํ•œ์ง€ ๋ณด์—ฌ์ฃผ๋Š” ์˜ˆ์ œ๋ฅผ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

ModifiersOrder.swiftswift
// Example 1: padding THEN background
Text("SwiftUI")
    .padding()           // 1. Adds 16pt of space around the text
    .background(.blue)   // 2. Blue background covers text + padding
    .foregroundColor(.white)
// Result: white text on blue rectangle with margins

// Example 2: background THEN padding (reversed order!)
Text("SwiftUI")
    .background(.blue)   // 1. Tight blue background around text only
    .padding()           // 2. Transparent padding around blue background
    .foregroundColor(.white)
// Result: white text on small blue rectangle, surrounded by empty space

์ฐจ์ด์ ์€ ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ ์˜ˆ์ œ์—์„œ๋Š” padding์ด ๋ฐฐ๊ฒฝ์˜ "์•ˆ์ชฝ"์— ์žˆ๊ณ , ๋‘ ๋ฒˆ์งธ์—์„œ๋Š” "๋ฐ”๊นฅ์ชฝ"์— ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฏธ๋ฌ˜ํ•œ ์ฐจ์ด์ด์ง€๋งŒ ๋ ˆ์ด์•„์›ƒ์„ ์ •ํ™•ํ•˜๊ฒŒ ์ œ์–ดํ•˜๋Š” ๋ฐ ํ•„์ˆ˜์ ์ธ ์ง€์‹์ž…๋‹ˆ๋‹ค.

Stack์„ ํ™œ์šฉํ•œ ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌ์„ฑ

SwiftUI๋Š” View๋ฅผ ๊ตฌ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ์„ธ ๊ฐ€์ง€ ์ฃผ์š” ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ฝ˜ํ…์ธ ๋ฅผ ์„œ๋กœ ๋‹ค๋ฅธ ๋ฐฉ์‹์œผ๋กœ ๋ฐฐ์น˜ํ•˜๋Š” ์ƒ์ž๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

VStack, HStack, ZStack

  • VStack (Vertical Stack): ์š”์†Œ๋ฅผ ์œ„์—์„œ ์•„๋ž˜๋กœ ์Œ“์Šต๋‹ˆ๋‹ค
  • HStack (Horizontal Stack): ์š”์†Œ๋ฅผ ์™ผ์ชฝ์—์„œ ์˜ค๋ฅธ์ชฝ์œผ๋กœ ๋‚˜์—ดํ•ฉ๋‹ˆ๋‹ค
  • ZStack (Z์ถ• Stack): ์š”์†Œ๋ฅผ ๊ฒน์ณ์„œ ๋ฐฐ์น˜ํ•ฉ๋‹ˆ๋‹ค

์„ธ ๊ฐ€์ง€ Stack์„ ๋ชจ๋‘ ์กฐํ•ฉํ•˜์—ฌ ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์นด๋“œ๋ฅผ ๊ตฌ์ถ•ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ์‚ฌ์ง„ ์œ„์— ์ธ์ฆ ๋ฐฐ์ง€๋ฅผ ๊ฒน์น˜๊ณ , ์˜†์— ์‚ฌ์šฉ์ž ์ด๋ฆ„๊ณผ ์ง์ฑ…์„ ํ‘œ์‹œํ•˜๋Š” ๊ฒƒ์ด ๋ชฉํ‘œ์ž…๋‹ˆ๋‹ค.

ProfileCard.swiftswift
struct ProfileCard: View {
    var body: some View {
        // Main HStack: photo on left, info on right
        HStack(spacing: 16) {

            // ZStack to overlay the badge on the photo
            ZStack(alignment: .bottomTrailing) {
                // Profile image (circle)
                Image("avatar")
                    .resizable()
                    .frame(width: 60, height: 60)
                    .clipShape(Circle())

                // Green "verified" badge at bottom right
                Image(systemName: "checkmark.circle.fill")
                    .foregroundColor(.green)
                    .background(Circle().fill(.white))  // White circle behind
            }

            // VStack to stack name and role vertically
            VStack(alignment: .leading, spacing: 4) {
                Text("Marie Dupont")
                    .font(.headline)

                Text("iOS Developer")
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }

            // Spacer pushes everything to the left
            Spacer()

            // Chevron on right indicates it's tappable
            Image(systemName: "chevron.right")
                .foregroundColor(.gray)
        }
        .padding()
        .background(Color(.systemGray6))
        .cornerRadius(12)
    }
}

์—ฌ๊ธฐ์„œ ํ•ต์‹ฌ์€ Spacer()์ž…๋‹ˆ๋‹ค. ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ๊ณต๊ฐ„์„ ์ฐจ์ง€ํ•˜๋Š” ๋ณด์ด์ง€ ์•Š๋Š” ์š”์†Œ์ž…๋‹ˆ๋‹ค. Spacer๊ฐ€ ์—†์œผ๋ฉด ์š”์†Œ๋“ค์ด ์ค‘์•™์— ๋ฐฐ์น˜๋˜์ง€๋งŒ, Spacer๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์š”์†Œ๋“ค์ด ์™ผ์ชฝ์œผ๋กœ ๋ฐ€๋ ค๋‚˜๊ณ  ์…ฐ๋ธŒ๋ก ์ด ์˜ค๋ฅธ์ชฝ ๋์— ๊ณ ์ •๋ฉ๋‹ˆ๋‹ค.

Xcode Tip

Xcode์—์„œ ์ž„์˜์˜ View๋ฅผ โŒ˜ + ํด๋ฆญํ•˜๋ฉด ๋น„์ฃผ์–ผ ์ธ์ŠคํŽ™ํ„ฐ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฝ”๋“œ๋ฅผ ์ง์ ‘ ์ž…๋ ฅํ•˜์ง€ ์•Š๊ณ ๋„ ๋ชจ๋””ํŒŒ์ด์–ด๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@State์™€ @Binding์„ ํ™œ์šฉํ•œ ์ƒํƒœ ๊ด€๋ฆฌ

์ƒํƒœ ๊ด€๋ฆฌ๋Š” SwiftUI์˜ ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค. ์ƒํƒœ๋ž€ ๋ณ€๊ฒฝ๋  ์ˆ˜ ์žˆ์œผ๋ฉฐ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ฐฑ์‹ ํ•ด์•ผ ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด SwiftUI๋Š” ์˜ํ–ฅ์„ ๋ฐ›๋Š” View๋ฅผ ์ž๋™์œผ๋กœ ์žฌ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค.

@State: View์˜ ๋กœ์ปฌ ์ƒํƒœ

@State๋Š” ํ”„๋กœํผํ‹ฐ ๋ž˜ํผ๋กœ, SwiftUI์— "์ด ๋ณ€์ˆ˜๋ฅผ ๊ฐ์‹œํ•˜๊ณ , ๋ณ€๊ฒฝ๋˜๋ฉด View๋ฅผ ๊ฐฑ์‹ ํ•˜์‹ญ์‹œ์˜ค"๋ผ๊ณ  ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ๋‹จ์ผ View์˜ ๋กœ์ปฌ ์ƒํƒœ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

๋Œ€ํ™”ํ˜• ์นด์šดํ„ฐ๋ฅผ ๋งŒ๋“ค์–ด ์ด ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ์ดํ•ดํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

CounterView.swiftswift
struct CounterView: View {
    // @State creates a "source of truth" for this view
    // private because state shouldn't be modified from outside
    @State private var count = 0

    var body: some View {
        VStack(spacing: 30) {
            // This Text updates automatically when count changes
            Text("\(count)")
                .font(.system(size: 72, weight: .bold))

            HStack(spacing: 40) {
                // Decrement button
                Button(action: {
                    count -= 1  // Modifies state โ†’ view refreshes
                }) {
                    Image(systemName: "minus.circle.fill")
                        .font(.largeTitle)
                }

                // Increment button
                Button(action: {
                    count += 1
                }) {
                    Image(systemName: "plus.circle.fill")
                        .font(.largeTitle)
                }
            }
        }
    }
}

๋ฒ„ํŠผ์„ ํƒญํ•˜๋ฉด count๊ฐ€ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค. SwiftUI๋Š” ์ด ๋ณ€๊ฒฝ์„ ๊ฐ์ง€ํ•˜๊ณ  body๋ฅผ ๋‹ค์‹œ ์‹คํ–‰ํ•˜์—ฌ ํ™”๋ฉด์„ ๊ฐฑ์‹ ํ•ฉ๋‹ˆ๋‹ค. ๋ ˆ์ด๋ธ”์„ ์ˆ˜๋™์œผ๋กœ ์—…๋ฐ์ดํŠธํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ๊ฒƒ์ด ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

@Binding: View ๊ฐ„ ์ƒํƒœ ๊ณต์œ ํ•˜๊ธฐ

์ž์‹ View๊ฐ€ ๋ถ€๋ชจ View์˜ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋•Œ @Binding์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด @State์— ๋Œ€ํ•œ ์–‘๋ฐฉํ–ฅ ์—ฐ๊ฒฐ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

๊ตฌ์ฒด์ ์ธ ์˜ˆ์ œ๋กœ, ์‹ค์‹œ๊ฐ„ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๊ฐ€ ํฌํ•จ๋œ ์‚ฌ์šฉ์ž ์ด๋ฆ„ ์ž…๋ ฅ ํ•„๋“œ๋ฅผ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

UsernameValidation.swiftswift
// Parent view: owns the state
struct SignupForm: View {
    @State private var username = ""       // Source of truth
    @State private var isValid = false     // Validation state

    var body: some View {
        VStack(spacing: 20) {
            // We pass BINDINGS (with $) to the child view
            UsernameField(username: $username, isValid: $isValid)

            Button("Create Account") {
                // Submit the form
            }
            .disabled(!isValid)  // Disabled if invalid
            .buttonStyle(.borderedProminent)
        }
        .padding()
    }
}

์ž์‹ View๋Š” ๋ฐ”์ธ๋”ฉ์„ ์ „๋‹ฌ๋ฐ›์•„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

swift
// Child view: receives and modifies state via @Binding
struct UsernameField: View {
    @Binding var username: String   // Connection to parent's @State
    @Binding var isValid: Bool

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            TextField("Username", text: $username)
                .textFieldStyle(.roundedBorder)
                .onChange(of: username) { oldValue, newValue in
                    // Validation: at least 3 characters
                    isValid = newValue.count >= 3
                }

            // Visual feedback
            HStack {
                Image(systemName: isValid ? "checkmark.circle" : "xmark.circle")
                Text("Minimum 3 characters")
            }
            .font(.caption)
            .foregroundColor(isValid ? .green : .red)
        }
    }
}

์‚ฌ์šฉ์ž๊ฐ€ TextField์— ์ž…๋ ฅํ•˜๋ฉด username์ด ๋ฐ”์ธ๋”ฉ์„ ํ†ตํ•ด ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค. ๋ถ€๋ชจ View๋Š” ์ด ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์ธ์‹ํ•˜๊ณ  ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊น”๋”ํ•œ ์–‘๋ฐฉํ–ฅ ํ†ต์‹ ์ด ๊ตฌํ˜„๋œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

Warning

@State๋Š” ๋‹จ์ˆœํ•œ ๋กœ์ปฌ ์ƒํƒœ์—๋งŒ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๋Ÿฌ ํ™”๋ฉด์— ๊ฑธ์นœ ๋ฐ์ดํ„ฐ๋‚˜ ๋ณต์žกํ•œ ๋กœ์ง์—๋Š” @Observable(iOS 17 ์ด์ƒ) ๋˜๋Š” MVVM ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

iOS ๋ฉด์ ‘ ์ค€๋น„๊ฐ€ ๋˜์…จ๋‚˜์š”?

์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ, flashcards, ๊ธฐ์ˆ  ํ…Œ์ŠคํŠธ๋กœ ์—ฐ์Šตํ•˜์„ธ์š”.

๋™์  ๋ฆฌ์ŠคํŠธ ํ‘œ์‹œํ•˜๊ธฐ

๋ฆฌ์ŠคํŠธ๋Š” ๋ชจ๋ฐ”์ผ ์•ฑ ์–ด๋””์—๋‚˜ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. SwiftUI๋Š” ๋„ค์ดํ‹ฐ๋ธŒ iOS ์Šคํƒ€์ผ๋ง(๊ตฌ๋ถ„์„ , ์Šค์™€์ดํ”„ ์•ก์…˜ ๋“ฑ)์„ ๊ฐ–์ถ˜ ๋ฐ์ดํ„ฐ ์ปฌ๋ ‰์…˜ ํ‘œ์‹œ๋ฅผ ์œ„ํ•ด List๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

๊ฐ„๋‹จํ•œ ๋ฆฌ์ŠคํŠธ ๋งŒ๋“ค๊ธฐ

๋ฆฌ์ŠคํŠธ๋ฅผ ํ‘œ์‹œํ•˜๋ ค๋ฉด ๋ฐ์ดํ„ฐ์™€ ๊ทธ๊ฒƒ์„ ์‹๋ณ„ํ•˜๋Š” ๋ฐฉ๋ฒ• ๋‘ ๊ฐ€์ง€๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. Identifiable ํ”„๋กœํ† ์ฝœ์„ ํ†ตํ•ด SwiftUI๋Š” ์–ด๋–ค View๊ฐ€ ์–ด๋–ค ๋ฐ์ดํ„ฐ์— ๋Œ€์‘ํ•˜๋Š”์ง€ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋จผ์ € ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

Models/Interview.swiftswift
// Identifiable lets SwiftUI track each element
struct Interview: Identifiable {
    let id = UUID()           // Auto-generated unique identifier
    let technology: String
    let difficulty: String
    let questionCount: Int
}

์ด์ œ ๋ฆฌ์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ForEach๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜๋ณต ์ฒ˜๋ฆฌํ•˜๊ณ  ๊ฐ ์š”์†Œ์— ๋Œ€ํ•œ ํ–‰์„ ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

InterviewListView.swiftswift
struct InterviewListView: View {
    // Data to display (in reality, this would come from an API)
    @State private var interviews = [
        Interview(technology: "iOS", difficulty: "Intermediate", questionCount: 25),
        Interview(technology: "Android", difficulty: "Advanced", questionCount: 30),
        Interview(technology: "React", difficulty: "Beginner", questionCount: 20)
    ]

    var body: some View {
        // NavigationStack enables the navigation bar
        NavigationStack {
            List {
                // ForEach iterates over each interview
                // Thanks to Identifiable, no need to specify id:
                ForEach(interviews) { interview in
                    InterviewRow(interview: interview)
                }
            }
            .navigationTitle("My Interviews")
        }
    }
}

๊ฐ ํ–‰์˜ View๋ฅผ ๋…๋ฆฝ๋œ ์ปดํฌ๋„ŒํŠธ๋กœ ๋ถ„๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๊ฐ€๋…์„ฑ๊ณผ ์žฌ์‚ฌ์šฉ์„ฑ์ด ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค.

InterviewRow.swiftswift
struct InterviewRow: View {
    let interview: Interview

    var body: some View {
        HStack {
            // Left column: title and subtitle
            VStack(alignment: .leading, spacing: 4) {
                Text(interview.technology)
                    .font(.headline)

                Text(interview.difficulty)
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }

            Spacer()

            // Badge with question count
            Text("\(interview.questionCount) Q")
                .font(.caption)
                .fontWeight(.medium)
                .padding(.horizontal, 8)
                .padding(.vertical, 4)
                .background(Color.blue.opacity(0.1))
                .cornerRadius(8)
        }
        .padding(.vertical, 4)
    }
}

InterviewRow๋ฅผ ๋…๋ฆฝ๋œ struct๋กœ ๋ถ„๋ฆฌํ•จ์œผ๋กœ์จ ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ๊ณผ ์žฌ์‚ฌ์šฉ์„ฑ์ด ํฌ๊ฒŒ ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ SwiftUI์˜ ๋ชจ๋ฒ” ์‚ฌ๋ก€์ž…๋‹ˆ๋‹ค.

์•ก์…˜ ์ถ”๊ฐ€: ์‚ญ์ œ์™€ ์žฌ์ •๋ ฌ

iOS ๋ฆฌ์ŠคํŠธ๋Š” ์‚ญ์ œ(์™ผ์ชฝ ์Šค์™€์ดํ”„)์™€ ์žฌ์ •๋ ฌ(๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ)์„ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. SwiftUI์—์„œ๋Š” .onDelete์™€ .onMove๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

InterviewListView.swift (enhanced version)swift
struct InterviewListView: View {
    @State private var interviews = [/* ... data ... */]

    var body: some View {
        NavigationStack {
            List {
                ForEach(interviews) { interview in
                    InterviewRow(interview: interview)
                }
                // Swipe to delete
                .onDelete(perform: deleteInterview)
                // Drag and drop to reorder
                .onMove(perform: moveInterview)
            }
            .navigationTitle("My Interviews")
            .toolbar {
                // "Edit" button that activates edit mode
                EditButton()
            }
        }
    }

    // Deletes elements at specified indices
    private func deleteInterview(at offsets: IndexSet) {
        interviews.remove(atOffsets: offsets)
    }

    // Moves elements from one position to another
    private func moveInterview(from source: IndexSet, to destination: Int) {
        interviews.move(fromOffsets: source, toOffset: destination)
    }
}

๋‹จ 4์ค„์˜ ์ถ”๊ฐ€ ์ฝ”๋“œ(.onDelete, .onMove, EditButton, ๋‘ ๊ฐœ์˜ ํ•จ์ˆ˜)๋งŒ์œผ๋กœ ์™„์ „ํ•œ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ๋ฆฌ์ŠคํŠธ๊ฐ€ ์™„์„ฑ๋ฉ๋‹ˆ๋‹ค. SwiftUI์˜ ์ง„๊ฐ€๊ฐ€ ์—ฌ๊ธฐ์„œ ๋“œ๋Ÿฌ๋‚ฉ๋‹ˆ๋‹ค.

์ธํ„ฐํŽ˜์ด์Šค ์• ๋‹ˆ๋ฉ”์ด์…˜

SwiftUI๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์—์„œ ํƒ์›”ํ•œ ์„ฑ๋Šฅ์„ ๋ฐœํœ˜ํ•ฉ๋‹ˆ๋‹ค. UIKit์—์„œ ๋Œ€๋Ÿ‰์˜ ์ฝ”๋“œ๊ฐ€ ํ•„์š”ํ–ˆ๋˜ ์• ๋‹ˆ๋ฉ”์ด์…˜๋„ ์—ฌ๊ธฐ์„œ๋Š” ๋ชจ๋‘ ์„ ์–ธ์ ์œผ๋กœ ๊ธฐ์ˆ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ตœ์ข… ์ƒํƒœ๋ฅผ ๊ธฐ์ˆ ํ•˜๋ฉด SwiftUI๊ฐ€ ์ „ํ™˜์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

withAnimation์„ ํ™œ์šฉํ•œ ์•”์‹œ์  ์• ๋‹ˆ๋ฉ”์ด์…˜

์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ ์šฉํ•˜๋Š” ๊ฐ€์žฅ ๊ฐ„๋‹จํ•œ ๋ฐฉ๋ฒ•์€ ์ƒํƒœ ๋ณ€๊ฒฝ์„ withAnimation์œผ๋กœ ๊ฐ์‹ธ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. SwiftUI๊ฐ€ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ๊ฐ์ง€ํ•˜๊ณ  ์˜ํ–ฅ์„ ๋ฐ›๋Š” ์‹œ๊ฐ์  ์†์„ฑ์„ ์ž๋™์œผ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

AnimatedCard.swiftswift
struct AnimatedCard: View {
    @State private var isExpanded = false

    var body: some View {
        VStack {
            RoundedRectangle(cornerRadius: 20)
                .fill(.blue)
                // Dimensions change based on state
                .frame(
                    width: isExpanded ? 300 : 150,
                    height: isExpanded ? 200 : 100
                )

            Button(isExpanded ? "Collapse" : "Expand") {
                // withAnimation animates ALL visual changes
                // resulting from this state change
                withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) {
                    isExpanded.toggle()
                }
            }
            .padding(.top)
        }
    }
}

๋ฒ„ํŠผ์„ ํƒญํ•˜๋ฉด isExpanded๊ฐ€ ํ† ๊ธ€๋ฉ๋‹ˆ๋‹ค. withAnimation ๋•๋ถ„์— ์‚ฌ๊ฐํ˜•์˜ ํฌ๊ธฐ ๋ณ€๊ฒฝ์ด ์Šคํ”„๋ง ํšจ๊ณผ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค. ๋ฌด์—‡์„ ์• ๋‹ˆ๋ฉ”์ด์…˜ํ• ์ง€ ์ง€์ •ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. SwiftUI๊ฐ€ ์ž๋™์œผ๋กœ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค.

ํŠธ๋žœ์ง€์…˜: ํ‘œ์‹œ์™€ ์‚ฌ๋ผ์ง์˜ ์• ๋‹ˆ๋ฉ”์ด์…˜

ํŠธ๋žœ์ง€์…˜์€ View๊ฐ€ ์–ด๋–ป๊ฒŒ ๋‚˜ํƒ€๋‚˜๊ณ  ์‚ฌ๋ผ์ง€๋Š”์ง€๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ ํŽ˜์ด๋“œ(opacity)์ด์ง€๋งŒ ๋งž์ถค ์„ค์ •์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

TransitionDemo.swiftswift
struct TransitionDemo: View {
    @State private var showDetails = false

    var body: some View {
        VStack(spacing: 20) {
            Button("Show Details") {
                withAnimation(.easeInOut(duration: 0.3)) {
                    showDetails.toggle()
                }
            }

            // This view appears/disappears with a transition
            if showDetails {
                DetailCard()
                    // Asymmetric transition: different for entry and exit
                    .transition(
                        .asymmetric(
                            insertion: .scale.combined(with: .opacity),  // Entry: zoom + fade
                            removal: .slide                               // Exit: slide
                        )
                    )
            }
        }
    }
}

struct DetailCard: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Interview Details")
                .font(.headline)
            Text("25 questions โ€ข 45 minutes")
                .foregroundColor(.secondary)
        }
        .padding()
        .background(Color(.systemGray6))
        .cornerRadius(16)
    }
}

์ด ์˜ˆ์ œ์—์„œ ์นด๋“œ๋Š” ์ค‘์•™์—์„œ ํ™•๋Œ€(scale + opacity)๋˜๋ฉฐ ๋‚˜ํƒ€๋‚˜๊ณ , ์˜†์œผ๋กœ ์Šฌ๋ผ์ด๋“œ(slide)ํ•˜๋ฉฐ ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๋งˆ์ดํฌ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ธํ„ฐํŽ˜์ด์Šค์— ์ƒ๋™๊ฐ๊ณผ ์ „๋ฌธ์ ์ธ ๋А๋‚Œ์„ ๋ถ€์—ฌํ•ฉ๋‹ˆ๋‹ค.

Performance

SwiftUI๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ž๋™์œผ๋กœ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค. ์ž์—ฐ์Šค๋Ÿฌ์šด ๋™์ž‘์„ ์œ„ํ•ด .spring()์„ ์‚ฌ์šฉํ•˜๊ณ , ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ถˆํŽธ์„ ์ฃผ๋Š” ๊ณผ๋„ํ•˜๊ฒŒ ๊ธด ์• ๋‹ˆ๋ฉ”์ด์…˜(0.5์ดˆ ์ด์ƒ)์€ ํ”ผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ

์‹ค์ œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ๋ฐ์ดํ„ฐ๋Š” ๋Œ€๋ถ€๋ถ„ API์—์„œ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. Swift Concurrency(async/await)๋Š” .task ๋ชจ๋””ํŒŒ์ด์–ด๋ฅผ ํ†ตํ•ด SwiftUI์™€ ์›ํ™œํ•˜๊ฒŒ ํ†ตํ•ฉ๋ฉ๋‹ˆ๋‹ค.

Loading / Error / Success ํŒจํ„ด

API ๋ฐ์ดํ„ฐ๋ฅผ ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•œ ํ‘œ์ค€ ํŒจํ„ด์„ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค. ๋กœ๋”ฉ ์ค‘, ์˜ค๋ฅ˜, ์„ฑ๊ณต์˜ ์„ธ ๊ฐ€์ง€ ์ƒํƒœ๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

AsyncDataView.swiftswift
struct AsyncDataView: View {
    @State private var questions: [Question] = []
    @State private var isLoading = true
    @State private var errorMessage: String?

    var body: some View {
        Group {
            if isLoading {
                // State: loading in progress
                ProgressView("Loading...")

            } else if let error = errorMessage {
                // State: error
                VStack(spacing: 16) {
                    Image(systemName: "exclamationmark.triangle")
                        .font(.largeTitle)
                        .foregroundColor(.orange)
                    Text(error)
                    Button("Retry") {
                        Task { await loadQuestions() }
                    }
                }

            } else {
                // State: success, display data
                List(questions) { question in
                    Text(question.title)
                }
            }
        }
        // .task runs automatically when the view appears
        .task {
            await loadQuestions()
        }
        // Pull-to-refresh
        .refreshable {
            await loadQuestions()
        }
    }

    private func loadQuestions() async {
        isLoading = true
        errorMessage = nil

        do {
            // Async API call
            questions = try await QuestionService.shared.fetchQuestions()
        } catch {
            errorMessage = "Unable to load questions"
        }

        isLoading = false
    }
}

.task ๋ชจ๋””ํŒŒ์ด์–ด๋Š” ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค. View๊ฐ€ ํ‘œ์‹œ๋  ๋•Œ ๋น„๋™๊ธฐ ํƒœ์Šคํฌ๋ฅผ ์‹คํ–‰ํ•˜๊ณ , View๊ฐ€ ์‚ฌ๋ผ์ง€๋ฉด ์ž๋™์œผ๋กœ ์ทจ์†Œํ•ฉ๋‹ˆ๋‹ค. ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜์˜ ์œ„ํ—˜์ด ์—†์Šต๋‹ˆ๋‹ค.

๊ฒฐ๋ก 

SwiftUI๋Š” ๋ชจ๋˜ iOS ๊ฐœ๋ฐœ์—์„œ ํ•„์ˆ˜์ ์ธ ์กด์žฌ๊ฐ€ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. iOS 18์„ ํ†ตํ•ด ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์ถฉ๋ถ„ํ•œ ์„ฑ์ˆ™๋„์— ๋„๋‹ฌํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ•ต์‹ฌ ์š”์•ฝ

  • ์„ ์–ธ์  ํŒจ๋Ÿฌ๋‹ค์ž„: ๊ตฌ์ถ• ๋ฐฉ๋ฒ•์ด ์•„๋‹Œ, ํ‘œ์‹œํ•˜๊ณ  ์‹ถ์€ ๋‚ด์šฉ์„ ๊ธฐ์ˆ ํ•œ๋‹ค
  • @State์™€ @Binding: ์ƒํƒœ๋ฅผ ๋ฆฌ์•กํ‹ฐ๋ธŒํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜๊ณ  View ๊ฐ„์— ์ „๋‹ฌํ•œ๋‹ค
  • Stack: VStack, HStack, ZStack์„ ์กฐํ•ฉํ•˜์—ฌ ์œ ์—ฐํ•œ ๋ ˆ์ด์•„์›ƒ์„ ๊ตฌ์ถ•ํ•œ๋‹ค
  • List: ์ตœ์†Œํ•œ์˜ ์ฝ”๋“œ๋กœ ๋„ค์ดํ‹ฐ๋ธŒ ์ธํ„ฐ๋ž™์…˜์ด ํฌํ•จ๋œ ์ปฌ๋ ‰์…˜์„ ํ‘œ์‹œํ•œ๋‹ค
  • ์• ๋‹ˆ๋ฉ”์ด์…˜: withAnimation์œผ๋กœ ๋ถ€๋“œ๋Ÿฌ์šด ์ž๋™ ์ „ํ™˜์„ ๊ตฌํ˜„ํ•œ๋‹ค
  • Async/await: .task๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋”ฉํ•˜๊ณ  ๋กœ๋”ฉ/์—๋Ÿฌ ์ƒํƒœ๋ฅผ ์ฒ˜๋ฆฌํ•œ๋‹ค

์ฒดํฌ๋ฆฌ์ŠคํŠธ

  • ๋ช…๋ นํ˜•(UIKit)๊ณผ ์„ ์–ธํ˜•(SwiftUI)์˜ ์ฐจ์ด๋ฅผ ์ดํ•ดํ•˜๊ณ  ์žˆ๋‹ค
  • ๋ชจ๋””ํŒŒ์ด์–ด์™€ ์ ์šฉ ์ˆœ์„œ๋ฅผ ์ˆ™๋‹ฌํ•˜๊ณ  ์žˆ๋‹ค
  • @State, @Binding, @Observable์˜ ์‚ฌ์šฉ ๊ตฌ๋ถ„์„ ํŒŒ์•…ํ•˜๊ณ  ์žˆ๋‹ค
  • Stack์„ ํ™œ์šฉํ•œ ๋ ˆ์ด์•„์›ƒ ๊ตฌ์ถ•์ด ๊ฐ€๋Šฅํ•˜๋‹ค
  • ์•ก์…˜์ด ํฌํ•จ๋œ ๋ฆฌ์ŠคํŠธ(์‚ญ์ œ, ์ด๋™)๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค
  • withAnimation์„ ํ™œ์šฉํ•œ ์ƒํƒœ ๋ณ€๊ฒฝ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค

์—ฐ์Šต์„ ์‹œ์ž‘ํ•˜์„ธ์š”!

๋ฉด์ ‘ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์™€ ๊ธฐ์ˆ  ํ…Œ์ŠคํŠธ๋กœ ์ง€์‹์„ ํ…Œ์ŠคํŠธํ•˜์„ธ์š”.

SwiftUI๋Š” Apple ์ƒํƒœ๊ณ„ ์ „๋ฐ˜์— ๊ฑธ์ณ ์„ธ๋ จ๋œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธธ์„ ์—ด์–ด์ค๋‹ˆ๋‹ค. ๊ฐ€์žฅ ์ข‹์€ ํ•™์Šต ๋ฐฉ๋ฒ•์€ ์‹ค์ฒœ์ž…๋‹ˆ๋‹ค. ์ž‘์€ ๊ฐœ์ธ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค๊ณ  ์ด ๊ธ€์—์„œ ์†Œ๊ฐœํ•œ ๊ฐ ๊ฐœ๋…์„ ์ง์ ‘ ์‹œํ—˜ํ•ด ๋ณด์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

ํƒœ๊ทธ

#swiftui
#ios
#swift
#ui
#apple

๊ณต์œ 

๊ด€๋ จ ๊ธฐ์‚ฌ

SwiftUI LazyVStack๊ณผ ๋ณต์žกํ•œ ๋ฆฌ์ŠคํŠธ ์„ฑ๋Šฅ ์ตœ์ ํ™”

SwiftUI ์„ฑ๋Šฅ: LazyVStack๊ณผ ๋ณต์žกํ•œ ๋ฆฌ์ŠคํŠธ ์ตœ์ ํ™”

LazyVStack๊ณผ SwiftUI ๋ฆฌ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ์ตœ์ ํ™” ๊ธฐ๋ฒ•. ๋ฉ”๋ชจ๋ฆฌ ์†Œ๋น„๋ฅผ ์ค„์ด๊ณ  ์Šคํฌ๋กค ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ค๋ฉฐ ํ”ํ•œ ํ•จ์ •์„ ํ”ผํ•ฉ๋‹ˆ๋‹ค.

iOS์—์„œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋””์ž์ธ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•˜๊ธฐ ์œ„ํ•œ SwiftUI ViewModifier

SwiftUI ์ปค์Šคํ…€ ViewModifier: ๋””์ž์ธ ์‹œ์Šคํ…œ์„ ์œ„ํ•œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํŒจํ„ด

์ผ๊ด€๋œ ๋””์ž์ธ ์‹œ์Šคํ…œ์„ ์œ„ํ•ด SwiftUI์—์„œ ์ปค์Šคํ…€ ViewModifier๋ฅผ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค. iOS ๋ทฐ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ์Šคํƒ€์ผ๋งํ•˜๊ธฐ ์œ„ํ•œ ํŒจํ„ด, ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค, ์‹ค์šฉ์ ์ธ ์˜ˆ์‹œ๋ฅผ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

iOS ๊ฐœ๋ฐœ์ž๋ฅผ ์œ„ํ•œ SwiftUI์—์„œ์˜ @Observable๊ณผ @State ๋น„๊ต

SwiftUI @Observable vs @State: 2026๋…„์— ๋ฌด์—‡์„ ์–ธ์ œ ์‚ฌ์šฉํ• ๊นŒ

SwiftUI์—์„œ @Observable๊ณผ @State์˜ ์ฐจ์ด๋ฅผ ๋งˆ์Šคํ„ฐํ•˜๊ณ  iOS ์•ฑ์— ์ ํ•ฉํ•œ ์ƒํƒœ ๊ด€๋ฆฌ ๋„๊ตฌ๋ฅผ ์„ ํƒํ•ด๋ณด์„ธ์š”.