기초
변수와 상수
변하지 않는 값에는 기본적으로 let을 사용; 변이가 필요할 때만 var로 전환. Swift는 컴파일 타임에 타입을 추론하지만, 복잡하거나 모호한 타입에는 명시적 주석이 가독성을 개선.
var name = "Alice" // mutable
let age = 30 // immutable constant
let pi: Double = 3.14159 // explicit type
var count: Int = 0
count = 10
print(type(of: age)) // Int옵셔널
옵셔널은 값의 부재를 나타냅니다. 안전한 언래핑에는 if-let을, 기본값에는 ??를, 그리고 값이 존재한다고 확신할 때만 !를 사용(위험—크래시 가능).
var nickname: String? = nil
nickname = "Al"
if let n = nickname {
print("Hi, \(n)")
}
print(nickname ?? "Anonymous") // nil-coalescing
let force: String = nickname! // force unwrap (crashes if nil)튜플
튜플은 여러 값을 단일 복합 값으로 묶습니다. 함수에서 여러 값을 반환하는 데 유용. 명명된 요소가 가독성을 개선.
let person = (name: "Alice", age: 30)
print(person.name) // Alice
print(person.0) // Alice
let (n, a) = person
print("\(n), \(a)") // Alice, 30타입 변환
Swift는 타입 간에 암묵적 변환을 하지 않습니다. 모호함을 피하고 미묘한 버그를 방지하기 위해 항상 명시적 변환(예: Double(intVal))을 사용하세요.
let intVal = 42
let doubleVal = Double(intVal)
let strVal = String(intVal)
let fromStr = Int("100")! // 100
let invalid = Int("abc") // nil
print(doubleVal, strVal)단언과 전제조건
디버깅 검사에는 assert를(릴리스 빌드에서 제거) 사용하고, 모든 빌드에서 검사되는 중요한 불변 조건에는 precondition을 사용하세요. 둘 다 논리 오류를 조기에 잡는 데 도움.
let age = -5
assert(age >= 0, "Age cannot be negative")
precondition(age >= 0, "Age must be non-negative")
// In debug builds, assert crashes if false
// precondition checks in release builds too문자열과 문자
문자열 보간
\(expr) 문자열 보간은 모든 표현식을 문자열에 삽입. 타입 안전하고 컴파일 타임에 평가되어, 형식 문자열보다 안전.
let name = "Alice"
let age = 30
let msg = "Name: \(name), Age: \(age)"
let calc = "5 + 3 = \(5 + 3)"
print(msg) // Name: Alice, Age: 30
print(calc) // 5 + 3 = 8일반 문자열 메서드
Swift 문자열은 Unicode 정확, count가 바이트가 아닌 실제 문자(grapheme cluster)를 반영. 안전을 위해 수동 인덱싱 대신 이 메서드들을 사용.
let s = "Hello, World"
print(s.count) // 12
print(s.uppercased()) // HELLO, WORLD
print(s.lowercased()) // hello, world
print(s.hasPrefix("Hello")) // true
print(s.contains("World")) // true
print(s.reversed()) // "dlroW ,olleH"Substring과 인덱싱
문자열 인덱스는 정수가 아닙니다—문자마다 바이트 크기가 다를 수 있기 때문. 탐색에는 index(_:offsetBy:)을 사용. Substring은 원본과 메모리를 공유—장기 저장에는 String으로 변환.
let s = "Hello, World"
let idx = s.index(s.startIndex, offsetBy: 7)
print(s[idx]) // W
let sub = s[idx...] // "World"
let newStr = String(sub) // convert to String
print(s.prefix(5)) // "Hello"여러 줄 문자열
삼중 따옴표 문자열은 줄 바꿈과 들여쓰기를 보존. 닫는 """가 기준 들여쓰기를 결정. HTML, JSON, 또는 긴 텍스트 블록에 이상적.
let poem = """
Roses are red,
Violets are blue,
Swift is great,
And so are you.
"""
print(poem)
// Use \\(expr) for interpolation in multiline문자열 빌딩과 분할
joined(separator:)로 구분자와 연결하고 split(separator:)로 토큰화. + 연결이 있는 수동 루프보다 더 효율적.
let parts = ["apple", "banana", "cherry"]
let joined = parts.joined(separator: ", ")
print(joined) // apple, banana, cherry
let csv = "a,b,c"
let fields = csv.split(separator: ",")
print(fields) // ["a", "b", "c"]
let reversed = String(s.reversed())데이터 구조
Array
배열은 순서가 있는 0부터 시작하는 컬렉션. 추가에는 append/insert, 변환에는 filter/map/reduce. 스레드 안전을 위해 값 타입(Array는 struct)을 선호.
var nums = [1, 2, 3]
nums.append(4)
nums.insert(0, at: 0)
nums[1] = 20
print(nums.count) // 5
print(nums.contains(20)) // true
let evens = nums.filter { $0 % 2 == 0 }
print(nums.first, nums.last)Dictionary
사전은 O(1) 평균 조회로 키-값 쌍을 저장. 누락된 키 접근 시 nil을 피하기 위해 default:를 사용. 키는 Hashable이어야 함.
var ages: [String: Int] = ["Alice": 30]
ages["Bob"] = 25
ages["Alice"] = 31
print(ages["Alice", default: 0]) // 31
for (name, age) in ages {
print("\(name): \(age)")
}
print(ages.keys.sorted())Set
집합은 O(1) 멤버십 테스트로 고유 값을 저장. 중복 제거와 집합 연산(union, intersection, difference)에 이상적. 요소는 Hashable이어야 함.
var a: Set<Int> = [1, 2, 3]
var b: Set<Int> = [3, 4, 5]
print(a.union(b)) // {1,2,3,4,5}
print(a.intersection(b)) // {3}
print(a.subtracting(b)) // {1,2}
print(a.isSubset(of: b)) // false
a.insert(6)Range
..<은 반개 구간(상한 제외), ...은 폐구간(둘 다 포함). Range는 루프, 슬라이싱, ~= 연산자와 패턴 매칭에서 유용.
for i in 0..<5 { print(i) } // 0,1,2,3,4
for i in 0...5 { print(i) } // 0,1,2,3,4,5
let nums = Array(1...5) // [1,2,3,4,5]
let prefix = nums.prefix(3) // [1,2,3]
if 5...10 ~= 7 { print("in range") }고차 함수
map은 각 요소를 변환, filter는 일치하는 요소를 선택, reduce는 모두를 하나로 결합. 이들은 Swift의 함수형 프로그래밍의 기반이며 간결하고 가독성 좋은 데이터 파이프라인을 가능하게.
let nums = [1, 2, 3, 4, 5]
let doubled = nums.map { $0 * 2 } // [2,4,6,8,10]
let evens = nums.filter { $0 % 2 == 0 } // [2,4]
let sum = nums.reduce(0, +) // 15
let strs = nums.map { String($0) } // ["1","2",...]
print(doubled, evens, sum)제어 흐름
If / Else If / Else
표준 조건 분기. 조건은 Boolean이어야 함. Swift는 Apple의 goto fail 취약점 같은 버그를 방지하기 위해 단일 문장에도 중괄호를 요구.
let score = 85
if score >= 90 {
print("A")
} else if score >= 80 {
print("B")
} else if score >= 70 {
print("C")
} else {
print("F")
}Switch(패턴 매칭)
Swift switch는 강력: 값 바인딩, 튜플, where 가드, 범위 지원. 철저해야(나머지 케이스는 default가 커버) 하고 기본적으로 폴스루하지 않음.
let point = (2, 0)
switch point {
case (0, 0):
print("origin")
case (_, 0):
print("on x-axis")
case (0, _):
print("on y-axis")
case let (x, y) where x == y:
print("on diagonal")
default:
print("elsewhere: \(point.0),\(point.1)")
}For-In 루프
For-in은 범위, 배열, 사전, 모든 Sequence를 순회. 인덱스가 필요할 때 enumerated()를 사용. 사용하지 않는 루프 변수는 _로 무시.
for i in 0..<5 { print(i) }
let fruits = ["apple", "banana"]
for (i, fruit) in fruits.enumerated() {
print("\(i): \(fruit)")
}
let dict = ["a": 1, "b": 2]
for (k, v) in dict { print("\(k)=\(v)") }
for _ in 0..<3 { print("tick") }While과 Repeat-While
while은 각 반복 전에 조건 검사; repeat-while은 후에 검사(C의 do-while처럼). 본문이 최소 한 번 실행되어야 할 때 repeat-while을 사용.
var n = 5
while n > 0 {
print(n)
n -= 1
}
var x = 0
repeat {
x += 1
} while x < 3
print(x) // 3Guard(조기 종료)
guard는 전제조건이 충족되지 않았을 때 조기 종료 제공. 언래핑된 옵셔널은 스코프의 나머지에서 사용 가능, 중첩을 줄임. 깊은 if-let 중첩보다 guard를 선호.
func greet(_ name: String?) {
guard let n = name, !n.isEmpty else {
print("No name provided")
return
}
print("Hello, \(n)")
// n is unwrapped and available here
}
greet("Alice")
greet(nil)함수와 클로저
함수 정의
가독성을 위해 인수 레이블을 생략하려면 _를 사용. 기본 매개변수 값은 매개변수를 선택적으로 만듦. 단일 표현식 함수는 암묵적 반환(Swift 5.9+).
func greet(_ name: String, greeting: String = "Hello") -> String {
return "\(greeting), \(name)!"
}
print(greet("Alice")) // Hello, Alice!
print(greet("Bob", greeting: "Hi")) // Hi, Bob!
func add(_ a: Int, _ b: Int) -> Int { a + b }
print(add(3, 4)) // 7다중 반환 값
튜플은 여러 명명된 값 반환을 가능하게. 옵셔널 튜플을 반환하면 실패 가능성을 알림. r.min이나 r.0으로 접근—명명된 요소가 더 명확.
func minMax(_ nums: [Int]) -> (min: Int, max: Int)? {
guard let first = nums.first else { return nil }
var mn = first, mx = first
for n in nums { mn = min(mn, n); mx = max(mx, n) }
return (mn, mx)
}
if let r = minMax([3, 1, 4, 1, 5]) {
print("min=\(r.min), max=\(r.max)")
}클로저
클로저는 자체 포함된 기능 블록. 단축 인수 이름으로 $0, $1을 사용. 후행 클로저 구문(마지막 인수 레이블 생략)이 map/filter/reduce에 관용적.
let square: (Int) -> Int = { x in x * x }
print(square(5)) // 25
let add: (Int, Int) -> Int = { $0 + $1 }
print(add(3, 4)) // 7
let nums = [1, 2, 3]
let doubled = nums.map { $0 * 2 }
print(doubled) // [2, 4, 6]Escaping과 Auto-Closures
@escaping은 함수 반환 후 저장되거나 호출되는 클로저를 표시(비동기 콜백에 필요). @autoclosure는 표현식을 클로저로 감싸 평가를 지연(assert에서 사용).
var handlers: [() -> Void] = []
func saveHandler(_ fn: @escaping () -> Void) {
handlers.append(fn)
}
saveHandler { print("done") }
handlers.first?()
// @autoclosure delays evaluation
func logIfTrue(_ cond: @autoclosure () -> Bool) {
if cond() { print("true") }
}
logIfTrue(2 > 1)inout 매개변수
inout 매개변수는 함수가 호출자의 변수를 수정(참조로 전달)하게 허용. 가독성을 위해 새 값을 반환하는 것을 선호—절제해서 사용. & 접두사가 변이 지점을 표시.
func swap(_ a: inout Int, _ b: inout Int) {
let temp = a
a = b
b = temp
}
var x = 10, y = 20
swap(&x, &y)
print(x, y) // 20 10클래스와 구조체
Struct(값 타입)
구조체는 값 타입—대입 시 복사. 단순 데이터 컨테이너에 사용. 계산 속성(distance)은 접근 시 계산. mutating 메서드는 self를 수정.
struct Point {
var x: Double
var y: Double
var distance: Double {
(x * x + y * y).squareRoot()
}
mutating func moveBy(dx: Double, dy: Double) {
x += dx; y += dy
}
}
var p = Point(x: 3, y: 4)
print(p.distance) // 5.0Class(참조 타입)
클래스는 참조 타입—참조로 공유, 상속과 deinit 지원. 식별성, 공유 가변 상태, 또는 Objective-C 상호운용성이 필요할 때 사용. 그 외에는 구조체를 선호.
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
deinit { print("\(name) deallocated") }
}
let p = Person(name: "Alice", age: 30)
print(p.name) // Alice상속과 Override
상위 클래스 메서드를 재정의하려면 override를 사용. Swift는 동적 디스패치를 사용하여 하위 클래스 버전이 호출. 성능을 위해 추가 재정의를 방지하려면 메서드를 final로 표시.
class Animal {
func speak() { print("...") }
}
class Dog: Animal {
override func speak() { print("Woof!") }
}
class Cat: Animal {
override func speak() { print("Meow!") }
}
let pets: [Animal] = [Dog(), Cat()]
for pet in pets { pet.speak() }속성(계산과 지연)
계산 속성은 get/set 블록을 가짐; set은 기본적으로 newValue 사용. lazy는 첫 접근까지 초기화를 지연—비싸거나 거의 필요 없는 속성에 유용. var여야 함.
class Circle {
var radius: Double
var area: Double {
get { .pi * radius * radius }
set { radius = (newValue / .pi).squareRoot() }
}
lazy var heavy = loadExpensiveData()
init(radius: Double) { self.radius = radius }
}
func loadExpensiveData() -> String { "data" }속성 관찰자
willSet/didSet이 속성 변경 관찰. didSet을 사용하여 변경을 검증하거나 반응(예: 값 클램핑, UI 업데이트 트리거). 관찰자는 init 중에 발생하지 않음.
class Counter {
var count: Int = 0 {
willSet { print("about to set \(newValue)") }
didSet {
print("was \(oldValue), now \(count)")
if count > 10 { count = 10 }
}
}
}
let c = Counter()
c.count = 5
c.count = 20 // clamped to 10프로토콜과 확장
프로토콜 정의
프로토콜은 메서드와 속성의 청사진을 정의. 타입은 이를 구현하여 준수. 추상화, 다형성, 결합 해제에 사용—Java/C#의 인터페이스와 유사.
protocol Greetable {
var name: String { get }
func greet() -> String
}
struct User: Greetable {
let name: String
func greet() -> String { "Hi, \(name)" }
}
let u = User(name: "Alice")
print(u.greet()) // Hi, Alice프로토콜 확장(기본 구현)
프로토콜 확장은 기본 구현을 제공. 이는 소급 모델링과 기반 클래스 없는 코드 재사용을 가능하게. 관련 없는 타입 간에 동작을 공유하는 강력한 기능.
protocol Describable {
func describe() -> String
}
extension Describable {
func describe() -> String { "A \(type(of: self))" }
}
struct Box: Describable { }
let b = Box()
print(b.describe()) // A Box확장
확장은 기존 타입(Int처럼 소유하지 않은 것도)에 기능을 추가. 코드 조직, 계산 속성 추가, 또는 프로토콜 준수에 사용. 저장 속성은 추가할 수 없음.
extension Int {
var squared: Int { self * self }
func times(_ block: () -> Void) {
for _ in 0..<self { block() }
}
}
print(5.squared) // 25
3.times { print("hi") } // prints hi 3 times제네릭
제네릭은 타입 안전을 보존하면서 모든 타입과 작동하는 유연하고 재사용 가능한 코드를 작성. 타입 매개변수에 <T>를 사용. 제약(where T: Equatable)은 허용 타입을 제한.
func stackOf<T>(_ items: T...) -> [T] {
var arr: [T] = []
for item in items { arr.append(item) }
return arr
}
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ e: Element) { items.append(e) }
}
let s = stackOf(1, 2, 3) // [Int]연관 타입이 있는 프로토콜
연관 타입은 프로토콜이 자리 표시자 타입을 사용하게 함, 프로토콜을 위한 제네릭처럼. 준수 타입이 실제 타입을 지정. 명시적으로 하려면 typealias를 사용하거나 Swift가 추론하게 두세요.
protocol Container {
associatedtype Item
var count: Int { get }
mutating func append(_ item: Item)
subscript(i: Int) -> Item { get }
}
struct IntStack: Container {
typealias Item = Int
private var items: [Int] = []
var count: Int { items.count }
mutating func append(_ item: Int) { items.append(item) }
subscript(i: Int) -> Int { items[i] }
}오류 처리
오류 정의와 던지기
오류는 Error 프로토콜을 준수(주로 enum). throw가 오류를 알림. 연관 값(coinsNeeded)이 컨텍스트를 전달. 실패할 수 있는 함수를 표시하려면 throws를 사용.
enum VendingError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
func vend(item: String, coins: Int) throws -> String {
guard item == "Candy" else { throw VendingError.invalidSelection }
guard coins >= 2 else { throw VendingError.insufficientFunds(coinsNeeded: 2) }
return "Dispensing \(item)"
}Do-Catch와 Try
do-catch가 던져진 오류를 처리. try가 던지는 호출을 표시. 대상 처리를 위해 특정 케이스에 패턴 매칭. catch-all이 예상치 못한 오류를 처리. 오류는 호출 스택을 따라 전파.
do {
let result = try vend(item: "Candy", coins: 1)
print(result)
} catch VendingError.insufficientFunds(let needed) {
print("Need \(needed) more coins")
} catch VendingError.outOfStock {
print("Sold out")
} catch {
print("Other error: \(error)")
}Try? / Try!
try?는 오류를 nil로 변환(Optional 반환). try!는 성공을 가정하고 오류 시 크래시—실패가 진정으로 불가능할 때만 사용. 우아한 저하를 위해 옵셔널 바인딩과 try?를 선호.
let result1 = try? vend(item: "Candy", coins: 1)
print(result1) // nil (error converted to optional)
let result2 = try! vend(item: "Candy", coins: 5)
print(result2) // Dispensing Candy (crashes if error)
if let r = try? vend(item: "Candy", coins: 5) {
print(r)
}Result 타입
Result는 성공이나 실패를 값으로 인코딩, throw 없이 비동기 오류 처리를 가능하게. 콜백, 비동기 API, 또는 결과를 저장/체이닝할 때 사용. .get()은 실패 시 throw.
enum FetchError: Error { case network, parse }
func fetch(_ url: String) -> Result<String, FetchError> {
url.isEmpty ? .failure(.network) : .success("data from \(url)")
}
switch fetch("https://api.com") {
case .success(let data): print(data)
case .failure(let err): print("Error: \(err)")
}
let value = try? fetch("").get()Defer(정리)
defer는 스코프 종료 시 실행할 정리 코드를 예약(정상 반환, throw, 오류와 무관). 자원 해제(파일, 잠금)에 사용. 여러 defer는 LIFO 순서로 실행.
func processFile(_ path: String) throws {
let handle = openFile(path)
defer { closeFile(handle) }
// ... work that might throw ...
let data = try read(handle)
// closeFile always runs, even if throw
print("processed \(data)")
}파일 I/O와 날짜/시간
파일 읽기와 쓰기
Foundation은 단순한 파일 API를 제공. write(toFile:atomically:)가 안전하게 쓰기(임시 파일 + 이름 변경). 텍스트에는 String(contentsOfFile:)을 사용. 대용량 파일의 경우 스트리밍을 위해 FileHandle을 사용.
import Foundation
let path = "test.txt"
let content = "Hello, File I/O!"
try content.write(toFile: path, atomically: true,
encoding: .utf8)
let read = try String(contentsOfFile: path, encoding: .utf8)
print(read) // Hello, File I/O!
let lines = read.split(separator: "\n")
print(lines.count)URL과 FileManager
FileManager는 파일시스템 작업(생성, 삭제, 존재 확인)을 처리. 현대 API에는 경로가 아닌 URL을 사용. .documentDirectory는 iOS/macOS에서 앱의 영구 저장소.
import Foundation
let fm = FileManager.default
let url = fm.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent("data.json")
try "data".write(to: url, atomically: true, encoding: .utf8)
let exists = fm.fileExists(atPath: url.path)
print("Exists: \(exists)")
try fm.removeItem(at: url) // deleteDate와 DateFormatter
Date는 시점을 나타냄(내부적으로 UTC). DateFormatter는 Date와 String 간 변환—로케일 버그를 피하기 위해 고정 형식 파싱에는 항상 locale을 en_US_POSIX로 설정.
import Foundation
let now = Date()
let fmt = DateFormatter()
fmt.dateFormat = "yyyy-MM-dd HH:mm:ss"
fmt.locale = Locale(identifier: "en_US_POSIX")
let str = fmt.string(from: now)
print(str) // e.g. 2024-01-15 14:30:00
let parsed = fmt.date(from: "2024-01-01 00:00:00")
print(parsed ?? "invalid")JSON 인코딩/디코딩
Codable이 JSON 직렬화를 자동화. JSONEncoder/JSONDecoder가 변환을 처리. 타입을 Codable에 준수—컴파일러가 로직을 합성. 키 이름을 사용자 정의하려면 CodingKeys를 사용.
struct User: Codable {
let name: String
let age: Int
}
let user = User(name: "Alice", age: 30)
let json = try JSONEncoder().encode(user)
let str = String(data: json, encoding: .utf8)!
print(str) // {"name":"Alice","age":30}
let decoded = try JSONDecoder().decode(User.self, from: json)
print(decoded.name) // Alice날짜 계산
Calendar가 시간대와 DST를 존중하며 날짜 산술을 처리. dateComponents로 필드를 추출하거나 차이를 계산. 날짜 수학에 원시 초를 절대 사용하지 마세요—Calendar API를 사용.
import Foundation
let cal = Calendar.current
let now = Date()
let tomorrow = cal.date(byAdding: .day, value: 1, to: now)!
let components = cal.dateComponents([.year, .month, .day], from: now)
print(components.year!, components.month!)
let diff = cal.dateComponents([.hour], from: now, to: tomorrow)
print(diff.hour!) // 24동시성과 비동기
Async / Await
async/await(Swift 5.5+)는 비동기 코드를 동기 코드처럼 읽히게. Task가 새 비동기 컨텍스트를 생성. async let이 동시에 실행하고 모두 대기. 콜백 지옥을 제거—완료 핸들러보다 선호.
func fetchUser(_ id: Int) async -> String {
try? await Task.sleep(nanoseconds: 1_000_000_000)
return "User \(id)"
}
Task {
let user = await fetchUser(42)
print(user) // User 42
async let u1 = fetchUser(1)
async let u2 = fetchUser(2)
let users = await [u1, u2] // concurrent
print(users)
}Task와 취소
Task는 비동기 작업 단위를 나타냄. Task.isCancelled를 통한 협력적 취소—장기 실행 작업은 주기적으로 확인해야. task.value가 결과를 대기. 취소는 협력적, 강제가 아님.
let task = Task {
for i in 1...100 {
if Task.isCancelled { return }
try? await Task.sleep(nanoseconds: 100_000_000)
print(i)
}
}
Task {
try? await Task.sleep(nanoseconds: 500_000_000)
task.cancel() // request cancellation
}
await task.value // wait for completionActor(스레드 안전)
Actor(Swift 5.5+)는 접근을 직렬화하여 데이터 경쟁으로부터 가변 상태를 보호. 모든 접근은 await를 거침. 공유 상태를 위해 잠금/큐 대신 사용. 컴파일러가 안전을 검증.
actor Counter {
private var count = 0
func increment() { count += 1 }
func value() -> Int { count }
}
let counter = Counter()
Task {
await counter.increment()
print(await counter.value())
}
// Actors serialize access—no data racesGCD(Grand Central Dispatch)
GCD는 전통적인 동시성 API. 백그라운드 작업에는 DispatchQueue.global(), UI 업데이트에는 .main. QoS(.userInitiated, .background)가 작업 우선순위 지정. 비동기가 아닌 코드에 여전히 유용.
import Dispatch
DispatchQueue.global(qos: .userInitiated).async {
let result = heavyComputation()
DispatchQueue.main.async {
print("UI update: \(result)")
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
print("delayed by 2s")
}Async Sequence
AsyncSequence는 비동기 값(스트림처럼)을 순회 가능. 소비에는 for await를 사용. 페이지 매김 API, 서버 전송 이벤트, 또는 시간이 지남에 따라 값을 생성하는 모든 소스에 이상적.
let urls = ["url1", "url2", "url3"]
func fetchAll() async {
for url in urls {
let data = await fetchData(url)
print(data)
}
}
func fetchData(_ url: String) async -> String {
"data from \(url)"
}
// AsyncSequence: for await item in stream { ... }프로토콜 심층 분석
연관 타입이 있는 프로토콜
연관 타입(associatedtype)은 프로토콜이 준수 타입이 지정하는 자리 표시자 타입을 선언하게 함. 이것이 프로토콜을 위한 제네릭 타입 매개변수의 Swift 동등물. 타입은 준수 타입의 메서드에서 추론. 연관 타입을 제약하려면 'where' 절을 사용. PAT(연관 타입이 있는 프로토콜)는 타입 소거 또는(Swift 5.7+) 'any Container' 없이 실존적 타입으로 직접 사용할 수 없음.
protocol Container {
associatedtype Item
var count: Int { get }
mutating func append(_ item: Item)
subscript(i: Int) -> Item { get }
}
struct IntStack: Container {
// associatedtype Item inferred as Int
private var items: [Int] = []
var count: Int { items.count }
mutating func append(_ item: Int) { items.append(item) }
subscript(i: Int) -> Int { items[i] }
}
// Generic constraint with associated type
func sum<C: Container>(_ c: C) -> Int where C.Item == Int {
var total = 0
for i in 0..<c.count { total += c[i] }
return total
}프로토콜 확장(기본 구현)
프로토콜 확장은 기본 구현을 제공—준수 타입은 메서드를 무료로 받지만 재정의 가능. 이것이 Swift가 타 입에 소급으로 기능을 추가하는 방법. 제약 확장(where Element: Numeric)이 제약을 충족하는 타입에만 메서드 추가. 이것이 표준 라이브러리가 모든 Collection에 map/filter/reduce를 추가하는 방식.
protocol Describable {
var description: String { get }
}
// Default implementation via extension
extension Describable {
var description: String { "A \(type(of: self))" }
}
struct Point: Describable {
let x, y: Int
// Uses default description: "A Point"
}
struct Person: Describable {
let name: String
var description: String { "Person named (name)" } // override
}
// Extension with constraints
extension Collection where Element: Numeric {
var sum: Element { reduce(0, +) }
}
[1, 2, 3].sum // 6프로토콜 합성과 실존적 타입
프로토콜 합성(A & B)은 값이 여러 프로토콜을 준수하도록 요구. 실존적 타입(any Protocol)은 모든 준수 타입을 보유할 수 있지만 호출당 위너스 테이블 조회의 런타임 디스패치 오버헤드가 있음. 하나의 특정 타입을 반환하지만 숨기고 싶을 때 'some Protocol'(불투명 반환 타입)을 사용. 성능을 위해 실존적보다 제네릭을 선호; 이질적 컬렉션이 필요할 때 실존적 사용.
protocol Named { var name: String { get } }
protocol Aged { var age: Int { get } }
struct Person: Named, Aged {
let name: String
let age: Int
}
// Protocol composition: requires both protocols
func wishHappyBirthday(to celebrant: Named & Aged) {
print("Happy birthday, (celebrant.name), you're (celebrant.age)!")
}
let p = Person(name: "Alice", age: 30)
wishHappyBirthday(to: p)
// Existential types (any) — type erasure overhead
let items: [any Named] = [p, Pet(name: "Rex")]
for item in items { print(item.name) }
// Swift 5.7: 'any' keyword required for existentials
// 'some' for opaque types (returns one concrete type)프로토콜 지향 프로그래밍
프로토콜 지향 프로그래밍은 OOP에서의 Swift의 패러다임 전환. 클래스 계층 대신 기본 구현이 있는 프로토콜 중심으로 설계. 이점: 값 타입(struct/enum)과 작동, 소급 준수 지원(소유하지 않은 타입 확장), 다중 '상속' 가능(타입이 여러 프로토콜 준수 가능). 클래스는 참조 의미론과 Objective-C 상호운용성에 여전히 유용하지만, 대부분의 모델에는 struct+프로토콜이 선호.
// POP: design around protocols, not inheritance
protocol Drawable {
func draw(on canvas: Canvas)
}
extension Drawable {
// Default implementation + polymorphism
func drawTwice(on canvas: Canvas) {
draw(on: canvas)
draw(on: canvas)
}
}
struct Circle: Drawable { let radius: Double
func draw(on canvas: Canvas) { /* ... */ }
}
struct Square: Drawable { let side: Double
func draw(on canvas: Canvas) { /* ... */ }
}
// Value types + protocols = no inheritance needed
let shapes: [any Drawable] = [Circle(radius: 5), Square(side: 3)]
shapes.forEach { $0.draw(on: canvas) }
// Retroactive modeling (extend types you don't own)
extension Int: Drawable {
func draw(on canvas: Canvas) { /* draw the number */ }
}사용자 정의 프로토콜 위트니스
의존성으로서의 프로토콜(Repository)이 테스트와 유연성을 위해 구현을 교체(실제 vs 목) 가능하게. 연관 타입이 프로토콜을 제네릭으로 만듦. 이것이 Swift의 의존성 주입—프로토콜 준수 타입 주입. '프로토콜 위트니스'는 구체적 동작을 제공하는 준수 타입. 이 패턴(저장소, 데이터 소스)은 Swift 아키텍처(VIPER, Clean Architecture)에서 흔함.
// Protocol with requirements
protocol Repository {
associatedtype Entity
func get(_ id: Int) -> Entity?
func save(_ entity: Entity)
}
// Concrete implementation
struct UserRepo: Repository {
typealias Entity = User
func get(_ id: Int) -> User? { /* fetch */ nil }
func save(_ user: User) { /* persist */ }
}
// Generic function using protocol
func display<R: Repository>(_ repo: R, id: Int) where R.Entity: CustomStringConvertible {
if let entity = repo.get(id) {
print(entity.description)
}
}
// Test double for testing
struct MockUserRepo: Repository {
typealias Entity = User
var stored: [Int: User] = [:]
func get(_ id: Int) -> User? { stored[id] }
func save(_ user: User) { /* no-op for tests */ }
}제네릭 심층 분석
제네릭 함수와 타입
제네릭은 타입 안전을 보존하면서 모든 타입과 작동하는 유연하고 재사용 가능한 코드를 작성. T는 타입 매개변수(자리 표시자). 컴파일러가 성능을 위해 특수화된 버전 생성(박싱 없음). 제네릭 타입(Stack<T>)은 타입 매개변수를 유지. Element는 Array의 제네릭 타입. 제네릭은 컴파일 타임에 해결—실존적과 달리 런타임 오버헤드 없음.
// Generic function
func swap<T>(_ a: inout T, _ b: inout T) {
let temp = a; a = b; b = temp
}
var x = 1, y = 2
swap(&x, &y) // x=2, y=1
// Generic type
struct Stack<T> {
private var items: [T] = []
mutating func push(_ item: T) { items.append(item) }
mutating func pop() -> T? { items.popLast() }
}
var intStack = Stack<Int>()
intStack.push(42)
var strStack = Stack<String>()
strStack.push("hello")
// Generic method on non-generic type
extension Array {
func chunked(into size: Int) -> [[Element]] {
stride(from: 0, to: count, by: size).map {
Array(self[$0..<Swift.min($0 + size, count)])
}
}
}타입 제약
타입 제약은 사용할 수 있는 타입을 제한: T: Equatable은 T가 프로토콜을 준수하도록 요구, T: SomeClass는 클래스 계층을 요구. where 절이 더 복잡한 제약(예: 연관 타입 매칭)을 추가. 제약은 프로토콜의 메서드(== for Equatable, < for Comparable)를 사용하게 함. 제약이 없으면 T를 할당하고 전달만 가능—연산 없음.
// Constraint: T must conform to Equatable
func indexOf<T: Equatable>(_ item: T, in array: [T]) -> Int? {
for (i, elem) in array.enumerated() {
if elem == item { return i } // needs Equatable
}
return nil
}
// Multiple constraints
func max<T: Comparable>(_ a: T, _ b: T) -> T {
return a > b ? a : b
}
// Constraint on associated type
protocol Repository {
associatedtype Entity: Identifiable
func find(_ id: Entity.ID) -> Entity?
}
// where clause for complex constraints
func merge<C1: Collection, C2: Collection>(
_ c1: C1, _ c2: C2
) -> [C1.Element] where C1.Element == C2.Element {
Array(c1) + Array(c2)
}불투명 타입(some)
불투명 타입(some Protocol, Swift 5.1)은 호출자로부터 숨겨진 특정 구체적 타입을 반환. 'any'(실존적)와 달리 타입이 고정되고 컴파일러에 알려짐—박싱 없음, 컴파일 타임 디스패치. 이것이 SwiftUI의 'some View'의 기반. 구체적 타입을 숨기면서 성능을 유지하고 싶을 때 'some'을 사용. 호출자는 프로토콜 메서드를 사용할 수 있지만 특정 타입에 의존할 수 없음.
// Opaque return type: returns ONE concrete type
// (hidden from caller, but fixed per call site)
func makeStack() -> some Collection<Int> {
return [1, 2, 3] // concrete type is [Int]
}
let s = makeStack() // type is 'some Collection<Int>'
print(s.count) // 3
// Useful for hiding implementation details
protocol Shape { func draw() }
struct Circle: Shape { func draw() { print("circle") } }
func makeShape() -> some Shape {
return Circle() // caller doesn't know it's Circle
}
// Difference from existential (any):
// - some: one concrete type, no boxing, compile-time dispatch
// - any: can be different types, runtime dispatch, boxing
// Opaque types in properties
struct View {
var body: some View { Text("Hello") } // SwiftUI
}조건부 준수
조건부 준수는 타입 매개변수가 특정 조건을 충족할 때만 프로토콜을 준수하게 함. Array<Int>는 Equatable인데 Int가 Equatable이기 때문; Array<MyStruct>는 MyStruct가 Equatable이 아니면 아님. 이는 전파: [[Int]]는 [Int]가 Equatable이므로 Equatable. 표준 라이브러리가 이를 광범위하게 사용—Array, Optional, Dictionary 모두 Equatable, Hashable, Codable에 조건부로 준수.
// Array conforms to Equatable only if Element is Equatable
extension Array: Equatable where Element: Equatable {
static func == (lhs: [Element], rhs: [Element]) -> Bool {
return lhs.count == rhs.count && lhs.elementsEqual(rhs)
}
}
[1, 2, 3] == [1, 2, 3] // true (Int: Equatable)
// [SomeStruct] == [SomeStruct] // error if SomeStruct isn't Equatable
// Conditional conformance for Hashable
extension Stack: Equatable where T: Equatable {
static func == (lhs: Stack, rhs: Stack) -> Bool {
return lhs.items == rhs.items
}
}
// Enables: [Stack<Int>] == [Stack<Int>] (transitively)Result와 제네릭 오류 처리
Result<Success, Failure>는 실패할 수 있는 연산을 위한 제네릭 enum, 값이나 오류 중 하나를 전달. 오류 처리를 지연하거나 결과를 저장할 때 유용. map은 성공 값을 변환; flatMap은 실패할 수도 있는 연산을 체인. Result { try ... }가 던지는 함수를 변환. .get()은 다시 throw로 변환. async/await로 Result는 덜 필요하지만 저장/전달된 오류에 여전히 유용.
// Result: built-in generic for success/failure
enum Result<Success, Failure: Error> {
case success(Success)
case failure(Failure)
}
func fetchUser(_ id: Int) -> Result<User, FetchError> {
if id < 0 { return .failure(.invalidId) }
return .success(User(id: id))
}
switch fetchUser(42) {
case .success(let user): print(user)
case .failure(let err): print(err)
}
// map/flatMap for chaining
let result = fetchUser(1)
.map { $0.name }
.flatMap { name in fetchAvatar(name) }
// Throwing function → Result
let result = Result { try throwingFunction() }
// Result → throwing
let value = try result.get()ARC와 메모리 관리
자동 참조 카운팅(ARC)
ARC(자동 참조 카운팅)는 클래스 인스턴스(참조 타입)의 메모리를 관리. 각 강한 참조가 카운트를 증가; 제거가 감소. 카운트가 0에 도달하면 객체가 즉시 해제(가비지 컬렉션과 달리 결정적). struct/enum(값 타입)은 ARC를 사용하지 않음—복사됨. ARC는 참조 순환을 처리할 수 없음—weak/unowned로 끊어야 함.
class Person {
let name: String
init(name: String) { self.name = name; print("\(name) initialized") }
deinit { print("\(name) deinitialized") }
}
var p1: Person? = Person(name: "Alice") // "Alice initialized"
var p2 = p1 // strong ref count = 2
p1 = nil // ref count = 1, not deinitialized
p2 = nil // ref count = 0, "Alice deinitialized"
// ARC tracks strong references:
// - +1 on assignment/copy
// - -1 when variable goes out of scope
// - Object freed when count hits 0
// Unlike GC, deterministic (freed immediately when count=0)Strong, Weak, Unowned 참조
Strong 참조(기본)는 객체를 살아있게 유지. weak 참조는 객체를 살아있게 유지하지 않고 객체가 해제되면 nil이 됨(optional var여야 함). unowned 참조는 객체를 살아있게 유지하지 않지만 non-optional—참조된 객체가 참조보다 오래 살 것이라고 확신할 때만 사용(해제 후 접근 시 크래시). delegate 패턴에는 weak를; 부모가 항상 자식보다 오래 사는 부모-자식에는 unowned를 사용.
class Person {
var name: String
var apartment: Apartment? // strong
init(name: String) { self.name = name }
deinit { print("\(name) deinit") }
}
class Apartment {
var unit: String
weak var tenant: Person? // weak breaks cycle
init(unit: String) { self.unit = unit }
deinit { print("Apartment \(unit) deinit") }
}
var alice: Person? = Person(name: "Alice")
var apt: Apartment? = Apartment(unit: "4A")
alice?.apartment = apt
apt?.tenant = alice // weak, doesn't keep alice alive
alice = nil // "Alice deinit" (no strong cycle)
apt = nil // "Apartment 4A deinit"
// unowned: non-optional, assumed to always have a value
// Use when the referenced object outlives the reference
class Customer {
var card: CreditCard? // strong
}
class CreditCard {
unowned let owner: Customer // non-nil, non-optional
init(owner: Customer) { self.owner = owner }
}순환 유지와 클로저
클로저는 기본적으로 참조를 강하게 캡처. 클래스가 self를 캡처하는 클로저를 저장하면 순환 유지가 있음(둘 다 서로를 살아있게 유지 → 메모리 누수). 캡처 목록으로 해결: [weak self](self가 optional, guard let 사용) 또는 [unowned self](non-optional, nil이면 크래시). 속성으로 저장되거나 오래 사는 객체(관찰자, 비동기 작업)에 전달된 클로저에는 항상 weak self를 사용.
class ViewModel {
var name = "VM"
var onUpdate: (() -> Void)?
func setup() {
// BAD: closure captures self strongly → cycle
onUpdate = {
print(self.name) // strong capture
}
// ViewModel holds closure, closure holds ViewModel → leak!
// GOOD: capture list with weak self
onUpdate = { [weak self] in
guard let self = self else { return }
print(self.name)
}
// GOOD: unowned if self outlives closure
onUpdate = { [unowned self] in
print(self.name)
}
}
deinit { print("VM deinit") }
}Escaping과 Non-escaping 클로저
Non-escaping 클로저(기본)는 함수 내에서 실행되고 버려짐—순환 유지 위험 없음, 클로저에 self. 필요 없음. @escaping 클로저는 함수보다 오래 살음(속성에 저장, 비동기로 디스패치 등)—순환 유지를 일으킬 수 있으므로 self.를 명시적으로 사용하고 [weak self]를 고려해야. 컴파일러가 이를 강제. 대부분의 완료 핸들러는 @escaping; map/filter/reduce는 non-escaping.
class DataLoader {
var storedHandler: (() -> Void)?
// @escaping: closure outlives the function (stored or async)
func load(with handler: @escaping (Data) -> Void) {
storedHandler = { handler(cachedData) } // stored
DispatchQueue.global().async {
handler(self.fetch()) // escapes via async
}
}
// Non-escaping (default): closure runs during the function
func process(_ transform: (Int) -> Int) -> [Int] {
return [1, 2, 3].map(transform) // runs now, no storage
}
// No need for [weak self]—self is alive during the call
}
// @escaping requires explicit self. in closures
class View {
func load() {
DataLoader().load { [self] data in // self. required
print(data)
}
}
}메모리 안전과 배타성
Swift는 배타성을 강제—변수가 겹치는 스코프에서 동시에 접근(읽기+쓰기 또는 쓰기+쓰기)될 수 없음. 이는 데이터 경쟁과 정의되지 않은 동작을 방지. inout 매개변수는 호출 기간 동안 배타적 접근. struct mutating 메서드는 self에 배타적 접근 보유. Actor(Swift 5.5+)은 공유 가변 상태에 대해 언어 수준에서 배타성을 강제, 설계상 데이터 경쟁을 제거.
// Exclusivity: Swift prevents overlapping access to a variable
// This is a compile-time or runtime error:
var step = 1
func increment(_ n: inout Int) {
step += 1 // accessing global while n is inout to step
}
// increment(&step) // ERROR: overlapping access
// Fix with explicit copy
func safeIncrement(_ n: inout Int) {
var copy = step
copy += 1
step = copy
}
// Struct mutating methods have exclusive access
struct Counter {
var count = 0
mutating func increment(_ other: inout Counter) {
count += other.count // ok, different instances
}
}
// 'inout' parameters: exclusive access for duration
// 'isolation' (Swift 5.5+): actors enforce exclusivitySwiftUI 기초
View와 Modifier
SwiftUI는 Apple의 선언적 UI 프레임워크. View는 'body' 속성과 함께 View를 준수하는 struct. Modifier(.font, .padding)는 새로 감싼 view를 반환—변이하지 않음. View는 가벼운 값 타입; SwiftUI가 실제 UI를 업데이트하기 위해 diff. 선언적 스타일은 주어진 상태에 대해 UI가 어떻게 보여야 하는지를 설명하고, SwiftUI가 전환을 처리.
import SwiftUI
struct ContentView: View {
var body: some View {
VStack(spacing: 20) {
Text("Hello, World!")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.blue)
Button(action: { print("tapped") }) {
HStack {
Image(systemName: "star.fill")
Text("Tap me")
}
}
.padding()
.background(Color.orange)
.cornerRadius(10)
}
.padding()
}
}
// Views are value types (structs), declarative
// Modifiers return new views (immutability)State와 Binding
@State는 로컬 가변 view 상태용—변경 시 SwiftUI가 view를 재렌더링. @Binding은 $ 접두사로 하위 view가 부모의 @State를 읽기/쓰기 가능(바인딩 생성). State는 단일 진실 소스여야 함; 수정할 값을 바인딩으로 전달. view 간 공유/복잡 상태에는 ObservableObject와 함께 @StateObject/@ObservedObject/@EnvironmentObject를 사용.
struct CounterView: View {
@State private var count = 0 // local, mutable state
var body: some View {
VStack {
Text("Count: \(count)")
Button("Increment") { count += 1 }
}
}
}
// @Binding: child receives a reference to parent's state
struct StepperView: View {
@Binding var value: Int
var body: some View {
HStack {
Button("-") { value -= 1 }
Text("\(value)")
Button("+") { value += 1 }
}
}
}
// Parent passes binding with $
struct ParentView: View {
@State var score = 0
var body: some View {
StepperView(value: $score) // $ creates a Binding
}
}ObservableObject와 EnvironmentObject
ObservableObject(클래스)는 여러 view 간 공유 상태용. @Published 속성이 변경 시 view 업데이트 트리거. @StateObject가 객체를 생성하고 소유(계층 상단에서 사용). @EnvironmentObject가 객체를 view 트리에 주입—바인딩을 통해 전달 없이 모든 하위 항목이 접근 가능. @ObservedObject는 기존 객체를 받음(소유하지 않음). 소유에는 @StateObject를, 받는 데는 @ObservedObject/@EnvironmentObject를 사용.
// Model: ObservableObject for shared state
class AppModel: ObservableObject {
@Published var username: String = "" // @Published triggers UI update
@Published var isLoggedIn: Bool = false
func login() {
// ... auth logic ...
isLoggedIn = true
}
}
// @StateObject: owns the model (created once)
struct RootView: View {
@StateObject var model = AppModel()
var body: some View {
// Inject into environment
ContentView().environmentObject(model)
}
}
// @EnvironmentObject: receives from environment
struct ProfileView: View {
@EnvironmentObject var model: AppModel
var body: some View {
Text("Hello, \(model.username)")
}
}
// @ObservedObject: receives from parent (doesn't own)
struct LoginView: View {
@ObservedObject var model: AppModel
}List와 Navigation
List가 스크롤 가능한 행을 렌더링(UITableView처럼). 항목은 Identifiable을 준수해야(또는 id: 제공). NavigationStack(iOS 16+)이 탐색 관리; NavigationLink가 목적지를 푸시. LazyVStack/LazyHStack이 성능을 위해 콘텐츠를 지연 로드. ForEach는 다른 컨테이너 내부에 반복 view용. 표 형식 데이터에는 List를, 사용자 정의 레이아웃에는 ScrollView+LazyVStack을 사용.
struct Item: Identifiable {
let id = UUID()
let name: String
}
struct ListView: View {
let items = [Item(name: "Apple"), Item(name: "Banana")]
var body: some View {
NavigationStack {
List(items) { item in
NavigationLink(item.name) {
DetailView(item: item)
}
}
.navigationTitle("Fruits")
}
}
}
struct DetailView: View {
let item: Item
var body: some View {
Text("Detail: \(item.name)")
.navigationTitle(item.name)
}
}
// ForEach for custom layouts
ScrollView {
LazyVStack {
ForEach(items) { item in
Text(item.name)
}
}
}Form과 Sheet
Form이 설정 화면을 위해 컨트롤을 자동으로 스타일링(그룹화, 플랫폼 적절). 일반 컨트롤: TextField, Toggle, Slider, Picker, Stepper. .sheet가 모달 제시; .fullScreenCover는 전체 화면용. 바인딩($)이 컨트롤을 상태에 연결. Form은 플랫폼에 적응(iOS 그룹화 목록, macOS form 레이아웃). 데이터 입력과 설정에는 Form을; 사용자 정의 레이아웃에는 VStack을 사용.
struct SettingsView: View {
@State private var name = ""
@State private var notifications = true
@State private var volume: Double = 0.5
@State private var theme = "Light"
@State private var showSheet = false
var body: some View {
Form {
Section("Profile") {
TextField("Name", text: $name)
Toggle("Notifications", isOn: $notifications)
}
Section("Audio") {
Slider(value: $volume, in: 0...1)
}
Section("Appearance") {
Picker("Theme", selection: $theme) {
Text("Light").tag("Light")
Text("Dark").tag("Dark")
}
}
Button("Show Modal") { showSheet = true }
}
.sheet(isPresented: $showSheet) {
Text("Modal content")
}
}
}클로저 심층 분석
클로저 구문 변형
클로저는 많은 구문 형태를 가짐. 전체 형태: { (params) -> Type in body }. 타입 추론으로 타입 생략 가능. 단축 인수($0, $1)가 명명된 매개변수 대체. 후행 클로저 구문은 마지막 인수일 때 클로저를 () 밖으로 이동. 여러 후행 클로저(Swift 5.3+)는 추가 클로저에 이름 부여. 가독성이 좋은 가장 간결한 형태를 사용—단축 인수는 map 같은 짧은 클로저에 적합.
// Full closure
let add: (Int, Int) -> Int = { (a: Int, b: Int) -> Int in
return a + b
}
// Type inference
let add2: (Int, Int) -> Int = { a, b in a + b }
// Shorthand argument names ($0, $1...)
let add3: (Int, Int) -> Int = { $0 + $1 }
// Trailing closure syntax
let result = [1, 2, 3].map { $0 * 2 } // [2, 4, 6]
// Multiple trailing closures (Swift 5.3+)
Button {
print("action") // action
} label: {
Text("Click") // label
}
// No return for Void closures
let log: (String) -> Void = { msg in print(msg) }값 캡처
클로저는 둘러싼 스코프에서 변수를 캡처. 기본적으로 참조로 캡처(캡처된 변수의 변경이 보임). 캡처 목록 [foo]는 값으로 캡처(클로저 생성 시 스냅샷). 참조 타입(클래스)의 경우 [weak self] 또는 [unowned self]가 순환 유지를 끊음. 값 타입(struct)을 참조로 캡처하면 클로저가 box를 보유하므로 업데이트가 보임. 캡처 목록은 매개변수 목록 앞에 옴.
// Closures capture surrounding state
func makeIncrementer(amount: Int) -> () -> Int {
var total = 0
return {
total += amount // captures total and amount
return total
}
}
let inc = makeIncrementer(amount: 5)
inc() // 5
inc() // 10
// total persists between calls (captured by reference)
// Capture list controls how
class Foo { var x = 0 }
let foo = Foo()
let closure = { [foo] in // captures foo by value (snapshot)
print(foo.x)
}
foo.x = 100
closure() // prints 0 (captured the old value)
// Without capture list, foo is captured by reference
let refClosure = { print(foo.x) }
foo.x = 200
refClosure() // prints 200Escaping과 Autoclosure
@autoclosure는 표현식을 0-인수 클로저로 자동 감싸, 호출자가 중괄호 없이 표현식을 직접 작성. 이는 지연 평가를 가능하게—assert()가 릴리스 빌드에서 조건을 건너뛰는 데 사용. @escaping과 결합하면 평가를 지연 가능. &&와 || 연산자가 단락 평가를 위해 @autoclosure 사용. @autoclosure는 절제해서 사용—코드가 지연된다는 것을 숨겨 읽는 사람을 혼란스럽게 할 수 있음.
// @autoclosure: wraps an expression in a closure automatically
func log(_ condition: @autoclosure () -> Bool, _ message: String) {
if condition() { // evaluated here, not at call
print(message)
}
}
log(2 > 1, "two is greater") // no { } needed
// assert uses @autoclosure to avoid evaluating in release
assert(isValid, "Invalid state") // isValid not checked in release
// @autoclosure + @escaping
func deferred(_ block: @autoclosure @escaping () -> Int) {
DispatchQueue.main.async { print(block()) }
}
deferred(expensiveComputation()) // runs later, on main
// Short-circuit evaluation with @autoclosure
func &&(lhs: @autoclosure () -> Bool, rhs: @autoclosure () -> Bool) -> Bool {
return lhs() ? rhs() : false // rhs not evaluated if lhs is false
}고차 함수
Swift의 고차 함수(map, filter, reduce, flatMap, compactMap)가 컬렉션에 함수형 프로그래밍을 가능하게. map은 변환, filter는 선택, reduce는 집계, flatMap은 평탄화, compactMap은 nil 제거. 이들은 새 컬렉션을 반환(불변성). 체이닝이 연산을 조합. 대용량 데이터셋의 경우 중간 배열을 피하기 위해 .lazy를 사용. 이들은 함수형 Swift의 기반—데이터 변환 시 명령형 for 루프보다 선호.
let nums = [1, 2, 3, 4, 5, 6]
// map: transform each element
let doubled = nums.map { $0 * 2 } // [2, 4, 6, 8, 10, 12]
// filter: keep elements matching predicate
let evens = nums.filter { $0 % 2 == 0 } // [2, 4, 6]
// reduce: combine all into one
let sum = nums.reduce(0, +) // 21
let product = nums.reduce(1, *) // 720
// flatMap: transform + flatten
let nested = [[1, 2], [3, 4]]
let flat = nested.flatMap { $0 } // [1, 2, 3, 4]
// compactMap: transform + filter nils
let strings = ["1", "abc", "3"]
let ints = strings.compactMap { Int($0) } // [1, 3]
// Chaining (lazy evaluation)
let result = nums
.filter { $0 % 2 == 0 }
.map { $0 * $0 }
.reduce(0, +) // 4 + 16 + 36 = 56완료 핸들러로서의 클로저
완료 핸들러는 async/await(Swift 5.5) 이전의 표준 비동기 패턴. @escaping(나중에 실행)이며 오류 처리를 위해 종종 Result 사용. 단점은 중첩 콜백('파멸의 피라미드'). async/await가 이를 선형적이고 가독성 좋게 만듦. 새 코드는 async/await를 사용해야; 완료 핸들러는 delegate API와 Objective-C 상호운용성에 남음. 완료 핸들러를 withCheckedContinuation으로 감싸 async/await와 함께 사용 가능.
// Completion handler pattern (pre-async/await)
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data,
let user = try? JSONDecoder().decode(User.self, from: data) else {
completion(.failure(DecodeError()))
return
}
completion(.success(user))
}.resume()
}
// Usage (pyramid of doom)
fetchUser(id: 42) { result in
switch result {
case .success(let user):
fetchAvatar(user) { avatarResult in
// nested...
}
case .failure(let error):
print(error)
}
}
// Modern: async/await replaces this
func fetchUserAsync(id: Int) async throws -> User {
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}Property Wrapper
Property Wrapper 정의
Property wrapper(@propertyWrapper)는 재사용 가능한 속성 동작을 캡슐화. wrappedValue 속성이 사용자가 상호작용하는 것. init은 초기값과 사용자 정의 인수를 받음. SwiftUI가 property wrapper를 광범위하게 사용: @State, @Binding, @Published, @AppStorage. 검증, 캐싱, 로깅, 또는 기본값을 위해 자신만의 것을 정의. wrapper는 저장소를 관리하는 struct/class.
@propertyWrapper
struct Clamped<Value: Comparable> {
var value: Value
let range: ClosedRange<Value>
var wrappedValue: Value {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
init(wrappedValue: Value, _ range: ClosedRange<Value>) {
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
self.range = range
}
}
// Usage
struct Slider {
@Clamped(0...100) var progress: Int = 50
}
var s = Slider()
s.progress = 150 // clamped to 100
s.progress = -10 // clamped to 0
print(s.progress) // 0투사된 값
projectedValue($로 접근)는 속성에 대한 보조 인터페이스. SwiftUI가 이를 많이 사용: @State의 $가 하위 view를 위한 Binding 제공, @Published의 $가 Publisher 제공. wrapper가 감싼 값 이외의 것(바인딩, 퍼블리셔, wrapper 자체)을 노출해야 할 때 projectedValue를 정의. $ 접두사 구문이 이를 인체공학적으로 만듦. 모든 wrapper가 projectedValue가 필요한 것은 아님—선택 사항.
@propertyWrapper
struct Observable<T> {
var value: T
var wrappedValue: T {
get { value }
set { value = newValue; onChange() }
}
var onChange: () -> Void = {}
// projectedValue: accessed via $ (like @State's binding)
var projectedValue: Observable<T> { self }
}
struct View {
@Observable var count = 0
// count: the wrapped value (Int)
// $count: the projected value (Observable<Int>)
}
// SwiftUI's @State projects a Binding
struct Counter: View {
@State var value = 0
var body: some View {
Stepper("Value: \(value)", value: $value) // Binding<Int>
}
}내장 Property Wrapper
SwiftUI가 많은 property wrapper를 제공: @State(로컬 상태), @Binding(부모 상태), @ObservedObject/@StateObject(외부 모델), @EnvironmentObject(주입된 모델), @AppStorage(UserDefaults), @SceneStorage(장면 복원), @FocusState(키보드 포커스), @ScaledMetric(Dynamic Type), @Namespace(애니메이션). 각각 저장소와 수명주기를 관리. Combine의 @Published가 UI 업데이트를 트리거. 언제 무엇을 사용할지 아는 것이 SwiftUI의 핵심.
import SwiftUI
import Combine
struct ContentView: View {
@State var count = 0 // local view state
@Binding var externalValue: Int // parent-owned
@EnvironmentObject var model: AppModel // injected
@AppStorage("username") var username = "" // UserDefaults
@SceneStorage("draft") var draft = "" // per-scene storage
@FocusState var isFocused: Bool // focus management
@ScaledMetric var size = 16 // Dynamic Type scaling
@Namespace var animationNamespace // matched geometry
// Combine
@Published var data = [Item]() // in ObservableObject
}
// @AppStorage auto-syncs with UserDefaults
// @SceneStorage restores state per scene (iPad multitasking)검증을 위한 사용자 정의 Wrapper
Property wrapper는 횡단 관심사에 빛을 발. NonEmpty는 설정 시 검증. Cached는 지연 계산하고 메모이제이션. 다른 아이디어: Logged(변경 로깅), Trimmed(공백 제거), Formatted(get 시 형식화), Clamped(범위 제한), UserDefaults 기반. wrapper가 보일러플레이트를 줄임—논리를 한 번 작성하고 @로 적용. 조합 가능: @Logged @Clamped(0...100) var value. wrapper를 하나의 관심사에 집중시키세요.
@propertyWrapper
struct NonEmpty {
private var value: String = ""
var wrappedValue: String {
get { value }
set {
guard !newValue.isEmpty else {
print("Warning: cannot set to empty")
return
}
value = newValue
}
}
init(wrappedValue: String) {
self.wrappedValue = wrappedValue
}
}
@propertyWrapper
struct Cached<T> {
private var cached: T?
private var generator: () -> T
var wrappedValue: T {
mutating get {
if cached == nil { cached = generator() }
return cached!
}
}
init(wrappedValue: @autoclosure @escaping () -> T) {
self.generator = wrappedValue
}
}
struct Config {
@NonEmpty var name: String = "default"
@Cached var expensiveValue: Int = computeExpensive()
}프로토콜과 제네릭의 Property Wrapper
Property wrapper는 제네릭일 수 있고 프로토콜에서 사용 가능. 그러나 제한이 있음: 감싼 속성이 있는 struct는 멤버별 이니셜라이저를 받지 못함(init을 수동으로 작성해야), 복사가 까다로울 수 있음(wrapper 상태 포함 복사). 클래스의 경우 덜 문제. 제한에도 불구하고 wrapper는 선언적이고 재사용 가능한 속성 동작에 강력. SwiftUI의 설계가 이에 크게 의존.
// Apply to protocol requirements
protocol Configurable {
@NonEmpty var name: String { get set }
}
// Generic property wrapper
@propertyWrapper
struct Validated<T> {
private var value: T?
let validator: (T) -> Bool
var wrappedValue: T? {
get { value }
set {
if let v = newValue, validator(v) { value = v }
}
}
init(wrappedValue: T?, validator: @escaping (T) -> Bool) {
self.validator = validator
self.wrappedValue = wrappedValue
}
}
struct Form {
@Validated(validator: { $0.count >= 8 }) var password: String? = nil
}
// Limitation: property wrappers in structs can't be
// initialized from another instance easily (copy issues).
// They work best as stored properties, not computed.Combine 프레임워크
Publisher와 Subscriber
Combine은 Apple의 반응형 프레임워크(RxSwift처럼). Publisher가 값을 방출; Subscriber가 받음. sink가 클로저로 subscriber를 생성. assign이 출력을 속성에 바인딩. 연산자(map, filter, reduce)가 publisher를 선언적으로 변환. Combine은 선언적—파이프라인을 설명하고 값이 흘러감. 비동기 작업, UI 바인딩, 이벤트 처리에 사용. 현대 Swift는 async/await를 선호하지만 복잡한 파이프라인에 Combine은 남음.
import Combine
// Publisher: emits values over time
let publisher = [1, 2, 3].publisher
// Subscriber: receives values
publisher.sink { value in
print(value) // 1, 2, 3
}
// With completion
publisher.sink(receiveCompletion: { completion in
print("done: \(completion)")
}, receiveValue: { value in
print(value)
})
// Assign to a property
class ViewModel {
var data: String = ""
}
let vm = ViewModel()
Just("hello").assign(to: &vm.data)
// Operators transform publishers
[1, 2, 3, 4].publisher
.map { $0 * 2 }
.filter { $0 > 4 }
.sink { print($0) } // 6, 8@Published와 ObservableObject
@Published는 속성을 감싸고 변경될 때마다 새 값을 subscriber에게 방출. $ 접두사로 publisher에 접근. 이것이 Combine과 SwiftUI를 연결—ObservableObject의 @Published가 view 업데이트를 트리거. 구독은 AnyCancellable이 할당 해제될 때 취소되므로(예: Set에) 저장. 디바운싱, 스로틀링, 여러 비동기 소스 결합—async/await가 덜 우아하게 처리하는 것들에 Combine을 사용.
class UserStore: ObservableObject {
@Published var name: String = "" // publishes on change
@Published var age: Int = 0
// $name is a Publisher<String, Never>
func demo() {
$name.sink { print("name changed to \($0)") }
name = "Alice" // prints "name changed to Alice"
}
}
// Combine with SwiftUI
struct ProfileView: View {
@ObservedObject var store: UserStore
var body: some View {
Text(store.name) // updates when name changes
}
}
// Cancellable: store subscriptions to keep them alive
class Service {
var cancellables = Set<AnyCancellable>()
let store = UserStore()
init {
store.$name
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.sink { name in saveToServer(name) }
.store(in: &cancellables) // keep alive
}
}연산자
Combine 연산자가 publisher를 변환, 필터, 결합, 시간 제어. map/scan은 변환; filter/선택자는 선택; merge/zip/combineLatest는 여러 스트림 결합; debounce/throttle은 시간 제어. 연산자는 새 publisher를 반환(불변성, 체이닝). debounce는 조용한 기간 대기(입력하며 검색); throttle은 비율 제한(버튼 탭). collect가 모든 값을 배열로 모음. 이들이 선언적 반응형 파이프라인을 가능하게.
let publisher = [1, 2, 3, 4, 5].publisher
// Transforming
publisher.map { $0 * 2 } // [2, 4, 6, 8, 10]
publisher.scan(0, +) // [1, 3, 6, 10, 15] (running sum)
// Filtering
publisher.filter { $0 % 2 == 0 } // [2, 4]
publisher.removeDuplicates() // consecutive dupes
publisher.first() // just 1
// Combining
let p1 = [1, 2].publisher
let p2 = [3, 4].publisher
p1.merge(with: p2) // interleaved: 1, 2, 3, 4 (or any order)
p1.zip(p2) // pairs: (1,3), (2,4)
p1.combineLatest(p2) // latest of each
// Timing
publisher.delay(for: 1, scheduler: RunLoop.main)
publisher.throttle(for: 1, scheduler: .main, latest: true)
publisher.debounce(for: 0.5, scheduler: .main)
publisher.collect() // gather all into [1,2,3,4,5]Subject(수동 발행)
Subject는 수동으로 값을 보낼 수 있는 가변 publisher—명령형과 반응형 코드를 연결. PassthroughSubject는 저장 없이 방출(이벤트 스트림). CurrentValueSubject는 최신 값을 저장(상태). CurrentValueSubject의 새 subscriber는 즉시 현재 값을 받음. Subject를 사용하여 delegate, 알림, 또는 UI 이벤트를 Combine 파이프라인으로 감쌈. @Published는 본질적으로 ObservableObject와 통합된 CurrentValueSubject.
import Combine
// PassthroughSubject: broadcasts to subscribers, no current value
let subject = PassthroughSubject<Int, Never>()
subject.sink { print($0) } // subscriber 1
subject.send(1) // prints 1
subject.send(2) // prints 2
// CurrentValueSubject: holds current value, new subs get it
let current = CurrentValueSubject<Int, Never>(0)
current.sink { print($0) } // prints 0 (current)
current.send(1) // prints 1
current.sink { print($0) } // prints 1 (current)
current.value // 1
// Use cases:
// - PassthroughSubject: events (button taps, notifications)
// - CurrentValueSubject: state (like @Published but more control)
// - Bridge imperative code to Combine
// Finish a subject
subject.send(completion: .finished)오류 처리
Combine publisher는 Failure 타입을 가짐(실패 불가는 Never). catch가 오류를 폴백 publisher로 교체. retry가 실패 시 재구독(불안정한 네트워크에 좋음). mapError가 오류 타입을 변환. assertNoFailure는 오류 발생 시 크래시(확신할 때 사용). Failure == Never인 publisher는 어디서든 사용 가능; 오류가 있는 것은 처리가 필요. 이것이 반응형 파이프라인에서 오류 처리를 명시적이고 조합 가능하게 만듦.
enum APIError: Error { case network, parse }
// Publishers can fail with an error
let publisher = Fail<Int, APIError>(error: .network)
// catch: replace error with fallback publisher
publisher
.catch { _ in Just(0) } // fallback to 0
.sink { print($0) } // 0
// retry: retry on failure
URLSession.shared.dataTaskPublisher(for: url)
.retry(3) // retry up to 3 times
.sink(receiveCompletion: { _ in }, receiveValue: { _ in })
// mapError: transform error type
publisher
.mapError { _ in NSError(domain: "x", code: 1) }
// assertNoFailure: crash if error (when you're sure it won't)
Just(1).assertNoFailure().sink { print($0) }
// Result publisher
let resultPublisher = [1, 2, 3].publisher.setFailureType(to: Error.self)접근 제어와 코드 조직
접근 수준
Swift는 5개의 접근 수준을 가짐. private(가장 작은 스코프, 선언 내), fileprivate(소스 파일 내), internal(모듈 내, 기본값), public(가져오는 누구나), open(public + 하위 클래스 가능/재정의 가능, 클래스 전용). Swift 5.9는 SPM 모듈을 위해 package 추가. 제한적으로 시작하고 필요에 따라 넓히세요. 라이브러리 API에는 public, 앱 코드에는 internal, 구현 세부에는 private. 하위 클래스 설계를 위한 프레임워크 클래스에는 open.
// public: accessible from any module that imports this
public class PublicAPI {
public func connect() {}
}
// internal (default): accessible within the module
class InternalClass {
func helper() {} // internal by default
}
// fileprivate: accessible within the source file
fileprivate func fileHelper() {}
// private: accessible within the enclosing declaration
class Counter {
private var count = 0 // only Counter can access
fileprivate func reset() { count = 0 } // same file
}
// Swift 5.9: package (for Swift Package Manager)
package struct PackageType {
package var value: Int
}
// Guidelines:
// - Start restrictive (private), open up as needed
// - public for framework API, internal for implementation
// - private for encapsulation within a type확장과 조직
확장은 타입의 기능을 여러 파일로 분할(String+Validation.swift, String+Parsing.swift)하고 소급으로 준수 추가 가능. 이는 파일을 집중되고 관리 가능하게 유지. 프로토콜 준수는 주 타입 정의와 별도의 확장/파일에 있을 수 있음. 조건부 준수(where Element: ...)가 조건부로 준수 추가. 조직을 위해 확장을 사용: 핵심 타입을 최소로 유지, 집중된 확장 파일에 기능을 추가.
// Split a type across files with extensions
// String+Validation.swift
extension String {
var isValidEmail: Bool {
contains("@") && contains(".")
}
}
// String+Parsing.swift
extension String {
func toURL() -> URL? { URL(string: self) }
}
// Conform to protocols in extensions
extension String: Comparable { // already conforms, just showing
// protocol methods
}
// Conditional conformance
extension Array: MyProtocol where Element: MyProtocol {}
// Protocol conformance in a separate file
// (keeps the main type definition clean)
struct User { let id: Int; let name: String }
extension User: Codable {} // synthesized
extension User: Equatable {} // synthesized
extension User: CustomStringConvertible {
var description: String { "User(\(id), \(name))" }
}모듈과 패키지
Swift Package Manager(SPM)는 Package.swift로 모듈을 정의. 각 타겟은 모듈—internal 접근은 타겟 내, public은 모듈 간. 의존성은 다른 패키지(git URL). product가 패키지가 노출하는 것. SPM이 Swift 코드를 관리하는 현대적 방법(CocoaPods/Carthage 대체). Xcode가 SPM을 기본 통합. 구조: 코드는 Sources/MyLibrary/, 테스트는 Tests/MyLibraryTests/. 관심사 분리와 접근 제어를 위해 모듈을 사용.
// Package.swift (Swift Package Manager)
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "MyLibrary",
products: [
.library(name: "MyLibrary", targets: ["MyLibrary"]),
],
dependencies: [
.package(url: "https://github.com/.../Logging", from: "1.0.0"),
],
targets: [
.target(name: "MyLibrary", dependencies: ["Logging"]),
.testTarget(name: "MyLibraryTests", dependencies: ["MyLibrary"]),
]
)
// Importing modules
import Foundation
import MyLibrary // your module
import Logging // dependency
// Each target is a module
// internal access is within a target/module
// public access crosses module boundaries이니셜라이저와 지정/편의
클래스는 지정 이니셜라이저(완전히 init, super의 지정 호출 필요)와 편의 이니셜라이저(같은 클래스의 다른 init에 위임)를 가짐. 이 이중 시스템이 완전한 초기화를 보장. struct는 멤버별 이니셜라이저를 무료로 받음. 실패 가능 이니셜라이저(init?)은 실패 시 nil 반환. 필수 이니셜라이저(required init)는 하위 클래스가 구현해야. 대부분의 코드에서 클래스보다 struct를 선호(더 단순한 초기화).
class Person {
let name: String
let age: Int
// Designated initializer: fully initializes
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
class Employee: Person {
let employeeId: String
// Designated init must call super's designated init
init(name: String, age: Int, employeeId: String) {
self.employeeId = employeeId
super.init(name: name, age: age) // must call super
}
// Convenience init must call another init (same class)
convenience init(name: String) {
self.init(name: name, age: 0, employeeId: "TEMP")
}
}
// Structs: memberwise initializer (free)
struct Point { let x, y: Int }
let p = Point(x: 1, y: 2) // synthesized
// Failable initializers
struct Config {
let port: Int
init?(port: Int) {
guard port > 0 else { return nil }
self.port = port
}
}옵셔널 심층 분석
옵셔널은 Swift의 null 안전 메커니즘—.some(value) 또는 .none(nil)인 enum. 값에 접근하려면 언래핑해야. 옵셔널 체이닝(??.)이 안전하게 탐색; nil 병합(??)이 기본값 제공; if let/guard let이 안전하게 언래핑. 확신하지 않는 한 !(강제 언래핑)을 피하세요—nil 시 크래시. 옵셔널이 부재를 명시적으로 처리하도록 강제, 다른 언어에서 흔한 null 참조 예외를 제거.
// Optional: enum with .some(Value) and .none
let maybeInt: Int? = 42
let nilInt: Int? = nil
// Optional chaining (short-circuits on nil)
let name: String? = user?.profile?.name // nil if any link is nil
// Nil-coalescing (default value)
let value = maybeInt ?? 0 // 42
let value2 = nilInt ?? 0 // 0
// Forced unwrapping (crashes if nil)
let forced = maybeInt! // 42
// let crash = nilInt! // RUNTIME CRASH
// Optional binding
if let actual = maybeInt {
print(actual) // 42, actual is Int (not optional)
}
// Guard for early exit
func process(_ data: Data?) {
guard let data = data else { return }
// data is non-optional here
}
// Optional try
let result = try? parse(json) // nil on error
let result2 = try! parse(json) // crashes on error
// Multiple binding
if let a = optA, let b = optB, a < b { /* ... */ }메모리 관리(ARC/weak)
ARC 기초
자동 참조 카운팅(ARC)은 강한 참조를 추적. 카운트가 0에 도달하면 객체가 할당 해제. ARC는 결정적(가비지 컬렉션과 달리): 마지막 참조가 해제되면 즉시 deinit 실행. 대부분의 경우 ARC는 그냥 작동. 참조 순환에서 문제 발생: 두 객체가 서로를 강하게 참조하여 할당 해제 방지. 순환을 끊으려면 weak 또는 unowned를 사용.
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
var reference1: Person?
var reference2: Person?
reference1 = Person(name: "Alice")
// Prints: Alice is being initialized
reference2 = reference1 // Strong reference count: 2
reference1 = nil // Count: 1
reference2 = nil // Count: 0, deinit runs
// Prints: Alice is being deinitializedWeak 참조
weak 참조는 참조된 객체를 살아있게 유지하지 않음. 객체가 할당 해제되면 weak 참조는 자동으로 nil이 됨. weak는 optional이어야 함(var tenant: Person?). 참조된 객체의 수명이 더 짧을 때 weak를 사용(세입자가 아파트를 떠날 수 있음). 고전적 사용 사례는 delegate 패턴: 순환을 피하기 위해 delegate는 weak. weak는 약간의 오버헤드가 있음(nil 설정을 위해 런타임에 등록).
class Person {
var name: String
var apartment: Apartment? // Strong (person owns apartment)
init(name: String) { self.name = name }
deinit { print("\(name) deinit") }
}
class Apartment {
var unit: String
weak var tenant: Person? // Weak (breaks cycle)
init(unit: String) { self.unit = unit }
deinit { print("Apartment \(unit) deinit") }
}
var alice: Person? = Person(name: "Alice")
var apt: Apartment? = Apartment(unit: "4A")
alice!.apartment = apt
apt!.tenant = alice // Weak reference, no cycle
alice = nil // Alice deinit (apartment's tenant becomes nil)
apt = nil // Apartment deinit
// weak must be optional (becomes nil when deallocated)Unowned 참조
unowned 참조는 weak와 마찬가지로 객체를 살아있게 유지하지 않음. weak와 달리 unowned는 non-optional이고 nil이 되지 않음. 할당 해제 후 unowned 참조 접근 시 크래시. 참조된 객체가 참조보다 오래 살 것이라고 보장할 때 사용(예: 신용카드는 고객 없이 존재할 수 없음). unowned는 weak보다 오버헤드가 적음. 선택: 불확실한 수명에는 weak, 보장된 수명에는 unowned.
class Customer {
let name: String
var card: CreditCard? // Strong
init(name: String) { self.name = name }
deinit { print("\(name) deinit") }
}
class CreditCard {
let number: UInt
unowned let customer: Customer // Non-optional, no cycle
init(number: UInt, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card \(number) deinit") }
}
var john: Customer? = Customer(name: "John")
john!.card = CreditCard(number: 1234, customer: john!)
john = nil
// Customer deinit, then CreditCard deinit
// unowned: non-optional, assumed to always have a value
// CRASH if accessed after deallocation
// Use when the referenced object has same or longer lifetime클로저와 캡처 목록
클로저는 기본적으로 강하게 참조를 캡처, 클로저가 self에 저장될 때 순환을 일으킴. 캡처 목록([weak self] 또는 [unowned self])이 순환을 끊음. weak self는 옵셔널 처리 필요(guard let self = self). self 전체를 캡처하지 않으려면 특정 값을 캡처(let name = self.name). 함수에 전달된(저장되지 않은) 클로저는 순환을 일으키지 않음. self를 참조하는 저장된 클로저에는 항상 캡처 목록을 사용.
class ViewController {
var name = "MyVC"
var onComplete: (() -> Void)?
// BAD: Strong reference cycle
func setupBad() {
onComplete = {
print("\(self.name) completed") // Captures self strongly
}
}
// GOOD: Capture list breaks cycle
func setupGood() {
onComplete = { [weak self] in
guard let self = self else { return }
print("\(self.name) completed")
}
}
// unowned for guaranteed lifetime
func setupUnowned() {
onComplete = { [unowned self] in
print("\(self.name) completed")
}
}
// Capture specific values
func setupCapture() {
let name = self.name // Capture value, not self
onComplete = {
print("\(name) completed")
}
}
deinit { print("VC deinit") }
}누수 감지와 수정
일반적인 누수源: delegate가 강하게 저장됨(weak 사용), Timer가 target을 유지(deinit에서 invalidate), NotificationCenter 관찰자(deinit에서 제거). Xcode Memory Graph Debugger가 객체 그래프를 시각화하고 누수를 강조. 할당 추적을 얻으려면 MallocStackLogging으로 실행. 참조를 nil로 설정하고 deinit이 실행되는지 확인하여 테스트. Instruments(Leaks 도구)가 런타임에 누수를 감지. 항상 설정과 해제를 짝지으세요.
// Xcode memory graph debugger: visualize reference cycles
// Product > Debug > View Memory Graph Hierarchy
// Common leak patterns:
// 1. Delegate stored as strong
class View {
weak var delegate: ViewDelegate? // MUST be weak
}
// 2. Timer retains target
class TimerHolder {
var timer: Timer?
func start() {
// BAD: timer retains self via #selector
timer = Timer.scheduledTimer(timeInterval: 1,
target: self, selector: #selector(fire),
userInfo: nil, repeats: true)
}
@objc func fire() {}
deinit { timer?.invalidate() } // MUST invalidate
}
// 3. NotificationCenter observer
class Observer {
init() {
NotificationCenter.default.addObserver(
self, selector: #selector(handle),
name: .someNotification, object: nil)
}
@objc func handle() {}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
// Use Xcode's Debug Memory Graph to find leaksActor와 async/await
async/await 기초
async/await(Swift 5.5+)는 비동기 코드를 동기처럼 보이게. async가 일시 중지 가능한 함수를 표시; await가 일시 중지 지점을 표시. Task가 동기를 비동기 컨텍스트로 연결. async let이 동시 작업을 시작; await가 결과를 수집. 병렬 실행에는 async let을, 순차에는 일반 await를 사용. 오류는 throws/try로 전파. 컴파일러가 일시 중지 지점에서 await를 강제. async/await가 완료 핸들러와 Combine을 많은 사용 사례에서 대체.
// Async function
func fetchUser(id: Int) async throws -> User {
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
// Calling async functions
func loadProfile() async {
do {
let user = try await fetchUser(id: 1)
print("Loaded: \(user.name)")
} catch {
print("Error: \(error)")
}
}
// Task: bridge async to sync context
Task {
await loadProfile()
}
// Concurrent execution
func loadMultiple() async {
async let user1 = fetchUser(id: 1)
async let user2 = fetchUser(id: 2)
async let user3 = fetchUser(id: 3)
let users = try await [user1, user2, user3]
print("Loaded \(users.count) users")
}
// Sequenced vs concurrent
let sequential = try await fetchUser(id: 1) // Waits
let concurrent = try await fetchUser(id: 2) // Then waits againActor
Actor(Swift 5.5+)는 직렬화된 접근을 가진 참조 타입, 데이터 경쟁을 방지. 한 번에 하나의 메서드만 actor에서 실행—수동 잠금 불필요. actor 메서드는 await로 호출해야(일시 중지 가능). 속성은 격리되어 외부에서 직접 접근 불가. actor는 동시 코드의 공유 가변 상태를 위한 안전한 기본값. 대부분의 동시 상태에 잠금이 있는 클래스 대신 actor를 사용.
// Actor: reference type with serialized access
actor BankAccount {
private var balance: Decimal
init(balance: Decimal) {
self.balance = balance
}
func deposit(_ amount: Decimal) {
balance += amount
}
func withdraw(_ amount: Decimal) -> Bool {
guard balance >= amount else { return false }
balance -= amount
return true
}
func getBalance() -> Decimal { balance }
}
// Usage (must be async)
let account = BankAccount(balance: 100)
Task {
await account.deposit(50)
let balance = await account.getBalance()
print("Balance: \(balance)") // 150
}
// Actors prevent data races automatically
// Only one method runs at a time per actor instance
// No need for locks or queuesActor 격리
Actor 격리가 상태를 보호. nonisolated 멤버는 await 없이 접근 가능(순수 값이나 격리된 상태를 건드리지 않는 메서드용). @MainActor는 메인 스레드 격리를 위한 전역 actor(UI 업데이트). @globalActor가 사용자 정의 전역 actor 생성. actor 경계를 넘으려면 await 필요. 컴파일러가 컴파일 타임에 격리를 검증, 데이터 경쟁을 방지. ViewModel과 UI 코드에는 @MainActor를; 도메인별 격리에는 사용자 정의 actor를 사용.
actor Counter {
private var count = 0
// Isolated method (default)
func increment() { count += 1 }
// Nonisolated: can be called without await
nonisolated var description: String { "Counter" }
// nonisolated function
nonisolated func id() -> String { "counter-\(ObjectIdentifier(self))" }
}
// Global actor: @MainActor runs on main thread
@MainActor
class ViewModel: ObservableObject {
@Published var data: [Item] = []
func loadData() async {
// Always runs on main thread
data = try await fetchItems()
}
}
// Mark specific functions
class Service {
@MainActor
func updateUI(_ result: Result) {
// UI updates on main thread
}
}
// globalActor lets you apply to types, functions, properties
@globalActor
actor MyActor {
static let shared = MyActor()
}구조적 동시성
Task group이 동적 병렬 계산을 가능하게. addTask가 하위 작업을 생성; for await group이 완료되는 대로 결과 수집. Task group이 구조적 동시성을 강제: 모든 하위가 부모가 계속하기 전에 완료. 취소가 부모에서 하위로 자동 전파. 장기 실행 작업에서 Task.isCancelled를 확인. withThrowingTaskGroup이 오류를 전파, throw 시 형제를 취소. fan-out/fan-in 패턴에 task group을 사용.
// TaskGroup: dynamic parallel computation
func fetchAllUsers(ids: [Int]) async throws -> [User] {
try await withThrowingTaskGroup(of: User.self) { group in
for id in ids {
group.addTask { try await fetchUser(id: id) }
}
var users: [User] = []
for try await user in group {
users.append(user)
}
return users
}
}
// Task tree: parent-child relationship
func process() async {
await withTaskGroup(of: Void.self) { group in
for i in 1...5 {
group.addTask {
await processItem(i)
}
}
// All tasks complete before group returns
}
print("All done")
}
// Cancellation
func longRunning() async {
await withTaskGroup(of: Void.self) { group in
group.addTask {
for i in 1...100 {
if Task.isCancelled { return }
await doWork(i)
}
}
}
}
// Cancel from outside
let task = Task { await longRunning() }
task.cancel()AsyncSequence와 Stream
AsyncSequence는 Sequence의 비동기 버전, for await 루프를 가능하게. AsyncStream이 콜백 기반 또는 delegate 기반 API를 async/await로 연결. yield가 값을 방출, finish가 종료. onTermination이 자원 정리. AsyncThrowingStream이 오류를 지원. 페이지 매김 API, 실시간 데이터, 또는 시간이 지남에 따른 값 스트림에 AsyncSequence를 사용. 표준 라이브러리가 URL에 .lines를 제공하여 한 줄씩 파일 읽기. AsyncSequence가 작업 취소와 통합.
// AsyncSequence: iterate asynchronously
struct Counter: AsyncSequence {
struct AsyncIterator: AsyncIteratorProtocol {
var current: Int
let max: Int
mutating func next() async -> Int? {
guard current <= max else { return nil }
defer { current += 1 }
try? await Task.sleep(nanoseconds: 100_000_000)
return current
}
}
let start: Int
let max: Int
func makeAsyncIterator() -> AsyncIterator {
AsyncIterator(current: start, max: max)
}
}
// Usage
for await num in Counter(start: 1, max: 5) {
print(num) // 1 2 3 4 5 (with delays)
}
// AsyncStream: bridge callbacks to async
func urlLines(_ url: URL) -> AsyncThrowingStream<String, Error> {
AsyncThrowingStream { continuation in
let task = Task {
do {
for try await line in url.lines {
continuation.yield(line)
}
continuation.finish()
} catch {
continuation.finish(throwing: error)
}
}
continuation.onTermination = { _ in task.cancel() }
}
}테스팅
XCTest 기초
XCTest는 표준 테스트 프레임워크입니다. XCTestCase가 관련 테스트를 그룹화합니다. setUp/tearDown이 각 테스트 전후에 실행됩니다. 어설션: XCTAssertEqual, XCTAssertNil, XCTAssertThrowsError, XCTAssertTrue. @testable import으로 내부 심볼에 접근합니다. 테스트는 기본적으로 메인 스레드에서 실행됩니다. 옵셔널에는 XCTAssertNotNil을 사용하세요. Cmd+U로 테스트를 실행합니다. Xcode가 테스트 결과를 인라인으로 표시합니다. 빠른 피드백을 위해 코드와 함께 테스트를 작성하세요.
import XCTest
@testable import MyApp
class UserTests: XCTestCase {
var user: User!
override func setUp() {
super.setUp()
user = User(name: "Alice", age: 30)
}
override func tearDown() {
user = nil
super.tearDown()
}
func testName() {
XCTAssertEqual(user.name, "Alice")
}
func testAgeAfterBirthday() {
user.haveBirthday()
XCTAssertEqual(user.age, 31)
}
func testValidation() {
XCTAssertThrowsError(try User(name: "", age: -1)) { error in
guard let error = error as? UserError else {
XCTFail("Wrong error type")
return
}
XCTAssertEqual(error, .invalidName)
}
}
func testOptional() {
XCTAssertNotNil(user.email)
XCTAssertNil(user.address)
}
}
// Run: Cmd+U in Xcode비동기 테스트
Swift 5.5+는 async 테스트 메서드를 직접 지원합니다(async throws func test...). 테스트 내에서 try await를 사용하세요. 이전 코드의 경우 XCTestExpectation을 사용하세요: expectation을 생성하고, 콜백에서 fulfill()을 호출하고, wait(for:timeout:)으로 대기합니다. 멈춤을 잡으려면 합리적인 타임아웃을 설정하세요. 비동기 시퀀스를 테스트하려면 for await 루프를 사용하세요. URLProtocol이나 의존성 주입으로 네트워크 호출을 목킹하세요. 성공 및 실패 경로를 모두 테스트하세요. 비동기 테스트는 기본적으로 동시에 실행됩니다—순서가 중요하면 .serialized 트레이트를 사용하세요.
class APITests: XCTestCase {
func testFetchUser() async throws {
let user = try await fetchUser(id: 1)
XCTAssertEqual(user.name, "Alice")
}
func testFetchFailure() async {
do {
_ = try await fetchUser(id: -1)
XCTFail("Should have thrown")
} catch {
// Expected
}
}
// Old style with expectations (still works)
func testAsyncWithExpectation() {
let expectation = XCTestExpectation(description: "Fetch completes")
fetchUserAsync { result in
switch result {
case .success(let user):
XCTAssertEqual(user.name, "Alice")
case .failure:
XCTFail("Should succeed")
}
expectation.fulfill()
}
wait(for: [expectation], timeout: 5)
}
}목킹 & 스텁
의존성 주입으로 테스트가 가능해집니다: 의존성에 대한 프로토콜을 정의하고, 테스트에서 목을 주입합니다. MockNetworkService가 미리 정해진 결과를 반환합니다. mock.fetchUserResult를 설정하여 성공 및 실패 케이스를 테스트합니다. 이 패턴은 테스트를 네트워크에서 분리하여 빠르고 결정론적으로 만듭니다. 복잡한 목에는 Cuckoo나 Mockingbird 같은 라이브러리를 사용하세요. 타입의 공개 API를 테스트하고, 구현 세부 사항은 테스트하지 마세요. 목은 단순하고 집중되어야 합니다.
// Protocol for dependency injection
protocol NetworkService {
func fetchUser(id: Int) async throws -> User
}
// Production implementation
class APIService: NetworkService {
func fetchUser(id: Int) async throws -> User {
// Real network call
}
}
// Mock for testing
class MockNetworkService: NetworkService {
var fetchUserResult: Result<User, Error>?
func fetchUser(id: Int) async throws -> User {
switch fetchUserResult {
case .success(let user): return user
case .failure(let error): throw error
case .none: throw URLError(.badURL)
}
}
}
class ViewModelTests: XCTestCase {
func testLoadUser() async throws {
let mock = MockNetworkService()
mock.fetchUserResult = .success(User(name: "Alice", age: 30))
let vm = ViewModel(service: mock)
await vm.load()
XCTAssertEqual(vm.userName, "Alice")
}
}UI 테스트
UI 테스트는 접근성 식별자를 통해 사용자 상호작용을 자동화합니다. XCUIApplication이 앱을 실행합니다. 접근성 식별자로 요소를 찾습니다(코드에서 .accessibilityIdentifier로 설정). 액션: tap, typeText, swipeLeft/Right. 어설션: waitForExistence, exists, hittable. UI 테스트는 단위 테스트보다 느리지만 통합 버그를 잡습니다. SwiftUI에서 .accessibilityIdentifier("Email")로 접근성 식별자를 설정하세요. 커버리지를 높이기 위해 여러 기기/시뮬레이터에서 UI 테스트를 실행하세요.
import XCTest
class AppUITests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
let app = XCUIApplication()
app.launch()
}
func testLoginFlow() {
let app = XCUIApplication()
let emailField = app.textFields["Email"]
let passwordField = app.secureTextFields["Password"]
let loginButton = app.buttons["Log In"]
emailField.tap()
emailField.typeText("[email protected]")
passwordField.tap()
passwordField.typeText("password123")
loginButton.tap()
// Verify navigation
XCTAssertTrue(app.staticTexts["Welcome, Alice!"].waitForExistence(timeout: 5))
}
func testSwipeAndTap() {
let cell = app.cells["Item 5"]
cell.swipeLeft()
app.buttons["Delete"].tap()
XCTAssertFalse(cell.exists)
}
}성능 & 코드 커버리지
measure가 코드 성능을 벤치마킹하여 베이스라인과 비교합니다. Xcode에서 베이스라인을 설정하고, 회귀가 임계값을 초과하면 테스트가 실패합니다. measureMetrics로 특정 메트릭(피크 메모리, CPU)을 추적합니다. 스킴에서 코드 커버리지를 활성화하여 테스트되지 않은 코드 경로를 확인하세요. 비즈니스 로직에서 80% 이상의 커버리지를 목표로 하세요. 스냅샷 테스트(swift-snapshot-testing)가 UI를 캡처하여 시각적 회귀를 검사합니다. 더 깊은 분석을 위해 Instruments(Time Profiler, Allocations)로 프로파일링하세요. 성능 테스트는 회귀를 조기에 잡습니다.
class PerformanceTests: XCTestCase {
func testSortingPerformance() {
let data = (1...10000).shuffled()
measure {
_ = data.sorted()
}
}
func testMemoryUsage() {
measureMetrics([XCTPerformanceMetric.peakMemory]) {
_ = (0..<10000).map { _ in [Int](repeating: 0, count: 100) }
}
}
// Baseline comparison
func testWithBaselines() {
let options = XCTMeasureOptions()
options.defaultOptions.measurementUnits = .seconds
measure(options) {
// Code to benchmark
}
}
}
// Code coverage: enable in Edit Scheme > Test > Options
// View coverage: Cmd+Shift+Y (Report Navigator) > Coverage
// Snapshot testing (third-party)
// func testAppearance() {
// assertSnapshot(of: view, as: .image)
// }Swift 동시성
async/await
async/await(Swift 5.5+)는 비동기 코드를 단순화합니다. async는 일시 중단할 수 있는 함수를 표시합니다. await는 일시 중단 지점을 표시합니다. Task가 비동기 컨텍스트를 생성합니다. 완료 핸들러보다 훨씬 깔끔합니다. 오류는 try로 전파됩니다.
func fetchData() async throws -> String {
let (data, _) = try await URLSession.shared.data(from: url)
return String(data: data, encoding: .utf8) ?? ""
}
// Call from async context
Task {
let result = try await fetchData()
print(result)
}Actor
actor(Swift 5.5+)는 자동 상호 배제 기능이 있는 참조 타입입니다. 한 번에 하나의 작업만 상태에 접근합니다. 외부에서 호출 시 메서드는 암시적으로 async가 됩니다. 공유 가변 상태를 위한 락과 디스패치 큐를 대체합니다. 설계상 스레드 안전합니다.
actor Counter {
private var count = 0
func increment() {
count += 1
}
func getValue() -> Int {
count
}
}
// Usage
Task {
let counter = Counter()
await counter.increment()
let value = await counter.getValue()
}Async Sequence
AsyncSequence는 Sequence의 비동기 버전입니다. for await가 비동기적으로 반복합니다. 스트리밍 데이터(네트워크, 파일 라인)에 유용합니다. AsyncIteratorProtocol을 구현하여 커스텀 비동기 시퀀스를 생성하세요. 컴파일러가 일시 중단과 취소를 처리합니다.
for try await item in asyncSequence {
print(item)
}
// Custom AsyncSequence
struct Counter: AsyncSequence {
struct AsyncIterator: AsyncIteratorProtocol {
var current = 0
mutating func next() async -> Int? {
current += 1
return current <= 5 ? current : nil
}
}
func makeAsyncIterator() -> AsyncIterator { AsyncIterator() }
}Task Group
Task group은 여러 작업을 동시에 실행하고 결과를 수집합니다. addTask가 자식 작업을 추가합니다. for await가 완료되 는 대로 결과를 반복합니다. 그룹이 반환되기 전에 모든 작업이 완료되어야 합니다. 구조화된 동시성: 취소가 자식에게 전파됩니다. 결과가 안전하게 수집됩니다.
let results = await withTaskGroup(of: Int.self) { group in
for i in 1...5 {
group.addTask { i * i }
}
var sum = 0
for await result in group {
sum += result
}
return sum
}Continuation
withCheckedContinuation이 완료 핸들러 API를 async/await로 브릿지합니다. resume(returning:)이 비동기 함수를 재개합니다. 정확히 한 번 호출되어야 합니다. withCheckedThrowingContinuation은 오류를 지원합니다. 기존 API로 async/await를 도입할 때 유용합니다.
func fetchWithCompletion(_ completion: @escaping (String) -> Void) {
// Legacy completion handler API
completion("data")
}
// Wrap in async
func fetchAsync() async -> String {
await withCheckedContinuation { continuation in
fetchWithCompletion { result in
continuation.resume(returning: result)
}
}
}SwiftUI 심화
View Modifier
Modifier가 뷰를 감싸 외형이나 동작을 변경합니다. 순서가 중요합니다: 나중의 modifier가 이전 것을 감쌉니다. .background 앞의 .padding은 패딩을 배경 안에 넣습니다. Modifier는 새로운 View 인스턴스를 반환합니다. 복잡한 스타일링을 위해 체인하세요.
Text("Hello")
.font(.title)
.foregroundColor(.blue)
.padding()
.background(Color.gray)
.cornerRadius(8)
.shadow(radius: 4)List
List가 스크롤 가능한 행을 표시합니다. ForEach가 데이터에서 행을 생성합니다. onDelete가 스와이프하여 삭제를 활성화합니다. 다른 modifier: onMove, onInsert. ForEach에는 식별 가능한 데이터(id 프로퍼티)가 필요합니다. List 스타일: .plain, .insetGrouped, .sidebar.
List {
ForEach(items) { item in
HStack {
Text(item.name)
Spacer()
Text("\(item.price)")
}
}
.onDelete { indexSet in
items.remove(atOffsets: indexSet)
}
}
.listStyle(.insetGrouped)Navigation
NavigationStack이 뷰 스택을 관리합니다. NavigationLink가 목적지를 푸시합니다. navigationTitle이 제목을 설정합니다. toolbar가 내비게이션 바 버튼을 추가합니다. iOS 16 이상에서 사용하세요. 이전 버전에는 NavigationView를 사용하세요. 시트와 얼럿은 .sheet와 .alert modifier를 사용합니다.
NavigationStack {
List(items) { item in
NavigationLink(item.name) {
DetailView(item: item)
}
}
.navigationTitle("Items")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Add") { addItem() }
}
}
}상태 관리
@State는 로컬 뷰 상태(값 타입)에 사용합니다. @Binding은 전달된 상태에 사용합니다. @StateObject는 소유한 observable 객체(한 번 생성)에 사용합니다. @ObservedObject는 외부 observable 객체에 사용합니다. @EnvironmentObject는 앱 전체 상태에 사용합니다. @State 변경이 뷰 업데이트를 트리거합니다.
struct CounterView: View {
@State private var count = 0
@Binding var selected: Int
@StateObject var viewModel = ViewModel()
var body: some View {
Button("\(count)") { count += 1 }
}
}애니메이션
withAnimation이 상태 변경을 애니메이션합니다. .spring, .easeInOut은 애니메이션 곡선입니다. .animation(value:)는 값이 변경될 때 애니메이션합니다. .scaleEffect, .opacity, .offset은 애니메이션 가능합니다. 암시적 애니메이션은 .animation modifier를 사용합니다. 명시적은 withAnimation을 사용합니다.
struct AnimatedView: View {
@State private var scale: CGFloat = 1.0
var body: some View {
Button("Tap") {
withAnimation(.spring(response: 0.3)) {
scale = scale == 1 ? 1.5 : 1
}
}
.scaleEffect(scale)
.animation(.easeInOut(duration: 0.3), value: scale)
}
}Combine 프레임워크
Publisher & Subscriber
Combine은 Apple의 반응형 프레임워크입니다. Publisher가 시간에 따라 값을 방출합니다. Subscriber가 이를 받습니다. sink가 subscriber를 생성합니다. Just가 단일 값을 방출합니다. PassthroughSubject는 수동 publisher입니다. Cancellable을 유지해야 하며, 그렇지 않으면 구독이 취소됩니다. RxSwift와 유사합니다.
import Combine
let publisher = Just(42)
let cancellable = publisher.sink { value in
print("Received: \(value)")
}
// Just emits one value then finishes
// PassthroughSubject: manual values
let subject = PassthroughSubject<Int, Never>()
subject.send(1)
subject.send(2)Operator
Operator가 publisher 출력을 변환합니다. map이 값을 변환합니다. filter가 값을 선택합니다. debounce가 조용할 때까지 지연합니다. throttle이 속도를 제한합니다. combineLatest가 스트림을 병합합니다. flatMap이 publisher를 체인합니다. Operator는 지연 방식입니다: sink가 구독하기 전에는 아무 일도 일어나지 않습니다.
let publisher = (1...10).publisher
.map { $0 * 2 }
.filter { $0 > 10 }
.sink { print($0) } // 12, 14, 16, 18, 20
// Debounce
let debounced = subject
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.sink { print($0) }@Published
@Published가 프로퍼티를 Combine publisher로 노출합니다. $ 접두사로 publisher에 접근합니다. 변경이 새 값을 방출합니다. SwiftUI를 위해 ObservableObject와 함께 작동합니다. publisher가 구독 시 현재 값을 방출합니다. 수동 알림 없이 반응형 UI 업데이트에 유용합니다.
class ViewModel: ObservableObject {
@Published var count = 0
}
let vm = ViewModel()
let cancellable = vm.$count.sink { newCount in
print("Count changed: \(newCount)")
}
vm.count = 1 // Prints "Count changed: 1"
vm.count = 2 // Prints "Count changed: 2"Future
Future가 완료 핸들러 API를 publisher로 감쌉니다. promise(.success)가 값을 방출합니다. promise(.failure)가 오류를 방출합니다. Future는 정확히 한 번 방출합니다. 콜백 API를 Combine으로 브릿지하는 데 유용합니다. .delay나 deferred로 감싸지 않는 한 클로저는 즉시 실행됩니다.
func fetchUser() -> Future<User, Error> {
Future { promise in
URLSession.shared.dataTask(with: url) { data, _, error in
if let error = error { promise(.failure(error)) }
else { promise(.success(parse(data!))) }
}.resume()
}
}
// Usage
fetchUser().sink(receiveCompletion: { _ in }, receiveValue: { user in print(user) })오류 처리
Fail이 즉시 오류를 방출합니다. catch가 실패한 publisher를 다른 것으로 교체합니다. retry가 실패 시 재구독합니다. assertNoFailure가 오류 시 충돌합니다(디버깅용). completion 이벤트가 더 이상 값이 없음을 알립니다. 잡히지 않는 한 오류는 다운스트림으로 전파됩니다.
let publisher = Fail<Int, MyError>(error: .notFound)
.catch { error in
Just(0) // Fallback value
}
.sink { print($0) } // 0
// retry on failure
let retried = requestPublisher
.retry(3)
.sink(receiveCompletion: { _ in }, receiveValue: { _ in })메모리 관리
ARC 기초
ARC(Automatic Reference Counting)가 참조를 추적합니다. 카운트가 0이 되면 deinit이 실행되고 메모리가 해제됩니다. Strong 참조는 카운트를 증가시킵니다. 참조 사이클은 할당 해제를 방지합니다. ARC는 결정론적입니다(GC와 달리). 마지막 참조가 떨어질 때 deinit이 동기적으로 실행됩니다.
class Person {
let name: String
init(name: String) { self.name = name }
deinit { print("\(name) is being deinitialized") }
}
var p1: Person? = Person(name: "Alice")
p1 = nil // Prints "Alice is being deinitialized"Weak 참조
weak 참조는 retain 카운트를 증가시키지 않습니다. 객체가 할당 해제될 때 자동으로 nil이 됩니다. optional var이어야 합니다. 사이클을 끊기 위해 delegate와 observer에 사용하세요. weak 참조는 zeroing 방식입니다: 객체가 사라진 후에 접근해도 안전합니다. retain 사이클의 가장 흔한 수정 방법입니다.
class View {
weak var delegate: Delegate?
}
class Delegate {
var view: View? // strong
}
// weak: does not keep object alive, auto-set to nil
// Use for delegate patterns to avoid cyclesUnowned 참조
unowned 참조는 retain 카운트를 증가시키지 않습니다. weak와 달리 non-optional이고 zeroing되지 않습니다. 할당 해제 후 접근하면 충돌합니다. 참조된 객체가 참조자보다 오래 살거나 함께 죽을 때 사용하세요. weak보다 빠릅니다(nil 체크 없음). self가 클로저보다 오래 살 때 클로저에서 self를 캡처하는 경우가 흔합니다.
class Customer {
var card: CreditCard?
}
class CreditCard {
unowned let customer: Customer
init(customer: Customer) { self.customer = customer }
}
// unowned: does not keep alive, but not optional
// CRASHES if accessed after dealloc
// Use when the other object has same or shorter lifetimeRetain 사이클
객체가 서로 강하게 참조할 때 retain 사이클이 발생합니다. 어느 것도 할당 해제되지 않아 메모리 누수가 발생합니다. weak나 unowned로 해결하세요. delegate, observer, 클로저에서 흔합니다. 클로저는 기본적으로 self를 강하게 캡처합니다. [weak self]나 [unowned self]가 사이클을 끊습니다. Memory Graph Debugger로 사이클을 찾으세요.
// BAD: retain cycle
class Node {
var next: Node?
var prev: Node? // strong -> cycle
}
// GOOD: break with weak
class Node {
var next: Node?
weak var prev: Node?
}
// Closures: use [weak self]
{ [weak self] in self?.update() }클로저 & 캡처
클로저는 변수를 참조로 캡처합니다. 저장 클로저(escaping)가 self를 캡처하면 retain 사이클을 생성합니다. [weak self]가 캡처를 optional하고 weak하게 만듭니다. @escaping은 함수 호출보다 오래 사는 클로저를 표시합니다. non-escaping 클로저(기본)는 사이클을 일으킬 수 없습니다. 컴파일러가 잠재적 사이클에 대해 경고합니다.
class Timer {
var handler: (() -> Void)?
func start() {
handler = { [weak self] in
self?.tick() // weak to avoid cycle
}
}
func tick() { /* ... */ }
}
// Escaping closure: stored for later
// @escaping marks escaping closures오류 처리 심화
커스텀 오류
커스텀 오류는 Error를 준수합니다. LocalizedError가 errorDescription을 제공합니다. 연관된 값이 컨텍스트를 전달합니다. Enum이 오류를 정의하는 관용적인 방법입니다. 각 case는 고유한 실패를 나타냅니다. Switch의 완전성이 모든 오류가 처리됨을 보장합니다. 디버그 출력을 위해 CustomStringConvertible을 준수하세요.
enum APIError: Error, LocalizedError {
case invalidURL
case unauthorized
case serverError(Int)
case decodingFailed(Error)
var errorDescription: String? {
switch self {
case .invalidURL: return "Invalid URL"
case .unauthorized: return "Unauthorized"
case .serverError(let code): return "Server error: \(code)"
case .decodingFailed(let err): return "Decode failed: \(err)"
}
}
}Result 타입
Result는 타입이 지정된 성공/실패 enum입니다. 실패할 수 있는 동기 작업에 유용합니다. map이 성공 값을 변환합니다. flatMap이 작업을 체인합니다. get()이 throw하여 try/catch로 변환합니다. Result는 저장되거나 전달되는 결과에 대해 throwing 함수보다 선호됩니다. 오류 처리와 값 의미론을 결합합니다.
func fetchUser(id: Int) -> Result<User, APIError> {
guard id > 0 else { return .failure(.invalidURL) }
return .success(User(name: "Alice"))
}
// Usage
switch fetchUser(id: 1) {
case .success(let user): print(user)
case .failure(let error): print(error)
}
// map and flatMap
let result = fetchUser(id: 1).map { $0.name }Rethrows
rethrows가 클로저 매개변수에서 오류를 전파합니다. 클로저가 throw하지 않으면 함수도 throw하지 않습니다. 이는 non-throwing 클로저를 전달하는 호출자에게 try를 강제하지 않습니다. map, filter 및 기타 고차 함수에서 사용됩니다. 함수 자체는 클로저와 독립적으로 throw할 수 없습니다.
func withLock<T>(_ lock: NSLock, _ body: () throws -> T) rethrows -> T {
lock.lock()
defer { lock.unlock() }
return try body()
}
// Only throws if body throws
let x = withLock(lock) { 5 } // no try needed
let y = try withLock(lock) { try throwingFunc() }오류 전파
try가 오류를 호출자에게 전파합니다. try?가 오류를 nil로 변환합니다(Optional 반환). try!가 오류 시 충돌합 니다(확실할 때 사용). 오류는 잡힐 때까지 호출 스택을 타고 올라갑니다. throw하는 함수는 throws로 표시되어야 합니다. 컴파일러가 try 마커를 강제하여 처리되지 않은 오류를 방지합니다. defer는 throw된 오류와 관계없이 실행됩니다.
func process() throws {
let data = try fetchData() // may throw
let user = try parse(data) // may throw
save(user) // does not throw
}
// try?: converts to optional, nil on error
let user = try? fetchUser()
// try!: asserts no error, crashes if thrown
let user = try! fetchUser()defer로 정리
defer가 스코프 종료 시 실행할 정리를 예약합니다. 스코프가 어떻게 종료되든 실행됩니다(return, throw, 또는 fall-through). 여러 defer는 LIFO 순서로 실행됩니다. 파일 닫기, 락 해제, 리소스 해제에 유용합니다. defer는 throw나 break/continue를 할 수 없습니다. 클로저는 변수를 참조로 캡처합니다.
func processFile() throws {
let file = open("file.txt")
defer { close(file) } // runs on scope exit
let data = try read(file)
// ... process ...
// close() runs here even if read throws
}
// Multiple defers run in reverse order (LIFO)
defer { print("1") }
defer { print("2") } // prints 2 then 1관련 Swift 스니펫
Copy-paste ready code for common tasks.
Optionals
Use optional binding, nil-coalescing, chaining, and guard for safe unwrapping.
Closures
Define closure expressions, capture state, and pass escaping callbacks.
Protocols
Define contracts, conform with structs, and add default behavior via extensions.
Generics
Write type-parameterized functions and types with protocol constraints.
Structs and Classes
Compare value-type structs with reference-type classes and inheritance.
Error Handling
Throw and catch typed errors with do-catch, try?, try!, and rethrows.
Concurrency (async/await)
Run async functions, parallelize with async let, and fan out with task groups.
String Manipulation
Trim, split, join, replace, and index strings using Swift's Unicode API.
Was this helpful?