์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐ŸŽ Swift์™€ SwiftUI๋กœ ์‹œ์ž‘ํ•˜๋Š” iOS ์•ฑ ๊ฐœ๋ฐœ ์™„์ „์ •๋ณต

๐ŸŽ Swift์™€ SwiftUI๋กœ ์‹œ์ž‘ํ•˜๋Š” iOS ์•ฑ ๊ฐœ๋ฐœ ์™„์ „์ •๋ณต

์• ํ”Œ ์ƒํƒœ๊ณ„์˜ ์ตœ์‹  ๊ธฐ์ˆ ๋กœ ๋‚˜๋งŒ์˜ ์•ฑ ๋งŒ๋“ค๊ธฐ

์•ผ ์ง„์งœ ์š”์ฆ˜ iOS ์•ฑ ๊ฐœ๋ฐœ ์‹œ์ž‘ํ•˜๋ ค๋ฉด Swift๋ž‘ SwiftUI ๋ชจ๋ฅด๋ฉด ์•ˆ ๋˜๋Š” ์‹œ๋Œ€๊ฐ€ ์™”์–ด ใ…‹ใ…‹ใ…‹ ์˜ˆ์ „์—๋Š” Objective-C๋กœ ๊ฐœ๋ฐœํ•˜๊ณ  UIKit์œผ๋กœ UI ๋งŒ๋“ค์—ˆ๋Š”๋ฐ, ์ง€๊ธˆ์€ ์™„์ „ ํŒจ๋Ÿฌ๋‹ค์ž„์ด ๋ฐ”๋€Œ์—ˆ๋‹ค๋‹ˆ๊นŒ? ๐Ÿš€

์• ํ”Œ์ด 2014๋…„์— Swift๋ฅผ ๋ฐœํ‘œํ•˜๊ณ , 2019๋…„์— SwiftUI๋ฅผ ๊ณต๊ฐœํ•˜๋ฉด์„œ iOS ๊ฐœ๋ฐœ์ด ์ง„์งœ ํ˜๋ช…์ ์œผ๋กœ ๋ณ€ํ–ˆ์–ด. ์ฝ”๋“œ ์–‘์€ ์ค„์–ด๋“ค๊ณ , ์ƒ์‚ฐ์„ฑ์€ ๋ฏธ์นœ ๋“ฏ์ด ์˜ฌ๋ผ๊ฐ€๊ณ , ๋ญ”๊ฐ€ ๋งŒ๋“ค ๋•Œ ์žฌ๋ฏธ๋„ ํ›จ์”ฌ ๋”ํ•ด์กŒ์ง€ ใ…Žใ…Ž

์ด ๊ธ€์—์„œ๋Š” Swift ์–ธ์–ด์˜ ํ•ต์‹ฌ ๊ฐœ๋…๋ถ€ํ„ฐ SwiftUI๋กœ ์‹ค์ œ ์•ฑ์„ ๋งŒ๋“œ๋Š” ๊ณผ์ •๊นŒ์ง€ ์™„์ „ ๋””ํ…Œ์ผํ•˜๊ฒŒ ๋‹ค๋ค„๋ณผ ๊ฑฐ์•ผ. ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์ดˆ๋ณด๋“  ๋‹ค๋ฅธ ์–ธ์–ด ๊ฒฝํ—˜์ž๋“  ์ƒ๊ด€์—†์ด ๋”ฐ๋ผ์˜ฌ ์ˆ˜ ์žˆ๊ฒŒ ์„ค๋ช…ํ•  ํ…Œ๋‹ˆ๊นŒ ๊ฑฑ์ • ใ„ดใ„ด ๐Ÿ˜Ž

Swift ๊ฐ•๋ ฅํ•œ ์–ธ์–ด SwiftUI ์„ ์–ธํ˜• UI iOS ์•ฑ ๊ฐœ๋ฐœ ํฌ๋กœ์Šค ํ”Œ๋žซํผ

๐ŸŽฏ Swift ์–ธ์–ด์˜ ํƒ„์ƒ๊ณผ ์ฒ ํ•™

Swift๋Š” ์• ํ”Œ์ด Chris Lattner๋ฅผ ์ค‘์‹ฌ์œผ๋กœ ๊ฐœ๋ฐœํ•œ ํ˜„๋Œ€์ ์ธ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด์•ผ. 2014๋…„ WWDC์—์„œ ์ฒ˜์Œ ๊ณต๊ฐœ๋์„ ๋•Œ ๊ฐœ๋ฐœ์ž๋“ค ์‚ฌ์ด์—์„œ ์™„์ „ ๋‚œ๋ฆฌ ๋‚ฌ์—ˆ์Œ ใ…‹ใ…‹ใ…‹

๐Ÿ”ฅ Swift๊ฐ€ ๋“ฑ์žฅํ•œ ์ด์œ 

Objective-C๋Š” 1980๋…„๋Œ€์— ๋งŒ๋“ค์–ด์ง„ ์–ธ์–ด๋ผ ํ˜„๋Œ€์ ์ธ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ํŒจ๋Ÿฌ๋‹ค์ž„์„ ๋ฐ˜์˜ํ•˜๊ธฐ ์–ด๋ ค์› ์–ด. ๋ฌธ๋ฒ•๋„ ๋ณต์žกํ•˜๊ณ , ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ๋„ ๊นŒ๋‹ค๋กœ์› ์ง€. ์• ํ”Œ์€ ๋” ์•ˆ์ „ํ•˜๊ณ , ๋น ๋ฅด๊ณ , ํ‘œํ˜„๋ ฅ ์žˆ๋Š” ์–ธ์–ด๊ฐ€ ํ•„์š”ํ–ˆ๋˜ ๊ฑฐ์•ผ.

Swift์˜ ํ•ต์‹ฌ ์ฒ ํ•™์€ ํฌ๊ฒŒ ์„ธ ๊ฐ€์ง€๋กœ ์š”์•ฝํ•  ์ˆ˜ ์žˆ์–ด:

1. ์•ˆ์ „์„ฑ(Safety) ๐Ÿ’ช
Swift๋Š” ์ปดํŒŒ์ผ ํƒ€์ž„์— ์ตœ๋Œ€ํ•œ ๋งŽ์€ ์—๋Ÿฌ๋ฅผ ์žก์•„๋‚ด๋ ค๊ณ  ์„ค๊ณ„๋์–ด. Optional ํƒ€์ž…, ํƒ€์ž… ์•ˆ์ „์„ฑ, ์ž๋™ ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ(ARC) ๊ฐ™์€ ๊ธฐ๋Šฅ๋“ค์ด ๋Ÿฐํƒ€์ž„ ํฌ๋ž˜์‹œ๋ฅผ ํš๊ธฐ์ ์œผ๋กœ ์ค„์—ฌ์ค˜.

2. ์†๋„(Speed) โšก
C++์ด๋‚˜ Objective-C์™€ ๋น„๊ตํ•ด๋„ ์„ฑ๋Šฅ์ด ๋’ค์ง€์ง€ ์•Š์•„. LLVM ์ปดํŒŒ์ผ๋Ÿฌ ๊ธฐ๋ฐ˜์œผ๋กœ ์ตœ์ ํ™”๊ฐ€ ์—„์ฒญ ์ž˜ ๋˜์–ด ์žˆ๊ฑฐ๋“ . ๋ฒค์น˜๋งˆํฌ ํ…Œ์ŠคํŠธ์—์„œ๋„ Python๋ณด๋‹ค 100๋ฐฐ ์ด์ƒ ๋น ๋ฅธ ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„.

3. ํ‘œํ˜„๋ ฅ(Expressiveness) โœจ
์ฝ”๋“œ๊ฐ€ ์ฝ๊ธฐ ์‰ฝ๊ณ  ์ง๊ด€์ ์ด์•ผ. ํ•จ์ˆ˜ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ, ํ”„๋กœํ† ์ฝœ ์ง€ํ–ฅ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๊ฐ™์€ ํ˜„๋Œ€์ ์ธ ํŒจ๋Ÿฌ๋‹ค์ž„์„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ง€์›ํ•˜์ง€.

๐Ÿ’ก ๊ฟ€ํŒ

Swift๋Š” ์˜คํ”ˆ์†Œ์Šค์•ผ! Swift.org์—์„œ ์†Œ์Šค ์ฝ”๋“œ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ๊ณ , ๋ฆฌ๋ˆ…์Šค๋‚˜ ์œˆ๋„์šฐ์—์„œ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด. iOS ๊ฐœ๋ฐœ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๊ฐœ๋ฐœ(Vapor, Kitura)๋„ ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ๊ฑฐ ์•Œ๊ณ  ์žˆ์—ˆ์–ด?

๐Ÿ“š Swift ๊ธฐ๋ณธ ๋ฌธ๋ฒ• ๋งˆ์Šคํ„ฐํ•˜๊ธฐ

์ž ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ Swift ๋ฌธ๋ฒ•์„ ํŒŒํ—ค์ณ๋ณด์ž ใ…Žใ…Ž ๋‹ค๋ฅธ ์–ธ์–ด ๊ฒฝํ—˜์ด ์žˆ๋‹ค๋ฉด ๊ธˆ๋ฐฉ ์ตํž ์ˆ˜ ์žˆ์„ ๊ฑฐ์•ผ!

๋ณ€์ˆ˜์™€ ์ƒ์ˆ˜ ์„ ์–ธ

Swift์—์„œ๋Š” var๋กœ ๋ณ€์ˆ˜๋ฅผ, let์œผ๋กœ ์ƒ์ˆ˜๋ฅผ ์„ ์–ธํ•ด. ๊ฐ€๋Šฅํ•˜๋ฉด let์„ ์“ฐ๋Š” ๊ฒŒ ์ข‹์•„ - ๋ถˆ๋ณ€์„ฑ์ด ์ฝ”๋“œ๋ฅผ ๋” ์•ˆ์ „ํ•˜๊ฒŒ ๋งŒ๋“ค๊ฑฐ๋“ .

// ๋ณ€์ˆ˜ ์„ ์–ธ
var userName = "๊น€๊ฐœ๋ฐœ"
var age = 25

// ์ƒ์ˆ˜ ์„ ์–ธ
let birthYear = 1999
let pi = 3.14159

// ํƒ€์ž… ๋ช…์‹œ (ํƒ€์ž… ์ถ”๋ก ๋„ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ ๋ช…์‹œ์ ์œผ๋กœ ์“ธ ์ˆ˜๋„ ์žˆ์–ด)
var score: Int = 100
let message: String = "Hello, Swift!"

๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ ํƒ€์ž… ๐Ÿ“Š

Swift๋Š” ํƒ€์ž…์— ์—„๊ฒฉํ•œ ์–ธ์–ด์•ผ. ์ฃผ์š” ๋ฐ์ดํ„ฐ ํƒ€์ž…๋“ค์„ ์•Œ์•„๋ณด์ž:

// ์ •์ˆ˜ํ˜•
let smallNumber: Int8 = 127
let normalNumber: Int = 1000000
let bigNumber: Int64 = 9223372036854775807

// ์‹ค์ˆ˜ํ˜•
let height: Float = 175.5
let weight: Double = 68.5  // Double์ด ๋” ์ •๋ฐ€ํ•ด

// ๋ถˆ๋ฆฌ์–ธ
let isStudent: Bool = true
let hasLicense: Bool = false

// ๋ฌธ์ž์—ด
let greeting: String = "์•ˆ๋…•ํ•˜์„ธ์š”"
let multiLine = """
์—ฌ๋Ÿฌ ์ค„์˜
๋ฌธ์ž์—ด๋„
์ด๋ ‡๊ฒŒ ์“ธ ์ˆ˜ ์žˆ์–ด์š”
"""

// ๋ฌธ์ž
let firstLetter: Character = "A"

โœ… ํƒ€์ž… ์ถ”๋ก ์˜ ๋งˆ๋ฒ•

Swift ์ปดํŒŒ์ผ๋Ÿฌ๋Š” ๋˜‘๋˜‘ํ•ด์„œ ์ดˆ๊ธฐ๊ฐ’์„ ๋ณด๊ณ  ํƒ€์ž…์„ ์ž๋™์œผ๋กœ ์ถ”๋ก ํ•ด์ค˜. let name = "์ฒ ์ˆ˜"๋ผ๊ณ  ์“ฐ๋ฉด ์ž๋™์œผ๋กœ String ํƒ€์ž…์œผ๋กœ ์ธ์‹ํ•˜๋Š” ๊ฑฐ์ง€. ๊ทผ๋ฐ ๋ช…์‹œ์ ์œผ๋กœ ํƒ€์ž…์„ ์จ์ฃผ๋Š” ๊ฒŒ ๊ฐ€๋…์„ฑ ๋ฉด์—์„œ ๋” ์ข‹์„ ๋•Œ๋„ ์žˆ์–ด!

์ปฌ๋ ‰์…˜ ํƒ€์ž… ๐Ÿ—‚๏ธ

๋ฐฐ์—ด(Array), ๋”•์…”๋„ˆ๋ฆฌ(Dictionary), ์ง‘ํ•ฉ(Set) ์„ธ ๊ฐ€์ง€ ์ฃผ์š” ์ปฌ๋ ‰์…˜์ด ์žˆ์–ด:

// ๋ฐฐ์—ด - ์ˆœ์„œ๊ฐ€ ์žˆ๋Š” ๋ฐ์ดํ„ฐ ๋ชจ์Œ
var fruits = ["์‚ฌ๊ณผ", "๋ฐ”๋‚˜๋‚˜", "์˜ค๋ Œ์ง€"]
fruits.append("ํฌ๋„")
print(fruits[0])  // "์‚ฌ๊ณผ"

// ๋”•์…”๋„ˆ๋ฆฌ - ํ‚ค-๊ฐ’ ์Œ์œผ๋กœ ์ €์žฅ
var studentScores = ["์ฒ ์ˆ˜": 95, "์˜ํฌ": 88, "๋ฏผ์ˆ˜": 92]
studentScores["์ง€์˜"] = 90
print(studentScores["์ฒ ์ˆ˜"] ?? 0)  // 95

// ์ง‘ํ•ฉ - ์ค‘๋ณต ์—†๋Š” ๋ฐ์ดํ„ฐ ๋ชจ์Œ
var uniqueNumbers: Set = [1, 2, 3, 3, 4, 5]
print(uniqueNumbers)  // [1, 2, 3, 4, 5] ์ˆœ์„œ๋Š” ๋ณด์žฅ ์•ˆ ๋จ

์˜ต์…”๋„(Optional) - Swift์˜ ํ‚ฌ๋Ÿฌ ํ”ผ์ฒ˜ ๐ŸŽ

์ด๊ฑฐ ์ง„์งœ ์ค‘์š”ํ•ด! ์˜ต์…”๋„์€ "๊ฐ’์ด ์žˆ์„ ์ˆ˜๋„ ์žˆ๊ณ  ์—†์„ ์ˆ˜๋„ ์žˆ๋‹ค"๋Š” ๊ฑธ ํƒ€์ž… ์‹œ์Šคํ…œ์œผ๋กœ ํ‘œํ˜„ํ•œ ๊ฑฐ์•ผ. Null Pointer Exception ๊ฐ™์€ ๋Ÿฐํƒ€์ž„ ์—๋Ÿฌ๋ฅผ ์ปดํŒŒ์ผ ํƒ€์ž„์— ์žก์•„๋‚ผ ์ˆ˜ ์žˆ์ง€.

// ์˜ต์…”๋„ ์„ ์–ธ (ํƒ€์ž… ๋’ค์— ? ๋ถ™์ž„)
var optionalName: String? = "ํ™๊ธธ๋™"
var optionalAge: Int? = nil

// ์˜ต์…”๋„ ๋ฐ”์ธ๋”ฉ - ์•ˆ์ „ํ•˜๊ฒŒ ๊ฐ’ ์ถ”์ถœ
if let name = optionalName {
    print("์ด๋ฆ„์€ \(name)์ž…๋‹ˆ๋‹ค")
} else {
    print("์ด๋ฆ„์ด ์—†์Šต๋‹ˆ๋‹ค")
}

// guard let - ์กฐ๊ธฐ ์ข…๋ฃŒ ํŒจํ„ด
func greet(name: String?) {
    guard let unwrappedName = name else {
        print("์ด๋ฆ„์„ ์•Œ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค")
        return
    }
    print("์•ˆ๋…•ํ•˜์„ธ์š”, \(unwrappedName)๋‹˜!")
}

// Nil ๋ณ‘ํ•ฉ ์—ฐ์‚ฐ์ž
let displayName = optionalName ?? "์ต๋ช…"

// ์˜ต์…”๋„ ์ฒด์ด๋‹
let uppercasedName = optionalName?.uppercased()

โš ๏ธ ๊ฐ•์ œ ์–ธ๋ž˜ํ•‘ ์ฃผ์˜!

optionalName! ์ด๋ ‡๊ฒŒ ๋А๋‚Œํ‘œ๋กœ ๊ฐ•์ œ๋กœ ๊ฐ’์„ ๊บผ๋‚ด๋Š” ๊ฑด ์œ„ํ—˜ํ•ด. ๊ฐ’์ด nil์ด๋ฉด ์•ฑ์ด ํฌ๋ž˜์‹œ ๋‚˜๊ฑฐ๋“  ใ… ใ…  ์ •๋ง ํ™•์‹คํ•  ๋•Œ๋งŒ ์“ฐ๊ณ , ๊ฐ€๋Šฅํ•˜๋ฉด ์˜ต์…”๋„ ๋ฐ”์ธ๋”ฉ์ด๋‚˜ nil ๋ณ‘ํ•ฉ ์—ฐ์‚ฐ์ž๋ฅผ ์‚ฌ์šฉํ•˜์ž!

์ œ์–ด๋ฌธ๊ณผ ๋ฐ˜๋ณต๋ฌธ ๐Ÿ”„

// if-else
let temperature = 25
if temperature > 30 {
    print("๋”์›Œ์š”")
} else if temperature > 20 {
    print("์ ๋‹นํ•ด์š”")
} else {
    print("์ถ”์›Œ์š”")
}

// switch - Swift์˜ switch๋Š” ์—„์ฒญ ๊ฐ•๋ ฅํ•ด!
let grade = "A"
switch grade {
case "A":
    print("์šฐ์ˆ˜")
case "B", "C":
    print("๋ณดํ†ต")
case "D"..."F":  // ๋ฒ”์œ„๋„ ๊ฐ€๋Šฅ!
    print("๋ฏธํก")
default:
    print("์•Œ ์ˆ˜ ์—†์Œ")
}

// for-in ๋ฃจํ”„
for i in 1...5 {  // 1๋ถ€ํ„ฐ 5๊นŒ์ง€ (5 ํฌํ•จ)
    print(i)
}

for i in 1..<5 {  // 1๋ถ€ํ„ฐ 4๊นŒ์ง€ (5 ๋ฏธํฌํ•จ)
    print(i)
}

// ๋ฐฐ์—ด ์ˆœํšŒ
for fruit in fruits {
    print(fruit)
}

// ๋”•์…”๋„ˆ๋ฆฌ ์ˆœํšŒ
for (name, score) in studentScores {
    print("\(name): \(score)์ ")
}

// while ๋ฃจํ”„
var count = 0
while count < 5 {
    print(count)
    count += 1
}

๐Ÿ—๏ธ ํ•จ์ˆ˜์™€ ํด๋กœ์ €

ํ•จ์ˆ˜๋Š” ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ฝ”๋“œ ๋ธ”๋ก์ด์•ผ. Swift์˜ ํ•จ์ˆ˜๋Š” ์ผ๊ธ‰ ๊ฐ์ฒด(First-class citizen)๋ผ์„œ ๋ณ€์ˆ˜์— ํ• ๋‹นํ•˜๊ฑฐ๋‚˜ ๋‹ค๋ฅธ ํ•จ์ˆ˜์˜ ์ธ์ž๋กœ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์–ด!

๊ธฐ๋ณธ ํ•จ์ˆ˜ ์„ ์–ธ

// ํŒŒ๋ผ๋ฏธํ„ฐ์™€ ๋ฐ˜ํ™˜๊ฐ’์ด ์žˆ๋Š” ํ•จ์ˆ˜
func add(a: Int, b: Int) -> Int {
    return a + b
}

let result = add(a: 5, b: 3)  // 8

// ํŒŒ๋ผ๋ฏธํ„ฐ ๋ ˆ์ด๋ธ” (์™ธ๋ถ€/๋‚ด๋ถ€ ์ด๋ฆ„)
func greet(to name: String, from hometown: String) {
    print("\(name)๋‹˜, \(hometown)์—์„œ ์˜ค์…จ๊ตฐ์š”!")
}

greet(to: "์ฒ ์ˆ˜", from: "์„œ์šธ")

// ๊ธฐ๋ณธ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ’
func introduce(name: String, age: Int = 20) {
    print("\(name), \(age)์‚ด")
}

introduce(name: "์˜ํฌ")  // ์˜ํฌ, 20์‚ด
introduce(name: "๋ฏผ์ˆ˜", age: 25)  // ๋ฏผ์ˆ˜, 25์‚ด

// ๊ฐ€๋ณ€ ํŒŒ๋ผ๋ฏธํ„ฐ
func sum(_ numbers: Int...) -> Int {
    return numbers.reduce(0, +)
}

print(sum(1, 2, 3, 4, 5))  // 15

ํด๋กœ์ € - ์ต๋ช… ํ•จ์ˆ˜์˜ ๊ฐ•๋ ฅํ•จ ๐Ÿš€

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

// ๊ธฐ๋ณธ ํด๋กœ์ € ๋ฌธ๋ฒ•
let multiply = { (a: Int, b: Int) -> Int in
    return a * b
}

print(multiply(4, 5))  // 20

// ๋ฐฐ์—ด ๋ฉ”์„œ๋“œ์™€ ํด๋กœ์ €
let numbers = [1, 2, 3, 4, 5]

// map - ๊ฐ ์š”์†Œ๋ฅผ ๋ณ€ํ™˜
let doubled = numbers.map { $0 * 2 }
print(doubled)  // [2, 4, 6, 8, 10]

// filter - ์กฐ๊ฑด์— ๋งž๋Š” ์š”์†Œ๋งŒ ์„ ํƒ
let evenNumbers = numbers.filter { $0 % 2 == 0 }
print(evenNumbers)  // [2, 4]

// reduce - ํ•˜๋‚˜์˜ ๊ฐ’์œผ๋กœ ์ถ•์•ฝ
let total = numbers.reduce(0) { $0 + $1 }
print(total)  // 15

// ํ›„ํ–‰ ํด๋กœ์ € (Trailing Closure)
func performOperation(a: Int, b: Int, operation: (Int, Int) -> Int) -> Int {
    return operation(a, b)
}

let result1 = performOperation(a: 10, b: 5) { x, y in
    return x - y
}  // 5

๐Ÿ’ก ํด๋กœ์ € ์ถ•์•ฝ ๋ฌธ๋ฒ•

Swift ํด๋กœ์ €๋Š” ๋ฌธ๋งฅ์— ๋”ฐ๋ผ ์—„์ฒญ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์“ธ ์ˆ˜ ์žˆ์–ด. $0, $1 ๊ฐ™์€ ๋‹จ์ถ• ์ธ์ž ์ด๋ฆ„์„ ์“ฐ๊ฑฐ๋‚˜, ํƒ€์ž… ์ถ”๋ก ์„ ํ™œ์šฉํ•˜๋ฉด ์ฝ”๋“œ๊ฐ€ ํ›จ์”ฌ ๊น”๋”ํ•ด์ ธ. ๊ทผ๋ฐ ๋„ˆ๋ฌด ์ถ•์•ฝํ•˜๋ฉด ๊ฐ€๋…์„ฑ์ด ๋–จ์–ด์งˆ ์ˆ˜ ์žˆ์œผ๋‹ˆ ์ ์ ˆํžˆ ์กฐ์ ˆํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด!

๐ŸŽจ ๊ตฌ์กฐ์ฒด์™€ ํด๋ž˜์Šค

Swift๋Š” ๊ฐ์ฒด์ง€ํ–ฅ ํ”„๋กœ๊ทธ๋ž˜๋ฐ๊ณผ ํ”„๋กœํ† ์ฝœ ์ง€ํ–ฅ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์„ ๋ชจ๋‘ ์ง€์›ํ•ด. ๊ตฌ์กฐ์ฒด(Struct)์™€ ํด๋ž˜์Šค(Class)์˜ ์ฐจ์ด๋ฅผ ์ดํ•ดํ•˜๋Š” ๊ฒŒ ์ง„์งœ ์ค‘์š”ํ•ด!

๊ตฌ์กฐ์ฒด(Struct) - ๊ฐ’ ํƒ€์ž…

struct Person {
    var name: String
    var age: Int
    
    // ๋ฉ”์„œ๋“œ
    func introduce() {
        print("์•ˆ๋…•ํ•˜์„ธ์š”, \(name)์ž…๋‹ˆ๋‹ค. \(age)์‚ด์ด์—์š”.")
    }
    
    // mutating ํ‚ค์›Œ๋“œ - ๊ตฌ์กฐ์ฒด ๋‚ด๋ถ€ ๊ฐ’ ๋ณ€๊ฒฝ
    mutating func haveBirthday() {
        age += 1
    }
}

var person1 = Person(name: "์ฒ ์ˆ˜", age: 20)
var person2 = person1  // ๋ณต์‚ฌ๋จ!

person2.name = "์˜ํฌ"
print(person1.name)  // "์ฒ ์ˆ˜" - ์›๋ณธ์€ ๋ณ€ํ•˜์ง€ ์•Š์Œ
print(person2.name)  // "์˜ํฌ"

ํด๋ž˜์Šค(Class) - ์ฐธ์กฐ ํƒ€์ž…

class Vehicle {
    var brand: String
    var speed: Int
    
    // ์ดˆ๊ธฐํ™” ๋ฉ”์„œ๋“œ
    init(brand: String, speed: Int) {
        self.brand = brand
        self.speed = speed
    }
    
    func accelerate() {
        speed += 10
        print("\(brand) ์†๋„: \(speed)km/h")
    }
    
    // ์†Œ๋ฉธ์ž
    deinit {
        print("\(brand) ๊ฐ์ฒด๊ฐ€ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ํ•ด์ œ๋จ")
    }
}

// ์ƒ์†
class Car: Vehicle {
    var numberOfDoors: Int
    
    init(brand: String, speed: Int, doors: Int) {
        self.numberOfDoors = doors
        super.init(brand: brand, speed: speed)
    }
    
    override func accelerate() {
        speed += 20  // ์ž๋™์ฐจ๋Š” ๋” ๋นจ๋ฆฌ ๊ฐ€์†
        print("\(brand) ์ž๋™์ฐจ ์†๋„: \(speed)km/h")
    }
}

let car1 = Car(brand: "ํ˜„๋Œ€", speed: 0, doors: 4)
let car2 = car1  // ๊ฐ™์€ ๊ฐ์ฒด๋ฅผ ์ฐธ์กฐ!

car2.brand = "๊ธฐ์•„"
print(car1.brand)  // "๊ธฐ์•„" - ์ฐธ์กฐ๋ผ์„œ ์›๋ณธ๋„ ๋ณ€๊ฒฝ๋จ

๐Ÿค” ๊ตฌ์กฐ์ฒด vs ํด๋ž˜์Šค, ์–ธ์ œ ๋ญ˜ ์จ์•ผ ํ• ๊นŒ?

๊ตฌ์กฐ์ฒด๋ฅผ ์“ฐ๋Š” ๊ฒฝ์šฐ:
โ€ข ๋ฐ์ดํ„ฐ๋ฅผ ์บก์Аํ™”ํ•˜๋Š” ๊ฒŒ ์ฃผ ๋ชฉ์ ์ผ ๋•Œ
โ€ข ์ƒ์†์ด ํ•„์š” ์—†์„ ๋•Œ
โ€ข ๋ณต์‚ฌ ๋™์ž‘์ด ํ•ฉ๋ฆฌ์ ์ผ ๋•Œ
โ€ข ์ž‘๊ณ  ๊ฐ„๋‹จํ•œ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ

ํด๋ž˜์Šค๋ฅผ ์“ฐ๋Š” ๊ฒฝ์šฐ:
โ€ข ์ƒ์†์ด ํ•„์š”ํ•  ๋•Œ
โ€ข ์ฐธ์กฐ ์˜๋ฏธ๋ก ์ด ํ•„์š”ํ•  ๋•Œ (์—ฌ๋Ÿฌ ๊ณณ์—์„œ ๊ฐ™์€ ์ธ์Šคํ„ด์Šค ๊ณต์œ )
โ€ข Objective-C์™€ ์ƒํ˜ธ์šด์šฉ์ด ํ•„์š”ํ•  ๋•Œ

์• ํ”Œ ๊ณต์‹ ๊ฐ€์ด๋“œ์—์„œ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๊ตฌ์กฐ์ฒด๋ฅผ ๊ถŒ์žฅํ•˜๊ณ , ๊ผญ ํ•„์š”ํ•  ๋•Œ๋งŒ ํด๋ž˜์Šค๋ฅผ ์“ฐ๋ผ๊ณ  ํ•ด!

ํ”„๋กœํผํ‹ฐ์™€ ๋ฉ”์„œ๋“œ ๐Ÿ“ฆ

struct Rectangle {
    // ์ €์žฅ ํ”„๋กœํผํ‹ฐ
    var width: Double
    var height: Double
    
    // ์—ฐ์‚ฐ ํ”„๋กœํผํ‹ฐ
    var area: Double {
        return width * height
    }
    
    var perimeter: Double {
        get {
            return 2 * (width + height)
        }
    }
    
    // ํ”„๋กœํผํ‹ฐ ์˜ต์ €๋ฒ„
    var name: String = "์‚ฌ๊ฐํ˜•" {
        willSet {
            print("์ด๋ฆ„์ด \(newValue)๋กœ ๋ณ€๊ฒฝ๋  ์˜ˆ์ •")
        }
        didSet {
            print("์ด๋ฆ„์ด \(oldValue)์—์„œ \(name)๋กœ ๋ณ€๊ฒฝ๋จ")
        }
    }
    
    // ํƒ€์ž… ํ”„๋กœํผํ‹ฐ (static)
    static var description = "4๊ฐœ์˜ ๋ณ€์„ ๊ฐ€์ง„ ๋„ํ˜•"
    
    // ํƒ€์ž… ๋ฉ”์„œ๋“œ
    static func printDescription() {
        print(description)
    }
}

var rect = Rectangle(width: 10, height: 5)
print(rect.area)  // 50.0
rect.name = "์ง์‚ฌ๊ฐํ˜•"  // ์˜ต์ €๋ฒ„ ๋™์ž‘

๐Ÿ”Œ ํ”„๋กœํ† ์ฝœ๊ณผ ์ต์Šคํ…์…˜

ํ”„๋กœํ† ์ฝœ์€ ๋‹ค๋ฅธ ์–ธ์–ด์˜ ์ธํ„ฐํŽ˜์ด์Šค์™€ ๋น„์Šทํ•œ๋ฐ, Swift์—์„œ๋Š” ํ›จ์”ฌ ๊ฐ•๋ ฅํ•ด! ํ”„๋กœํ† ์ฝœ ์ง€ํ–ฅ ํ”„๋กœ๊ทธ๋ž˜๋ฐ(POP)์€ Swift์˜ ํ•ต์‹ฌ ํŒจ๋Ÿฌ๋‹ค์ž„ ์ค‘ ํ•˜๋‚˜์•ผ.

ํ”„๋กœํ† ์ฝœ ์ •์˜์™€ ์ฑ„ํƒ

// ํ”„๋กœํ† ์ฝœ ์ •์˜
protocol Drawable {
    var color: String { get set }
    func draw()
}

protocol Resizable {
    mutating func resize(by factor: Double)
}

// ํ”„๋กœํ† ์ฝœ ์ฑ„ํƒ
struct Circle: Drawable, Resizable {
    var color: String
    var radius: Double
    
    func draw() {
        print("\(color) ์ƒ‰์ƒ์˜ ์›์„ ๊ทธ๋ฆฝ๋‹ˆ๋‹ค")
    }
    
    mutating func resize(by factor: Double) {
        radius *= factor
    }
}

// ํ”„๋กœํ† ์ฝœ์„ ํƒ€์ž…์œผ๋กœ ์‚ฌ์šฉ
func drawShape(_ shape: Drawable) {
    shape.draw()
}

let circle = Circle(color: "๋นจ๊ฐ•", radius: 5.0)
drawShape(circle)  // "๋นจ๊ฐ• ์ƒ‰์ƒ์˜ ์›์„ ๊ทธ๋ฆฝ๋‹ˆ๋‹ค"

ํ”„๋กœํ† ์ฝœ ์ต์Šคํ…์…˜ - ๊ธฐ๋ณธ ๊ตฌํ˜„ ์ œ๊ณต โœจ

protocol Identifiable {
    var id: String { get }
    func displayID()
}

// ํ”„๋กœํ† ์ฝœ์— ๊ธฐ๋ณธ ๊ตฌํ˜„ ์ œ๊ณต
extension Identifiable {
    func displayID() {
        print("ID: \(id)")
    }
}

struct User: Identifiable {
    var id: String
    var name: String
    // displayID() ๊ตฌํ˜„ ์•ˆ ํ•ด๋„ ๋จ!
}

let user = User(id: "user123", name: "๊น€์ฒ ์ˆ˜")
user.displayID()  // "ID: user123"

์ต์Šคํ…์…˜์œผ๋กœ ๊ธฐ๋Šฅ ํ™•์žฅ ๐Ÿš€

// ๊ธฐ์กด ํƒ€์ž…์— ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
extension Int {
    func squared() -> Int {
        return self * self
    }
    
    var isEven: Bool {
        return self % 2 == 0
    }
}

print(5.squared())  // 25
print(4.isEven)  // true

// String ํ™•์žฅ
extension String {
    func removeWhitespace() -> String {
        return self.replacingOccurrences(of: " ", with: "")
    }
    
    var length: Int {
        return self.count
    }
}

let text = "Hello World"
print(text.removeWhitespace())  // "HelloWorld"
print(text.length)  // 11

โœ… ํ”„๋กœํ† ์ฝœ ์ง€ํ–ฅ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์˜ ์žฅ์ 

โ€ข ๋‹ค์ค‘ ์ƒ์† ๋ฌธ์ œ ํ•ด๊ฒฐ (ํ”„๋กœํ† ์ฝœ์€ ์—ฌ๋Ÿฌ ๊ฐœ ์ฑ„ํƒ ๊ฐ€๋Šฅ)
โ€ข ๊ฐ’ ํƒ€์ž…(๊ตฌ์กฐ์ฒด)์—์„œ๋„ ๋‹คํ˜•์„ฑ ๊ตฌํ˜„ ๊ฐ€๋Šฅ
โ€ข ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์‰ฌ์šด ์ฝ”๋“œ ์ž‘์„ฑ ๊ฐ€๋Šฅ
โ€ข ์ฝ”๋“œ ์žฌ์‚ฌ์šฉ์„ฑ ๊ทน๋Œ€ํ™”

โšก ์—๋Ÿฌ ์ฒ˜๋ฆฌ์™€ ์˜ต์…”๋„ ์ฒด์ด๋‹

Swift์˜ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋Š” ๋ช…์‹œ์ ์ด๊ณ  ์•ˆ์ „ํ•ด. ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์ฝ”๋“œ๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ํ‘œ์‹œํ•˜๊ณ  ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ด.

// ์—๋Ÿฌ ํƒ€์ž… ์ •์˜
enum NetworkError: Error {
    case badURL
    case requestFailed
    case unknown
}

// throwing ํ•จ์ˆ˜
func fetchData(from urlString: String) throws -> String {
    guard !urlString.isEmpty else {
        throw NetworkError.badURL
    }
    
    // ๋„คํŠธ์›Œํฌ ์š”์ฒญ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
    let success = Bool.random()
    
    if success {
        return "๋ฐ์ดํ„ฐ ์ˆ˜์‹  ์„ฑ๊ณต"
    } else {
        throw NetworkError.requestFailed
    }
}

// do-catch๋กœ ์—๋Ÿฌ ์ฒ˜๋ฆฌ
do {
    let data = try fetchData(from: "https://api.example.com")
    print(data)
} catch NetworkError.badURL {
    print("์ž˜๋ชป๋œ URL์ž…๋‹ˆ๋‹ค")
} catch NetworkError.requestFailed {
    print("์š”์ฒญ์ด ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค")
} catch {
    print("์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ: \(error)")
}

// try? - ์˜ต์…”๋„๋กœ ๋ณ€ํ™˜
let result = try? fetchData(from: "https://api.example.com")
// ์„ฑ๊ณตํ•˜๋ฉด String?, ์‹คํŒจํ•˜๋ฉด nil

// try! - ์—๋Ÿฌ๊ฐ€ ์ ˆ๋Œ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค๊ณ  ํ™•์‹ ํ•  ๋•Œ (์œ„ํ—˜!)
// let data = try! fetchData(from: "https://api.example.com")
SwiftUI ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ 1. ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ 2. View ๋””์ž์ธ 3. ๋กœ์ง ๊ตฌํ˜„ 4. ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ 5. ํ…Œ์ŠคํŠธ & ๋””๋ฒ„๊น… 6. ๋ฐฐํฌ

๐ŸŽจ SwiftUI ์‹œ์ž‘ํ•˜๊ธฐ

์ž ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ SwiftUI๋กœ ๋„˜์–ด๊ฐ€๋ณด์ž! SwiftUI๋Š” 2019๋…„์— ๋‚˜์˜จ ์• ํ”Œ์˜ ์ตœ์‹  UI ํ”„๋ ˆ์ž„์›Œํฌ์•ผ. ๊ธฐ์กด UIKit๊ณผ๋Š” ์™„์ „ํžˆ ๋‹ค๋ฅธ ์„ ์–ธํ˜•(Declarative) ๋ฐฉ์‹์œผ๋กœ UI๋ฅผ ๋งŒ๋“ค์–ด ใ…‹ใ…‹ใ…‹

๐ŸŒŸ SwiftUI์˜ ํ˜๋ช…์ ์ธ ํŠน์ง•

์„ ์–ธํ˜• ๋ฌธ๋ฒ•: "์–ด๋–ป๊ฒŒ(How)" ๋Œ€์‹  "๋ฌด์—‡์„(What)" ํ‘œํ˜„ํ•ด. UI๊ฐ€ ์–ด๋–ป๊ฒŒ ๋ณด์—ฌ์•ผ ํ•˜๋Š”์ง€๋งŒ ์„ ์–ธํ•˜๋ฉด SwiftUI๊ฐ€ ์•Œ์•„์„œ ๋ Œ๋”๋งํ•ด์ค˜.

์‹ค์‹œ๊ฐ„ ํ”„๋ฆฌ๋ทฐ: ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•˜๋ฉด ์ฆ‰์‹œ ์บ”๋ฒ„์Šค์—์„œ ๊ฒฐ๊ณผ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์–ด. ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ์‹คํ–‰ ์•ˆ ํ•ด๋„ ๋ผ!

ํฌ๋กœ์Šค ํ”Œ๋žซํผ: ๊ฐ™์€ ์ฝ”๋“œ๋กœ iOS, iPadOS, macOS, watchOS, tvOS ์•ฑ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด.

๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ: ๋ฐ์ดํ„ฐ์™€ UI๊ฐ€ ์ž๋™์œผ๋กœ ๋™๊ธฐํ™”๋ผ. ์ƒํƒœ ๊ด€๋ฆฌ๊ฐ€ ์—„์ฒญ ์‰ฌ์›Œ์กŒ์ง€.

์ฒซ ๋ฒˆ์งธ SwiftUI ๋ทฐ ๋งŒ๋“ค๊ธฐ ๐ŸŽ‰

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello, SwiftUI!")
            .font(.largeTitle)
            .foregroundColor(.blue)
            .padding()
    }
}

// ํ”„๋ฆฌ๋ทฐ
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

์ด๊ฒŒ ๋์ด์•ผ! UIKit์—์„œ๋Š” UILabel ๋งŒ๋“ค๊ณ , ์ œ์•ฝ์กฐ๊ฑด ์„ค์ •ํ•˜๊ณ ... ์ฝ”๋“œ๊ฐ€ ์—„์ฒญ ๊ธธ์—ˆ๋Š”๋ฐ, SwiftUI๋Š” ์ด๋ ‡๊ฒŒ ๊ฐ„๋‹จํ•ด ใ…Žใ…Ž

๊ธฐ๋ณธ ๋ทฐ ์ปดํฌ๋„ŒํŠธ๋“ค ๐Ÿ“ฑ

struct BasicViewsExample: View {
    var body: some View {
        VStack(spacing: 20) {
            // ํ…์ŠคํŠธ
            Text("์•ˆ๋…•ํ•˜์„ธ์š”!")
                .font(.title)
                .fontWeight(.bold)
            
            // ์ด๋ฏธ์ง€
            Image(systemName: "star.fill")
                .resizable()
                .frame(width: 50, height: 50)
                .foregroundColor(.yellow)
            
            // ๋ฒ„ํŠผ
            Button(action: {
                print("๋ฒ„ํŠผ ํด๋ฆญ!")
            }) {
                Text("ํด๋ฆญํ•˜์„ธ์š”")
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(10)
            }
            
            // ํ…์ŠคํŠธ ํ•„๋“œ
            TextField("์ด๋ฆ„์„ ์ž…๋ ฅํ•˜์„ธ์š”", text: .constant(""))
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding(.horizontal)
            
            // ํ† ๊ธ€
            Toggle("์•Œ๋ฆผ ๋ฐ›๊ธฐ", isOn: .constant(true))
                .padding(.horizontal)
        }
        .padding()
    }
}

๋ ˆ์ด์•„์›ƒ ์ปจํ…Œ์ด๋„ˆ ๐Ÿ“

SwiftUI๋Š” VStack, HStack, ZStack์œผ๋กœ ๋ ˆ์ด์•„์›ƒ์„ ๊ตฌ์„ฑํ•ด. ์ง„์งœ ์ง๊ด€์ ์ด์•ผ!

struct LayoutExample: View {
    var body: some View {
        VStack {  // ์„ธ๋กœ ์Šคํƒ
            Text("์œ„")
            
            HStack {  // ๊ฐ€๋กœ ์Šคํƒ
                Text("์™ผ์ชฝ")
                Spacer()  // ๊ณต๊ฐ„ ์ฑ„์šฐ๊ธฐ
                Text("์˜ค๋ฅธ์ชฝ")
            }
            
            ZStack {  // ๊ฒน์น˜๊ธฐ ์Šคํƒ
                Rectangle()
                    .fill(Color.blue)
                    .frame(width: 100, height: 100)
                
                Text("๊ฒน์นจ")
                    .foregroundColor(.white)
            }
        }
        .padding()
    }
}

๐Ÿ’ก Modifier ์ˆœ์„œ๊ฐ€ ์ค‘์š”ํ•ด!

SwiftUI์˜ modifier๋Š” ์ˆœ์„œ์— ๋”ฐ๋ผ ๊ฒฐ๊ณผ๊ฐ€ ๋‹ฌ๋ผ์ ธ. .background(Color.blue).padding()๊ณผ .padding().background(Color.blue)๋Š” ์™„์ „ํžˆ ๋‹ค๋ฅธ ๊ฒฐ๊ณผ๋ฅผ ๋งŒ๋“ค์–ด. ์ฒซ ๋ฒˆ์งธ๋Š” ๋ฐฐ๊ฒฝ ๋จผ์ € ์ ์šฉํ•˜๊ณ  ํŒจ๋”ฉ์„ ์ฃผ๊ณ , ๋‘ ๋ฒˆ์งธ๋Š” ํŒจ๋”ฉ ๋จผ์ € ์ฃผ๊ณ  ๋ฐฐ๊ฒฝ์„ ์ ์šฉํ•˜๊ฑฐ๋“ !

๐Ÿ”„ ์ƒํƒœ ๊ด€๋ฆฌ - State, Binding, ObservableObject

SwiftUI์˜ ์ง„์งœ ํŒŒ์›Œ๋Š” ์ƒํƒœ ๊ด€๋ฆฌ์— ์žˆ์–ด! ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€ํ•˜๋ฉด UI๊ฐ€ ์ž๋™์œผ๋กœ ์—…๋ฐ์ดํŠธ๋˜๋Š” ๋งˆ๋ฒ• ๊ฐ™์€ ์ผ์ด ์ผ์–ด๋‚˜์ง€ ใ…‹ใ…‹ใ…‹

@State - ๋ทฐ ๋‚ด๋ถ€ ์ƒํƒœ

struct CounterView: View {
    @State private var count = 0
    
    var body: some View {
        VStack(spacing: 20) {
            Text("์นด์šดํŠธ: \(count)")
                .font(.largeTitle)
            
            HStack(spacing: 15) {
                Button("-") {
                    count -= 1
                }
                .buttonStyle(.bordered)
                
                Button("+") {
                    count += 1
                }
                .buttonStyle(.bordered)
            }
        }
        .padding()
    }
}

@State๋ฅผ ๋ถ™์ด๋ฉด SwiftUI๊ฐ€ ์ด ๋ณ€์ˆ˜๋ฅผ ๊ด€์ฐฐํ•˜๋‹ค๊ฐ€ ๊ฐ’์ด ๋ฐ”๋€Œ๋ฉด ์ž๋™์œผ๋กœ ๋ทฐ๋ฅผ ๋‹ค์‹œ ๊ทธ๋ ค์ค˜. ๊ฐœ๋ฐœ์ž๋Š” UI ์—…๋ฐ์ดํŠธ ์ฝ”๋“œ๋ฅผ ์ „ํ˜€ ์•ˆ ์จ๋„ ๋ผ!

@Binding - ๋ถ€๋ชจ-์ž์‹ ๋ทฐ ๊ฐ„ ๋ฐ์ดํ„ฐ ๊ณต์œ 

struct ParentView: View {
    @State private var isOn = false
    
    var body: some View {
        VStack {
            Text(isOn ? "์ผœ์ง" : "๊บผ์ง")
                .font(.title)
            
            ChildView(isOn: $isOn)  // $ ๋ถ™์—ฌ์„œ ๋ฐ”์ธ๋”ฉ ์ „๋‹ฌ
        }
    }
}

struct ChildView: View {
    @Binding var isOn: Bool  // ๋ถ€๋ชจ์˜ ์ƒํƒœ๋ฅผ ๋ฐ›์Œ
    
    var body: some View {
        Toggle("์Šค์œ„์น˜", isOn: $isOn)
            .padding()
    }
}

@ObservableObject - ๋ณต์žกํ•œ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ

// ๋ทฐ๋ชจ๋ธ ์ •์˜
class UserViewModel: ObservableObject {
    @Published var username = ""
    @Published var email = ""
    @Published var isLoggedIn = false
    
    func login() {
        // ๋กœ๊ทธ์ธ ๋กœ์ง
        isLoggedIn = true
    }
    
    func logout() {
        username = ""
        email = ""
        isLoggedIn = false
    }
}

// ๋ทฐ์—์„œ ์‚ฌ์šฉ
struct LoginView: View {
    @StateObject private var viewModel = UserViewModel()
    
    var body: some View {
        VStack(spacing: 20) {
            if viewModel.isLoggedIn {
                Text("ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค, \(viewModel.username)๋‹˜!")
                Button("๋กœ๊ทธ์•„์›ƒ") {
                    viewModel.logout()
                }
            } else {
                TextField("์‚ฌ์šฉ์ž๋ช…", text: $viewModel.username)
                    .textFieldStyle(.roundedBorder)
                
                TextField("์ด๋ฉ”์ผ", text: $viewModel.email)
                    .textFieldStyle(.roundedBorder)
                
                Button("๋กœ๊ทธ์ธ") {
                    viewModel.login()
                }
                .buttonStyle(.borderedProminent)
            }
        }
        .padding()
    }
}

๐ŸŽฏ ์ƒํƒœ ๊ด€๋ฆฌ ํ”„๋กœํผํ‹ฐ ๋ž˜ํผ ์ •๋ฆฌ

@State: ๋ทฐ ๋‚ด๋ถ€์˜ ๊ฐ„๋‹จํ•œ ์ƒํƒœ (Int, String, Bool ๋“ฑ)

@Binding: ๋ถ€๋ชจ ๋ทฐ์˜ @State๋ฅผ ์ž์‹ ๋ทฐ์—์„œ ์ˆ˜์ •ํ•  ๋•Œ

@StateObject: ๋ทฐ๊ฐ€ ์†Œ์œ ํ•˜๋Š” ObservableObject (๋ทฐ ์ƒ๋ช…์ฃผ๊ธฐ์™€ ํ•จ๊ป˜)

@ObservedObject: ์™ธ๋ถ€์—์„œ ์ƒ์„ฑ๋œ ObservableObject๋ฅผ ๊ด€์ฐฐ

@EnvironmentObject: ์•ฑ ์ „์ฒด์—์„œ ๊ณต์œ ํ•˜๋Š” ๋ฐ์ดํ„ฐ

๐Ÿ“‹ ๋ฆฌ์ŠคํŠธ์™€ ๋„ค๋น„๊ฒŒ์ด์…˜

์‹ค์ œ ์•ฑ์—์„œ ๊ฐ€์žฅ ๋งŽ์ด ์“ฐ๋Š” ํŒจํ„ด์ด ๋ฆฌ์ŠคํŠธ์™€ ๋„ค๋น„๊ฒŒ์ด์…˜์ด์•ผ. SwiftUI์—์„œ๋Š” ์ด๊ฒƒ๋„ ์—„์ฒญ ์‰ฌ์›Œ!

List ๋ทฐ ๋งŒ๋“ค๊ธฐ

struct TodoItem: Identifiable {
    let id = UUID()
    var title: String
    var isCompleted: Bool
}

struct TodoListView: View {
    @State private var todos = [
        TodoItem(title: "SwiftUI ๊ณต๋ถ€ํ•˜๊ธฐ", isCompleted: false),
        TodoItem(title: "์•ฑ ๋งŒ๋“ค๊ธฐ", isCompleted: false),
        TodoItem(title: "์•ฑ์Šคํ† ์–ด ์ถœ์‹œ", isCompleted: false)
    ]
    
    var body: some View {
        NavigationView {
            List {
                ForEach(todos) { todo in
                    HStack {
                        Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle")
                            .foregroundColor(todo.isCompleted ? .green : .gray)
                        
                        Text(todo.title)
                            .strikethrough(todo.isCompleted)
                    }
                    .onTapGesture {
                        if let index = todos.firstIndex(where: { $0.id == todo.id }) {
                            todos[index].isCompleted.toggle()
                        }
                    }
                }
                .onDelete { indexSet in
                    todos.remove(atOffsets: indexSet)
                }
                .onMove { source, destination in
                    todos.move(fromOffsets: source, toOffset: destination)
                }
            }
            .navigationTitle("ํ•  ์ผ ๋ชฉ๋ก")
            .toolbar {
                EditButton()
            }
        }
    }
}

NavigationLink๋กœ ํ™”๋ฉด ์ „ํ™˜

struct MainView: View {
    var body: some View {
        NavigationView {
            List {
                NavigationLink("ํ”„๋กœํ•„", destination: ProfileView())
                NavigationLink("์„ค์ •", destination: SettingsView())
                NavigationLink("์ •๋ณด", destination: AboutView())
            }
            .navigationTitle("๋ฉ”๋‰ด")
        }
    }
}

struct ProfileView: View {
    var body: some View {
        Text("ํ”„๋กœํ•„ ํ™”๋ฉด")
            .navigationTitle("ํ”„๋กœํ•„")
            .navigationBarTitleDisplayMode(.inline)
    }
}

โœ… List ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ

โ€ข Identifiable ํ”„๋กœํ† ์ฝœ ์ฑ„ํƒํ•ด์„œ ๊ฐ ํ•ญ๋ชฉ์„ ๊ณ ์œ ํ•˜๊ฒŒ ์‹๋ณ„
โ€ข ๋ณต์žกํ•œ ๊ณ„์‚ฐ์€ computed property๋‚˜ ๋ฉ”์„œ๋“œ๋กœ ๋ถ„๋ฆฌ
โ€ข LazyVStack/LazyHStack ์‚ฌ์šฉํ•˜๋ฉด ํ•„์š”ํ•  ๋•Œ๋งŒ ๋ทฐ ์ƒ์„ฑ
โ€ข ์ด๋ฏธ์ง€๋Š” ๋น„๋™๊ธฐ๋กœ ๋กœ๋“œํ•˜๊ณ  ์บ์‹ฑ ํ™œ์šฉ

๐ŸŽญ ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ์ œ์Šค์ฒ˜

SwiftUI์˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์€ ์ง„์งœ ์‰ฝ๊ณ  ์ž์—ฐ์Šค๋Ÿฌ์›Œ. ๋ช‡ ์ค„ ์ฝ”๋“œ๋กœ ๋ฉ‹์ง„ ํšจ๊ณผ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด!

๊ธฐ๋ณธ ์• ๋‹ˆ๋ฉ”์ด์…˜

struct AnimationExample: View {
    @State private var isExpanded = false
    
    var body: some View {
        VStack {
            RoundedRectangle(cornerRadius: 20)
                .fill(Color.blue)
                .frame(width: isExpanded ? 300 : 100, 
                       height: isExpanded ? 300 : 100)
                .animation(.spring(response: 0.5, dampingFraction: 0.6), value: isExpanded)
            
            Button(isExpanded ? "์ถ•์†Œ" : "ํ™•๋Œ€") {
                isExpanded.toggle()
            }
            .padding()
        }
    }
}

๋‹ค์–‘ํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํƒ€์ž…

struct AnimationTypesView: View {
    @State private var offset: CGFloat = 0
    
    var body: some View {
        VStack(spacing: 30) {
            Circle()
                .fill(Color.red)
                .frame(width: 50, height: 50)
                .offset(x: offset)
                .animation(.linear(duration: 1), value: offset)
            
            Circle()
                .fill(Color.green)
                .frame(width: 50, height: 50)
                .offset(x: offset)
                .animation(.easeInOut(duration: 1), value: offset)
            
            Circle()
                .fill(Color.blue)
                .frame(width: 50, height: 50)
                .offset(x: offset)
                .animation(.spring(), value: offset)
            
            Button("์• ๋‹ˆ๋ฉ”์ด์…˜ ์‹œ์ž‘") {
                offset = offset == 0 ? 150 : 0
            }
        }
    }
}

์ œ์Šค์ฒ˜ ์ธ์‹ ๐Ÿ‘†

struct GestureExample: View {
    @State private var offset = CGSize.zero
    @State private var scale: CGFloat = 1.0
    @State private var rotation: Angle = .zero
    
    var body: some View {
        VStack(spacing: 50) {
            // ๋“œ๋ž˜๊ทธ ์ œ์Šค์ฒ˜
            Circle()
                .fill(Color.blue)
                .frame(width: 100, height: 100)
                .offset(offset)
                .gesture(
                    DragGesture()
                        .onChanged { value in
                            offset = value.translation
                        }
                        .onEnded { _ in
                            withAnimation(.spring()) {
                                offset = .zero
                            }
                        }
                )
            
            // ํ™•๋Œ€/์ถ•์†Œ ์ œ์Šค์ฒ˜
            Rectangle()
                .fill(Color.green)
                .frame(width: 100, height: 100)
                .scaleEffect(scale)
                .gesture(
                    MagnificationGesture()
                        .onChanged { value in
                            scale = value
                        }
                        .onEnded { _ in
                            withAnimation {
                                scale = 1.0
                            }
                        }
                )
            
            // ํšŒ์ „ ์ œ์Šค์ฒ˜
            RoundedRectangle(cornerRadius: 20)
                .fill(Color.purple)
                .frame(width: 100, height: 100)
                .rotationEffect(rotation)
                .gesture(
                    RotationGesture()
                        .onChanged { value in
                            rotation = value
                        }
                        .onEnded { _ in
                            withAnimation {
                                rotation = .zero
                            }
                        }
                )
        }
    }
}

๐ŸŒ ๋„คํŠธ์›Œํ‚น๊ณผ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ

์‹ค์ œ ์•ฑ์€ ์„œ๋ฒ„์™€ ํ†ต์‹ ํ•ด์•ผ ํ•˜์ž–์•„? Swift์˜ URLSession๊ณผ Codable์„ ํ™œ์šฉํ•˜๋ฉด API ํ†ต์‹ ์ด ์—„์ฒญ ์‰ฌ์›Œ!

JSON ๋ฐ์ดํ„ฐ ๋ชจ๋ธ๋ง

// JSON ์‘๋‹ต ๊ตฌ์กฐ
struct User: Codable, Identifiable {
    let id: Int
    let name: String
    let email: String
    let username: String
}

struct Post: Codable, Identifiable {
    let id: Int
    let userId: Int
    let title: String
    let body: String
}

// API ์„œ๋น„์Šค ํด๋ž˜์Šค
class APIService {
    static let shared = APIService()
    private let baseURL = "https://jsonplaceholder.typicode.com"
    
    func fetchUsers() async throws -> [User] {
        guard let url = URL(string: "\(baseURL)/users") else {
            throw URLError(.badURL)
        }
        
        let (data, _) = try await URLSession.shared.data(from: url)
        let users = try JSONDecoder().decode([User].self, from: data)
        return users
    }
    
    func fetchPosts() async throws -> [Post] {
        guard let url = URL(string: "\(baseURL)/posts") else {
            throw URLError(.badURL)
        }
        
        let (data, _) = try await URLSession.shared.data(from: url)
        let posts = try JSONDecoder().decode([Post].self, from: data)
        return posts
    }
}

๋ทฐ๋ชจ๋ธ์—์„œ API ํ˜ธ์ถœ

class PostsViewModel: ObservableObject {
    @Published var posts: [Post] = []
    @Published var isLoading = false
    @Published var errorMessage: String?
    
    func loadPosts() {
        isLoading = true
        errorMessage = nil
        
        Task {
            do {
                let fetchedPosts = try await APIService.shared.fetchPosts()
                
                await MainActor.run {
                    self.posts = fetchedPosts
                    self.isLoading = false
                }
            } catch {
                await MainActor.run {
                    self.errorMessage = "๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค: \(error.localizedDescription)"
                    self.isLoading = false
                }
            }
        }
    }
}

// ๋ทฐ์—์„œ ์‚ฌ์šฉ
struct PostsView: View {
    @StateObject private var viewModel = PostsViewModel()
    
    var body: some View {
        NavigationView {
            Group {
                if viewModel.isLoading {
                    ProgressView("๋กœ๋”ฉ ์ค‘...")
                } else if let error = viewModel.errorMessage {
                    VStack {
                        Text(error)
                            .foregroundColor(.red)
                        Button("๋‹ค์‹œ ์‹œ๋„") {
                            viewModel.loadPosts()
                        }
                    }
                } else {
                    List(viewModel.posts) { post in
                        VStack(alignment: .leading, spacing: 8) {
                            Text(post.title)
                                .font(.headline)
                            Text(post.body)
                                .font(.subheadline)
                                .foregroundColor(.secondary)
                                .lineLimit(2)
                        }
                        .padding(.vertical, 4)
                    }
                }
            }
            .navigationTitle("๊ฒŒ์‹œ๊ธ€")
            .onAppear {
                viewModel.loadPosts()
            }
        }
    }
}

๐Ÿ’ก async/await์˜ ์žฅ์ 

Swift 5.5๋ถ€ํ„ฐ ๋„์ž…๋œ async/await๋Š” ๋น„๋™๊ธฐ ์ฝ”๋“œ๋ฅผ ๋™๊ธฐ ์ฝ”๋“œ์ฒ˜๋Ÿผ ์ฝ๊ธฐ ์‰ฝ๊ฒŒ ๋งŒ๋“ค์–ด์ค˜. ์ฝœ๋ฐฑ ์ง€์˜ฅ์—์„œ ๋ฒ—์–ด๋‚  ์ˆ˜ ์žˆ์ง€ ใ…‹ใ…‹ใ…‹ MainActor.run์„ ์‚ฌ์šฉํ•˜๋ฉด UI ์—…๋ฐ์ดํŠธ๋ฅผ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ์–ด!

๐Ÿ’พ ๋ฐ์ดํ„ฐ ์˜์†์„ฑ - UserDefaults, CoreData, SwiftData

์•ฑ์„ ์ข…๋ฃŒํ•ด๋„ ๋ฐ์ดํ„ฐ๊ฐ€ ๋‚จ์•„์žˆ์–ด์•ผ ํ•˜์ž–์•„? ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” ์—ฌ๋Ÿฌ ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž!

UserDefaults - ๊ฐ„๋‹จํ•œ ์„ค์ •๊ฐ’ ์ €์žฅ

class SettingsManager: ObservableObject {
    @Published var isDarkMode: Bool {
        didSet {
            UserDefaults.standard.set(isDarkMode, forKey: "isDarkMode")
        }
    }
    
    @Published var fontSize: Double {
        didSet {
            UserDefaults.standard.set(fontSize, forKey: "fontSize")
        }
    }
    
    init() {
        self.isDarkMode = UserDefaults.standard.bool(forKey: "isDarkMode")
        self.fontSize = UserDefaults.standard.double(forKey: "fontSize")
        
        if fontSize == 0 {
            fontSize = 16.0  // ๊ธฐ๋ณธ๊ฐ’
        }
    }
}

struct SettingsView: View {
    @StateObject private var settings = SettingsManager()
    
    var body: some View {
        Form {
            Section("ํ™”๋ฉด ์„ค์ •") {
                Toggle("๋‹คํฌ ๋ชจ๋“œ", isOn: $settings.isDarkMode)
                
                HStack {
                    Text("๊ธ€์ž ํฌ๊ธฐ")
                    Slider(value: $settings.fontSize, in: 12...24, step: 1)
                    Text("\(Int(settings.fontSize))")
                }
            }
        }
        .navigationTitle("์„ค์ •")
    }
}

CoreData - ๋ณต์žกํ•œ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ

// CoreData ์Šคํƒ ์„ค์ •
class PersistenceController {
    static let shared = PersistenceController()
    
    let container: NSPersistentContainer
    
    init() {
        container = NSPersistentContainer(name: "MyApp")
        container.loadPersistentStores { description, error in
            if let error = error {
                fatalError("CoreData ๋กœ๋“œ ์‹คํŒจ: \(error)")
            }
        }
    }
    
    func save() {
        let context = container.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                print("์ €์žฅ ์‹คํŒจ: \(error)")
            }
        }
    }
}

// ๋ทฐ์—์„œ CoreData ์‚ฌ์šฉ
struct NoteListView: View {
    @Environment(\.managedObjectContext) private var viewContext
    
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Note.timestamp, ascending: false)],
        animation: .default)
    private var notes: FetchedResults<Note>
    
    var body: some View {
        NavigationView {
            List {
                ForEach(notes) { note in
                    VStack(alignment: .leading) {
                        Text(note.title ?? "์ œ๋ชฉ ์—†์Œ")
                            .font(.headline)
                        Text(note.content ?? "")
                            .font(.subheadline)
                            .lineLimit(2)
                    }
                }
                .onDelete(perform: deleteNotes)
            }
            .navigationTitle("๋…ธํŠธ")
            .toolbar {
                Button(action: addNote) {
                    Label("์ถ”๊ฐ€", systemImage: "plus")
                }
            }
        }
    }
    
    private func addNote() {
        withAnimation {
            let newNote = Note(context: viewContext)
            newNote.id = UUID()
            newNote.title = "์ƒˆ ๋…ธํŠธ"
            newNote.content = ""
            newNote.timestamp = Date()
            
            PersistenceController.shared.save()
        }
    }
    
    private func deleteNotes(offsets: IndexSet) {
        withAnimation {
            offsets.map { notes[$0] }.forEach(viewContext.delete)
            PersistenceController.shared.save()
        }
    }
}

SwiftData - iOS 17์˜ ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ ํ”„๋ ˆ์ž„์›Œํฌ ๐Ÿ†•

import SwiftData

// ๋ชจ๋ธ ์ •์˜
@Model
class Task {
    var id: UUID
    var title: String
    var isCompleted: Bool
    var createdAt: Date
    
    init(title: String) {
        self.id = UUID()
        self.title = title
        self.isCompleted = false
        self.createdAt = Date()
    }
}

// ์•ฑ์—์„œ SwiftData ์„ค์ •
@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: Task.self)
    }
}

// ๋ทฐ์—์„œ ์‚ฌ์šฉ
struct TaskListView: View {
    @Environment(\.modelContext) private var modelContext
    @Query(sort: \Task.createdAt, order: .reverse) private var tasks: [Task]
    
    var body: some View {
        List {
            ForEach(tasks) { task in
                HStack {
                    Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
                        .onTapGesture {
                            task.isCompleted.toggle()
                        }
                    Text(task.title)
                }
            }
            .onDelete { indexSet in
                for index in indexSet {
                    modelContext.delete(tasks[index])
                }
            }
        }
    }
}

๐ŸŽฏ ๋ฐ์ดํ„ฐ ์ €์žฅ ๋ฐฉ๋ฒ• ์„ ํƒ ๊ฐ€์ด๋“œ

UserDefaults: ๊ฐ„๋‹จํ•œ ์„ค์ •๊ฐ’, ์ž‘์€ ๋ฐ์ดํ„ฐ (๋‹คํฌ๋ชจ๋“œ, ์–ธ์–ด ์„ค์ • ๋“ฑ)

FileManager: ์ด๋ฏธ์ง€, ๋ฌธ์„œ ํŒŒ์ผ ๋“ฑ ํฐ ํŒŒ์ผ

CoreData: ๋ณต์žกํ•œ ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ, iOS 3.0๋ถ€ํ„ฐ ์‚ฌ์šฉ ๊ฐ€๋Šฅ

SwiftData: ํ˜„๋Œ€์ ์ด๊ณ  ๊ฐ„๊ฒฐํ•œ ๋ฌธ๋ฒ•, iOS 17 ์ด์ƒ

Realm: ์„œ๋“œํŒŒํ‹ฐ DB, ๋น ๋ฅธ ์„ฑ๋Šฅ๊ณผ ์‰ฌ์šด ์‚ฌ์šฉ

iOS ์•ฑ ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด MVC Model-View-Controller ์ „ํ†ต์ ์ธ ํŒจํ„ด ๊ฐ„๋‹จํ•œ ์•ฑ์— ์ ํ•ฉ UIKit ๊ธฐ๋ณธ ํŒจํ„ด MVVM Model-View-ViewModel SwiftUI ๊ถŒ์žฅ ํŒจํ„ด ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ ํ…Œ์ŠคํŠธ ์šฉ์ด TCA Composable Architecture ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„ ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ์ƒํƒœ ๋Œ€๊ทœ๋ชจ ์•ฑ์— ์ ํ•ฉ VIPER View-Interactor-Presenter ๋ช…ํ™•ํ•œ ์ฑ…์ž„ ๋ถ„๋ฆฌ ๋ณต์žก๋„ ๋†’์Œ ๋Œ€๊ธฐ์—… ํ”„๋กœ์ ํŠธ Clean Architecture ๊ณ„์ธตํ˜• ๊ตฌ์กฐ ์˜์กด์„ฑ ์—ญ์ „ ์œ ์ง€๋ณด์ˆ˜ ์šฉ์ด ํ™•์žฅ์„ฑ ์šฐ์ˆ˜

๐Ÿ›๏ธ ์•ฑ ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด

์ฝ”๋“œ๊ฐ€ ๋งŽ์•„์ง€๋ฉด ๊ตฌ์กฐ๊ฐ€ ์ค‘์š”ํ•ด์ ธ. ์ข‹์€ ์•„ํ‚คํ…์ฒ˜๋Š” ์œ ์ง€๋ณด์ˆ˜๋ฅผ ์‰ฝ๊ฒŒ ๋งŒ๋“ค๊ณ , ๋ฒ„๊ทธ๋ฅผ ์ค„์—ฌ์ฃผ๊ณ , ํŒ€ ํ˜‘์—…์„ ์›ํ™œํ•˜๊ฒŒ ํ•ด์ค˜!

MVVM ํŒจํ„ด - SwiftUI์˜ ๋ฒ ์ŠคํŠธ ํ”„๋ Œ๋“œ ๐Ÿค

MVVM(Model-View-ViewModel)์€ SwiftUI์™€ ์ฐฐ๋–ก๊ถํ•ฉ์ด์•ผ. ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์ด ์ž๋™์œผ๋กœ ๋˜๋‹ˆ๊นŒ ์ฝ”๋“œ๊ฐ€ ์—„์ฒญ ๊น”๋”ํ•ด์ง€์ง€.

// Model - ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ
struct Product: Identifiable {
    let id: UUID
    var name: String
    var price: Double
    var quantity: Int
}

// ViewModel - ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง
class ShoppingCartViewModel: ObservableObject {
    @Published var products: [Product] = []
    
    var totalPrice: Double {
        products.reduce(0) { $0 + ($1.price * Double($1.quantity)) }
    }
    
    var itemCount: Int {
        products.reduce(0) { $0 + $1.quantity }
    }
    
    func addProduct(_ product: Product) {
        if let index = products.firstIndex(where: { $0.id == product.id }) {
            products[index].quantity += 1
        } else {
            products.append(product)
        }
    }
    
    func removeProduct(_ product: Product) {
        products.removeAll { $0.id == product.id }
    }
    
    func updateQuantity(for product: Product, quantity: Int) {
        if let index = products.firstIndex(where: { $0.id == product.id }) {
            if quantity > 0 {
                products[index].quantity = quantity
            } else {
                products.remove(at: index)
            }
        }
    }
}

// View - UI ํ‘œํ˜„
struct ShoppingCartView: View {
    @StateObject private var viewModel = ShoppingCartViewModel()
    
    var body: some View {
        NavigationView {
            VStack {
                if viewModel.products.isEmpty {
                    Text("์žฅ๋ฐ”๊ตฌ๋‹ˆ๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค")
                        .foregroundColor(.secondary)
                } else {
                    List {
                        ForEach(viewModel.products) { product in
                            HStack {
                                VStack(alignment: .leading) {
                                    Text(product.name)
                                        .font(.headline)
                                    Text("\(product.price, specifier: "%.0f")์›")
                                        .font(.subheadline)
                                        .foregroundColor(.secondary)
                                }
                                
                                Spacer()
                                
                                Stepper("\(product.quantity)", 
                                       value: Binding(
                                           get: { product.quantity },
                                           set: { viewModel.updateQuantity(for: product, quantity: $0) }
                                       ),
                                       in: 0...99)
                            }
                        }
                        .onDelete { indexSet in
                            indexSet.forEach { index in
                                viewModel.removeProduct(viewModel.products[index])
                            }
                        }
                    }
                    
                    VStack(spacing: 10) {
                        HStack {
                            Text("์ด \(viewModel.itemCount)๊ฐœ ์ƒํ’ˆ")
                            Spacer()
                            Text("\(viewModel.totalPrice, specifier: "%.0f")์›")
                                .font(.title2)
                                .fontWeight(.bold)
                        }
                        .padding()
                        
                        Button("๊ฒฐ์ œํ•˜๊ธฐ") {
                            // ๊ฒฐ์ œ ๋กœ์ง
                        }
                        .buttonStyle(.borderedProminent)
                        .controlSize(.large)
                    }
                    .background(Color(.systemBackground))
                }
            }
            .navigationTitle("์žฅ๋ฐ”๊ตฌ๋‹ˆ")
        }
    }
}

โœ… MVVM์˜ ์žฅ์ 

โ€ข View์™€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ์™„์ „ํžˆ ๋ถ„๋ฆฌ๋ผ
โ€ข ViewModel์„ ๋…๋ฆฝ์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์–ด
โ€ข SwiftUI์˜ @Published์™€ ์™„๋ฒฝํ•˜๊ฒŒ ํ˜ธํ™˜๋ผ
โ€ข ์ฝ”๋“œ ์žฌ์‚ฌ์šฉ์„ฑ์ด ๋†’์•„์ ธ
โ€ข ์—ฌ๋Ÿฌ View์—์„œ ๊ฐ™์€ ViewModel์„ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์–ด

๐Ÿ” ๋ณด์•ˆ๊ณผ ์ธ์ฆ

์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃจ๋Š” ์•ฑ์ด๋ผ๋ฉด ๋ณด์•ˆ์ด ์ง„์งœ ์ค‘์š”ํ•ด! ํ‚ค์ฒด์ธ, ์ƒ์ฒด ์ธ์ฆ ๊ฐ™์€ ๊ฑธ ์•Œ์•„๋ณด์ž.

Keychain - ๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ ์•ˆ์ „ํ•˜๊ฒŒ ์ €์žฅ

import Security

class KeychainManager {
    static let shared = KeychainManager()
    
    func save(key: String, data: Data) -> Bool {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecValueData as String: data
        ]
        
        SecItemDelete(query as CFDictionary)  // ๊ธฐ์กด ํ•ญ๋ชฉ ์‚ญ์ œ
        
        let status = SecItemAdd(query as CFDictionary, nil)
        return status == errSecSuccess
    }
    
    func load(key: String) -> Data? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecReturnData as String: true
        ]
        
        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        
        return status == errSecSuccess ? result as? Data : nil
    }
    
    func delete(key: String) -> Bool {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key
        ]
        
        let status = SecItemDelete(query as CFDictionary)
        return status == errSecSuccess
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
extension KeychainManager {
    func saveToken(_ token: String) -> Bool {
        guard let data = token.data(using: .utf8) else { return false }
        return save(key: "authToken", data: data)
    }
    
    func loadToken() -> String? {
        guard let data = load(key: "authToken") else { return nil }
        return String(data: data, encoding: .utf8)
    }
    
    func deleteToken() -> Bool {
        return delete(key: "authToken")
    }
}

์ƒ์ฒด ์ธ์ฆ (Face ID / Touch ID) ๐Ÿ‘ค

import LocalAuthentication

class BiometricAuthManager {
    static let shared = BiometricAuthManager()
    
    func canUseBiometrics() -> Bool {
        let context = LAContext()
        var error: NSError?
        return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
    }
    
    func getBiometricType() -> String {
        let context = LAContext()
        _ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
        
        switch context.biometryType {
        case .faceID:
            return "Face ID"
        case .touchID:
            return "Touch ID"
        case .none:
            return "์—†์Œ"
        @unknown default:
            return "์•Œ ์ˆ˜ ์—†์Œ"
        }
    }
    
    func authenticate(reason: String, completion: @escaping (Bool, Error?) -> Void) {
        let context = LAContext()
        var error: NSError?
        
        guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
            completion(false, error)
            return
        }
        
        context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, 
                              localizedReason: reason) { success, error in
            DispatchQueue.main.async {
                completion(success, error)
            }
        }
    }
}

// SwiftUI์—์„œ ์‚ฌ์šฉ
struct BiometricLoginView: View {
    @State private var isUnlocked = false
    @State private var showError = false
    @State private var errorMessage = ""
    
    var body: some View {
        VStack(spacing: 30) {
            if isUnlocked {
                Text("์ธ์ฆ ์„ฑ๊ณต! ๐ŸŽ‰")
                    .font(.title)
                    .foregroundColor(.green)
            } else {
                Image(systemName: "faceid")
                    .resizable()
                    .frame(width: 100, height: 100)
                    .foregroundColor(.blue)
                
                Text("์•ฑ์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค")
                    .multilineTextAlignment(.center)
                
                Button("์ธ์ฆํ•˜๊ธฐ") {
                    authenticate()
                }
                .buttonStyle(.borderedProminent)
            }
        }
        .alert("์ธ์ฆ ์‹คํŒจ", isPresented: $showError) {
            Button("ํ™•์ธ", role: .cancel) { }
        } message: {
            Text(errorMessage)
        }
    }
    
    private func authenticate() {
        BiometricAuthManager.shared.authenticate(reason: "์•ฑ์— ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•ด ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค") { success, error in
            if success {
                isUnlocked = true
            } else {
                errorMessage = error?.localizedDescription ?? "์ธ์ฆ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค"
                showError = true
            }
        }
    }
}

๐Ÿ“ฑ ์‹ค์ „ ํ”„๋กœ์ ํŠธ - ๋‚ ์”จ ์•ฑ ๋งŒ๋“ค๊ธฐ

์ด๋ก ๋งŒ ๋ฐฐ์šฐ๋ฉด ์žฌ๋ฏธ์—†์ž–์•„ ใ…‹ใ…‹ใ…‹ ์‹ค์ œ๋กœ ๋™์ž‘ํ•˜๋Š” ๋‚ ์”จ ์•ฑ์„ ๋งŒ๋“ค์–ด๋ณด์ž! OpenWeatherMap API๋ฅผ ์‚ฌ์šฉํ•  ๊ฑฐ์•ผ.

1๋‹จ๊ณ„: ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ์ •์˜

// ๋‚ ์”จ ์‘๋‹ต ๋ชจ๋ธ
struct WeatherResponse: Codable {
    let name: String
    let main: MainWeather
    let weather: [Weather]
    let wind: Wind
}

struct MainWeather: Codable {
    let temp: Double
    let feelsLike: Double
    let humidity: Int
    
    enum CodingKeys: String, CodingKey {
        case temp
        case feelsLike = "feels_like"
        case humidity
    }
}

struct Weather: Codable {
    let id: Int
    let main: String
    let description: String
    let icon: String
}

struct Wind: Codable {
    let speed: Double
}

2๋‹จ๊ณ„: API ์„œ๋น„์Šค ๊ตฌํ˜„

class WeatherService {
    static let shared = WeatherService()
    private let apiKey = "YOUR_API_KEY"  // OpenWeatherMap์—์„œ ๋ฐœ๊ธ‰๋ฐ›์€ ํ‚ค
    private let baseURL = "https://api.openweathermap.org/data/2.5/weather"
    
    func fetchWeather(city: String) async throws -> WeatherResponse {
        var components = URLComponents(string: baseURL)
        components?.queryItems = [
            URLQueryItem(name: "q", value: city),
            URLQueryItem(name: "appid", value: apiKey),
            URLQueryItem(name: "units", value: "metric"),
            URLQueryItem(name: "lang", value: "kr")
        ]
        
        guard let url = components?.url else {
            throw URLError(.badURL)
        }
        
        let (data, response) = try await URLSession.shared.data(from: url)
        
        guard let httpResponse = response as? HTTPURLResponse,
              httpResponse.statusCode == 200 else {
            throw URLError(.badServerResponse)
        }
        
        let weather = try JSONDecoder().decode(WeatherResponse.self, from: data)
        return weather
    }
    
    func getWeatherIcon(code: String) -> String {
        // ๋‚ ์”จ ์•„์ด์ฝ˜ ๋งคํ•‘
        switch code {
        case "01d": return "sun.max.fill"
        case "01n": return "moon.stars.fill"
        case "02d", "02n": return "cloud.sun.fill"
        case "03d", "03n", "04d", "04n": return "cloud.fill"
        case "09d", "09n": return "cloud.rain.fill"
        case "10d", "10n": return "cloud.heavyrain.fill"
        case "11d", "11n": return "cloud.bolt.fill"
        case "13d", "13n": return "snow"
        case "50d", "50n": return "cloud.fog.fill"
        default: return "questionmark"
        }
    }
}

3๋‹จ๊ณ„: ViewModel ๊ตฌํ˜„

class WeatherViewModel: ObservableObject {
    @Published var weather: WeatherResponse?
    @Published var isLoading = false
    @Published var errorMessage: String?
    
    func loadWeather(for city: String) {
        guard !city.isEmpty else { return }
        
        isLoading = true
        errorMessage = nil
        
        Task {
            do {
                let fetchedWeather = try await WeatherService.shared.fetchWeather(city: city)
                
                await MainActor.run {
                    self.weather = fetchedWeather
                    self.isLoading = false
                }
            } catch {
                await MainActor.run {
                    self.errorMessage = "๋‚ ์”จ ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"
                    self.isLoading = false
                }
            }
        }
    }
}

4๋‹จ๊ณ„: UI ๊ตฌํ˜„

struct WeatherView: View {
    @StateObject private var viewModel = WeatherViewModel()
    @State private var cityName = "Seoul"
    
    var body: some View {
        NavigationView {
            ZStack {
                // ๋ฐฐ๊ฒฝ ๊ทธ๋ผ๋””์–ธํŠธ
                LinearGradient(
                    colors: [Color.blue.opacity(0.6), Color.purple.opacity(0.4)],
                    startPoint: .topLeading,
                    endPoint: .bottomTrailing
                )
                .ignoresSafeArea()
                
                VStack(spacing: 20) {
                    // ๊ฒ€์ƒ‰ ๋ฐ”
                    HStack {
                        TextField("๋„์‹œ ์ด๋ฆ„", text: $cityName)
                            .textFieldStyle(.roundedBorder)
                        
                        Button(action: {
                            viewModel.loadWeather(for: cityName)
                        }) {
                            Image(systemName: "magnifyingglass")
                                .padding(10)
                                .background(Color.white.opacity(0.3))
                                .clipShape(Circle())
                        }
                    }
                    .padding()
                    
                    if viewModel.isLoading {
                        ProgressView()
                            .scaleEffect(1.5)
                            .tint(.white)
                    } else if let error = viewModel.errorMessage {
                        Text(error)
                            .foregroundColor(.white)
                            .padding()
                    } else if let weather = viewModel.weather {
                        weatherContent(weather)
                    } else {
                        Text("๋„์‹œ๋ฅผ ๊ฒ€์ƒ‰ํ•ด์ฃผ์„ธ์š”")
                            .foregroundColor(.white)
                            .font(.title3)
                    }
                    
                    Spacer()
                }
            }
            .navigationTitle("๋‚ ์”จ")
            .navigationBarTitleDisplayMode(.inline)
        }
        .onAppear {
            viewModel.loadWeather(for: cityName)
        }
    }
    
    @ViewBuilder
    private func weatherContent(_ weather: WeatherResponse) -> some View {
        VStack(spacing: 30) {
            // ๋„์‹œ ์ด๋ฆ„
            Text(weather.name)
                .font(.system(size: 40, weight: .medium))
                .foregroundColor(.white)
            
            // ๋‚ ์”จ ์•„์ด์ฝ˜
            if let weatherInfo = weather.weather.first {
                Image(systemName: WeatherService.shared.getWeatherIcon(code: weatherInfo.icon))
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width: 120, height: 120)
                    .foregroundColor(.white)
                    .shadow(radius: 10)
                
                Text(weatherInfo.description)
                    .font(.title2)
                    .foregroundColor(.white)
            }
            
            // ์˜จ๋„
            Text("\(Int(weather.main.temp))ยฐC")
                .font(.system(size: 70, weight: .thin))
                .foregroundColor(.white)
            
            Text("์ฒด๊ฐ ์˜จ๋„: \(Int(weather.main.feelsLike))ยฐC")
                .font(.title3)
                .foregroundColor(.white.opacity(0.8))
            
            // ์ƒ์„ธ ์ •๋ณด
            HStack(spacing: 50) {
                VStack {
                    Image(systemName: "humidity.fill")
                        .font(.title2)
                    Text("\(weather.main.humidity)%")
                        .font(.title3)
                    Text("์Šต๋„")
                        .font(.caption)
                }
                
                VStack {
                    Image(systemName: "wind")
                        .font(.title2)
                    Text("\(String(format: "%.1f", weather.wind.speed))m/s")
                        .font(.title3)
                    Text("ํ’์†")
                        .font(.caption)
                }
            }
            .foregroundColor(.white)
            .padding()
            .background(Color.white.opacity(0.2))
            .cornerRadius(15)
        }
        .padding()
    }
}

๐Ÿ’ก API ํ‚ค ๋ณด์•ˆ

์‹ค์ œ ์•ฑ์„ ๋ฐฐํฌํ•  ๋•Œ๋Š” API ํ‚ค๋ฅผ ์ฝ”๋“œ์— ์ง์ ‘ ๋„ฃ์œผ๋ฉด ์•ˆ ๋ผ! .xcconfig ํŒŒ์ผ์ด๋‚˜ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜, ์„œ๋ฒ„๋ฅผ ๊ฑฐ์ณ์„œ API๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ฒŒ ์•ˆ์ „ํ•ด. GitHub์— ์˜ฌ๋ฆด ๋•Œ๋„ .gitignore์— API ํ‚ค ํŒŒ์ผ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฑฐ ์žŠ์ง€ ๋งˆ!

๐Ÿงช ํ…Œ์ŠคํŠธ์™€ ๋””๋ฒ„๊น…

๋ฒ„๊ทธ ์—†๋Š” ์ฝ”๋“œ๋Š” ์—†์–ด ใ…‹ใ…‹ใ…‹ ํ…Œ์ŠคํŠธ์™€ ๋””๋ฒ„๊น… ๊ธฐ์ˆ ์„ ์ตํ˜€๋‘๋ฉด ๊ฐœ๋ฐœ ์†๋„๊ฐ€ ํ›จ์”ฌ ๋นจ๋ผ์ ธ!

Unit Test - ๋‹จ์œ„ ํ…Œ์ŠคํŠธ

import XCTest
@testable import MyApp

class CalculatorTests: XCTestCase {
    var calculator: Calculator!
    
    override func setUp() {
        super.setUp()
        calculator = Calculator()
    }
    
    override func tearDown() {
        calculator = nil
        super.tearDown()
    }
    
    func testAddition() {
        let result = calculator.add(5, 3)
        XCTAssertEqual(result, 8, "5 + 3์€ 8์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค")
    }
    
    func testDivision() {
        let result = calculator.divide(10, 2)
        XCTAssertEqual(result, 5, "10 / 2๋Š” 5์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค")
    }
    
    func testDivisionByZero() {
        XCTAssertThrowsError(try calculator.divide(10, 0)) { error in
            XCTAssertEqual(error as? CalculatorError, .divisionByZero)
        }
    }
}

// ViewModel ํ…Œ์ŠคํŠธ
class WeatherViewModelTests: XCTestCase {
    var viewModel: WeatherViewModel!
    
    override func setUp() {
        super.setUp()
        viewModel = WeatherViewModel()
    }
    
    func testLoadWeatherSuccess() async {
        // Given
        let expectation = XCTestExpectation(description: "๋‚ ์”จ ๋กœ๋“œ ์„ฑ๊ณต")
        
        // When
        viewModel.loadWeather(for: "Seoul")
        
        // Then
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            XCTAssertNotNil(self.viewModel.weather)
            XCTAssertNil(self.viewModel.errorMessage)
            XCTAssertFalse(self.viewModel.isLoading)
            expectation.fulfill()
        }
        
        await fulfillment(of: [expectation], timeout: 5)
    }
}

UI Test - UI ํ…Œ์ŠคํŠธ

import XCTest

class MyAppUITests: XCTestCase {
    var app: XCUIApplication!
    
    override func setUp() {
        super.setUp()
        continueAfterFailure = false
        app = XCUIApplication()
        app.launch()
    }
    
    func testLoginFlow() {
        // ๋กœ๊ทธ์ธ ํ™”๋ฉด ์š”์†Œ ํ™•์ธ
        let usernameField = app.textFields["username"]
        XCTAssertTrue(usernameField.exists)
        
        let passwordField = app.secureTextFields["password"]
        XCTAssertTrue(passwordField.exists)
        
        // ์ž…๋ ฅ
        usernameField.tap()
        usernameField.typeText("testuser")
        
        passwordField.tap()
        passwordField.typeText("password123")
        
        // ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ํƒญ
        app.buttons["๋กœ๊ทธ์ธ"].tap()
        
        // ๊ฒฐ๊ณผ ํ™•์ธ
        let welcomeText = app.staticTexts["ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค"]
        XCTAssertTrue(welcomeText.waitForExistence(timeout: 3))
    }
    
    func testNavigationFlow() {
        // ํƒญ ๋ฐ” ํ…Œ์ŠคํŠธ
        let homeTab = app.tabBars.buttons["ํ™ˆ"]
        homeTab.tap()
        XCTAssertTrue(app.navigationBars["ํ™ˆ"].exists)
        
        let profileTab = app.tabBars.buttons["ํ”„๋กœํ•„"]
        profileTab.tap()
        XCTAssertTrue(app.navigationBars["ํ”„๋กœํ•„"].exists)
    }
}

๋””๋ฒ„๊น… ํŒ๊ณผ ํŠธ๋ฆญ ๐Ÿ”

// 1. print ๋Œ€์‹  os_log ์‚ฌ์šฉ
import os.log

let logger = Logger(subsystem: "com.myapp", category: "network")

func fetchData() {
    logger.debug("๋ฐ์ดํ„ฐ ์š”์ฒญ ์‹œ์ž‘")
    logger.info("URL: \(url)")
    logger.error("์—๋Ÿฌ ๋ฐœ์ƒ: \(error)")
}

// 2. Breakpoint ํ™œ์šฉ
// Xcode์—์„œ ์ค„ ๋ฒˆํ˜ธ ํด๋ฆญํ•˜๋ฉด ๋ธŒ๋ ˆ์ดํฌํฌ์ธํŠธ ์„ค์ •
// ์กฐ๊ฑด๋ถ€ ๋ธŒ๋ ˆ์ดํฌํฌ์ธํŠธ: ์šฐํด๋ฆญ > Edit Breakpoint

// 3. LLDB ๋ช…๋ น์–ด
// po (print object): ๊ฐ์ฒด ์ถœ๋ ฅ
// p (print): ๊ฐ’ ์ถœ๋ ฅ
// expr (expression): ์ฝ”๋“œ ์‹คํ–‰

// 4. View Hierarchy ๋””๋ฒ„๊น…
// Debug > View Debugging > Capture View Hierarchy

// 5. Memory Graph ๋””๋ฒ„๊น…
// Debug > Memory Graph
// ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ์ฐพ๊ธฐ์— ์œ ์šฉ

๐Ÿš€ ์•ฑ ๋ฐฐํฌ์™€ ์ตœ์ ํ™”

์•ฑ์„ ๋งŒ๋“ค์—ˆ์œผ๋ฉด ์ด์ œ ์„ธ์ƒ์— ๊ณต๊ฐœํ•  ์ฐจ๋ก€์•ผ! ์•ฑ์Šคํ† ์–ด ๋ฐฐํฌ ๊ณผ์ •๊ณผ ์ตœ์ ํ™” ํŒ์„ ์•Œ์•„๋ณด์ž.

์•ฑ์Šคํ† ์–ด ๋ฐฐํฌ ์ฒดํฌ๋ฆฌ์ŠคํŠธ โœ…

1. ๊ฐœ๋ฐœ์ž ๊ณ„์ • ์ค€๋น„ โ€ข Apple Developer Program ๊ฐ€์ž… (์—ฐ $99)
โ€ข App Store Connect ์ ‘๊ทผ ๊ถŒํ•œ ํ™•์ธ

2. ์•ฑ ์ •๋ณด ์ค€๋น„
โ€ข ์•ฑ ์ด๋ฆ„, ๋ถ€์ œ๋ชฉ, ์„ค๋ช…
โ€ข ํ‚ค์›Œ๋“œ (์ตœ๋Œ€ 100์ž)
โ€ข ์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ
โ€ข ์—ฐ๋ น ๋“ฑ๊ธ‰ ์„ค์ •

3. ์Šคํฌ๋ฆฐ์ƒท๊ณผ ํ”„๋ฆฌ๋ทฐ
โ€ข ๋‹ค์–‘ํ•œ ๊ธฐ๊ธฐ ํฌ๊ธฐ๋ณ„ ์Šคํฌ๋ฆฐ์ƒท (ํ•„์ˆ˜)
โ€ข ์•ฑ ํ”„๋ฆฌ๋ทฐ ๋น„๋””์˜ค (์„ ํƒ)
โ€ข ์•„์ด์ฝ˜ (1024x1024px)

4. ๊ฐœ์ธ์ •๋ณด ๋ณดํ˜ธ
โ€ข ๊ฐœ์ธ์ •๋ณด ์ฒ˜๋ฆฌ๋ฐฉ์นจ URL
โ€ข ์ˆ˜์ง‘ํ•˜๋Š” ๋ฐ์ดํ„ฐ ์œ ํ˜• ๋ช…์‹œ
โ€ข ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ ๋ชฉ์  ์„ค๋ช…

5. ๋นŒ๋“œ ์—…๋กœ๋“œ
โ€ข Archive ์ƒ์„ฑ
โ€ข Xcode Organizer์—์„œ ์—…๋กœ๋“œ
โ€ข TestFlight ๋ฒ ํƒ€ ํ…Œ์ŠคํŠธ (์„ ํƒ)

์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ธฐ๋ฒ• โšก

// 1. ์ด๋ฏธ์ง€ ์ตœ์ ํ™”
struct OptimizedImageView: View {
    let imageURL: URL
    
    var body: some View {
        AsyncImage(url: imageURL) { phase in
            switch phase {
            case .empty:
                ProgressView()
            case .success(let image):
                image
                    .resizable()
                    .aspectRatio(contentMode: .fill)
            case .failure:
                Image(systemName: "photo")
            @unknown default:
                EmptyView()
            }
        }
        .frame(width: 200, height: 200)
        .clipped()
    }
}

// 2. LazyStack ์‚ฌ์šฉ
struct OptimizedListView: View {
    let items: [Item]
    
    var body: some View {
        ScrollView {
            LazyVStack(spacing: 10) {  // Lazy๋กœ ํ•„์š”ํ•  ๋•Œ๋งŒ ๋กœ๋“œ
                ForEach(items) { item in
                    ItemRow(item: item)
                }
            }
        }
    }
}

// 3. ๊ณ„์‚ฐ ๊ฒฐ๊ณผ ์บ์‹ฑ
class DataProcessor {
    private var cache: [String: ProcessedData] = [:]
    
    func process(_ input: String) -> ProcessedData {
        if let cached = cache[input] {
            return cached  // ์บ์‹œ๋œ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜
        }
        
        let result = expensiveOperation(input)
        cache[input] = result
        return result
    }
    
    private func expensiveOperation(_ input: String) -> ProcessedData {
        // ๋ฌด๊ฑฐ์šด ์—ฐ์‚ฐ
        return ProcessedData()
    }
}

// 4. ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ ํ™œ์šฉ
func loadLargeData() {
    DispatchQueue.global(qos: .userInitiated).async {
        let data = performHeavyTask()
        
        DispatchQueue.main.async {
            // UI ์—…๋ฐ์ดํŠธ๋Š” ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ
            self.updateUI(with: data)
        }
    }
}

// 5. ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ
class ImageCache {
    static let shared = ImageCache()
    private var cache = NSCache<NSString, UIImage>()
    
    init() {
        cache.countLimit = 100  // ์ตœ๋Œ€ 100๊ฐœ
        cache.totalCostLimit = 50 * 1024 * 1024  // 50MB
    }
    
    func setImage(_ image: UIImage, forKey key: String) {
        cache.setObject(image, forKey: key as NSString)
    }
    
    func image(forKey key: String) -> UIImage? {
        return cache.object(forKey: key as NSString)
    }
}

โš ๏ธ ํ”ํ•œ ์‹ค์ˆ˜๋“ค

โ€ข ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ๋ฌด๊ฑฐ์šด ์ž‘์—… ์ˆ˜ํ–‰ โ†’ ์•ฑ์ด ๋ฒ„๋ฒ…๊ฑฐ๋ฆผ
โ€ข ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ (๊ฐ•ํ•œ ์ฐธ์กฐ ์ˆœํ™˜) โ†’ ์•ฑ ํฌ๋ž˜์‹œ
โ€ข ๊ณผ๋„ํ•œ ๋„คํŠธ์›Œํฌ ์š”์ฒญ โ†’ ๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ
โ€ข ์ด๋ฏธ์ง€ ์••์ถ• ์•ˆ ํ•จ โ†’ ์•ฑ ์šฉ๋Ÿ‰ ์ฆ๊ฐ€
โ€ข ์—๋Ÿฌ ์ฒ˜๋ฆฌ ์•ˆ ํ•จ โ†’ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ํฌ๋ž˜์‹œ

๐ŸŽ“ ํ•™์Šต ๋ฆฌ์†Œ์Šค์™€ ์ปค๋ฎค๋‹ˆํ‹ฐ

ํ˜ผ์ž ๊ณต๋ถ€ํ•˜๊ธฐ ํž˜๋“ค ๋•Œ๊ฐ€ ์žˆ์ž–์•„? ์ข‹์€ ํ•™์Šต ์ž๋ฃŒ์™€ ์ปค๋ฎค๋‹ˆํ‹ฐ๋ฅผ ์†Œ๊ฐœํ• ๊ฒŒ!

๐Ÿ“š ๊ณต์‹ ๋ฌธ์„œ โ€ข Swift.org - Swift ์–ธ์–ด ๊ณต์‹ ๋ฌธ์„œ
โ€ข Apple Developer Documentation - ๋ชจ๋“  ํ”„๋ ˆ์ž„์›Œํฌ ๋ฌธ์„œ
โ€ข Human Interface Guidelines - ๋””์ž์ธ ๊ฐ€์ด๋“œ๋ผ์ธ
โ€ข WWDC Videos - ์• ํ”Œ ๊ฐœ๋ฐœ์ž ์ปจํผ๋Ÿฐ์Šค ์˜์ƒ

๐Ÿ’ป ์˜จ๋ผ์ธ ๊ฐ•์˜
โ€ข Stanford CS193p - ๋ฌด๋ฃŒ SwiftUI ๊ฐ•์˜ (์ตœ๊ณ !)
โ€ข Hacking with Swift - Paul Hudson์˜ ๋ฌด๋ฃŒ ํŠœํ† ๋ฆฌ์–ผ
โ€ข Ray Wenderlich - ์œ ๋ฃŒ/๋ฌด๋ฃŒ ๊ฐ•์˜ ํ˜ผํ•ฉ
โ€ข Udemy - ๋‹ค์–‘ํ•œ ํ•œ๊ธ€ ๊ฐ•์˜

๐Ÿ‘ฅ ์ปค๋ฎค๋‹ˆํ‹ฐ
โ€ข Swift Forums - ๊ณต์‹ ํฌ๋Ÿผ
โ€ข Stack Overflow - ์งˆ๋ฌธ/๋‹ต๋ณ€
โ€ข Reddit r/swift, r/iOSProgramming
โ€ข ํ•œ๊ตญ iOS ๊ฐœ๋ฐœ์ž ๋ชจ์ž„ (ํŽ˜์ด์Šค๋ถ ๊ทธ๋ฃน)
โ€ข Let'Swift - ํ•œ๊ตญ Swift ์ปจํผ๋Ÿฐ์Šค

๐Ÿ“ฑ ์œ ์šฉํ•œ ๋„๊ตฌ
โ€ข SF Symbols - ์• ํ”Œ ์•„์ด์ฝ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
โ€ข Figma - UI ๋””์ž์ธ ๋„๊ตฌ
โ€ข Postman - API ํ…Œ์ŠคํŠธ
โ€ข Charles Proxy - ๋„คํŠธ์›Œํฌ ๋””๋ฒ„๊น…
โ€ข Instruments - ์„ฑ๋Šฅ ๋ถ„์„

๊ทธ๋ฆฌ๊ณ  ์žฌ๋Šฅ๋„ท์—์„œ๋„ iOS ๊ฐœ๋ฐœ ๊ด€๋ จ ๋ฉ˜ํ† ๋ง์ด๋‚˜ ๊ฐ•์˜๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ์–ด! ์‹ค๋ฌด ๊ฒฝํ—˜์ด ํ’๋ถ€ํ•œ ๊ฐœ๋ฐœ์ž๋“ค์—๊ฒŒ ์ง์ ‘ ๋ฐฐ์šธ ์ˆ˜ ์žˆ๋Š” ๊ธฐํšŒ๋‹ˆ๊นŒ ํ•œ๋ฒˆ ๋‘˜๋Ÿฌ๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์„ ๊ฑฐ์•ผ ใ…Žใ…Ž

๐Ÿ”ฎ iOS ๊ฐœ๋ฐœ์˜ ๋ฏธ๋ž˜

๊ธฐ์ˆ ์€ ๊ณ„์† ๋ฐœ์ „ํ•˜์ž–์•„? ์•ž์œผ๋กœ ์ฃผ๋ชฉํ•ด์•ผ ํ•  ํŠธ๋ Œ๋“œ๋“ค์„ ์‚ดํŽด๋ณด์ž!

SwiftUI์˜ ์ง„ํ™” ๐ŸŒฑ

SwiftUI๋Š” ๋งค๋…„ WWDC์—์„œ ์—„์ฒญ๋‚œ ์—…๋ฐ์ดํŠธ๋ฅผ ๋ฐ›๊ณ  ์žˆ์–ด. iOS 17์—์„œ๋Š” ๋” ๋งŽ์€ ๊ธฐ๋Šฅ์ด ์ถ”๊ฐ€๋๊ณ , UIKit๊ณผ์˜ ํ†ตํ•ฉ๋„ ๋” ์‰ฌ์›Œ์กŒ์ง€. ์•ž์œผ๋กœ ๋ช‡ ๋…„ ์•ˆ์— UIKit์„ ์™„์ „ํžˆ ๋Œ€์ฒดํ•  ๊ฐ€๋Šฅ์„ฑ๋„ ์žˆ์–ด!

Swift Concurrency โšก

async/await, Actor, TaskGroup ๊ฐ™์€ ๋™์‹œ์„ฑ ๊ธฐ๋Šฅ๋“ค์ด ๊ณ„์† ๋ฐœ์ „ํ•˜๊ณ  ์žˆ์–ด. ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์ด ํ›จ์”ฌ ์‰ฝ๊ณ  ์•ˆ์ „ํ•ด์กŒ์ง€. ์ด์ œ GCD๋‚˜ Operation Queue ๋Œ€์‹  Swift Concurrency๋ฅผ ์“ฐ๋Š” ๊ฒŒ ํ‘œ์ค€์ด ๋˜๊ณ  ์žˆ์–ด.

SwiftData ๐Ÿ’พ

iOS 17์—์„œ ๋‚˜์˜จ SwiftData๋Š” CoreData๋ฅผ ๋Œ€์ฒดํ•  ์ฐจ์„ธ๋Œ€ ๋ฐ์ดํ„ฐ ํ”„๋ ˆ์ž„์›Œํฌ์•ผ. ๋ฌธ๋ฒ•์ด ํ›จ์”ฌ ๊ฐ„๊ฒฐํ•˜๊ณ  SwiftUI์™€ ์™„๋ฒฝํ•˜๊ฒŒ ํ†ตํ•ฉ๋ผ. ์•ž์œผ๋กœ ์ƒˆ ํ”„๋กœ์ ํŠธ๋Š” SwiftData๋ฅผ ์“ฐ๋Š” ๊ฒŒ ๋Œ€์„ธ๊ฐ€ ๋  ๊ฑฐ์•ผ.

Vision Pro์™€ visionOS ๐Ÿฅฝ

์• ํ”Œ์˜ ๊ณต๊ฐ„ ์ปดํ“จํŒ… ํ”Œ๋žซํผ์ธ Vision Pro๊ฐ€ ์ถœ์‹œ๋˜๋ฉด์„œ visionOS ๊ฐœ๋ฐœ๋„ ์ฃผ๋ชฉ๋ฐ›๊ณ  ์žˆ์–ด. SwiftUI๋กœ 3D ๊ณต๊ฐ„ UI๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค๋‹ˆ ์ง„์งœ ๋ฏธ๋ž˜์ ์ด์ง€ ์•Š์•„? ใ…‹ใ…‹ใ…‹

AI/ML ํ†ตํ•ฉ ๐Ÿค–

Core ML, Create ML๋กœ ๋จธ์‹ ๋Ÿฌ๋‹ ๋ชจ๋ธ์„ ์•ฑ์— ํ†ตํ•ฉํ•˜๋Š” ๊ฒŒ ์ ์  ์‰ฌ์›Œ์ง€๊ณ  ์žˆ์–ด. ์ด๋ฏธ์ง€ ์ธ์‹, ์ž์—ฐ์–ด ์ฒ˜๋ฆฌ, ์ถ”์ฒœ ์‹œ์Šคํ…œ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ์ง์ ‘ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์ง€. ์•ž์œผ๋กœ๋Š” AI ๊ธฐ๋Šฅ์ด ์—†๋Š” ์•ฑ์ด ์ด์ƒํ•  ์ •๋„๊ฐ€ ๋  ๊ฑฐ์•ผ.

ํฌ๋กœ์Šค ํ”Œ๋žซํผ ๊ฐœ๋ฐœ ๐ŸŒ

SwiftUI๋Š” iOS, macOS, watchOS, tvOS๋ฅผ ๋ชจ๋‘ ์ง€์›ํ•ด. ํ•˜๋‚˜์˜ ์ฝ”๋“œ๋ฒ ์ด์Šค๋กœ ์—ฌ๋Ÿฌ ํ”Œ๋žซํผ์— ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฑด ์—„์ฒญ๋‚œ ์žฅ์ ์ด์•ผ. ๊ฐœ๋ฐœ ์‹œ๊ฐ„๊ณผ ๋น„์šฉ์„ ํฌ๊ฒŒ ์ค„์ผ ์ˆ˜ ์žˆ๊ฑฐ๋“ !

๐Ÿ’ผ ์ทจ์—…๊ณผ ์ปค๋ฆฌ์–ด

iOS ๊ฐœ๋ฐœ์ž๋กœ ์ทจ์—…ํ•˜๋ ค๋ฉด ๋ญ˜ ์ค€๋น„ํ•ด์•ผ ํ• ๊นŒ? ์‹ค๋ฌด์—์„œ ์š”๊ตฌํ•˜๋Š” ์Šคํ‚ฌ๋“ค์„ ์ •๋ฆฌํ•ด๋ดค์–ด!

โœ… ์ฃผ๋‹ˆ์–ด ๊ฐœ๋ฐœ์ž ํ•„์ˆ˜ ์Šคํ‚ฌ

๊ธฐ์ˆ  ์Šคํ‚ฌ:
โ€ข Swift ์–ธ์–ด ๊ธฐ์ดˆ (๋ฌธ๋ฒ•, ์˜ต์…”๋„, ํด๋กœ์ € ๋“ฑ)
โ€ข SwiftUI ๋˜๋Š” UIKit ์ค‘ ํ•˜๋‚˜ ์ด์ƒ
โ€ข Git/GitHub ์‚ฌ์šฉ๋ฒ•
โ€ข RESTful API ํ†ต์‹ 
โ€ข ๊ธฐ๋ณธ์ ์ธ ๋””์ž์ธ ํŒจํ„ด (MVC, MVVM)

์†Œํ”„ํŠธ ์Šคํ‚ฌ:
โ€ข ๋ฌธ์ œ ํ•ด๊ฒฐ ๋Šฅ๋ ฅ
โ€ข ์ฝ”๋“œ ๋ฆฌ๋ทฐ ๊ฒฝํ—˜
โ€ข ํ˜‘์—… ๋„๊ตฌ ์‚ฌ์šฉ (Jira, Slack ๋“ฑ)
โ€ข ๊ธฐ์ˆ  ๋ฌธ์„œ ์ž‘์„ฑ ๋Šฅ๋ ฅ
โ€ข ์ง€์†์ ์ธ ํ•™์Šต ์˜์ง€

ํฌํŠธํด๋ฆฌ์˜ค:
โ€ข ์•ฑ์Šคํ† ์–ด์— ์ถœ์‹œํ•œ ์•ฑ 1~2๊ฐœ
โ€ข GitHub์— ์ •๋ฆฌ๋œ ํ”„๋กœ์ ํŠธ
โ€ข ๊ธฐ์ˆ  ๋ธ”๋กœ๊ทธ ์šด์˜ (์„ ํƒ)
โ€ข ์˜คํ”ˆ์†Œ์Šค ๊ธฐ์—ฌ ๊ฒฝํ—˜ (์„ ํƒ)

์‹ค๋ฌด ํ”„๋กœ์ ํŠธ ์•„์ด๋””์–ด ๐Ÿ’ก

ํฌํŠธํด๋ฆฌ์˜ค์šฉ์œผ๋กœ ๋งŒ๋“ค๊ธฐ ์ข‹์€ ์•ฑ๋“ค์„ ์ถ”์ฒœํ•ด์ค„๊ฒŒ:

1. ํ•  ์ผ ๊ด€๋ฆฌ ์•ฑ
โ€ข CoreData/SwiftData๋กœ ๋ฐ์ดํ„ฐ ์ €์žฅ
โ€ข ์นดํ…Œ๊ณ ๋ฆฌ, ์šฐ์„ ์ˆœ์œ„, ๋งˆ๊ฐ์ผ ๊ธฐ๋Šฅ
โ€ข ์œ„์ ฏ ์ง€์›
โ€ข ๋‹คํฌ ๋ชจ๋“œ ์ง€์›

2. ๋‚ ์”จ ์•ฑ
โ€ข OpenWeatherMap API ์—ฐ๋™
โ€ข ์œ„์น˜ ๊ธฐ๋ฐ˜ ๋‚ ์”จ ์ •๋ณด
โ€ข ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ
โ€ข ์•Œ๋ฆผ ๊ธฐ๋Šฅ

3. SNS ํด๋ก 
โ€ข Firebase ๋ฐฑ์—”๋“œ
โ€ข ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ/๋‹ค์šด๋กœ๋“œ
โ€ข ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ…
โ€ข ํ‘ธ์‹œ ์•Œ๋ฆผ

4. ์˜ํ™”/์ฑ… ์ถ”์ฒœ ์•ฑ
โ€ข TMDB API ๋˜๋Š” ์•Œ๋ผ๋”˜ API
โ€ข ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ
โ€ข ์ฆ๊ฒจ์ฐพ๊ธฐ/์œ„์‹œ๋ฆฌ์ŠคํŠธ
โ€ข ๋ฆฌ๋ทฐ ์ž‘์„ฑ

5. ๊ฐ€๊ณ„๋ถ€ ์•ฑ
โ€ข ์ˆ˜์ž…/์ง€์ถœ ๊ด€๋ฆฌ
โ€ข ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ํ†ต๊ณ„
โ€ข ์ฐจํŠธ/๊ทธ๋ž˜ํ”„ ์‹œ๊ฐํ™”
โ€ข CSV ๋‚ด๋ณด๋‚ด๊ธฐ

๐Ÿ’ก ํฌํŠธํด๋ฆฌ์˜ค ์ž‘์„ฑ ํŒ

โ€ข README.md๋ฅผ ์ž์„ธํ•˜๊ฒŒ ์ž‘์„ฑํ•ด (๊ธฐ๋Šฅ, ๊ธฐ์ˆ  ์Šคํƒ, ์Šคํฌ๋ฆฐ์ƒท)
โ€ข ์ฝ”๋“œ์— ์ฃผ์„์„ ๋‹ฌ์•„์„œ ์˜๋„๋ฅผ ๋ช…ํ™•ํžˆ ํ•ด
โ€ข ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€๋ฅผ ์˜๋ฏธ์žˆ๊ฒŒ ์ž‘์„ฑํ•ด
โ€ข ์•ฑ์Šคํ† ์–ด ๋งํฌ๊ฐ€ ์žˆ์œผ๋ฉด ํฐ ํ”Œ๋Ÿฌ์Šค!
โ€ข ๊ธฐ์ˆ  ๋ธ”๋กœ๊ทธ์— ๊ฐœ๋ฐœ ๊ณผ์ •์„ ์ •๋ฆฌํ•˜๋ฉด ๋” ์ข‹์•„

๐ŸŽฏ ์‹ค๋ฌด ๊ฐœ๋ฐœ ํŒ

ํ•™๊ต๋‚˜ ๊ฐ•์˜์—์„œ๋Š” ์•ˆ ์•Œ๋ ค์ฃผ๋Š” ์‹ค๋ฌด ๊ฟ€ํŒ๋“ค์„ ๊ณต์œ ํ• ๊ฒŒ ใ…‹ใ…‹ใ…‹

1. ์ฝ”๋“œ ๋ฆฌ๋ทฐ ๋ฌธํ™” ๐Ÿ‘€

// ๋‚˜์œ ์˜ˆ
func getData() {
    // 100์ค„์˜ ๋ณต์žกํ•œ ์ฝ”๋“œ...
}

// ์ข‹์€ ์˜ˆ
func fetchUserData() {
    validateInput()
    performNetworkRequest()
    parseResponse()
    updateUI()
}

private func validateInput() {
    // ์ž…๋ ฅ ๊ฒ€์ฆ
}

private func performNetworkRequest() {
    // ๋„คํŠธ์›Œํฌ ์š”์ฒญ
}

// ํ•จ์ˆ˜๋ฅผ ์ž‘๊ฒŒ ๋‚˜๋ˆ„๊ณ , ์ด๋ฆ„์„ ๋ช…ํ™•ํ•˜๊ฒŒ!

2. ์—๋Ÿฌ ์ฒ˜๋ฆฌ ์ „๋žต ๐Ÿšจ

enum NetworkError: LocalizedError {
    case invalidURL
    case noData
    case decodingError
    case serverError(statusCode: Int)
    
    var errorDescription: String? {
        switch self {
        case .invalidURL:
            return "์ž˜๋ชป๋œ URL์ž…๋‹ˆ๋‹ค"
        case .noData:
            return "๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค"
        case .decodingError:
            return "๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค"
        case .serverError(let code):
            return "์„œ๋ฒ„ ์—๋Ÿฌ (์ฝ”๋“œ: \(code))"
        }
    }
}

// ์‚ฌ์šฉ์ž์—๊ฒŒ ์นœ์ ˆํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ œ๊ณต!

3. ์˜์กด์„ฑ ์ฃผ์ž… ๐Ÿ’‰

// ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์–ด๋ ค์šด ์ฝ”๋“œ
class UserViewModel {
    func loadUser() {
        let user = APIService.shared.fetchUser()  // ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์˜์กด์„ฑ
        // ...
    }
}

// ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์‰ฌ์šด ์ฝ”๋“œ
protocol UserServiceProtocol {
    func fetchUser() async throws -> User
}

class UserViewModel {
    private let userService: UserServiceProtocol
    
    init(userService: UserServiceProtocol = APIService.shared) {
        self.userService = userService
    }
    
    func loadUser() async {
        do {
            let user = try await userService.fetchUser()
            // ...
        } catch {
            // ์—๋Ÿฌ ์ฒ˜๋ฆฌ
        }
    }
}

// ํ…Œ์ŠคํŠธ์—์„œ Mock ๊ฐ์ฒด ์ฃผ์ž… ๊ฐ€๋Šฅ!

4. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ด€๋ฆฌ ๐Ÿ”

// Config.xcconfig ํŒŒ์ผ ์ƒ์„ฑ
API_BASE_URL = https:/$()/api.example.com
API_KEY = your_secret_key_here

// Info.plist์— ์ถ”๊ฐ€
<key>APIBaseURL</key>
<string>$(API_BASE_URL)</string>

// Swift์—์„œ ์ฝ๊ธฐ
enum Config {
    static var apiBaseURL: String {
        guard let url = Bundle.main.object(forInfoDictionaryKey: "APIBaseURL") as? String else {
            fatalError("API Base URL not found")
        }
        return url
    }
}

// ๊ฐœ๋ฐœ/์Šคํ…Œ์ด์ง•/ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ๋ณ„๋กœ ๋‹ค๋ฅธ ์„ค์ • ์‚ฌ์šฉ!

5. ๋กœ๊น… ์ „๋žต ๐Ÿ“

import os.log

enum LogCategory: String {
    case network = "Network"
    case database = "Database"
    case ui = "UI"
    case general = "General"
}

class AppLogger {
    static func log(_ message: String, category: LogCategory = .general, type: OSLogType = .default) {
        let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.app", category: category.rawValue)
        
        switch type {
        case .debug:
            logger.debug("\(message)")
        case .info:
            logger.info("\(message)")
        case .error:
            logger.error("\(message)")
        case .fault:
            logger.fault("\(message)")
        default:
            logger.log("\(message)")
        }
    }
}

// ์‚ฌ์šฉ
AppLogger.log("API ์š”์ฒญ ์‹œ์ž‘", category: .network, type: .info)
AppLogger.log("๋ฐ์ดํ„ฐ ์ €์žฅ ์‹คํŒจ", category: .database, type: .error)
iOS ๊ฐœ๋ฐœ ๋์—†๋Š” ๋ฐฐ์›€์˜ ์—ฌ์ • Swift SwiftUI Architecture Testing

๐ŸŽฌ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€... ์—ฌ๊ธฐ๊นŒ์ง€ ์ฝ์—ˆ๋‹ค๋ฉด ์ง„์งœ ๋Œ€๋‹จํ•ด ใ…‹ใ…‹ใ…‹ ์—„์ฒญ ๊ธด ๊ธ€์ด์—ˆ๋Š”๋ฐ ๋๊นŒ์ง€ ๋”ฐ๋ผ์™€์ค˜์„œ ๊ณ ๋งˆ์›Œ! ๐Ÿ˜Š

Swift์™€ SwiftUI๋กœ iOS ์•ฑ์„ ๊ฐœ๋ฐœํ•˜๋Š” ๊ฑด ์ฒ˜์Œ์—” ์–ด๋ ค์›Œ ๋ณด์ผ ์ˆ˜ ์žˆ์–ด. ๊ทผ๋ฐ ํ•˜๋‚˜์”ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ๋ฐฐ์šฐ๋‹ค ๋ณด๋ฉด ์–ด๋А์ƒˆ ๋ฉ‹์ง„ ์•ฑ์„ ๋งŒ๋“ค๊ณ  ์žˆ๋Š” ์ž์‹ ์„ ๋ฐœ๊ฒฌํ•˜๊ฒŒ ๋  ๊ฑฐ์•ผ. ๋‚˜๋„ ์ฒ˜์Œ์—” "Hello, World!" ์ถœ๋ ฅํ•˜๋Š” ๊ฒƒ๋„ ์‹ ๊ธฐํ–ˆ๋Š”๋ฐ, ์ง€๊ธˆ์€ ๋ณต์žกํ•œ ์•ฑ๋„ ๋š๋”ฑ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒŒ ๋๊ฑฐ๋“  ใ…Žใ…Ž

์ค‘์š”ํ•œ ๊ฑด ํฌ๊ธฐํ•˜์ง€ ์•Š๋Š” ๊ฑฐ์•ผ. ์—๋Ÿฌ๊ฐ€ ๋‚˜๋„, ์•ฑ์ด ํฌ๋ž˜์‹œ ๋‚˜๋„, ์ดํ•ด๊ฐ€ ์•ˆ ๋˜๋Š” ๊ฐœ๋…์ด ์žˆ์–ด๋„ ๊ณ„์† ๋„์ „ํ•˜๋‹ค ๋ณด๋ฉด ๋ฐ˜๋“œ์‹œ ์‹ค๋ ฅ์ด ๋Š˜์–ด. ์Šคํƒ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ, ๊ณต์‹ ๋ฌธ์„œ, ์ปค๋ฎค๋‹ˆํ‹ฐ๋ฅผ ์ ๊ทน ํ™œ์šฉํ•˜๊ณ , ์ž‘์€ ํ”„๋กœ์ ํŠธ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ ์ ์  ๊ทœ๋ชจ๋ฅผ ํ‚ค์›Œ๋‚˜๊ฐ€๋Š” ๊ฑฐ์ง€!

๊ทธ๋ฆฌ๊ณ  ํ˜ผ์ž ๊ณต๋ถ€ํ•˜๊ธฐ ํž˜๋“ค ๋•Œ๋Š” ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๋ฉ˜ํ† ๋ฅผ ์ฐพ์•„๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ. ์‹ค๋ฌด ๊ฒฝํ—˜์ด ์žˆ๋Š” ๊ฐœ๋ฐœ์ž์—๊ฒŒ ์ง์ ‘ ๋ฐฐ์šฐ๋ฉด ํ›จ์”ฌ ๋น ๋ฅด๊ฒŒ ์„ฑ์žฅํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ . ์ฝ”๋“œ ๋ฆฌ๋ทฐ๋„ ๋ฐ›๊ณ , ์‹ค๋ฌด ํŒ๋„ ์–ป๊ณ , ์ปค๋ฆฌ์–ด ์กฐ์–ธ๋„ ๋“ค์„ ์ˆ˜ ์žˆ์œผ๋‹ˆ๊นŒ ์ผ์„์‚ผ์กฐ์ง€ ใ…‹ใ…‹ใ…‹

์•ž์œผ๋กœ์˜ ํ•™์Šต ๋กœ๋“œ๋งต ๐Ÿ—บ๏ธ

1๋‹จ๊ณ„ (1~2๊ฐœ์›”): Swift ๊ธฐ๋ณธ ๋ฌธ๋ฒ• ์™„๋ฒฝํžˆ ์ตํžˆ๊ธฐ
๋ณ€์ˆ˜, ํ•จ์ˆ˜, ํด๋ž˜์Šค, ํ”„๋กœํ† ์ฝœ ๋“ฑ ๊ธฐ์ดˆ๋ฅผ ํƒ„ํƒ„ํžˆ!

2๋‹จ๊ณ„ (2~3๊ฐœ์›”): SwiftUI๋กœ ๊ฐ„๋‹จํ•œ ์•ฑ ๋งŒ๋“ค๊ธฐ
ํ•  ์ผ ๊ด€๋ฆฌ, ๊ณ„์‚ฐ๊ธฐ, ๋ฉ”๋ชจ์žฅ ๊ฐ™์€ ๊ธฐ๋ณธ ์•ฑ๋“ค

3๋‹จ๊ณ„ (3~4๊ฐœ์›”): API ์—ฐ๋™๊ณผ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ
๋„คํŠธ์›Œํ‚น, JSON ํŒŒ์‹ฑ, CoreData/SwiftData

4๋‹จ๊ณ„ (4~6๊ฐœ์›”): ์‹ค์ „ ํ”„๋กœ์ ํŠธ
์•ฑ์Šคํ† ์–ด์— ์ถœ์‹œํ•  ์ˆ˜ ์žˆ๋Š” ์™„์„ฑ๋„ ๋†’์€ ์•ฑ

5๋‹จ๊ณ„ (๊ณ„์†): ๊ณ ๊ธ‰ ์ฃผ์ œ์™€ ์ตœ์‹  ๊ธฐ์ˆ 
Combine, async/await, TCA, ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋“ฑ

๐ŸŽฏ ์ตœ์ข… ์กฐ์–ธ

โ€ข ๋งค์ผ ์ฝ”๋”ฉํ•˜๊ธฐ: ํ•˜๋ฃจ 30๋ถ„์ด๋ผ๋„ ๊พธ์ค€ํžˆ ํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด
โ€ข ์ž‘์€ ์„ฑ๊ณต ๊ฒฝํ—˜: ๊ฐ„๋‹จํ•œ ๊ธฐ๋Šฅ์ด๋ผ๋„ ์™„์„ฑํ•˜๋ฉด ์„ฑ์ทจ๊ฐ์ด ์ƒ๊ฒจ
โ€ข ์ปค๋ฎค๋‹ˆํ‹ฐ ์ฐธ์—ฌ: ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๋“ค๊ณผ ์†Œํ†ตํ•˜๋ฉด์„œ ๋ฐฐ์›Œ
โ€ข ์˜คํ”ˆ์†Œ์Šค ์ฝ๊ธฐ: ์ž˜ ๋งŒ๋“ค์–ด์ง„ ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด์„œ ๋ฐฐ์šฐ๋Š” ๊ฒŒ ๋งŽ์•„
โ€ข ๋ธ”๋กœ๊ทธ ์ž‘์„ฑ: ๋ฐฐ์šด ๊ฑธ ์ •๋ฆฌํ•˜๋ฉด ๋” ์˜ค๋ž˜ ๊ธฐ์–ต์— ๋‚จ์•„
โ€ข ์‹คํŒจ๋ฅผ ๋‘๋ ค์›Œํ•˜์ง€ ๋งˆ: ์—๋Ÿฌ๋Š” ๋ฐฐ์›€์˜ ๊ธฐํšŒ์•ผ!

iOS ๊ฐœ๋ฐœ์€ ์ •๋ง ์žฌ๋ฏธ์žˆ๊ณ  ๋ณด๋žŒ์ฐฌ ๋ถ„์•ผ์•ผ. ๋‚ด๊ฐ€ ๋งŒ๋“  ์•ฑ์„ ์ „ ์„ธ๊ณ„ ์‚ฌ๋žŒ๋“ค์ด ์‚ฌ์šฉํ•œ๋‹ค๋Š” ๊ฒŒ ์–ผ๋งˆ๋‚˜ ๋ฉ‹์ง„ ์ผ์ด์•ผ? ๐ŸŒ ๊ทธ๋ฆฌ๊ณ  iOS ๊ฐœ๋ฐœ์ž๋Š” ์ˆ˜์š”๋„ ๋งŽ๊ณ  ๋Œ€์šฐ๋„ ์ข‹์€ ํŽธ์ด๋ผ์„œ ์ปค๋ฆฌ์–ด ์ธก๋ฉด์—์„œ๋„ ์ข‹์€ ์„ ํƒ์ด์ง€.

์• ํ”Œ์€ ๋งค๋…„ ์ƒˆ๋กœ์šด ๊ธฐ์ˆ ๊ณผ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ๋ฐœํ‘œํ•˜๊ณ  ์žˆ์–ด. SwiftUI๋„ ๊ณ„์† ๋ฐœ์ „ํ•˜๊ณ  ์žˆ๊ณ , Vision Pro ๊ฐ™์€ ์ƒˆ๋กœ์šด ํ”Œ๋žซํผ๋„ ๋‚˜์˜ค๊ณ  ์žˆ์ž–์•„? ๊ทธ๋งŒํผ ๋ฐฐ์šธ ๊ฒŒ ๋งŽ๋‹ค๋Š” ๋œป์ด๊ธฐ๋„ ํ•˜์ง€๋งŒ, ๋™์‹œ์— ๊ธฐํšŒ๋„ ๋งŽ๋‹ค๋Š” ๊ฑฐ์•ผ. ์ง€๊ธˆ ์‹œ์ž‘ํ•˜๋ฉด ๋”ฑ ์ข‹์€ ํƒ€์ด๋ฐ์ด๋ผ๊ณ  ์ƒ๊ฐํ•ด!

์ž, ์ด์ œ Xcode๋ฅผ ์—ด๊ณ  ์ƒˆ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ด. "Hello, World!"๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ ์ ์  ๋ณต์žกํ•œ ์•ฑ์„ ๋งŒ๋“ค์–ด๊ฐ€๋Š” ๊ฑฐ์•ผ. ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ์œผ๋ฉด ๊ตฌ๊ธ€๋งํ•˜๊ณ , ์Šคํƒ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ ์ฐพ์•„๋ณด๊ณ , ๊ณต์‹ ๋ฌธ์„œ ์ฝ์–ด๋ณด๊ณ ... ๊ทธ๋ ‡๊ฒŒ ํ•˜๋‚˜์”ฉ ํ•ด๊ฒฐํ•ด๋‚˜๊ฐ€๋‹ค ๋ณด๋ฉด ์–ด๋А์ƒˆ ๋ฉ‹์ง„ iOS ๊ฐœ๋ฐœ์ž๊ฐ€ ๋˜์–ด ์žˆ์„ ๊ฑฐ์•ผ! ๐Ÿ’ช

ํ™”์ดํŒ…! ๋„ˆ๋ผ๋ฉด ์ถฉ๋ถ„ํžˆ ํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿš€โœจ

๐ŸŽ๐Ÿ“ฑ๐Ÿ’ป

Happy Coding with Swift & SwiftUI!

์ด ๊ธ€์ด ๋„์›€์ด ๋˜์—ˆ๋‹ค๋ฉด, ์ง์ ‘ ์•ฑ์„ ๋งŒ๋“ค์–ด๋ณด๋ฉด์„œ ์‹ค๋ ฅ์„ ํ‚ค์›Œ๋ณด์„ธ์š”!

Swift์™€ SwiftUI๋กœ ์—ฌ๋Ÿฌ๋ถ„๋งŒ์˜ ๋ฉ‹์ง„ ์•ฑ์„ ๋งŒ๋“ค์–ด๋ณด์„ธ์š” ๐ŸŽ‰

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.