위 코드에서 closureEx1 함수는 파라미터로 closure라는 클로저를 받아 내부에서 동작시키고 있다.
그런데, 함수는 1급객체이고, 클로저는 이름이 없는 함수인데... 함수내에서 클로저를 호출하는 걸로 그치지 않고
상수나 외부 변수에 저장하고 싶으면 코드를 어떻게 작성해야 할까?
아래와 같이 클로저를 파라미터로 받아서 상수에 저장하는 함수 closureEx2와 외부 변수에 저장하는 함수 closureEx3를 작성해보았다.
// 클로저를 상수에 저장
func closureEx2(closure: () -> ()) {
let function: () -> () = closure
}
// 클로저를 외부 변수에 저장
var functionSaved: () -> () = { print("프린트 출력") }
func closureEx3(closure: () -> ()) {
functionSaved = closure
}
위와 같이 코드를 작성하면 각각
"Using non-escaping parameter 'closure' in a context expecting an \@escaping closure""Assigning non-escaping parameter 'closure' to an \@escaping closure" 라는 에러가 뜬다.
에러를 잘 읽어보면, 둘 다 @escaping 클로저를 할당해야 하는데, "non-esacping" 클로저를 할당했기 때문에 발생했다는 것을 알 수 있다.
non-escaping는 무엇이고, @escaping 은 무엇일까?
non-escaping 클로저는
말 그대로 "탈출이 불가능한" 클로저를 의미한다.
함수가 시작되고 종료되기 전에 무조건 실행되는 클로저. 함수의 실행 흐름에서 탈출할 수 없는 클로저라고 이해하면 된다.
@escaping 클로저는
"탈출할 수 있는" 클로저를 의미한다.
함수가 종료된 후에도 실행될 수 있는 클로저. 함수의 실행 흐름에서 탈출할 수 있는 클로저라고 이해하면 된다.
왜 non-escaping 클로저와 @escaping 클로저를 구분해서 사용해야 할까?
이는 메모리와 연관이 있다.
non-escaping의 경우
함수 내에서만 실행이 될 수 있는 형태로, 함수가 실행될 때 스택에 올라가고 클로저가 종료되면 스택에서 내려가게 된다.
@escaping의 경우
함수가 종료된 후에도 실행될 수 있어야 하기 때문에, 함수가 실행되면 클로저를 힙에 올리고 메모리 주소를 스택에 가지고 있어야 한다.
\@escaping 클로저를 사용하게 되면, 해당 클로저가 사용되지 않을 때까지 Heap에 할당된 클로저 메모리를 추적해야 하기 때문에
non-escaping과 비교했을 때 성능이 떨어진다고 볼 수 있다.
따라서, 클로저의 동작 방식에 따라 구분하여 사용하여야 한다.
#
non-escaping과 @escaping의 차이를 알았으니, 앞서 사용했던 코드를 올바르게 바꿔보자.
// 클로저를 상수에 저장
func closureEx2(closure: @escaping () -> ()) {
let function: () -> () = closure
}
// 클로저를 외부 변수에 저장
var functionSaved: () -> () = { print("프린트 출력") }
func closureEx3(closure: @escaping () -> ()) {
functionSaved = closure
}
functionSaved() // 프린트 출력
closureEx3 {
print("클로저를 변수에 저장했다..!")
}
functionSaved() // 클로저를 변수에 저장했다..!
외부 변수 functionSaved의 내부 동작이 바뀐 것을 확인할 수 있다.
이렇게 외부변수나 상수에 클로저를 할당하는 경우 뿐만 아니라,
함수가 종료된 이후에 클로저가 실행될 수 있도록 하는 경우에도 @escaping 클로저를 사용한다.
대표적인 예로 GCD의 비동기 코드 사용이 있다.
func closureEx4(closure: @escaping (String) -> ()) {
var name = "타코"
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { // 3초 뒤 실행
closure(name)
}
}
closureEx4 {
print("내 이름은 \($0)")
}
위와 같이 closureEx4 내부에 DispatchQueue를 이용하여 3초 뒤에 클로저를 실행하는 코드를 작성함으로써
closureEx4가 DispatchQueue를 호출하고 종료된 후에 클로저가 실행되어 프린트가 출력되는 모습을 확인할 수 있다.
정리
@escaping closure는 "함수의 실행 흐름 탈출 가능"
외부변수나 상수에 대입 가능, 클로저를 사용하는 중첩함수 리턴 가능, 함수가 종료된 후에 실행 가능
@escaping 클로저와 non-escaping 클로저의 차이점은 무엇인가요?
우리가 알고 있는 기본적인 클로저의 형태는 다음과 같다.
위 코드에서 closureEx1 함수는 파라미터로 closure라는 클로저를 받아 내부에서 동작시키고 있다.
그런데,
함수는 1급객체이고, 클로저는 이름이 없는 함수
인데... 함수내에서 클로저를 호출하는 걸로 그치지 않고 상수나 외부 변수에 저장하고 싶으면 코드를 어떻게 작성해야 할까?아래와 같이 클로저를 파라미터로 받아서 상수에 저장하는 함수 closureEx2와 외부 변수에 저장하는 함수 closureEx3를 작성해보았다.
위와 같이 코드를 작성하면 각각 "Using non-escaping parameter 'closure' in a context expecting an \@escaping closure" "Assigning non-escaping parameter 'closure' to an \@escaping closure" 라는 에러가 뜬다.
에러를 잘 읽어보면, 둘 다 @escaping 클로저를 할당해야 하는데, "non-esacping" 클로저를 할당했기 때문에 발생했다는 것을 알 수 있다.
non-escaping는 무엇이고, @escaping 은 무엇일까?
non-escaping 클로저는
말 그대로 "탈출이 불가능한" 클로저를 의미한다.
함수가 시작되고 종료되기 전에 무조건 실행되는 클로저. 함수의 실행 흐름에서 탈출할 수 없는 클로저
라고 이해하면 된다.@escaping 클로저는
"탈출할 수 있는" 클로저를 의미한다.
함수가 종료된 후에도 실행될 수 있는 클로저. 함수의 실행 흐름에서 탈출할 수 있는 클로저
라고 이해하면 된다.왜 non-escaping 클로저와 @escaping 클로저를 구분해서 사용해야 할까?
이는 메모리와 연관이 있다.
non-escaping의 경우
함수 내에서만 실행이 될 수 있는 형태로,
함수가 실행될 때 스택에 올라가고 클로저가 종료되면 스택에서 내려가게 된다.
@escaping의 경우
함수가 종료된 후에도 실행될 수 있어야 하기 때문에,
함수가 실행되면 클로저를 힙에 올리고 메모리 주소를 스택에 가지고 있어야 한다.
\@escaping 클로저를 사용하게 되면, 해당 클로저가 사용되지 않을 때까지 Heap에 할당된 클로저 메모리를 추적해야 하기 때문에 non-escaping과 비교했을 때 성능이 떨어진다고 볼 수 있다.
따라서, 클로저의 동작 방식에 따라 구분하여 사용하여야 한다.
#
non-escaping과 @escaping의 차이를 알았으니, 앞서 사용했던 코드를 올바르게 바꿔보자.
외부 변수 functionSaved의 내부 동작이 바뀐 것을 확인할 수 있다.
이렇게 외부변수나 상수에 클로저를 할당하는 경우 뿐만 아니라,
함수가 종료된 이후에 클로저가 실행될 수 있도록 하는 경우
에도 @escaping 클로저를 사용한다.대표적인 예로 GCD의 비동기 코드 사용이 있다.
위와 같이 closureEx4 내부에 DispatchQueue를 이용하여 3초 뒤에 클로저를 실행하는 코드를 작성함으로써 closureEx4가 DispatchQueue를 호출하고 종료된 후에 클로저가 실행되어 프린트가 출력되는 모습을 확인할 수 있다.
정리