programming/Swift

[Swift/iOS] 클로저 (Closure) from 공식문서

마들브라더 2023. 4. 25. 07:00

! 공식문서를 참고한 글입니다

클로저?

클로저는 한마디로 말해서 코드블럭 { } 이다.

어렵게 생각하지 말자

 

클로저의 3가지 종류

클로저는 3가지 종류가 있다.

  • 전역 함수(Global function) : 이름이 있고(Named Closure) 어떤 값도 캡쳐하지 않는 클로저
  • 중첩 함수(Nested function) : 이름이 있고(Named Closure) 관련된 함수로부터 값을 캡쳐할 수 있는 클로저
  • 클로저 표현(Closure expression) : 경량화된 문법으로 쓰여지고 관련된 문맥으로(context) 값을 캡쳐할 수 있는 이름이 없는(Unnamed Closure) 클로저

설명을 들으면 어렵다

캡쳐한다는 뜻은 그냥 미뤄두자

 

전역 함수는 우리가 생각하는 일반적인 형태의 '함수'를 생각하면 된다

func globalFunction(stringParameter: String) -> String {
    return stringParameter
}

중첩 함수는 말 그대로 함수가 중첩된 것, 함수 안에 함수가 있는 것으로 보면 된다

func outter() {
    func nested() {
        
    }
}

 

전역 함수와 중첩 함수는 이름이 있는(Named Closure) 함수이고 Closure의 한 형태 이지만, 일반적으로 클로저를 지칭할 때에는 이름이 없는 클로저 표현을 말한다

 

클로저 표현 문법 Closure Expression Syntax

{ (<#parameters#>) -> <#return type#> in
   <#statements#>
}

{ 파라미터1: 파라미터타입1, 파라미터2: 파라미터타입2 -> 리턴타입 in
    코드
}

let 만나이계산기 = { (age: Int) -> String in
    let 만나이 = age - 1
    return "만나이는 \(만나이)입니다."
}

클로저 형태는 위와 같다

클로저를 만나이계산기 라는 상수에 할당했다

만나이 계산기의 타입을 보면 Int 파라미터를 갖고 String을 리턴값으로 갖는 클로저, 다시 말해서 함수 그 자체인 것을 알 수 있다

print(type(of: 만나이계산기)) // (Int) -> String

사용할 때는 아래와 같이 사용하면 된다

let value = 만나이계산기(10)
print(value) // 만나이는 9입니다.
print(type(of: value)) // String

 만나이계산기(10) 로 클로저를 담은 만나이계산기를 실행하고, String의 리턴값을 value에 할당했다

 

문맥에서 타입 추론 Inferring Type From Context

이번에는 함수 안에서 클로저를 사용해보자

func 더하기(closure: (Int, Int) -> String) {
    print(closure(1, 2))
}

"더하기" 라는 함수는 Int 파라미터 두개와 String을 리턴하는 클로저를 파라미터로 받는다

그리고 숫자 1과 2를 넣어서 클로저를 실행하고, 클로저의 return 값인 String을 Print하는 함수다

 

여기서 클로저를 사용하려면

더하기(closure: { (첫번째숫자: Int, 두번째숫자:Int) -> String in
    return "\(첫번째숫자 + 두번째숫자)입니다"
})
// 3입니다

와 같은 구조로 실행할 수 있다

 

처음에 함수를 정의할 때, closure라는 파라미터는 Int 타입 두개를 파라미터로 받는다는 것을 알고 있기 때문에 클로저의 파라미터 타입을 생략할 수 있다

더하기(closure: { 첫번째숫자, 두번째숫자 -> String in
    return "\(첫번째숫자 + 두번째숫자)입니다"
})

리턴 값 역시 String으로 반환된다는 것을 이미 알고 있기 때문에 생략 가능하다

더하기(closure: { 첫번째숫자, 두번째숫자 in
    return "\(첫번째숫자 + 두번째숫자)입니다"
})

 

단일 표현 클로저에서의 암시적 반환 Implicit Returns from Single-Express Closures

단일 표현, 곧 return 한 줄의 코드만 남는 경우에는 return 키워드 역시 생략가능하다

더하기(closure: { 첫번째숫자, 두번째숫자 in
    "\(첫번째숫자 + 두번째숫자)입니다"
})

 

인자 이름 축약 Shorthand Arguments Names

함수를 정의할 때 우리는 클로저의 파라미터들과 반환값의 타입이 정해져있다는 것이 아니라 그 형식 자체가 이미 정해져있다는 것을 알 수 있다

그래서 굳이 첫번째숫자, 두번째숫자 라는 인자의 이름또한 생략해줄 수 있다

그리고 생략해준 대신에 코드안에서 순서대로 $0, $1 ... 와 같이 사용할 수 있다

더하기(closure: {
    "\($0 + $1)입니다"
})
// 3입니다

처음에 비해 훨씬 간결해졌다

후위 클로저 Trailing Closures

함수의 마지막 인자가 클로저라면, 그 클로저를 후위 클로저로 사용할 수 있다

처음 더하기 함수의 파라미터가 하나뿐이기 때문에 해당 클로저를 후위 클로저로 사용해보자

클로저 파라미터를 함수 뒤로 빼서 위치를 바꾼다고 생각하면 된다

더하기(closure: {
    "\($0 + $1)입니다"
})

더하기() {
    "\($0 + $1)입니다"
}

후위 클로저를 사용한다면 괄호()까지 생략가능하다

더하기 {
    "\($0 + $1)입니다"
}
// 3입니다

 

값 캡쳐 (Capturing Values)

공식문서의 예제를 보면 조금 어렵고 헷갈린다

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

조금 단순화해보자

func add(addNumber: Int) -> () -> Int {
    var totalNumber = 0
    func adding() -> Int {
        totalNumber = totalNumber + addNumber
        return totalNumber
    }
    return adding
}

단순화 실패...

아무튼 add라는 함수는 Int를 파라미터로 받고, "Int를 반환값으로 갖는 함수( () -> Int )"를 반환 값으로 갖는다

    func adding() -> Int {
        totalNumber = totalNumber + addNumber
        return totalNumber
    }

가운데 adding 함수만 살펴보면 내부 코드에 totalNumber와 addNumber를 정의해주지 않는데 잘 돌아가는데,

이것은 외부 함수인 add에서 totalNumb와 addNumber를 캡쳐링하기 때문이다

위 사례는 중첩함수로서 처음에 언급했던 값을 캡쳐할 수 있는 클로저 이다

 

let plusTen = add(addNumber: 10)
print(type(of: plusTen)) // () -> Int
print(plusTen()) // 10
print(plusTen()) // 20
print(plusTen()) // 30

plusTen이라는 상수에 함수를 할당해보자

plusTen은 곧 Int값을 반환하는 함수가 된 것이니 이를 실행하면 처음에는 10을 반환한다

또 실행하면 20, 30을 반환하는 것을 알 수 있는데

totalNumber와 addNumber가 캡쳐링되기 때문에 계산이 계속 누적된다

 

클로저는 참조 타입 Closures are Reference Types

let anotherPlusTen = plusTen
print(type(of: anotherPlusTen)) // () -> Int
print(anotherPlusTen()) // 40

함수, 클로저는 참조타입이기 때문에 상수나 변수에 할당할 때는 해당 함수와 클로저의 참조(reference)가 할당된다

 

이스케이핑(탈출) 클로저 Escaping Closures

클로저를 파라미터로 넣을 수 있는 경우는 위에서 보았는데, 사용할 수 없는 경우가 있다

var handler: (() -> Void)?

func 함수(closure: () -> Void) {
    handler = closure
}

위와 같이 함수 외부에 정의된 변수에 저장하려면 

Assigning non-escaping parameter 'closure' to an @escaping closure

위와 같은 에러가 뜬다

기본 closure는 non-escaping 클로저 인데, 이를 외부 변수에 저장할 수 없다(클로저가 함수에서 빠져나갈 수 없다)

외부 변수에 저장하려면 아래와 같이 @escaping 키워드를 붙여야 한다(클로저가 함수에서 빠져나가서 다른 외부 변수에 할당)

 

var handler: (() -> Void)?

func 함수(closure: @escaping () -> Void) {
    handler = closure
}

오류 해결

 

 

탈출클로저와 자동클로저는 이어서...