🔥 후행 클로저

778자
10분

클로저 표현식을 함수의 마지막 인자로 전달해야 하고 클로저 표현식이 길다면, 후행 클로저(trailing closure) 문법을 사용하는 것이 유용할 수 있습니다. 후행 클로저는 함수 호출 괄호 뒤에 작성되지만, 여전히 함수의 인자로 취급됩니다. 후행 클로저 문법을 사용할 때는 함수 호출의 일부로 첫 번째 클로저의 인자 레이블을 작성하지 않습니다. 함수 호출은 여러 개의 후행 클로저를 포함할 수 있지만, 아래의 첫 번째 예제들은 단일 후행 클로저만 사용했습니다.

func someFunctionThatTakesAClosure(closure: () -> Void) {
    // 함수 본문이 여기에 들어갑니다
}
 
// 후행 클로저를 사용하지 않고 이 함수를 호출하는 방법입니다:
someFunctionThatTakesAClosure(closure: {
    // 클로저 본문이 여기에 들어갑니다
})
 
// 대신 후행 클로저를 사용하여 이 함수를 호출하는 방법입니다:
someFunctionThatTakesAClosure() {
    // 후행 클로저 본문이 여기에 들어갑니다
}
swift

위의 클로저 표현식 섹션에서의 문자열 정렬 클로저는 sorted(by:) 메서드의 괄호 밖에 후행 클로저로 작성될 수 있습니다:

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

클로저 표현식이 함수나 메서드의 유일한 인자로 제공되고 해당 표현식을 후행 클로저로 제공한다면, 함수를 호출할 때 함수나 메서드의 이름 뒤에 괄호 ()를 작성할 필요가 없습니다.

reversedNames = names.sorted { $0 > $1 }
swift

후행 클로저는 클로저가 충분히 길어서 한 줄에 인라인으로 작성하는 것이 불가능할 때 가장 유용합니다. 예를 들어, Swift의 Array 타입은 클로저 표현식을 단일 인자로 받는 map(_:) 메서드를 가지고 있습니다. 클로저는 배열의 각 항목에 대해 한 번씩 호출되고, 해당 항목에 대해 매핑된 대체 값(아마도 다른 타입의)을 반환합니다. map(_:)에 전달하는 클로저에 코드를 작성하여 매핑의 특성과 반환되는 값의 타입을 지정합니다.

제공된 클로저를 각 배열 요소에 적용한 후, map(_:) 메서드는 원래 배열의 해당 값과 동일한 순서로 새로운 매핑된 모든 값을 포함하는 새 배열을 반환합니다.

다음은 후행 클로저와 함께 map(_:) 메서드를 사용하여 Int 값의 배열을 String 값의 배열로 변환하는 방법입니다:

let digitNames = [
    0: "Zero", 1: "One", 2: "Two",   3: "Three", 4: "Four",
    5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]
swift

위의 코드는 정수 숫자와 숫자의 영어 이름 버전 사이의 매핑 딕셔너리를 생성합니다. 또한 문자열로 변환될 준비가 된 정수 배열을 정의합니다.

이제 numbers 배열을 사용하여 후행 클로저로 배열의 map(_:) 메서드에 클로저 표현식을 전달하여 String 값의 배열을 생성할 수 있습니다.

let strings = numbers.map { (number) -> String in
    // 숫자를 number 변수에 할당
    var number = number
    var output = ""
    repeat {
        // 숫자의 일의 자리를 구하여 digitNames에서 해당하는 문자열 조회
        output = digitNames[number % 10]! + output
        // 숫자를 10으로 나누어 다음 자리수 처리
        number /= 10
    } while number > 0
    // 완성된 문자열 반환
    return output
}
// strings는 [String] 타입으로 유추됨
// 값은 ["OneSix", "FiveEight", "FiveOneZero"]
swift

map(_:) 메서드는 배열의 각 항목에 대해 클로저 표현식을 한 번씩 호출합니다. 매핑될 배열의 값에서 클로저의 입력 매개변수 number의 타입을 유추할 수 있기 때문에 타입을 지정할 필요가 없습니다.

이 예제에서는 값이 클로저 본문 내에서 수정될 수 있도록 number 매개변수의 값으로 변수 number가 초기화됩니다. (함수와 클로저에 대한 매개변수는 항상 상수입니다.) 클로저 표현식은 또한 매핑된 출력 배열에 저장될 타입을 나타내기 위해 String의 반환 타입도 지정합니다.

클로저 표현식은 호출될 때마다 output이라는 문자열을 만듭니다. 나머지 연산자(number % 10)를 사용하여 number의 마지막 자릿수를 계산하고, 이 숫자를 사용하여 digitNames 딕셔너리에서 적절한 문자열을 조회합니다. 클로저는 0보다 큰 모든 정수의 문자열 표현을 만드는 데 사용될 수 있습니다.

digitNames 딕셔너리에서 검색된 문자열은 output에 추가되어 효과적으로 숫자의 문자열 버전을 거꾸로 만듭니다. (number % 10 표현식은 16에 대해 6, 58에 대해 8, 510에 대해 0을 제공합니다.)

그런 다음 number 변수를 10으로 나눕니다. 정수이므로 나누는 동안 내림되어 161이 되고, 585가 되며, 51051이 됩니다.

number0과 같아질 때까지 프로세스가 반복되며, 이때 output 문자열이 클로저에 의해 반환되고 map(_:) 메서드에 의해 출력 배열에 추가됩니다.

위의 예제에서 후행 클로저 문법을 사용하면 클로저가 지원하는 함수 바로 뒤에 클로저의 기능을 깔끔하게 캡슐화할 수 있습니다. map(_:) 메서드의 바깥 괄호로 전체 클로저를 감쌀 필요가 없습니다.

함수가 여러 개의 클로저를 사용한다면, 첫 번째 후행 클로저의 인자 레이블은 생략하고 나머지 후행 클로저에는 레이블을 지정해야 합니다. 예를 들어, 아래 함수는 사진 갤러리의 사진을 로드합니다:

func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) {
    if let picture = download("photo.jpg", from: server) {
        completion(picture)
    } else {
        onFailure()
    }
}
swift

사진을 로드하기 위해 이 함수를 호출할 때, 두 개의 클로저를 제공합니다. 첫 번째 클로저는 다운로드가 성공한 후 사진을 표시하는 완료 핸들러입니다. 두 번째 클로저는 사용자에게 오류를 표시하는 오류 핸들러입니다.

loadPicture(from: someServer) { picture in
    someView.currentPicture = picture
} onFailure: {
    print("다음 사진을 다운로드할 수 없습니다.")
}
swift

이 예제에서 loadPicture(from:completion:onFailure:) 함수는 네트워크 작업을 백그라운드로 보내고, 네트워크 작업이 완료되면 두 개의 완료 핸들러 중 하나를 호출합니다. 이런 방식으로 함수를 작성하면 성공적인 다운로드 후 사용자 인터페이스를 업데이트하는 코드와 네트워크 실패를 처리하는 코드를 깔끔하게 분리할 수 있습니다. 두 상황을 모두 처리하는 단일 클로저를 사용하는 대신 말이죠.

후행 클로저를 활용하면 코드의 가독성과 명확성을 높일 수 있습니다. 클로저 표현식이 길어질 때 특히 유용합니다. 함수나 메서드의 다른 인자와 시각적으로 구분되어 코드의 의도를 더 잘 전달할 수 있습니다.