반응형

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. 실무에서 기억해야 할 규칙

  1. 비동기 = escaping
  2. 함수 안에서만 실행되면 = non-escaping
  3. escaping에서는 반드시 [weak self] 고려
  4. escaping 클로저를 프로퍼티로 저장할 때는 특히 주의
  5. async/await 환경에서는 escaping보다 self 캡처 안전성이 더 중요
  6. non-escaping을 사용하면 성능·안전성이 더 좋고 API가 단순해짐
  7. 컴파일러가 “non-escaping인데 escaping처럼 쓰였다”고 경고하면 즉시 구조 점검

10. 요약

  • @escaping과 non-escaping의 구분은 Swift 클로저 설계의 핵심
  • 잘못 사용하면:
    • 메모리 누수
    • self 캡처 문제
    • 비동기 실행 타이밍 버그
    • 크래시
    • 컴파일 오류
  • 항상 클로저가 함수 밖에서 실행될 가능성이 있는가?를 먼저 판단해야 한다.

핵심 문장:

함수 밖에서 실행되면 @escaping, 함수 안에서만 실행되면 non-escaping.

반응형
Posted by 까칠코더
,