쉬운 설명
함수를 부를 때 같이 넘긴 코드 조각이 그 자리에서 바로 쓰이고 끝나면 non-escaping이다. 나중에 저장해 뒀다가 쓰면 escaping이다.
핵심은 문법이 아니라 수명이다.
closure가 현재 함수 호출이 끝난 뒤에도 살아남을 수 있으면 escaping,
현재 함수 실행 범위를 절대 벗어나지 않으면 non-escaping이다.
이 차이는 API 설계, self 캡처, 저장 가능성, 동시성 모델에 직접 영향을 준다.
2026-04-15
함수를 부를 때 같이 넘긴 코드 조각이 그 자리에서 바로 쓰이고 끝나면 non-escaping이다. 나중에 저장해 뒀다가 쓰면 escaping이다.
escaping 여부는 "closure가 함수 호출의 lexical scope를 넘어서 살아남는가"의 문제다. 이 한 줄을 이해하면 저장, 캡처, 메모리, 비동기 API 설계가 같이 정리된다.
| 항목 | non-escaping | escaping |
|---|---|---|
| 수명 | 현재 함수 실행 중에만 사용됨 | 함수 종료 후에도 살아남을 수 있음 |
| 저장 가능성 | 저장 불가 | 프로퍼티 저장, 배열 보관, 나중 실행 가능 |
| 대표 용도 | map, filter, sort, 즉시 평가 | completion handler, callback, 이벤트 핸들러 |
| 캡처 영향 | 수명이 짧아 메모리/캡처 부담이 상대적으로 작음 | self 캡처 시 retain cycle과 수명 문제가 중요해짐 |
func runImmediately(_ block: () -> Void) {
block()
}
runImmediately {
print("바로 실행")
}
이 closure는 runImmediately가 끝나기 전에 반드시 실행되고 끝난다.
저장되지도 않고, 함수 밖으로 살아남지도 않는다.
final class Loader {
private var completion: (() -> Void)?
func load(completion: @escaping () -> Void) {
self.completion = completion
}
func finish() {
completion?()
}
}
여기서는 closure를 프로퍼티에 저장한다.
즉 현재 함수 호출이 끝난 뒤에도 살아남으므로 @escaping이 필요하다.
escaping closure는 함수 호출 경계를 넘어 수명이 연장될 수 있으므로, 캡처 대상의 생명주기와 메모리 그래프가 훨씬 복잡해진다. Swift는 이 차이를 문법 차원에서 명시하게 해서 API 사용자가 "이 closure가 지금 끝나는지, 나중에도 남는지"를 알 수 있게 한다.
@escaping을 본 순간 호출자는 "이 closure는 저장되거나 비동기 시점에 실행될 수 있다"고 읽어야 한다.
non-escaping closure는 현재 함수 호출 안에서만 쓰이므로, 보통 retain cycle 논의의 중심이 되지 않는다. 반면 escaping closure는 객체가 그 closure를 저장할 수 있고, closure가 다시 self를 strong capture하면 순환 참조가 생긴다.
final class LoginViewModel {
var onFinish: (() -> Void)?
func bind() {
onFinish = { [weak self] in
self?.didFinish()
}
}
private func didFinish() { }
}non-escaping closure는 호출이 끝나면 같이 사라진다는 계약이 있으므로, 저장과 지연 실행을 고려한 추가 생명주기 비용이 적다. 그래서 컴파일러 최적화 여지도 더 크고, API 사용자가 추론하기도 쉽다.
DispatchQueue.async, URLSession completion, UI 이벤트 핸들러는 사실상 escaping이다.map, filter, forEach에 넘기는 closure는 보통 non-escaping이다.
기본값은 non-escaping으로 두는 편이 낫다.
정말로 나중에 실행하거나 저장해야 할 때만 @escaping을 붙인다.
즉 @escaping은 "이 API는 호출 시점 이후까지 closure 생명주기를 연장한다"는 강한 선언이다.
@escaping은 비동기 키워드가 아니다. 수명 키워드다.weak self가 필요한 이유와 @escaping인 이유는 연결되지만 같은 문제는 아니다.왜 `@escaping`은 구현 디테일이 아니라 API 계약인가
호출자 입장에서 closure 수명이 언제 끝나는지가 바뀌기 때문이다. non-escaping이면 현재 호출 컨텍스트 안에서만 reasoning하면 되지만, escaping이면 캡처 대상 생명주기, 저장 위치, 나중 실행 시점까지 함께 봐야 한다.
왜 closure를 다른 함수 인자로 다시 넘기면 escaping 판단이 어려워지나
현재 함수 안에서 직접 실행하는지, 다른 곳에 저장하는지, 비동기적으로 넘기는지를 API 경계마다 다시 따져야 하기 때문이다.
이 지점에서 withoutActuallyEscaping 같은 도구가 등장하지만, 일반 앱 코드에서 자주 쓸 것은 아니다.
왜 non-escaping closure가 optimization에 유리한가
컴파일러가 closure 수명이 현재 호출 범위를 넘지 않는다는 사실을 알기 때문이다. 저장, 지연 실행, 장기 캡처를 덜 고려해도 되므로 분석 여지가 커진다. 즉 성능보다도 reasoning cost와 분석 가능성이 낮아진다는 점이 더 중요하다.
escaping closure는 왜 retain cycle의 hot spot이 되나
객체가 closure를 저장하고, closure가 다시 객체를 잡는 구조가 만들어지기 쉽기 때문이다. 결국 escaping은 "closure가 객체 그래프 안에 정착할 수 있다"는 뜻이므로, strong capture 설계를 더 엄격히 봐야 한다.