본문 바로가기

Swift

[Swift] Closure? 함수 블록 클로저에 대해 알아보자

클로져(Closure)란 코드에서 전달되고 사용될 수 있는 자체적인(self-contained) 함수 블록입니다. 스위프트에서의 클로저는 C와 Objective-C에서의 블록과 유사하고 다른 프로그래밍 언어에서는 람다와 유사합니다.

 

클로저는 클로저가 선언된 컨텍스트에 있는 모든 상수나 변수들의 참조값을 저장하고 캡처할 수 있습니다. 이것은 상수나 변수들을 closing over 한다고 표현하기도 합니다. 스위프트는 캡처에 대한 모든 메모리 관리를 다룹니다.

 

값을 캡처하는 것(Capturing Values)에 대한 개념은 다음 포스팅에서 자세히 다루도록 하겠습니다.

 

흔히 알고있는 글로벌 함수, 중첩 함수(함수 내부에 선언된 함수) 들은 실제로는 클로저의 특별한 케이스 입니다. 클로저는 다음 3가지 중 하나의 형식을 취합니다.

  • 글로벌 함수(Global Functions)는 이름이 있고 어떤 값도 캡처하지 않는 클로저 입니다.
  • 중첩 함수(Nested Functions)는 이름이 있고 중첩 함수를 둘러 싸고 있는 함수의 값들을 캡처할 수 있는 클로저 입니다.
  • 클로저 표현식(Closure Expressions)은 주변 컨텍스트의 값을 캡처할 수 있는 간단한 문법으로 작성된 익명 클로저 입니다.

스위프트의 클로저 표현식은 깔끔하고 명확한 스타일을 가지고 있습니다. 일반적인 상황에서 복잡하지 않은 간결한 구문을 권장하는데에 최적화 되어있습니다. 최적화는 다음 내용을 포함합니다.

  • 컨텍스트의 매개 변수, 반환 값 타입 추론이 가능
  • 한 줄로 이루어진 클로저의 경우 결과가 바로 반환되므로 return 키워드 생략 가능
  • 인자 값의 약칭($0, $1)이 가능하고 이 때, in 키워드 생략이 가능
  • 클로저가 함수의 마지막 인자 값으로 사용될 경우 인자의 label을 생략하여 후행 클로저로 나타낼 수 있음

실제로 클로저가 어떻게 쓰이는지 코드 레벨에서 알아보도록 하겠습니다.

Closure Expressions

클로저 표현식은 인라인 클로저를 간결하고 집중된 구문으로 작성하는 하나의 방법입니다. 클로저 표현식은 명확성이나 의도를 잃지 않고 단축된 형태로 클로저를 작성하기 위한 몇 가지 구문을 최적화 하였습니다. 아래의 클로저 표현식 예제들은 동일한 기능을 보다 간결하게 표현하는 여러 방법을 sorted(by:) 메소드를 통해 나타낼 것입니다.

The Sorted Method

스위프트 표준 라이브러리는 sorted(by:) 메소드를 제공합니다. 이 메소드는 사용자가 제공한 정렬 클로저를 기반으로 명시된 타입의 값의 배열을 정렬시킵니다. 정렬이 완료되면 정렬된 새 배열을 반환합니다. 원래 배열은 수정되지 않습니다. 먼저 알파벳 정렬 예시를 위한 배열을 하나 선언하겠습니다.

 

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

 

sorted(by:) 메소드는 배열에 들어있는 값 타입과 동일한 타입(String)을 가진 두 개의 인수를 취하는 클로저를 인자로 받습니다. 값이 정렬될 때 첫 번째 값이 두 번째 값 앞에 나타나야 하는지 또는 뒤에 나타나야 하는지에 대한 여부(Bool)를 반환합니다. 첫 번째 값이 두 번째 값 앞에 나타나야 하는 경우 true를 반환해야 하며, 그렇지 않으면 false를 반환해야 합니다.

 

func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2 // 내림차순
}

var reversedNames = names.sorted(by: backward)
// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

 

첫 번째 문자열(s1)이 두 번째 문자열(s2)보다 크면 true를 반환합니다. 따라서 정렬된 배열의 s2 앞에 s1이 나타나야 함을 의미합니다. 결국 내림차순을 의미하게 되는 것입니다.

Closure Expression Syntax

아래는 클로저 표현식의 일반적인 형태 입니다.

 

{ (parameters) -> returnType in
    statements
}

 

sorted(:by) 메소드를 다양한 클로저 표현식을 적용하면서 어떻게 간결하게 변해가는지 알아보겠습니다. 아래는 가장 기본적인 클로저 표현식을 적용한 예시입니다.

 

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

 

클로저의 body가 너무 짧기 때문에 아래와 같이 한 줄로 나타내기도 합니다.

 

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 })

 

이것은 sorted(by:) 메소드에 대한 전반적인 호출이 그대로 유지된 것을 알 수 있습니다. 또한 여전히 메소드를 괄호로 감싸고 있습니다. 그러나 기존에 함수를 선언하여 인자로 전달하는 방식이 아닌 인라인 클로저(Inline Closure)가 된 것을 알 수 있습니다.

Inferring Type From Context

정렬 클로저는 sorted(by:) 메소드의 인자로 전달되기 때문에, 스위프트는 해당 매개 변수와 리턴 값의 타입을 유추할 수 있습니다. 즉, 굳이 (String, String)과 Bool 타입을 클로저 표현식의 정의에 쓸 필요가 없는 것입니다.

 

reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

Implicit Returns from Single-Expression Closures

단일(single) 표현식 클로저는 return 키워드를 제외하고 암시적으로 반환할 결과를 나타냅니다.

 

reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

 

여기서 sorted(by:) 메소드의 함수 유형은 Bool값을 클로저에 의해 반환해야 함이 명확합니다. 클로저 body에 Bool 값을 반환하는 단일 표현식(s1 > s2)이 포함되어 있기 때문에 모호함이 없으며 return 키워드를 생략할 수 있습니다.

Shorthand Argument Names

스위프트는 인라인 클로저의 인자에 대한 약칭형 이름을 자동으로 제공합니다. 이는 $0, $1, $2 등의 이름으로 인자 값을 참조하는데 사용할 수 있습니다. 약칭형 이름을 사용할 경우 클로저의 인수 정의 부분을 생략할 수 있으며 함수 유형에서 타입을 추론하게 됩니다. 또한, 클로저 표현식은 전적으로 body로 구성되어지기 때문에 in 키워드 또한 생략할 수 있습니다.

 

reversedNames = names.sorted(by: { $0 > $1 } )

 

여기서 $0과 $1은 클로저의 첫번째와 두번째 String 인자 값을 의미합니다.

Operator Methods

사실 위의 클로저 표현식보다 더 짧게 작성하는 방법이 있습니다. 스위프트의 String 타입에서 비교 연산자(>)의 경우에, 구현을 String 타입 2개의 파라미터를 가지고 Bool을 반환하는 것으로 정의되어 있습니다. 이것은 sorted(by:) 메소드에 필요한 유형과 정확히 일치합니다. 따라서 연산자를 그냥 통과시키면 스위프트는 문자열 비교를 사용하는 것으로 추론할 수 있습니다.

 

reversedNames = names.sorted(by: >)

Trailing Closures

함수의 마지막 인자값으로 클로저 표현식을 함수에 전달해야 하고 클로저 표현식이 긴 경우, 후행 클로저 방식으로 작성하는 것이 유용할 것입니다. 후행 클로저는 함수 호출의 괄호 뒤에 쓰여지는데, 여전히 함수의 인자로 받아들여지는 것은 그대로 입니다. 후행 클로저를 사용할 때는 함수 호출의 클로저 인수에 대한 label을 작성하면 안됩니다.

 

func someFunctionThatTakesAClosure(closure: () -> Void) {
    print("Hello World!")
}

// 후행 클로저를 사용하지 않았을 때
someFunctionThatTakesAClosure(closure: {
    print("Hello World!")
})

// 후행 클로저를 사용했을 때
someFunctionThatTakesAClosure() {
    print("Hello World!")
}

 

후행 클로저를 sorted(by:) 메소드에 적용해 보겠습니다.

 

// 후행 클로저를 사용하지 않았을 때
reversedNames = names.sorted() { $0 > $1 }

// 후행 클로저를 사용했을 때
reversedNames = names.sorted { $0 > $1 }

정리

  • 클로저는 전달되고 사용되는 자체적인 함수 블록입니다.
  • 클로저의 형식에는 글로벌 함수, 중첩 함수, 클로저 표현식이 있습니다.
  • 클로저 표현식은 간결하고 명확하게 작성가능한 여러 방식이 존재합니다.