iOS 개발자가 많이 하는 실수 - @escaping / non-escaping 클로저 차이를 잘못 이해해 크래시·경고가 발생하는 실수
Dev Study/iOS 2025. 12. 4. 20:58반응형
iOS 개발자가 많이 하는 실수 - @escaping / non-escaping 클로저 차이를 잘못 이해해 크래시·경고가 발생하는 실수
Swift에서 클로저는 기본적으로 두 가지 종류가 있습니다.
- non-escaping 클로저: 함수가 끝나기 전에 반드시 실행되는 클로저
- @escaping 클로저: 함수가 끝난 뒤에도 실행될 수 있는 클로저
이 두 개념을 정확히 이해하지 못하면 다음과 같은 문제가 발생합니다.
- 강한 순환 참조(메모리 누수)
- self 캡처 잘못으로 인해 크래시
- escaping/non-escaping 경고
- async 작업에서 self가 강하게 캡처되는 문제
- API 설계 시 비효율적 또는 위험한 구조가 생성
1. 기본 개념: non-escaping vs @escaping
1-1. non-escaping 클로저
Swift의 기본 클로저 파라미터는 모두 non-escaping입니다.
func perform(_ work: () -> Void) {
work() // 함수 안에서 실행 → non-escaping
}
특징:
- 반드시 함수가 끝나기 전에 실행됨
- Swift가 최적화를 더 많이 할 수 있음
- self 캡처 시 strong cycle 위험이 거의 없음
- 클로저가 비동기적으로 쓰일 수 없음
1-2. escaping 클로저 (@escaping)
함수 바깥에서 실행될 수 있는 클로저는 반드시 @escaping을 붙여야 합니다.
func performLater(_ work: @escaping () -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
work() // 함수가 끝난 뒤에 실행
}
}
특징:
- 함수 종료 후에도 실행 가능
- self 캡처 시 반드시 [weak self] 고려해야 함
- 비동기 네트워크, Timer, URLSession 등의 콜백은 모두 escaping
즉:
비동기라면 대부분 @escaping이다.
동기라면 non-escaping이다.
2. 실수 1: escaping 클로저인데 @escaping을 빼먹어 컴파일 오류 발생
예:
func fetchUser(completion: () -> Void) {
URLSession.shared.dataTask(with: url) { data, response, error in
completion() // ❌ 함수 종료 후 호출 → escaping
}
.resume()
}
오류 메시지:
Escaping closure captures non-escaping parameter 'completion'
해결:
func fetchUser(completion: @escaping () -> Void) {
URLSession.shared.dataTask(with: url) { data, response, error in
completion() // ✔️ valid
}.resume()
}
3. 실수 2: escaping 클로저에서 weak self 없는 strong capture → 메모리 누수
func load(completion: @escaping () -> Void) {
self.worker.doSomething {
self.updateUI() // ❌ strong self capture → memory leak 위험
completion()
}
}
해결:
func load(completion: @escaping () -> Void) {
self.worker.doSomething { [weak self] in
self?.updateUI() // ✔️ safe
completion()
}
}
4. 실수 3: non-escaping 클로저에서 self 캡처를 불필요하게 약하게 처리하는 경우
func updateUI(_ work: @escaping () -> Void) { // ❌ escaping 필요 없음
work()
}
이 함수는 동기적으로만 실행되는데 굳이 @escaping을 사용하면:
- self 캡처 시 weak self를 사용하게 됨 → 불필요하게 복잡해짐
- 컴파일러 최적화가 줄어듦
- API 디자인이 비효율적이 됨
실제 올바른 형태:
func updateUI(_ work: () -> Void) { // ✔️ non-escaping
work()
}
5. 실수 4: async/await 환경에서도 @escaping 개념을 혼동
다음 코드를 보자.
func doWorkAsync(operation: () async -> Void) {
Task {
await operation()
}
}
여기에서 많은 개발자가 “operation은 escaping인가?”라고 묻지만,
정확히는:
- async 클로저는 기본적으로 non-escaping
- 하지만 Task 내부에 넘기면 사실상 escaping처럼 동작할 수 있음
- Swift가 자동으로 메모리/수명 관리를 처리해줌
즉, async/await에서는:
escaping 개념보다는 “self를 어떻게 캡처하는가”가 더 중요해짐.
6. 실수 5: @escaping 클로저를 프로퍼티로 저장했는데 해제하지 않음
예:
class Worker {
var completion: (() -> Void)?
func doWork(_ completion: @escaping () -> Void) {
self.completion = completion // ❌ self가 completion을 강하게 잡음
}
}
만약 completion 내부에서 self를 strong capture하면:
self → worker → completion → self
강한 순환 참조 발생.
해결:
- completion 프로퍼티는 가능하면 weak self를 쓰도록 문서화
- 작업 끝나면 nil로 해제
- 혹은 delegate 패턴으로 교체
- 혹은 async/await로 구조 단순화
예:
class Worker {
var completion: (() -> Void)?
func doWork(_ completion: @escaping () -> Void) {
self.completion = { [weak self] in
guard self != nil else { return }
completion() // ✔️ safe
}
}
deinit {
completion = nil
}
}
7. 실수 6: escaping 클로저가 필요한데 non-escaping으로 선언해 UI 업데이트 누락
func loadData(using perform: () -> Void) {
DispatchQueue.main.async { // ❌ perform은 함수 종료 이후 호출
perform()
}
}
해결:
func loadData(using perform: @escaping () -> Void) {
DispatchQueue.main.async { // ✔️
perform()
}
}
8. 실수 7: escaping 클로저에서 mutating 메서드 호출 불가 개념 혼동
Swift 구조체에서 이런 코드는 불가능합니다.
struct Loader {
var value = 0
func load(completion: @escaping () -> Void) { // ❌
completion()
}
}
이유:
- 구조체는 값 타입
- @escaping 클로저는 구조체의 mutating 컨텍스트에서 사용될 수 없음
- Swift는 self의 수명 보장을 할 수 없음
해결:
- class로 변경
- completion을 non-escaping으로 유지
- async/await 사용 등
9. 실무에서 기억해야 할 규칙
- 비동기 = escaping
- 함수 안에서만 실행되면 = non-escaping
- escaping에서는 반드시 [weak self] 고려
- escaping 클로저를 프로퍼티로 저장할 때는 특히 주의
- async/await 환경에서는 escaping보다 self 캡처 안전성이 더 중요
- non-escaping을 사용하면 성능·안전성이 더 좋고 API가 단순해짐
- 컴파일러가 “non-escaping인데 escaping처럼 쓰였다”고 경고하면 즉시 구조 점검
10. 요약
- @escaping과 non-escaping의 구분은 Swift 클로저 설계의 핵심
- 잘못 사용하면:
- 메모리 누수
- self 캡처 문제
- 비동기 실행 타이밍 버그
- 크래시
- 컴파일 오류
- 항상 클로저가 함수 밖에서 실행될 가능성이 있는가?를 먼저 판단해야 한다.
핵심 문장:
함수 밖에서 실행되면 @escaping, 함수 안에서만 실행되면 non-escaping.
반응형

