본문 바로가기

Swift/꼬꼬무

Trailing Closures (후행 클로저)

후행 클로저는 말그대로 뒤에 행동하는 클로저 입니다.

이게 무슨 말이냐면 함수의 마지막 인수로 클로저 표현식이 사용되는 경우 후행 클로저라고 하고 후행 클로저는 특별하게 표현할 수 있는 방법이 있습니다.

 

클로저가 후행 클로저인 경우 편리하게 표현할 수 있는 방법!

  1. 후행 클로저는 함수의 인수지만 함수 소괄호 다음에 작성할 수 있습니다.
  2. 후행 클로저 구문이 여러개 일때 첫번째 후행 클로저는 인수명을 생략할 수 있습니다.

 

단일 후행 클로저를 사용하는 예제입니다.

func someFunctionThatTakesAClosure(closure: () -> Void) {
    // function body goes here
}

// 후행 클로저 없이 이 함수를 호출하는 방법

someFunctionThatTakesAClosure(closure: {
    // closure's body goes here
})

// 후행 클로저를 이용해서 함수를 호출하는 방법

someFunctionThatTakesAClosure() {
    // trailing closure's body goes here
}

 

Swift의 정렬 메서드인 sorted(by:) 는 후행 클로저로 메서드의 소괄호 바깥에 작설할 수 있습니다.

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

 

후행 클로저가 유일한 인수인 경우 호출시 소괄호를 생략할 수도 있습니다.

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

 

후행 클로저는 클로저가 길어서 한줄로 인라인으로 작성이 불가능할 때 유용합니다.

 

예를 들어서 Swift의 map(_:) 메서드를 통해 알아보겠습니다.

map(_:) 메서드는 배열에 각 아이템에 대해 한번씩 접근해서 매핑된 대체값(다른 타입도 가능)이 반환됩니다.

map(_:)에 전달한 클로저에 작성된 코드에 따라서 매핑된 특성과 반환 값의 타입을 지정합니다.

 

전달된 클로저에 각 배열의 요소를 적용한 후 map(_:) 메서드는 기존 배열의 값과 같은 순서로 새로 매핑된 새로운 배열을 반환합니다.

Int 값의 배열을 String 값의 배열로 바꾸기 위한 후행 클로저와 map(_:) 메서드의 사용에 대해 알아볼게요.

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]

 

정수와 그 정수에 맞는 영어 표기를 매핑하는 딕셔너리 digitName 를 정의하고,

문자열로 변환하기 위한 정수 배열 numbers도 정의합니다.

 

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

let strings = numbers.map { (number) -> String in
    var number = number
    var output = ""
    repeat {
        output = digitNames[number % 10]! + output
        number /= 10
    } while number > 0
    return output
}
// strings is inferred to be of type [String]
// its value is ["OneSix", "FiveEight", "FiveOneZero"]

 

map(_:) 메서드는 클로저 표현식을 호출합니다.

numbers 배열은 타입유추가 되어서 타입을 지정할 필요 없습니다.

 

클로저 본문의 클로저의 number 파라미터 값으로 초기화 된 number 변수는 수정될 수 있습니다.

(함수와 클로저의 파라미터는 항상 상수)

 

클로저 표현식은 출력 배열에 저장될 타입을 나타내기 위해 반환 타입을 String으로 지정합니다.

 

NOTE
딕셔너리 서브 스크립트는 키가 존재하지 않을 경우를 위해서 옵셔널 값을 반환합니다. 그래서 digitNames 딕셔너리의 서브 스크립트를 호출할때 ! 를 붙여 줍니다. 위 예제에서 digitNames 딕셔너리에 number & 10 은 실패할 가능성이 없기 때문에 !를 이용해서 강제 언래핑 합니다.

 

 

클로저 표현식에서 numbers의 각 항목으로 초기화된 number 변수는 몫이 0이 될때까지 10으로 나누면서 아래의 프로세스를 반복합니다.

  1. number를 10으로 나눈 나머지의 값으로 digitNames 배열에서 값을 찾아서 output 변수에 저장된 String 값 앞에 추가
  2. number 를 10으로 나눈 몫을 number 변수에 저장
  3. 1부터 반복

 

반복하게 되면

16은

  1. “number % 10” = 6
  2. digitNames[6]! = “Six”
  3. output = “Six”
  4. “number /= 10” → number = 1
  5. “number % 10“ = 1
  6. digitNames[1]! = “One”
  7. “One” + “Six” = “OneSix”

 

위 예에서 후행 클로저 구문을 사용하면 클로저가 지원하는 함수 바로 뒤에 있는 클로저의 기능을 깔끔하게 캡슐화 합니다.

전체 클로저는 map(_:) 메서드의 바깥 소괄호로 감쌀 필요가 없습니다.

함수가 여러개의 클로저를 가지면 첫번째 후행 클로저의 인수는 생략하고 나머지 후행 클로저의 인수는 표시합니다.

 

 

아래와 같이 사진을 불러오는 함수가 있습니다.

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

 

하나의 사진을 불러오기 위해 2개의 클로저를 제공합니다.

 

첫번째는 사진을 전달하는 클로저고, 두번째는 오류를 전달하는 클로저 입니다.

loadPicture(from: someServer) { picture in
    someView.currentPicture = picture
} onFailure: {
    print("Couldn't download the next picture.")
}

 

예제에서 loadPicture(from:completion:oneFailure:) 함수는 네트워크 작업을 백그라운드로 전달하고 작업이 끝나면 두 완료 처리기중에서 하나를 호출합니다.

 

이렇게 함수를 작성하면 두 상황을 모두 처리하는 하나의 클로저를 사용하는 대신에 네트워크 작업 이후 UI를 업데이트 하는 코드와 네트워크 오류를 처리하는 코드를 명확하게 분리할 수 있습니다.