iOS Development Guide

escaping closure와 non-escaping closure 차이

핵심은 문법이 아니라 수명이다. closure가 현재 함수 호출이 끝난 뒤에도 살아남을 수 있으면 escaping, 현재 함수 실행 범위를 절대 벗어나지 않으면 non-escaping이다. 이 차이는 API 설계, self 캡처, 저장 가능성, 동시성 모델에 직접 영향을 준다.

Swift closure lifetime API design and capture rules Knowledge item #7
학습 날짜

2026-04-15

쉬운 설명

함수를 부를 때 같이 넘긴 코드 조각이 그 자리에서 바로 쓰이고 끝나면 non-escaping이다. 나중에 저장해 뒀다가 쓰면 escaping이다.

한 줄 요약

escaping 여부는 "closure가 함수 호출의 lexical scope를 넘어서 살아남는가"의 문제다. 이 한 줄을 이해하면 저장, 캡처, 메모리, 비동기 API 설계가 같이 정리된다.

핵심 비교

항목 non-escaping escaping
수명 현재 함수 실행 중에만 사용됨 함수 종료 후에도 살아남을 수 있음
저장 가능성 저장 불가 프로퍼티 저장, 배열 보관, 나중 실행 가능
대표 용도 map, filter, sort, 즉시 평가 completion handler, callback, 이벤트 핸들러
캡처 영향 수명이 짧아 메모리/캡처 부담이 상대적으로 작음 self 캡처 시 retain cycle과 수명 문제가 중요해짐

non-escaping 예시

func runImmediately(_ block: () -> Void) {
    block()
}

runImmediately {
    print("바로 실행")
}

이 closure는 runImmediately가 끝나기 전에 반드시 실행되고 끝난다. 저장되지도 않고, 함수 밖으로 살아남지도 않는다.

escaping 예시

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가 지금 끝나는지, 나중에도 남는지"를 알 수 있게 한다.

실무적으로는 API 계약(contract) 문제다. @escaping을 본 순간 호출자는 "이 closure는 저장되거나 비동기 시점에 실행될 수 있다"고 읽어야 한다.

self 캡처가 왜 더 중요해지나

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은 상대적으로 가볍게 보나

non-escaping closure는 호출이 끝나면 같이 사라진다는 계약이 있으므로, 저장과 지연 실행을 고려한 추가 생명주기 비용이 적다. 그래서 컴파일러 최적화 여지도 더 크고, API 사용자가 추론하기도 쉽다.

실무에서 중요한 경계

설계 기준

기본값은 non-escaping으로 두는 편이 낫다. 정말로 나중에 실행하거나 저장해야 할 때만 @escaping을 붙인다. 즉 @escaping은 "이 API는 호출 시점 이후까지 closure 생명주기를 연장한다"는 강한 선언이다.

흔한 오해

  • @escaping은 비동기 키워드가 아니다. 수명 키워드다.
  • 이름이나 역할이 비동기 작업처럼 느껴져도, closure를 저장하지 않고 현재 함수 안에서 즉시 실행하고 끝내면 non-escaping이다.
  • weak self가 필요한 이유와 @escaping인 이유는 연결되지만 같은 문제는 아니다.

Then 1

왜 `@escaping`은 구현 디테일이 아니라 API 계약인가

호출자 입장에서 closure 수명이 언제 끝나는지가 바뀌기 때문이다. non-escaping이면 현재 호출 컨텍스트 안에서만 reasoning하면 되지만, escaping이면 캡처 대상 생명주기, 저장 위치, 나중 실행 시점까지 함께 봐야 한다.

Then 2

왜 closure를 다른 함수 인자로 다시 넘기면 escaping 판단이 어려워지나

현재 함수 안에서 직접 실행하는지, 다른 곳에 저장하는지, 비동기적으로 넘기는지를 API 경계마다 다시 따져야 하기 때문이다. 이 지점에서 withoutActuallyEscaping 같은 도구가 등장하지만, 일반 앱 코드에서 자주 쓸 것은 아니다.

Then 3

왜 non-escaping closure가 optimization에 유리한가

컴파일러가 closure 수명이 현재 호출 범위를 넘지 않는다는 사실을 알기 때문이다. 저장, 지연 실행, 장기 캡처를 덜 고려해도 되므로 분석 여지가 커진다. 즉 성능보다도 reasoning cost와 분석 가능성이 낮아진다는 점이 더 중요하다.

Then 4

escaping closure는 왜 retain cycle의 hot spot이 되나

객체가 closure를 저장하고, closure가 다시 객체를 잡는 구조가 만들어지기 쉽기 때문이다. 결국 escaping은 "closure가 객체 그래프 안에 정착할 수 있다"는 뜻이므로, strong capture 설계를 더 엄격히 봐야 한다.