티스토리 뷰

iOS

[Swift] 클로저 (Closure) (1)

Peppo 2022. 1. 30. 13:55
728x90

대망의 클로져 입니다.

제일 많이 쓰이지만 아직도 어렵고 헷갈리는 그것.

공부해 봅시다.


클로저 (Closure)

클로저는 코드블럭으로 C와 Objective-C의 블럭(blocks), 다른 언어의 람다(lambdas)와 비슷 합니다.

어떤 상수나 변수의 참조를 캡쳐(capture)해 저장할 수 있습니다.

Swift는 캡쳐와 관련된 모든 메모리를 다룹니다. 

 

전역 함수(Global Functions)와 중첩 함수(Nested Functions)는 클로저의 특별한 경우 입니다.

클로저는 세 가지 형태중 하나를 가집니다.

  • 이름을 갖고 있고, 어떤 값들도 캡처하지 않는 클로저 (전역 함수)
  • 이름을 갖고 있고 관련한 함수의 값 들을 캡처 할 수 있는 클로저 (중첩 함수)
  • 경량화된 문법으로 쓰여지고 관련된 문맥으로 부터 값을 캡처할 수 있는 이름이 없는 클로저 (클로저 표현식)

Swift의 클로저 표현식은 최적화 되어 간결하고 명확합니다.  

최적화에는 아래의 내용을 포함합니다.

  • 문맥에서의 파라미터와 반환 값 타입 추론
  • 단일 표현 클로저에서의 암시적 반환
  • 축약된 인자 이름
  • 후행 클로저 문법

클로저 표현 ( Closure Expressions )

클로저 표현은 코드의 명확성을 잃지 않으면서, 문법을 축약해 사용할 수 있는 최적화 방법을 제공 합니다.

 

아래는 sorted(by:) 메소드를 이용하여 여러 클로저 예시들을 보여줍니다. 

각 예제는 동일한 기능을 보다 간결하게 표현합니다.

 

정렬 메소드 (The Sorted Method)

Swift의 표준 라이브러리에서는 sorted(by:) 라는 배열의 값을 정렬하는 메소드를 제공 합니다. 

한번 정렬 프로세스가 끝이나면, sorted(by:) 메소드는 이전과 같은 형태지만 정렬된 새로운 배열을 반환하며, 기존에 있던 배열은 수정되지 않습니다. 

 

아래는 String 으로 구성된 배열을 알파벳 역순으로 정렬해보는 예 입니다. 

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

 

sorted(by:) 메소드는 배열의 콘텐츠에서 같은 타입인 두개의 인자를 인자로 사용하며,

name의 배열의 컨텐츠는 String 타입. 즉, (String, String) -> Bool 의 타입을 갖는 클로저를 사용합니다. 

 

클로저를 제공하는 방법중 하나는 위의 타입에 해당하는 함수를 만들어 sorted(by:) 메소드에 인자로 전달하는 것입니다.

 

func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}

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

 

 코드 해석

backward의 첫번째 파라미터 (s1)이 두번째 파라미터 (s2) 보다 크면 true를 반환 하며, 

알파벳 순으로는 A < Z 순이니 names 안에 E로 시작하는 Ewa가 가장 크니 제일 앞으로, 

A로 시작하는 Alex가 가장 작으니 제일 뒤로 보내게 됩니다. 

 

하지만 위의 코드를 더 줄일 수 있습니다.

 

 

클로저 표현 문법 (Closure Expression Syntax)

아래는 클로저의 형태 입니다.

{ (parameter) -> return type in
    statements
}
In 을 기준으로 
좌측: 파라미터, 리턴 타입 
우측: 구현하고자하는 기능 (body)

 

이전에 사용했던 backward 클로저를 클로저 표현을 이용해 아래와 같이 바꿀 수 있습니다.

 

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

위와 같이 함수로 따로 정의된 형태가 아닌 인자로 들어가 있는 형태의 클로저를 인라인 클로저 라 합니다.

만약 body가 짧을 경우 한줄로 끝낼 수도 있습니다.

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

 

문맥에서 타입 추론 (Inferring Type From Context)

sorted(by:) 의 메소드에서 (String, String) -> Bool 타입의 인자가 들어와야 하는걸 알기 때문에  클로저에서 이 타입들은 생략 할 수 있습니다. 

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

하지만 가독성과 코드의 모호성을 피하기 위해 타입을 명시하는걸 권장 합니다.

 

 

단일 표현 클로저에서의 암시적 반환 (Implicit Returns from Single-Express Closures)

단일 표현 클로저에서는 반환 (return) 키워드를 생략할 수 있습니다.

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

 

 

인자 이름 축약 (Shorthand Arguments Names)

Swift는 자동으로 인라인 클로저에 축약 인자 이름을 제공합니다. 

$0, $1, $2 등으로 사용할 수 있으며, in 키워드를 생략할 수 있고, $0 부터 인자값을 순서대로 사용 합니다.

reversedNames = names.sorted(by: { $0 > $1 })  // in 생략

 

코드 해석

  1. $0 과 $1 인자를 받습니다
  2. $0이 $1 보다 큰지 비교
  3. 결과 값 Bool (true, false) 반환 

 

연산자 메소드 (Operator Methods)

여기서 더 줄일 수 있습니다. 

sorted(by:) 메소드에는 인자 두개를 받아 두 값을 비교한다는걸 (>) 연산자 메소드로 추론할 수 있으므로 아래와 같이 축약도 가능 합니다.

reversedNames = names.sorted(by: > )

 

너무 많이 축약하게 되면 코드를 읽는데 어려움이 있으므로, 적당히 축약하자. 

 


 

후행 클로저 (Trailing Closure)

 

만약 함수의 마지막 인자로 클로저를 넣었을때 클로저가 너무 길다면 후행 클로저를 사용할 수 있습니다.

 

후행 클로저를 사용하기전 아래 형태의 함수와 클로저가 있다고 가정해봅시다.

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

 

 

후행 클로저를 사용하면 아래와 같이 사용할 수 있습니다. 

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

중괄호 { } 안에 body 내용을 적어 기능구현을 할 수 있습니다.

 

 

앞에 sorted() 메소드를 후행 클로저로 표현하면 이렇게 표현 가능합니다.

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

//  마지막 인자가 클로저이고 후행 클로저를 사용하면 괄호 () 생략 

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

 

 

 

 

아래는 후행 클로저를 이용해 숫자(Int)를 문자(String)으로 매핑(Mapping)하는 예제를 보겠습니다.

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]

 

map(_:) 메소드를 이용해 특정 값을 다른 특정 값으로 매핑할 수 있는 클로저를 구현합니다. 

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(_:) 메소드 다음에 있는 number 인자의 값은 상수로 변하지 않지만,

클로저 body 부분에서 number를 변수로 선언하면서 값을 변경해 줄수 있습니다.

NOTE

digitNames[number % 10]! 뒤에 느낌표(!)가 붙은 이유는 사전(dictionary)의 subscript는 옵셔널이기 때문입니다. 
사전에서 특정 key에 대한 값은 있을 수도 있고 없을 수도 있기 때문!

 

 

로직 풀이 

더보기

(number[0]의 첫번째 루프)

 

% = 나머지 

ex) 26 % 10 = 6  

  1. number = 16, output = "" 로 시작
  2. (반복문 repeat 부분)
    2-1. output = digitNames[16 % 10 = 6]! + "" 
    2-2. number = 16 / 10 --> number = 1 (타입추론으로 몫 1 추출)
  3. (while 조건) 
    3-1. number 는 1. 
    즉 0보다 큼으로 repeat 루프로 이동

(number[0]의 두번째 루프)

  1. number = 1, output = "Six"
  2. (반복문 repeat 부분)
    2-1. output = digitNames[1 % 10 = 1]! + "Six"
    2-2. number = 1 / 10 --> number = 0
  3. (while 조건)
    3-1. number 는 0
    즉, 0보다 작음으로 return output = "OneSix" 

(number[1]의 첫번째 루프)

위의 방법과 동일하게 루프를 돕니다.

 


 

값 캡쳐 (Capturing Values)

 

클로저는 정의된 주변 컨텍스트로부터 상수, 변수의 값을 캡처할 수 있습니다.

Swift에서 가장 간단한 클로저는 함수 내에 작성된 함수, 즉 중첩 함수 인데 

중첩 함수는 밖에 있는 상수, 변수를 캡처할 수 있고, 밖에 있는 함수의 인자도 캡처할 수 있습니다.

 

아래는 makeIncrementer 함수가 incrementer 중첩 함수를 포함하는 예제 입니다.

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

먼저 makeIncrementer함수는 (forIncrement amout: Int) 라는 파라미터를 받으며, 반환 타입은 () -> Int 입니다.

반환 값이 클로저 형태라는걸 짐작할 수 있죠.

 

중첩함수인 incrementer만 따로 해서 보겠습니다.

func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }

 

incrementer 중첩함수는 runningTotal과 amount값을 포함하고 있으며,

이 두 값들은 주변 컨텍스트(makeIncrementer)로부터 값을 캡처해 올수 있습니다.

값 들을 캡처해온 후 incrementer는 호출될 때마다 runningTotal과 amount의 값들을 더한 후 runningTotal 값에 대입시킵니다.

 

함수를 한번 실행 시켜보겠습니다.

var incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()   // 10
incrementByTen()   // 20
incrementByTen()   // 30
incrementByTen()   // 40

같은 함수가 4번 실행되지만 실제로는 함수 내부에 runningTotal과 amount의 캡처 되어 있던 값들이 누적으로 계산이 되는겁니다. 

 

 

로직 풀이 

더보기

첫번째 함수실행
  runningTotal = 0 , amount = 10

  1. makeIncrementer의 인자로 10을 파라미터에 전달 
  2. makeIncrementer함수가 실행되면서 중첩함수인 incrementer도 실행 
  3. incrementer 의 runningTotal은 0, amount는 인자로 전달받은값 10  runningTotal = 0 + 10 
  4. incrementer 에서 return 10
  5. makeIncrementer에서 incrementer를 return 10

두번째 함수실행 
  runningTotal = 10 (값 캡처) , amount = 10

  1. makeIncrementer의 인자로 10을 파라미터에 전달 
  2. 위 2번과 동일
  3. incrementer의 runningTotal은 10, amount는 인자로 전달받은값 10 runningTotal = 10 + 10 
  4. incrementer 에서 return 20
  5. makeIncrementer에서 incrementer를 return 20

... 위와 같은 방식으로 함수가 실행 됩니다.

 

만약 아래와 같이 새로운 클로저를 생성하게 되면 ?

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

다른 클로저이기 때문에 다른 저장소에 runningTotal과 amount를 캡처해 사용합니다. 

 

여기서 이전 클로저를 실행하면 ?

incrementByTen() // 50

위에도 언급했듯 각기 다른저장소에 저장되기 때문에 영향을 받지 않습니다. 

 


클로저는 참조 타입 (Closures Are Reference Types)

위 예제에서 incrementBySeven 과 incrementByTen은 상수지만, 

함수와 클로저는 참조타입이기 때문에 이 상수들이 참조하는 클로저는 캡처된 runningTotal을 계속 증가시킬 수 있습니다.

만약 한 클로저를 두 상수나 변수에 할당하면 그 두 상수나 변수는 같은 클로저를 참조 하게 됩니다.

let alsoIncrementByTen = incrementByTen  // 같은 클로저 참조
alsoIncrementByTen()    // 30
incrementByTen()        // 40

 

 

다음 클로저에서는 

Escaping Closure (탈출 클로저)

Autoclosure (자동 클로저)

에 대해 블로깅 하겠습니다~

728x90

'iOS' 카테고리의 다른 글

[iOS] CocoaPods 설치 및 실행  (0) 2022.02.06
[Swift] 클로저 (Closure) (2) - Escaping Closure, Autoclosure  (0) 2022.02.02
ModerRIBs_tutorial 2 - 2  (0) 2022.01.26
ModernRIBs_tutorial2 - 1  (0) 2022.01.23
[Swift] 함수 (Functions) (2)  (0) 2022.01.21