Search
Duplicate

Closure (클로저) - 문법 경량화 / @escaping / @autoclosure

생성일
2023/08/20 04:44
태그
Grammar

클로저를 간단하게 사용할 수 있는 경량 문법

 트레일링 클로저 (Trailing Closure)

함수의 마지막 파라미터가 클로저일 때, 이를 파라미터 값 형식이 아닌 함수 뒤에 붙여 작성하는 문법. 이때, Argument Label은 생략된다.
여기서 중요한 것은 마지막 파라미터가 클로저, Argument Label은 생략 이 두 가지이다!

 1. 파라미터가 클로저 하나인 함수

다음과 같이 클로저 하나만 파라미터로 받는 함수가 있을 때 
func doSomething(closure: () -> ()) { closure() }
Swift
복사
이 함수를 호출하려고 하려고 하면, 원래는
doSomething(closure: { () -> () in print("Hello!") })
Swift
복사
이렇게 해야했다.
이렇게 클로저가 파라미터의 값 형식으로 함수 호출 구문 () 안에 작성되어 있는 것을 Inline Closure 라고 부른다!
하지만 헷갈림.. 해석도 불편..
따라서 이 클로저를 파라미터 값 형식으로 보내는 것이 아닌, 함수의 가장 마지막에 클로저를 꼬리처럼 덧붙여서 쓸 수 있는데,
doSomething () { () -> () in print("Hello!") }
Swift
복사
이렇게 쓰는 것이 바로 Trailing Closure 이다!
 중요한 핵심은
1.
파라미터가 클로저 하나일 경우, 이 클로저는 첫 파라미터이자 마지막 파라미터 이므로 트레일링 클로저가 가능하다!
2.
“Closure” 라는 Argument Label은 트레일링 클로저에선 생략된다!
근데 여기서 파라미터가 클로저 하나일 경우엔 더 진화해서 호출 구문인 () 도 생략할 수 있다!
doSomething { () -> () in print("Hello!") }
Swift
복사

 2. 파라미터가 여러 개인 함수

다음과 같이 첫 번째 파라미터로 ‘success’ 란 클로저를 받고, 두 번째 파라미터로 ‘fail’ 이란 클로저를 받는 함수가 있다.
func fetchData(success: () -> (). fail: () -> ()) { // do something... }
Swift
복사
이런 함수가 있을 때, Inline Closure 의 경우,
fetchData(success: { () -> () in print("Success!") }, fail: { () -> () in print("Fail!") })
Swift
복사
이렇게 호출한다!
마지막 파라미터의 클로저는 함수 뒤에 붙여 쓸 수 있는트레일링 클로저 의 경우에는
fetchData(success: { () -> () in print("Success!") }) { () -> () in print("Fail!") }
Swift
복사
요런 모양으로 사용이 가능하다!
파라미터가 여러 개일 경우, 함수 호출 구문 () 를 마음대로 생략해서는 안된다.
success 란 파라미터는 파라미터 값 타입으로 넘겨주어야 하니까

 클로저의 경량 문법

문법을 최적화하여 클로저를 단순하게 쓸 수 있게 하는 것
다음과 같은 함수가 있다고 가정
func doSomething(closure: (Int, Int, Int) -> Int) { closure(1, 2, 3) }
Swift
복사
이 함수는 파라미터로 받은 클로저를 실행하는데, 이때 클로저의 파라미터로 1, 2, 3 이란 숫자를 넘겨주고 있다.
그렇다면, 실제 이 함수를 호출할 때 어떻게 했어야 했냐면,
doSomething(closure: { (a: Int, b: Int, c: Int) -> Int in return a + b + c })
Swift
복사
이렇게 클로저를 Inline Closure 방식으로 full로 작성했어야 했다.
이것을 경량 문법으로 바꿔본다면?

 1. 파라미터 형식과 리턴 형식을 생략할 수 있다.

여기서 파라미터 형식과 리턴 형식은
doSomething(closure: { (a: Int, b: Int, c: Int) -> Int in return a + b + c })
Swift
복사
이것을
doSomething(closure: { (a, b, c) in return a + b + c })
Swift
복사
이렇게 생략할 수 있다.

 2. Parameter Name은 Shortand Argument Names으로 대체하고, 이 경우 Parameter Name과 in 키워드르 삭제한다.

Shortand Argument Names? Parameter Name 대신 사용할 수 있는 것!
보통 Parameter Namein 키워드는
doSomething(closure: { (a, b, c) in return a + b + c })
Swift
복사
이렇게 사용한다.
이때, 이 a, b, c 라는 Parameter Name 대신에
a → $0,
b → $1,
c → $2
이런 식으로 $index 를 이용해 Parameter에 순서대로 접근하는 것이 바로 Shortand Argument Names 이다.
따라서, 경량 문법 규칙에 의해 위 구문은
doSomething(closure: { return $0 + $1 + $2 })
Swift
복사
이렇게 간단하게 작성할 수 있다.

 3. 단일 리턴문만 남을 경우, return 도 생략한다.

단일 리턴문 이란?
doSomething(closure: { return $0 + $1 + $2 })
Swift
복사
이렇게 클로저 내부에 코드가 return 구분 하나만 남은 경우를 말한다.
이때는 return 이란 키워드도 다음과 같이
doSomething(closure: { $0 + $1 + $2 }) doSomething(closure: { print("\($0), \($1), \($2)") $0 + $1 + $2 // Error! })
Swift
복사
생략이 가능하다.

 4. 클로저 파라미터가 마지막 파라미터면, 트레일링 클로저로 작성한다.

위에서 트레일링 클로저 를 배웠다.
따라서 마지막 파라미터인 클로저를 다음과 같이
doSomething() { $0 + $1 + $2 }
Swift
복사
이렇게 트레일링 클로저로 작성이 가능하단 것을 안다.
또한 파라미터가 하나인 경우 () 도 생략 가능하다고 배웠다.

 5. () 에 아무것도 없다면 생략한다.

doSomething { $0 + $1 + $2 }
Swift
복사
이렇게 간단하게 쓸 수 있다.

 @autoclosure

autoclosure는 파라미터로 전달된 일반 구문 & 함수를 클로저로 래핑(Wrapping) 하는 것을 말한다.
먼저, autoclosure는 파라미터 함수 타입 정의 바로 앞에다가 붙여야 한다. 다음과 같이
func doSomething(closure: @autoclosure () -> ()) { }
Swift
복사
이렇게 했을 경우, 이제 closure란 파라미터는 실제 클로저를 전달받지는 않았지만, 클로저처럼 사용이 가능해진다.
다만, 클로저와 다른 점은 실제 클로저를 전달하는 것이 아니기 때문에 파라미터로 값을 넘기는 것처럼 () 를 통해 구문을 넘겨줄 수가 있다.
doSomething(closure: 1 > 2)
Swift
복사
여기서 1 > 2 는 클로저가 아닌 일반 구문이지만, 실제 함수 내에서는
func doSomething(closure: @autoclosure () -> ()) { closure() }
Swift
복사
이렇게 일반 구문을 클로저처럼 사용할 수가 있다.
왜? 클로저로 래핑(Wrapping)한 것이니까!
하지만, 주의할 점은
autoclosure를 사용할 경우, 파라미터가 반드시 없어야 한다.
리턴 타입은 상관 없다!

 1. autoclosure 특징 : 지연된 실행

원래, 일반 구문은 작성되자마자 실행되어야 하는 것이 맞다.
근데 autoclosure로 작성하면, 함수 내에서 클로저를 실행할 때까지 구문이 실행되지 않는다.
왜냐하면 함수가 실행될 시점에 구문을 클로저로 만들어주니까.
따라서, autoclosure의 특징은
원래 바로 실행되어야 하는 구문이 지연되어 실행한다는 특징이 있다.

 @escaping

이때까지 사용하였던 클로저는
func doSomething(closure: () -> ()) { }
Swift
복사
모두 non-escaping Closure 이다. 무슨 말이냐면,
함수 내부에서 직접 실행하기 위해서만 사용한다.
따라서 파라미터로 받은 클로저를 변수나 상수에 대입할 수 없고, 중첩 함수에서 클로저를 사용할 경우, 중첩함수를 리턴할 수 없다.
함수의 실행 흐름을 탈출하지 않아, 함수가 종료되기 전에 무조건 실행되어야 한다.
실제로 상수에 클로저를 대입해보면
func doSomething(closure: () -> ()) { let f: () -> () = closure // Error }
Swift
복사
non-escaping parameter 라고 에러가 뜨는 것을 볼 수 있다.
또한 함수의 흐름을 탈출하지 않는다는 말은, 함수가 종료되고 나서 클로저가 실행될 수 없다는 말이다.
func doSomething(closure: () -> ()) { print("function start") DispatchQueue.main.asyncAfter(deadline: .now() + 10) { // Error closure() } print("function end") }
Swift
복사
따라서, 10초 뒤 클로저를 실행하는 구문을 넣으면, 함수가 끝나고 클로저가 실행되기 때문에 에러가 난다.
또한, 만약 중첩함수 내부에서 매개변수로 받은 클로저를 사용할 경우,
func outer(closure: () -> ()) -> () -> () { func inner() { closure() } return inner // Escaping local funcion captures non-escaping parameter 'closure' }
Swift
복사
중첩함수를 리턴할 수 없다.
이 모든 에러의 원인은 non-escaping closure 의 주변 값 capture 방식에 있는데,
이것은 ARC와 Closure와 관련이 있다 (다음 편!)
→ 중첩함수가 클로저를 return 할 경우 capture 문제 등
결과적으로, 이렇게 함수 실행을 벗어나서 함수가 끝난 후에도 클로저를 실행하거나, 중첩함수에서 실행 후 중첩함수를 리턴하고 싶거나, 변수 상수에 대입하고 싶은 경우,
이때 사용하는 것이 바로 @escaping 키워드이다.
func doSomething(closure: @escaping () -> ()) { }
Swift
복사
이렇게 클로저 파라미터 타입 앞에 @escaping 을 붙여주면 되고,
이럴 경우,
func doSomething(closure: @escaping () -> ()) { let f: () -> () = closure }
Swift
복사
변수나 상수에 파라미터로 받은 클로저를 대입할 수 있고,
또 다음과 같이
func doSomething(closure: @escaping () -> ()) { print("function start") DispatchQueue.main.asyncAfter(deadline: .now() + 10) { closure() } print("function end") } doSomething { print("closure") } // function start // function end // closure
Swift
복사
함수가 종료된 후에도 클로저가 실행될 수 있다.
하지만 @escaping 클로저를 사용할 경우 주의해야할 점은 메모리 관리와 관련된 부분이다.
예를들어, 만약 함수가 종료된 후 클로저를 실행하는데, 이때 클로저가 함수 내부 값을 사용한다. 그럼 이때 함수는 이미 종료 되었는데, 클로저는 함수 내부 값을 어떻게 사용할까? 이런 메모리 관련 부분에서이다!

 Reference