본문 바로가기
iOS/Swift

[iOS/Swift] Non-Escaping Closure 와 Escaping Closure(@escaping)

by iosdevlime 2023. 7. 6.

아래 코드는 흔히 프로젝트를 진행하며 활용하고, 마주치게 되는

비동기 처리 혹은 네트워크 관련 메서드입니다.

 

func fetchGithubProfiles(username: String,
                         completion: @escaping (Result<GithubProfile, Error>) -> Void) {
    
    // ...
    
    do {
        let decoder = JSONDecoder()
        let profiles = try decoder.decode(GithubProfile.self, from: data)
        completion(.success(profiles))
    } catch let error {
        completion(.failure(NetworkError.decodingError(error)))
    }
}

 

위 fetchGithubProfiles 메서드의 completion이란 명칭의 매개변수를 살펴보면,

@escaping 이란 키워드가 타입 앞에 떡하니 붙어서 사용되는데 말입니다..

(클로저가 매개변수로 전달되는 callback 함수 기능을 수행하고자 하는것 같긴 한데?!)

 

이번 포스팅에서는 자주 보니 익숙하지만, 구체적인 역할과 기능이 모호한

Escaping Closure에 대해 구체적으로 짚고 넘어가볼까 합니다.

 

 


 

 

Non-Escaping Closure (탈출이 불가능한 클로저)

 스코프(Scope)에서 벗어날 수 없고, 종료 전까지 반드시 실행되어야 하는 탈출 불가능 클로저

 

Escaping Closure (이하 '탈출 클로저')에 대해 다루기에 앞서,

 

우선 일반적이고 익숙하게 다뤄온

소위 '그냥 클로저', Non-Escaping Closure의 특징을 빠르게 짚고 넘어가도록 하겠습니다.

 

그 전에, 이 포스팅을 통해 클로저의 1급 객체함수 포스팅을 되짚어 보고 오시길 권장드립니다.

 

 

 

Non-Escaping Closure(탈출할 수 없는 클로저)의 특징

  • 일반적으로 활용하는 클로저는 모두 Non-Escaping Closure입니다.
    • 1급 객체함수 특징에 의해, 함수의 파라미터로 클로저를 활용할 수 있습니다.
    • 이는 소위 '콜백(Callback) 함수'로 불리며 사용됩니다.
class playerManager {

    static let shared = playerManager()
    
    // callback
    func checkPlayerInfo(name: () -> Void) {
        name()
    }
}

playerManager.shared.checkPlayerInfo {
    print("Kim")
}

// Kim

 

  • 위 PlayerManager 클래스 내부의 checkPlayerInfo 함수는 콜백 함수입니다.
    • 매개변수로 들어오는 클로저를 그대로 반환합니다.
    • 여기서 활용된 클로저는 프린트(print) 구문으로서, 아래와 같은 순서로 실행됩니다.
checkPlayerInfo 함수가 실행되는 순서

1️⃣ 프린트 구문(클로저)checkPlayerInfo 함수의 매개변수 값으로 할당
2️⃣ checkPlayerInfo 함수는 콜백 함수로서, 매개변수인 프린트 구문(클로저)를 실행
3️⃣ 이후, checkPlayerInfo 함수는 종료됨 (end)

 

  • checkPlayerInfo 함수가 완전히 종료되기 전에 프린트 구문(클로저)은 반드시 실행되어야 합니다.
    • 결과적으로, 프린트 구문(클로저)는 함수의 Scope를 벗어나 실행될 수 없습니다.
    • 그렇기 때문에 Non-escaping Closure 란 명칭을 가지게 됩니다.

 

 


 

 

 

Escaping Closure (탈출이 가능한 클로저)

외부 변수&상수에 그 자체로 저장할 수 있으며, 비동기 작업 처리가 가능한 탈출 가능 클로저

 

탈출 클로저는 기존에 알고있었던 (탈출 불가능)클로저와는 어떤 차이가 있을까요?

 

첫 번째,

외부 변수 혹은 상수에 저장할 수 있다!

 

두 번째,

함수가 종료된 이후에도 탈출 클로저는 비 동기적으로 실행될 수 있다! 

 

➟ Non-escaping 클로저에서 다룬 예시를 토대로 첫 번째 특징부터 살펴보도록 하겠습니다.

 

 

 

첫 번째, 외부 변수 혹은 상수에 저장

  • playerManager 클래스 내부에 playerName이란 명칭의 클로저를 담는 변수를 생성합니다.
    • checkPlayerInfo 함수의 name 클로저 값을 ➟ playersName 변수에 할당하고자 합니다.
    • 하지만, Swift 에서는 함수의 매개변수로 전달된 클로저는 해당 Scope를 벗어날 수 없습니다.
      (다시 말해, 내부에서만 활용이 가능하기 때문에 외부 변수 혹은 상수에 할당할 수 없습니다)
class playerManager {

    static let shared = playerManager()

    private var playersName: () -> Void = { }

    func checkPlayerInfo(name: () -> Void) {
        // Error : Assigning non-escaping parameter 'name' to an @escaping closure  
        playersName = name     
        name()
    }
}

 

  • 여기서 활용되는 것이 바로 Escaping Closure, 탈출 클로저 입니다. 
    • 매개변수의 타입 앞에 @escaping 키워드를 작성하면 탈출 클로저로 변경됩니다.
    • 외부에서 checkPlayerInfo 함수를 실행하게 되면, playerName 변수에 값이 할당됩니다. 
class playerManager {

    var playerName: () -> Void = { }

    static let shared = playerManager()
    
    // 1. 매개변수 타입(Type) 앞에 @escaping 키워드 작성
    func checkPlayerInfo(name: @escaping () -> Void) {
        playerName = name
        playerName()
    }
}

// 2. 매개변수에 프린트 구문을 할당
playerManager.shared.checkPlayerInfo {
    print("Kim")
}

// 3. 임의상수 PlayerNameCheck에 playerName을 할당
let playerNameCheck = playerManager.shared.playerName

// 4. 호출 시, 탈출 클로저에 의해 값이 저장된 것을 확인
playerNameCheck() // Kim (변수에 저장이 되었다!)

 


 

 

두 번째, 함수가 종료된 이후에 실행되는 비 동기적 작업처리

  • 아래 예시는 Github API를 활용하여 프로필 정보를 호출하기 위한 Network 코드입니다.
    • fetchGithubProfile 함수 호출을 통해 데이터를 호출하고, 디코딩을 실시합니다.
    • 함수의 매개변수로 활용되는 탈출 클로저는 함수가 종료된 이후에도 실행될 수 있습니다.
      (함수가 종료되기 전까지 클로저가 실행되어야 하는 Non-escaping closure와의 차이)
final class NetworkService {
	...
    
    // completion 클로저는 Escaping Closure로, 해당 호출함수가 종료된 이후에도 조건에 의해 순차적으로 실행될 수 있음
    
    // 데이터 요청을 위한 함수를 실행
    func fetchGithubProfiles(username: String,
                             completion: @escaping (Result<GithubProfile, Error>) -> Void) {
        
    ...
            
            // decoding
            do {
                // decoding 작업이 완료된 후 -> completion을 호출함
                let decoder = JSONDecoder()
                let profiles = try decoder.decode(GithubProfile.self, from: data)
                completion(.success(profiles))
            } catch let error {
                completion(.failure(NetworkError.decodingError(error)))
            }
        }
        task.resume()
    }
}

 

  • networkService 생성을 통해 Github API로 부터 데이터를 할당받고자 합니다.
    • fetchGithubProfile 함수가 종료된 이후에도, 클로저를 호출하여 비 동기적 작업을 수행합니다.
    • 데이터 요청 ➟ 함수 종료  응답 수신 ➟ 클로저 실행 순서로 원활한 작업을 진행할 수 있습니다.

      🚫 만약, Non-escaping Closure의 경우라면?

      ➟ 응답이 완벽하게 수신되기도 전에 함수를 종료(클로저를 실행)되는 문제 발생 우려
let networkService = NetworkService(configure: .default)

// 데이터 요청 
networkService.fetchGithubProfiles(username: "onthelots") { result in
    // completion (탈출 클로저) 호출
    switch result {
    case .failure(let error) :
        print("에러코드 : \(error)")
    case .success(let profiles) :
        print("출력결과 : \(profiles)")
    }
}

 


 

@Escaping 키워드를 활용한 탈출 클로저,

Escaping Closure에 대해 간단하게 살펴보았습니다.

 

기존 클로저는 함수 내부에서 종료됨으로서 메모리 관리가 깔끔하지만,

 

탈출 클로저는 지속적인 참조(Reference)과정을 거치기 때문에

순환참조가 발생하여 메모리 누수(Memory leak)가 발생할 가능성이 있습니다.

 

이와 관련된 내용은 다음 포스팅에서 자세하게 다뤄보도록 하겠습니다. 

댓글