-
Swift Closure Capture SemanticsSwift 2025. 11. 15. 09:07
안녕하세요. 그린입니다 🍏
이번 포스팅에서는 Swift Closure의 Capture Semantics에 대해 알아보겠습니다 🙋🏻
기본 문법은 모두 알고 계실테니, 실제로 헷갈리는 심화 내용들만 집중적으로 다뤄볼게요!

Reference vs Value: 캡처 시점의 차이
기본 동작
Swift 클로저는 기본적으로 외부 변수에 대한 참조를 캡처하며, 이 참조는 클로저가 실제로 실행되는 시점에 평가됩니다.
var counter = 0 let closure = { print(counter) } counter = 10 closure() // 10 출력 (0이 아님!)클로저 생성 시점이 아닌 실행 시점의 값이 사용됩니다.
Capture List로 값 복사하기
캡처 리스트를 사용하면 클로저 생성 시점의 값을 캡처할 수 있습니다.
var counter = 0 let closure = { [counter] in print(counter) } counter = 10 closure() // 0 출력
Value Type의 예기치 않은 동작
Value type은 외부 스코프로 복사본으로 전달되기 때문에 클로저 캡처 시 다소 복잡할 수 있습니다.
struct User { var name: String } class ViewModel { var user = User(name: "Green") lazy var displayName: () -> String = { // ⚠️ struct는 값 복사! return self.user.name } func updateUser() { user.name = "Updated" print(displayName()) // 여전히 "Green" 출력될 수 있음 } }데이터 레이스가 일어나거나 멀티 스레드 환경에서 정말 극히 드물겠지만 발생할 여지는 있죠.
해결방법 - 캡처 리스트에서 명시적으로 참조 캡처
lazy var displayName: () -> String = { [self] in return self.user.name }
Escaping Closure와 메모리 관리
언제 @escaping이 필요한가?
클로저가 함수의 인자로 전달되었지만 함수가 반환된 후에 호출되는 경우, 해당 클로저는 escaping합니다.
class NetworkManager { var handlers: [() -> Void] = [] // ✅ 저장되므로 @escaping 필요 func register(handler: @escaping () -> Void) { handlers.append(handler) } // ✅ 즉시 실행되므로 non-escaping (기본값) func execute(block: () -> Void) { block() } }
Self 명시의 숨은 의미
non-escaping 클로저에서는 self 키워드를 문제없이 사용할 수 있는데, 클로저가 함수가 반환되기 전에 실행되므로 self가 확실히 존재하기 때문입니다.
class ViewController { var name = "Green" // Non-escaping: self 생략 가능 func updateUI(with block: () -> Void) { print(name) // OK block() } // Escaping: self 명시 필수 func fetchData(completion: @escaping () -> Void) { DispatchQueue.main.async { print(self.name) // self 필수! } } }
Weak vs Unowned 선택 기준
Weak: Optional의 안전성
[weak self] 캡처 리스트를 사용할 때, 클로저 내부의 self 참조는 옵셔널이 되므로 옵셔널 체이닝으로 언래핑해야 합니다.
class DataManager { func fetchData() { networkCall { [weak self] result in guard let self else { return } self.process(result) } } }
Unowned: 다소 위험할 수 있는 최적화
unowned로 캡처된 객체에 접근할 때 객체가 nil이면 런타임 에러가 발생합니다.
class Parent { var child: Child? func setup() { child = Child(parent: self) } } class Child { unowned let parent: Parent // Parent가 먼저 해제되면 크래시! init(parent: Parent) { self.parent = parent } }Weak 사용: delegate, completion handler 등 대부분의 경우
Unowned 사용: 부모-자식 관계에서 자식이 부모보다 먼저 해제되는 것이 보장될 때만
클로저가 Heap에 저장되는 이유
클로저는 참조 타입으로 간주되며 스택이 아닌 힙에 저장됩니다.
캡처된 모든 value type은 클로저의 생명주기 동안 접근할 수 있도록 힙으로 이동됩니다.
여기서 사실 정확한건 value가 heap에 저장된다라고 표현하기 보다는 capture context가 힙 영역에 이동된다고 볼 수 있습니다!
func makeCounter() -> () -> Int { var count = 0 // Value type이지만... return { count += 1 // 클로저가 힙에 있으므로 count도 힙으로 이동! return count } } let counter = makeCounter() print(counter()) // 1 print(counter()) // 2 - count가 유지됨
Capture List 패턴
여러 변수 동시 캡처
class ViewModel { var user: User var settings: Settings func setupHandler() { someAsyncWork { [weak self, user, settings] in guard let self else { return } // self는 weak, user/settings는 strong 캡처 self.process(user: user, settings: settings) } } }Alias를 통한 명확한 의도 표현
class Manager { func fetchData() { api.request { [weak weakSelf = self] in weakSelf?.handleResponse() } } }최적화 메커니즘
최적화로서, Swift는 클로저에 의해 값이 변경되지 않고 클로저가 생성된 후에도 값이 변경되지 않는 경우, 값의 복사본을 캡처하고 저장할 수 있습니다.
let constant = 42 let closure = { print(constant) } // Swift가 자동으로 값 복사 최적화 수행
안티패턴과 해결 방법
❌ 잘못된 패턴: Nested Closure에서 self 중복 캡처
class ViewController { func fetchAndProcess() { networkManager.fetch { [weak self] data in guard let self else { return } // ⚠️ 내부 클로저에서 다시 캡처 필요! DispatchQueue.main.async { self.updateUI(with: data) // 강한 참조 발생! } } } }✅ 해결 패턴
func fetchAndProcess() { networkManager.fetch { [weak self] data in guard let self else { return } DispatchQueue.main.async { [weak self] in guard let self else { return } self.updateUI(with: data) } } }
Conclusion
Closure Capture Semantics의 핵심 포인트들을 정리해볼게요.
1️⃣ 기본 동작: 참조 캡처 + 실행 시점 평가
2️⃣ Value Type: 의도와 다르게 동작할 수 있으니 주의
3️⃣ Escaping: self 명시 + 메모리 관리 필수
4️⃣ Weak/Unowned: 안전한 weak를 기본으로, unowned는 신중하게
5️⃣ 최적화: Swift가 알아서 해주지만, 명시적 캡처로 의도를 분명히프로젝트에서 메모리 릭의 대부분은 클로저 캡처 때문이니, 항상 신경써서 작성해야겠습니다! 🔥
References
Documentation
docs.swift.org
[swift-evolution-announce] [Accepted] SE-0103: Make non-escaping closures the default
lists.swift.org
'Swift' 카테고리의 다른 글
NSAttributedString Performance Optimization (0) 2025.12.06 Swift Build Technologies (0) 2025.11.01 Swift SDK for Android - Android 앱을 Swift로 개발하기 (0) 2025.10.26 Iterate Over Parameter Packs (0) 2025.10.18 Swift Profile Recorder (0) 2025.10.12