[Swift] 클로저 (Closures)

Soomin Kim·2025년 4월 23일

Swift

목록 보기
12/12
post-thumbnail

클로저란?

: 명명된 함수 생성 없이 실행되는 코드 블럭
: 코드에서 주변에 전달과 사용할 수 있는 자체 포함된 기능 블럭

(다른 프로그래밍 언어의 클로저, 익명함수, 람다, 블럭과 비슷)

정의된 컨텍스트 안에서
모든 상수 / 변수에 대한 참조를 1) 캡처할 수 있고 2) 저장할 수 있다.

클로저의 3가지 형태

  1. 전역 함수 : 이름을 가지고(o) 어떠한 값도 캡처하지 않는(x)

  2. 중첩 함수 : 이름을 가지고(o) 둘러싼 함수로부터 값을 캡처할 수 있는(o)

  3. 클로저 표현식 : 이름이 없고(x) 주변 컨텍스트에서 값을 캡처할 수 있는(o)

클로저 표현식 최적화하기

클로저 표현식은 깔끔한 구문을 장려하는 최적화를 통해 깔끔하고 명확한 스타일을 가지고 있다.

  • 컨텍스트에서 파라미터와 반환값 타입 유추

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

  • 약식 인수 이름

  • 후행 클로저 구문

클로저 표현식

: 간단하고 집중적인 구문으로 "이름 없는 함수"를 작성하는 방법 -> 함수의 기능을 한 줄로 정의 !!

ex) 정렬 메서드

sorted(by: ) 메서드 : 배열을 정렬할 때 사용하는 메서드

1) 전통적인 방법 (일반 함수)

func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

//backward 함수 인수로 전달
var reversedNames = names.sorted(by: backward)
// reversedNames는 ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

2) 클로저 표현식을 사용한 방법 (backward 함수의 클로저 표현 버전)

in 키워드로 클로저의 본문을 시작한다.
-> 클로저의 파라미터와 리턴 타입 정의가 끝남 & 클로저의 본문이 시작함

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

위의 클로저 표현식을 단계별로 짧게 줄여보기

1. 컨텍스트로 타입 추론 (->, 소괄호 생략)

클로저를 인라인 표현식으로 전단할 때 항상 파라미터 타입과 반환 타입을 유추할 수 있다.

ex) 위 예제의 경우 문자열 배열에 대해서 호출했기때문에 "string 이겠구나!🤓"하고 유추 가능

즉, [->, 소괄호] 를 생략하여 한줄로도 작성할 수 있다.

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

2. 단일 표현 클로저의 암시적 반환 (return 생략)

return 키워드로 생략하여 단일 표현식으로 암시적으로 값을 반환할 수 있다!

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

3. 짧은 인수 이름 (인수이름, in 생략)

swift는 인라인 클로저의 [$0, $1, $2] 등 클로저의 인수값으로 참조하는데 사용할 수 있는 자동적으로 짧은 인수 이름을 제공한다.

또한 클로저 표현식이 본문으로 전체가 구성되기 때문에
in 키워드를 생략할 수도 있다.

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

$0 와 $1 은 클로저의 첫번째와 두번째 String 인수를 참조한다.
$1 가장 높은 숫자이므로 클로저는 2개의 인수가 있다고 이해한다.

4. 연산자 메서드

Swift의 String 타입은 보다 큰 연산자 (>)의 문자열 별 구현을 String 타입의 파라미터 2개가 있는 메서드로 정의하고 Bool 타입의 값을 반환한다.
= sorted(by:) 메서드와 정확하게 일치!!

아래와 같이 짧게 쓸 수 있다.

reversedNames = names.sorted(by: >)

후행 클로저

: 마지막 인수로 함수에 클로저 표현식을 전달해야하고, 클로저 표현식이 긴 경우 사용

1. 일반 클로저 전달 방식

func someFunction(closure: () -> Void) {
    // 함수 본문
}

someFunction(closure: {
    print("Hello, world!")
})func someFunction(closure: () -> Void) {
    // 함수 본문
}

someFunction(closure: {
    print("Hello, world!")
})

2. 후행 클로저 방식
후행 클로저는 함수의 인수이지만 함수 호출의 소괄호 다음에 작성한다.

someFunction() {
    print("Hello, world!")
}

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

ex)

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]

위 코드는

  • 정수와 그 정수에 맞는 영어 표기를 매핑하는 딕셔너리를 생성
  • 문자열로 변환하기 위한 정수의 배열도 정의

이 코드를 후행클로저 방식으로 바꾼다면?
-> 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"]

클로저 표현식은 호출될 때마다 output 이라는 문자열을 만든다.
나머지 연산자 (number % 10)를 이용하여 number 의 마지막 숫자를 계산하고 digitNames 딕셔너리에 적절한 숫자 문자열을 찾는다.

함수가 여러개의 클로저를 가지고 있다면?

: 첫번재 후행 클로저의 인수 라벨을 생략하고 남은 후행 클로저의 라벨은 표기

Ex) 사진 갤러리에서 사진 하나를 불러오는 함수

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

사진 하나를 불러오기 위해 2개의 클로저를 제공

//1. 사진 다운로드 완료 후에 사진을 보여주기 위한 완료 처리
loadPicture(from: someServer) { picture in
    someView.currentPicture = picture
} 

//2. 오류를 표시하는 오류 처리기
onFailure: {
    print("Couldn't download the next picture.")
}

loadPicture(from:completion:onFailure:) 함수는 네트워크 작업을 백그라운드로 전달하고 네트워크 작업이 완료되면 두 완료 처리기 중 하나를 호출한다.

두 상황을 모두 처리하는 하나의 클로저를 사용하는 대신 성공적인 다운로드 후 사용자 인터페이스를 업데이트 하는 코드에서 네트워크 오류를 처리하는 코드를 명확하게 분리할 수 있다.

캡처값

클로저는 둘러싸인 컨텍스트에서 상수와 변수의 값을 참조하고 수정할 수 있다.

ex) 중첩함수

바깥 함수의 어떠한 인수도 캡처할 수 있고 바깥 함수 내에 정의된 상수와 변수를 캡처할 수도 있다.

아래 코드는 incrementer 라는 중첩 함수가 포함된 makeIncrementer 라는 함수의 예제 !

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}
  1. 중첩된 incrementer() 함수는 둘러싸인 컨텍스트에 runningTotalamount 인 2개의 값을 캡처

  2. 캡처한 후에 incrementer 는 호출될 때마다 amountrunningTotal 을 증가시키는 클로저로 makeIncrementer 에 의해 반환

  3. makeIncrementer 호출이 종료될 때 runningTotalamount 가 사라지지 않고 다음에 incrementer 함수가 호출될 때 runningTotal 을 사용

동작을 어떻게 하는지 봐보면..! 😙

1. runningTotal 변수에 10 을 더하는 증가 함수를 참조하도록 incrementByTen 이라는 상수를 설정해보자.

let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()
// 1o
incrementByTen()
// 20
incrementByTen()
// 30

2. 두번째 증가를 생성해보자.
(새로운 분리된 runningTotal 변수에 참조 저장)

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// 7

3. 기존 증가 incrementByTen 을 다시 호출해보자.

incrementByTen()
// 40

그것의 runningTotal 변수는 이어서 증가되고 incrementBySeven 으로 캡처된 변수는 영향을 주지 않는다.

클로저는 참조타입

클로저는 참조타입이기때문에 위와 같이 캡처한 특정 변수를 계속 증가시킬 수 있는 것이다.

참조타입인 것을 더 확실하게 확인해보자면..!

let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// 50

incrementByTen()
// 60

위 예제에서 alsoIncrementByTen 호출은 incrementByTen 호출과 같음을 보여준다.
2개 모두 같은 클로저를 참조하기 때문에 둘 다 증가하고 같은 러닝 합계를 반환한다.

탈출 클로저

함수가 반환된 후 호출되는 클로저를 '함수를 탈출하다'라고 말한다.
클로저가 함수 안에서 실행되면 괜찮지만, 함수 밖으로 "탈출"해서 나중에 실행된다면 문제가 생김! -> 파라미터의 타입 전에 @escaping을 작성해서 이 클로저는 나중에 쓸거다는 의미를 나타내야한다.

var completionHandlers: [() -> Void] = []

func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler) // 함수 밖으로 탈출!
}

someFunctionWithEscapingClosure(_:) 함수는 인수로 클로저를 가지고 있고 함수 바깥에 선언된 배열에 추가한다.
함수의 파라미터에 @escaping 을 표시하지 않으면 컴파일 시 에러가 발생 !

자동 클로저

인수를 클로저처럼 감싸서 실행 시점을 지연시켜주는 문법이다.

func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}

serve(customer: customersInLine.remove(at: 0)) 
// 마치 그냥 표현식처럼 보이지만, 내부적으로 클로저로 감싸짐!
// Prints "Now serving Ewa!"

@autoclosure + @escaping 조합

클로저를 나중에 실행하고 싶고, 자동으로 감싸고 싶을 때 둘 다 붙이면 된다 👍

func collectCustomerProviders(_ customer: @autoclosure @escaping () -> String) {
    customerProviders.append(customer)
}

0개의 댓글