본문 바로가기
2023년 이전/swift

클로저

by JeongUPark 2020. 8. 30.
반응형

이 글은 swift 공부를 하면서 정리한 내용 입니다. 본 내용은 스위프트 프로그래밍 3판 (야곰 지음) 을 공부하면서 정리한 내용입니다.
코드는 직접 타이핑하거나 여기서 참조 하였습니다.


클로저란 일정 기능을 하는 코드를 하나의 블록으로 모아놓은 것을 말합니다.

 

그럼 String의 sorted함수를 통하여 클로저를 알아보도록 하겠습니다.

 

우선 String 의 sorted 함수의 정의를 보면

public func sorted(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows -> [Element]

이렇게 되어있습니다. 그중 by areInIncreasingOrder에 (Element, Element) throws -> Bool 이 부분에 클로저를 전달 인자로 전달 받을 수 있습니다.

 

그럼 내림차순 정렬을 할 수 있는 예제를 만들어 보겠습니다.

let names : [String] = ["eric", "jeongupark","joohea","jenny"]


func backwards(first:String, second :String)-> Bool {
    print("\(first) , \(second) 비교중")
    return first>second
}

let reserved : [String] = names.sorted(by: backwards)
print(reserved)

위 처럼 names 행렬이 있고 backwards함수는 첫번째가 두번째 보다 먼저 배치도면 true를 반대면 false를 반환합니다. 그리고 backwards를 아까 sorted에서 클로저 형태로 전달 인자를 받을 수 있는 부분에 넣어줍니다. (이게 가능한 이유는 함수도 클로저의 한 형태이기 때문 입니다.)

 

그리고 그 결과를 보면  ["joohea", "jeongupark", "jenny", "eric"] 이렇게 정렬 된 것을 볼 수 있습니다.

그런데 위 코드를 보면 backwards함수는 먼가 코드가 너무 많습니다. 그래서 위 함수를 클로저를 이용하여 쫌더 간결하게 만들어 보겠습니다.

클로저의 통상 표현 방식은 다음과같습니다.

{ (매개변수들) -> 반환 타입 in
  실행 코드
}

위의 통상 표현을 사용하여 backwards를 클로저 형태로 변경해 보겠습니다.

let names : [String] = ["eric", "jeongupark","joohea","jenny"]

let reserved : [String] = names.sorted(by: { (first , second) -> Bool in
    return first > second
})
print(reserved)

이렇게 코드가 훨씬 간결해 졌습니다. 이렇게 만들면 backwords함수가 어디있는지 찾을 필요가 없어집니다! (단 반복 사용을 위해서는 함수로 하는게 더 유리하겠죠?)

 

후행 클로저

후행 클로저는 함수나 메서드의 소괄호를 닫은 후 작성할 수 있습니다. 그리고 후행 클로저는 맨 만지막에 전달인자로 전달되는 클로저에만 해당되므로 전달인자로 여러개의 클로저를 전달 할떄는 맨 마지막 클로저만 후행 클로저로 사용할 수 있습니다.

글로써도 무슨 의미인지 쉽게 안 와닿으니 얼렁 코드를 확인 해 봅시다.

let reserved : [String] = names.sorted(){ (first , second) -> Bool in
    return first > second
}

or

//소괄호 까지 생략 가능
let reserved : [String] = names.sorted{ (first , second) -> Bool in
    return first > second
}

위의 코드 처럼 sorted(by: )에서 by:를 지우거나 또는 아예 소괄호를 지워서 사용 할 수 있습니다.

 

간소화

문맥을 이용한 타입 유추

메서드의 전달 인자로 전달되는 클로저는 메서드의 요구하는 형태로 전달해야 합니다. 그말은 즉, 이미 매개변수나 타입이나 개수, 반환 타입등이 같아야한다는 말이고, 이는 전달인자로 전달할 클로저는 이미 적합한 타입을 준수하고 있다는 말입니다. (그래서 위에서 first나 second 옆에 : String 이 없어도 되었습니다.) 그렇게 때문에 매개변수 타입과 반환 타입을 생략할 수 있습니다.

 

let reserved : [String] = names.sorted(){ (first , second) in
    return first > second
}

이렇게 매개변수 타입과 반환 타입을 생략하여도 정상 동작합니다.

 

단축인자 이름

스윗프트에서는 단축인자 이름을 제공해 줍니다. 그래서 위의 first와 second 말고 첫 번쨰 전달인자부터 $0, $1, $2 .... 순서로 단축인자를 표현 할 수 있습니다. (이를 통하여 코드를 또 줄일 수 있습니다.)

let reserved : [String] = names.sorted(){ return $0 > $1 }

암시적 반환

위의 코드에서 더 줄일 수 있습니다. 만약 클로저가 반환 값을 갖고, 그 실행문이 단 한 줄이라면 return마저도 줄일 수 있습니다.

let reserved : [String] = names.sorted(){  $0 > $1 }

 

그럼 처음 코드에서 얼마나 줄여졌는지 확인해 보겠습니다.

func backwards(first:String, second :String)-> Bool {
    print("\(first) , \(second) 비교중")
    return first>second
}

let reserved : [String] = names.sorted(by: backwards)

///

let reserved : [String] = names.sorted{ $0 > $1 }

이렇게 줄어든 것을 확인 할 수 있습니다.

 

연산자 함수

클로저는 매개변수의 타입과 반환 타입이 연산자를 구현한 함수의 모양과 동일하다면 연산자만 표기 하더라도 알아서 연산하고 반환합니다.

그 이유는 연산자가 일종의 함수 이기 때문입니다. 지금 우리가 사용한 > 연산자의 정의를 보면

public func > <T : Comparable>(lhs:T , rhs: T) -> Bool

함수임을 확인 할 수 있습니다. 그래서 다음과 같이 클로저로 사용할 수 있습니다.

let reserved : [String] = names.sorted(by: >)

값 획득

클로저는 자신이 정의된 위치의 주변 문맥을 통해 상수나 변수를 획득 할 수 있습니다. 이는 값 획득을 통하여 주변에 정의한 상수나 변수가 더이상 존재하지 않더라도 해당 상수나 변수의 값을 자신 내부에서 참조하거나 수정하 수 있다는 의미입니다. 이는 클로저는 비동기 작업에 많이 사용되기 때문입니다.

 

다음 코들르 통하여 더 자세히 알아 보겠습니다.

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

위 코드는 makeIncrementer라는 함수에 amount라는 값을 받고 ()->Int를 반환합니다. 그리고 안에 incrementer() 함수가 있는데 이 함수는 단독으로 있으면 잘못된 함수 입니다. 그 이유는 runingTotal과 amount를 가지지 않기 떄문입니다. 하지만 makeIncrementer에서 반환 함수로 작동함으로서 동작을 하게 되고, makeIncrementer의 runningTotal과 amount를 참조할게 되고, 참조를 하면 makeIncrementer함수의 실행이 끝나도 사라지지 않습니다. 그리고 incrementer함수가 호출 될 때마다 계속 사용할 수 있습니다.

let incrementByTwo: (() -> Int) = makeIncrementer(forIncrement: 2)

let first: Int = incrementByTwo()
let second: Int = incrementByTwo()
let third: Int = incrementByTwo()

print("\(first) | \(second) | \(third)")

실행하면 2 | 4 | 6 의 결과를 볼 수 있습니다.

 

그리고 makeIncrementer로 Incrementer를 여러개 생성하면 각각 다른 참조를 갖는 runningTotal의 변수 값을 확인 할 수 있습니다.

let incrementByTwo: (() -> Int) = makeIncrementer(forIncrement: 2)
let incrementByFive: (() -> Int) = makeIncrementer(forIncrement: 5)
let incrementByTen: (() -> Int) = makeIncrementer(forIncrement: 10)

let first: Int = incrementByTwo()
let second: Int = incrementByTwo()
let third: Int = incrementByTwo()
let first5: Int = incrementByFive()
let second5: Int = incrementByFive()
let third5: Int = incrementByFive()
let first10: Int = incrementByTen()
let second10: Int = incrementByTen()
let third10: Int = incrementByTen()
print("\(first) | \(second) | \(third)")
print("\(first5) | \(second5) | \(third5)")
print("\(first10) | \(second10) | \(third10)")

결과는

2 | 4 | 6

5 | 10 | 15

10 | 20 | 30

확인할 수 있습니다.

 

클로저는 참조 타입

위의 incrementByTwo, incrementByFive, incrementByTen이 각각의 RunningTotal을 갖을 수 있는 이유는 함수와 클로저는 참조 타입이기 때문 입니다. 즉, 값을 할당하는 것이 아니라 해당 클로즈의 참조를 할당하는 것입니다. 결국, 클로저의 참조를 다른 상수에 할당해준다면 이는 두 상수가 모두 같은 클로저를 가리킨다는 뜻입니다

let incrementByTwo: (() -> Int) = makeIncrementer(forIncrement: 2)
let sameWithIncrementByTwo: (() -> Int) = incrementByTwo

let first: Int = incrementByTwo()           
let second: Int = sameWithIncrementByTwo()  

위의 코드를 보면 incrementByTwo와 sameWithIncrementByTwo 둘다 같은 클로저를 참조하기 때문에 동일한 클로저도 동작하게 됩니다.

 

탈출 클로저

함수의 전달인자로 전달한 클로저가 함수 종료 후에 호출될 떄 클로저가 함수를 탈출 한다고 합니다. 클로저를 매개변수로 갖는 함수를 선언할 떄 매개변수 이름의 콜론(:) 뒤에 @escaping 키워드를 사용하여 클로저가 탈출하는 것을 허용한다고 명시해줄 수 있습니다. (명시를 하지 않는다면 기본적으로 비탈출 클로저 입니다. 함수로 전달된 클로저가 함수의 동작이 끝난 후 사용할 필요가 없을 경우 비탈출 클로저로 사용합니다.)

import Swift

// 코드 13-16 탈출 클로저를 매개변수로 갖는 함수
var completionHandlers: [() -> Void] = []

func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

// 코드 13-17 함수를 탈출하는 클로저의 예
typealias VoidVoidClosure = () -> Void

let firstClosure: VoidVoidClosure = {
    print("Closure A")
}

let secondClosure: VoidVoidClosure = {
    print("Closure B")
}

// first와 second 매개변수 클로저는 함수의 반환 값으로 사용될 수 있으므로 탈출 클로저입니다.
func returnOneClosure(first: @escaping VoidVoidClosure, second: @escaping VoidVoidClosure, shouldReturnFirstClosure: Bool) -> VoidVoidClosure {
    // 전달인자로 전달받은 클로저를 함수 외부로 다시 반환하기 때문에 함수를 탈출하는 클로저입니다.
    return shouldReturnFirstClosure ? first : second
}

// 함수에서 반환한 클로저가 함수 외부의 상수에 저장되었습니다.
let returnedClosure: VoidVoidClosure = returnOneClosure(first: firstClosure, second: secondClosure, shouldReturnFirstClosure: true)

returnedClosure()   // Closure A


var closures: [VoidVoidClosure] = []

// closure 매개변수 클로저는 함수 외부의 변수에 저장될 수 있으므로 탈출 클로저입니다.
func appendClosure(closure: @escaping VoidVoidClosure) {
    
    // 전달인자로 전달받은 클로저가 함수 외부의 변수 내부에 저장되므로 함수를 탈출합니다.
    closures.append(closure)
}

위 코드를 보면 전달 되어지는 클로저 앞에 @escaping 키워트를 통하여 탈출 클로저임을 명시해줍니다.  이렇게 해주지 않으면 컴파일시 오류가 납니다. 그 이유는  함수 외부로 다시 전달되어 외부에서 사용이 가능하다든가, 외부 변수에 저장되는 등 클로저의 탈출 조건을 모두 갖추고 있기 때문입니다. @escaping 키워드를 사용하면 탈출 클로저임을 명시한 경우, 클로저 내부에서 해당 타입의 프로퍼티나 메서드, 서브스크립트 등에 접근하려면 self 키워드를 명시적으로 사용해야 합니다. ( 비탈출 클로저일 경우에는 self 키워드를 꼭 써주지 않아도 됩니다.)

import Swift

// 코드 13-18 클래스 인스턴스 메서드에 사용되는 탈출, 비탈출 클로저
typealias VoidVoidClosure = () -> Void

func functionWithNoescapeClosure(closure: VoidVoidClosure) {
    closure()
}

func functionWithEscapingClosure(completionHandler: @escaping VoidVoidClosure) -> VoidVoidClosure {
    return completionHandler
}


class SomeClass {
    var x = 10
    
    func runNoescapeClosure() {
        // 비탈출 클로저에서 self 키워드 사용은 선택 사항입니다.
        functionWithNoescapeClosure { x = 200 }
    }
    
    func runEscapingClosure() -> VoidVoidClosure {
        // 탈출 클로저에서는 명시적으로 self를 사용해야 합니다.
        return functionWithEscapingClosure { self.x = 100 }
    }
}

let instance: SomeClass = SomeClass()
instance.runNoescapeClosure()
print(instance.x)   // 200

let returnedClosure: VoidVoidClosure = instance.runEscapingClosure()
returnedClosure()
print(instance.x)   // 100

위 코드를 보면 @escaping 키워드를 가진 코드는 self.x를 아닌 코드에서는 x로 참조를 하고 있습니다.

 

withoutActuallyEscaping

withoutActuallyEscaping를 사용하여 비탈출 클로저가 클로저 처럼 사용할 수 있습니다.

 

 

자통 클로저

함수의 전달인자로 전달하는 표현을 자동으로 변환해주는 클로저를 자동클로저라고 합니다. 자동 클로저는  전달인자를 갖지 않습니다. 자동 클로저는 호출되었을 떄 자신이 감싸고 있는 코드의 결과 값을 반환합니다.

자동 클로저는 클로저가 호출되기 전까지 클로저 내부의 코드가 동작하지 않습니다. 따라서 연산을 지연 시킬 수 있습니다.

var customersInLine: [String] = ["YoangWha", "SangYong", "SungHun", "HaMi"]
print(customersInLine.count)

// 클로저를 만들어두면 클로저 내부의 코드를 미리 실행(연산)하지 않고 가지고만 있습니다.
let customerProvider: () -> String = {
    return customersInLine.removeFirst()
}
print(customersInLine.count)

// 실제로 실행합니다.
print("Now serving \(customerProvider())!") // "Now serving YoangWha!"
print(customersInLine.count)

위 코드를 보면 customerProvider 상수에 클로저가 있고, 그 안에 removeFirst() 가 있습니다. removeFirst는 Array에서 첫번째 값을 제거하고 반환합니다. customerProvider를 선언했지만 클로저가 실행되지 않았기 때문에 안에 내부 연산이 반영되지 않았고,그래서 밑에 print함수는 4를 보여주고 다음 print에서 클로저가 실행되어 마지막 print에서 3이 반환됩니다.이렇게 클로저의 지연을 확인 해 볼 수 있습니다.

customersInLine = ["YoangWha", "SangYong", "SungHun", "HaMi"]

func serveCustomer(_ customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}
serveCustomer( { customersInLine.removeFirst() } )   // "Now serving YoangWha!"

위 코드는 일반적인 클로저의 사용과 동일합니다. 그래서 serveCustomer에 클로저 ()-> String이 들어가게 됩니다

 

하지만 자동클로저를 만들어서 다음과 같이 하면 자동클로저로써 기능을 하게 되고

var customersInLine = ["YoangWha", "SangYong", "SungHun", "HaMi"]

func serveCustomer(_ customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}
serveCustomer(customersInLine.removeFirst()) // "Now serving YoangWha!"

customerInLine.removeFirst의 결과인 String값(YoangWha)를 전달인자로 받게 됩니다.이렇게 String 값으로 전달된 전달인자가 자동으로 클로저로 변환되기 때문에 자동클로저라 부릅니다.

반응형

'2023년 이전 > swift' 카테고리의 다른 글

music player 만들기  (0) 2020.09.20
구문 이름표  (0) 2020.07.12
swift의switch 문  (0) 2020.07.12
튜플,배열, 딕셔너리,세트, 열거형  (0) 2020.07.05