🔥 클로저 표현식

1091자
15분

중첩 함수는 큰 함수 내부에 자체적으로 포함된 코드 블록에 이름을 붙이고 정의할 수 있는 편리한 방법이에요. 하지만 때로는 함수의 전체 선언부와 이름 없이 간결한 형태로 함수와 비슷한 구조를 작성하는 것이 유용할 때가 있죠. 특히 함수나 메서드의 인자로 함수를 전달할 때 더욱 그렇답니다.

클로저 표현식은 간결하고 읽기 쉬운 구문으로 인라인 클로저를 작성하는 방법이에요. 클로저 표현식은 명확성이나 의도를 잃지 않으면서 클로저를 간결하게 작성할 수 있도록 여러 구문 최적화를 제공한답니다. 아래의 클로저 표현식 예제는 sorted(by:) 메서드의 단일 예제를 여러 번 반복하여 이러한 최적화를 보여주는데, 각각은 동일한 기능을 더 간결한 방식으로 표현하고 있어요.

sorted(by:) 메서드

Swift의 표준 라이브러리는 sorted(by:) 메서드를 제공하는데, 이 메서드는 제공하는 정렬 클로저의 출력을 기반으로 알려진 타입의 값 배열을 정렬해요. 정렬 프로세스가 완료되면 sorted(by:) 메서드는 이전 배열과 동일한 타입과 크기의 새 배열을 반환하며, 요소는 올바른 정렬된 순서로 되어 있죠. 원래 배열은 sorted(by:) 메서드에 의해 수정되지 않습니다.

아래의 클로저 표현식 예제는 sorted(by:) 메서드를 사용하여 String 값의 배열을 알파벳 역순으로 정렬해요. 다음은 정렬할 초기 배열이에요:

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

sorted(by:) 메서드는 배열 내용과 동일한 타입의 두 인자를 취하고 값이 정렬된 후 첫 번째 값이 두 번째 값 앞에 나타나야 하는지 여부를 말하기 위해 Bool 값을 반환하는 클로저를 받아요. 정렬 클로저는 첫 번째 값이 두 번째 값 앞에 나타나야 하는 경우 true를 반환해야 하고, 그렇지 않으면 false를 반환해야 해요.

이 예제는 String 값의 배열을 정렬하고 있으므로 정렬 클로저는 (String, String) -> Bool 타입의 함수여야 해요.

정렬 클로저를 제공하는 한 가지 방법은 올바른 타입의 일반 함수를 작성하고 이를 sorted(by:) 메서드의 인자로 전달하는 것이에요:

func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames는 ["Ewa", "Daniella", "Chris", "Barry", "Alex"]와 같아요.
swift

첫 번째 문자열(s1)이 두 번째 문자열(s2)보다 크면 backward(_:_:) 함수는 true를 반환하여 정렬된 배열에서 s1s2 앞에 나타나야 함을 나타내요. 문자열의 문자에서 "보다 큼"은 "알파벳에서 나중에 나타남"을 의미해요. 이것은 문자 "B"가 문자 "A"보다 "크다"는 것을 의미하고, 문자열 "Tom"은 문자열 "Tim"보다 크다는 뜻이에요. 이렇게 하면 "Barry""Alex" 앞에 배치되는 식으로 알파벳 역순으로 정렬이 됩니다.

하지만 이것은 본질적으로 단일 표현식 함수(a > b)를 작성하는 다소 장황한 방법이에요. 이 예제에서는 클로저 표현식 구문을 사용하여 정렬 클로저를 인라인으로 작성하는 것이 좋겠네요.

클로저 표현식 구문

클로저 표현식 구문은 다음과 같은 일반 형식을 가지고 있어요:

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

클로저 표현식 구문에서 parameters는 in-out 매개변수일 수 있지만 기본값을 가질 수는 없어요. 가변 매개변수의 이름을 지정하면 가변 매개변수를 사용할 수 있죠. 튜플도 매개변수 타입과 반환 타입으로 사용할 수 있답니다.

아래 예제는 위의 backward(_:_:) 함수의 클로저 표현식 버전을 보여줘요:

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

이 인라인 클로저에 대한 매개변수와 반환 타입의 선언이 backward(_:_:) 함수의 선언과 동일하다는 점에 주목하세요. 두 경우 모두 (s1: String, s2: String) -> Bool로 작성되어 있죠. 그러나 인라인 클로저 표현식의 경우 매개변수와 반환 타입이 중괄호 밖이 아니라 안에 작성됩니다.

클로저 본문의 시작은 in 키워드로 소개되는데, 이 키워드는 클로저의 매개변수와 반환 타입의 정의가 끝났고 클로저의 본문이 시작될 것임을 나타내요.

클로저의 본문이 매우 짧기 때문에 한 줄로 작성할 수도 있어요:

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

이것은 sorted(by:) 메서드에 대한 전체 호출이 동일하게 유지되었음을 보여줍니다. 한 쌍의 괄호가 여전히 메서드의 전체 인자를 감싸고 있죠. 그러나 이제 해당 인자는 인라인 클로저예요.

컨텍스트에서 타입 유추

정렬 클로저가 메서드의 인자로 전달되기 때문에 Swift는 매개변수의 타입과 반환 값의 타입을 유추할 수 있어요. sorted(by:) 메서드는 문자열 배열에서 호출되고 있으므로 해당 인자는 (String, String) -> Bool 타입의 함수여야 해요. 이것은 (String, String)Bool 타입을 클로저 표현식의 정의 부분으로 작성할 필요가 없다는 것을 의미하죠. 모든 타입을 유추할 수 있기 때문에 반환 화살표(->)와 매개변수 이름 주위의 괄호도 생략할 수 있답니다:

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

클로저를 함수나 메서드의 인자로 인라인 클로저 표현식으로 전달할 때는 항상 매개변수 타입과 반환 타입을 유추할 수 있어요. 결과적으로 클로저가 함수나 메서드 인자로 사용될 때는 절대로 인라인 클로저를 가장 완전한 형태로 작성할 필요가 없죠.

그럼에도 불구하고 여전히 원한다면 타입을 명시적으로 만들 수 있으며, 이는 코드 읽는 사람에게 모호함을 피하기 위해 권장됩니다. sorted(by:) 메서드의 경우 정렬이 이루어지고 있다는 사실에서 클로저의 목적이 명확하고, 문자열 배열의 정렬을 돕고 있기 때문에 클로저가 String 값으로 작업할 가능성이 높다고 가정하는 것이 안전해요.

단일 표현식 클로저의 암시적 반환

단일 표현식 클로저는 이전 예제의 이 버전처럼 선언에서 return 키워드를 생략하여 단일 표현식의 결과를 암시적으로 반환할 수 있어요:

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

여기서 sorted(by:) 메서드의 인자 함수 타입은 클로저에 의해 Bool 값이 반환되어야 한다는 것을 분명히 하고 있어요. 클로저의 본문에 Bool 값을 반환하는 단일 표현식(s1 > s2)이 포함되어 있기 때문에 모호함이 없으며 return 키워드를 생략할 수 있죠.

단축 인자 이름

Swift는 인라인 클로저에 자동으로 단축 인자 이름을 제공하는데, 이것은 $0, $1, $2 등의 이름으로 클로저의 인자 값을 참조하는 데 사용할 수 있어요.

클로저 표현식 내에서 이러한 단축 인자 이름을 사용하는 경우 클로저의 인자 목록을 정의에서 생략할 수 있죠. 단축 인자 이름의 타입은 예상되는 함수 타입에서 유추되며, 사용하는 가장 높은 번호의 단축 인자가 클로저가 취하는 인자 수를 결정해요. 클로저 표현식이 전적으로 본문으로 구성되기 때문에 in 키워드도 생략할 수 있답니다:

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

여기서 $0$1은 클로저의 첫 번째와 두 번째 String 인자를 참조해요. $1이 가장 높은 번호의 단축 인자이기 때문에 클로저는 두 개의 인자를 취하는 것으로 이해됩니다. 여기서 sorted(by:) 함수는 인자가 모두 문자열인 클로저를 기대하므로 단축 인자 $0$1은 모두 String 타입이에요.

연산자 메서드

실제로 위의 클로저 표현식을 작성하는 더 짧은 방법이 있어요. Swift의 String 타입은 String 타입의 두 매개변수를 가지고 Bool 타입의 값을 반환하는 메서드로 greater-than 연산자(>)의 문자열별 구현을 정의하죠. 이것은 sorted(by:) 메서드에 필요한 메서드 타입과 정확히 일치해요. 따라서 greater-than 연산자를 그냥 전달할 수 있으며 Swift는 문자열별 구현을 사용하려는 것으로 유추할 거예요:

reversedNames = names.sorted(by: >)
swift

연산자 메서드에 대한 자세한 내용은 연산자 메서드를 참조하세요.

이렇게 클로저 표현식에 대해 알아보았습니다. 클로저 표현식은 코드를 간결하고 명확하게 작성할 수 있게 해주는 강력한 도구랍니다. 특히 함수나 메서드에 클로저를 전달할 때 유용하죠. 클로저 표현식의 다양한 최적화를 활용하여 코드를 더욱 간결하고 표현력 있게 만들 수 있어요.

클로저 표현식을 사용할 때는 가독성과 명확성을 잃지 않도록 주의해야 합니다. 때로는 타입을 명시적으로 작성하는 것이 코드의 의도를 명확히 전달하는 데 도움이 될 수 있어요. 클로저 표현식의 간결함과 명확성 사이의 균형을 잡는 것이 중요하답니다.

코드를 작성할 때는 항상 다른 개발자들이 쉽게 이해할 수 있도록 노력해야 해요. 클로저 표현식을 사용하면 코드를 간결하게 만들 수 있지만, 지나치게 축약하면 오히려 가독성이 떨어질 수 있죠. 상황에 따라 적절한 판단을 내리고, 필요하다면 타입이나 매개변수 이름을 명시적으로 작성하는 것이 좋습니다.

클로저 표현식은 강력한 도구이지만 책임감 있게 사용해야 해요. 코드의 명확성을 해치지 않는 선에서 클로저 표현식을 활용한다면 더욱 간결하고 읽기 좋은 코드를 작성할 수 있을 거예요.