클로저

Gooreum·2021년 10월 28일
0

Swift

목록 보기
8/16
post-custom-banner

클로저란

  • 스위프트에서 함수형 프로그래밍 패러다임을 접할 때 첫걸음으로 꼭 알아야 할 개념.
  • 클로저는 일정 기능을 하는 코드를 하나의 블록으로 모아놓은 것
  • 함수는 클로저의 한 형태임.
  • 클로저는 변수나 상수가 선언된 위치에서 참조(reference)를 획득(capture)하고 저장할 수 있다.
  • 클로저의 세가지 형태
    • 이름이 있으면서 어떤 값도 획득하지 않는 전역함수의 형태
    • 이름이 있으면서 다른 함수 내부의 값을 획득할 수 있는 중첩된 함수의 형태
    • 이름이 없고 주변 문맥에 따라 값을 획득할 수 있는 축약 문법으로 작성한 형태
  • 클로저의 당야한 표현
    • 클로저는 매개변수와 반환 값의 타입을 문맥을 통해 유추할 수 있기 때문에 매개변수와 반환 값의 타입을 생략 가능
    • 클로저에 단 한줄의 표현만 들어있다면 암시적으로 이를 반환 값으로 취급(return 키워드 생략가능)
    • 축약된 전달인자 이름을 사용가능
    • 후행 클로저 문법 사용가능

기본 클로저

  • sorted(by:) 메서드를 이용해 알아보자.
  • 스위프트 표준 라이브러리에서 제공하며, 배열의 값을 정렬해주는 역할을 함.
  • 기존의 배열은 변경하지 않고 정렬된 배열을 새로 생성하여 반환해줌.
public func sorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> [Element]

//정렬할 이름 배열
let names: [String] = ["wizplan", "eric", "yagom", "jenny"]
  • 클로저가 아닌 함수를 이용하여 names 정렬해보자.
func backwards(first: String, second: String) -> Bool {
    return first > second
}

//MARK: 매개변수로써의 함수
let reversed: [String] = names.sorted(by: backwards)

//MARK: 매개변수에 클로저 대입
let reversed = names.sorted(by: {(first: String, second: String) -> Bool in
   return  first > second
})

후행 클로저

  • 함수나 메서드의 마지막 전달인자로 위치하는 클로저는 함수나 메서드의 소괄호를 닫은 후 작성해도 된다.
  • 클로저가 조금 길어지거나 가독성이 조금 떨어진다 싶으면 후행 클로저 기능을 사용하면 좋다.
  • 단, 후행 클로저는 맨 마지막 전달인자로 전달되는 클로저에만 해당되므로 전달인자로 클로저 여러 개를 전달할 때는 맨 마지막 클로저만 후행 클로저로 사용할 수 있음.
  • 또한 sorted(by:) 메서드처럼 단 하나의 클로저만 전달인자로 전달하는 경우에는 소괄호를 생략 가능.
  • 매개변수에 클로저가 여러 개 있는 경우, 다중 후행 클로저 문법 사용 가능.
  • 다중 후행 클로저를 사용하는 경우, 중괄호를 열고 닫음으로써 클로저를 표현하며, 첫 번째 클로저의 전달인자 레이블은 생략.
//MARK: 후행 클로저 사용
let reversed = names.sorted() { (first: String, second: String) -> Bool in
   return  first > second
}

//MARK: 후행 클로저 사용 - sorted(by:) 메소드의 소괄호까지 생략 가능
let reversed = names.sorted { (first: String, second: String) -> Bool in
   return  first > second
}

func doSomething(do: (String) -> Void, 
								 onSuccess: (Any) -> Void,
								 onFailure: (Error) -> Void) { 
			// do something...
}

//MARK: 다중 후행 클로저의 사용
doSomething { (something: String) in 
		//do closure
} onSuccess: { (result: Any) in
		//success closure
} onFailure: { (error: Error) in 
		//failure closure
}

클로저 표현 간소화

  • 문맥을 이용한 타입유추
    • 메서드의 전달인자로 전달하는 클로저는 메서드에서 요구하는 형태로 전달해야 하는데, 다르게 생각하면 전달인자로 전달할 클로저는 이미 적합한 타입을 준수하고 있다고 유추할 수 있다.
    • 따라서 전달인자로 전달하는 클로저를 구현할 때는 매개변수의 타입이나 반환 값의 타입을 굳이 표현하지 않고 생략이 가능.
  • 단축 인자 이름
    • $0, $1, ... 순써로 $와 숫자의 조합으로 ㅍ현.
    • 단축 인자 표현을 사용하게 되면 매개변수 및 반환 타입과 실행 코드를 구분하기 위해 사용했던 키워드 in을 상요할 필요도 없어짐.
  • 암시적 반환 표현
    • 반환 값을 클로저 내부의 실행문이 단 한줄이라면, 암시적으로 그 실행문을 반환 값으로 사용가능 하므로 return 키워드를 생략할 수 있다.
  • 연산자 함수
    • 클로저 자리에 연산자 함수를 적용가능.
//MARK: 문맥 타입유추
let reversed = names.sorted { (first, second) -> Bool in
   return  first > second
}

//MARK: 단축인자 사용
let reversed = names.sorted {
    return  $0 > $1
}

//MARK: 암시적 반환 표현
let reversed = names.sorted {
    $0 > $1
}

//MARK: 클로저로서의 연산자 함수 사용
let reversed = names.sorted(by: >)

값 획득

  • 클로저는 자신이 정의된 위치의 주변 문맥을 통해 상수나 변수를 획득(capture)할 수 있다.
  • 값 획득을 통해 클로저는 주변에 정의한 상수나 변수가 더 이상 존재하지 않더라도 해당 상수나 변수의 값을 자신 내부에서 참조하거나 수정할 수 있다.
  • 클로저는 특히 비동기 작업에 많이 사용되는데, 클로저를 통해 비동기 콜백을 작성하는 경우, 현재 상태를 미리 획득해두지 않으면, 실제로 클로저의 기능을 실행하는 순간에는 주변의 상수나 변수가 이미 메모리에 존재하지 않는 경우가 발생한다.
  • 중첩 함수도 하나의 클로저 이므로, 중첩 함수 주변의 변수나 상수를 획득할 수 있다.
func makeIncrementer(forIncrement amount: Int) -> (() -> Int) {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

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

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

print(first)
print(second)
print(thrid)

---result---
2
4
6
  • incrementer() 함수 주변에 runningTotal과 amount 변수가 있으므로 incrementer() 함수는 두 변수의 참조를 획득한다.
  • 참조를 획득하면 runningTotal과 amount는 makeIncrementer 함수의 실행이 끝나도 사라지지 않고, incrementer가 호출될 때마다 계속해서 사용할 수 있다.

💡 클래스 인스턴스 프로퍼티로서의 클로저
클래스 인스턴스의 프로퍼티로 클로저를 할당한다면 클로저는 해당 인스턴스 또는 인스턴스의 멤버의 참조를 획득할 수 있으나, 클로저와 인스턴스 사이에 강한참조 순환 문제가 발생할 수 있다.

클로저는 참조 타입

  • 함수와 클로저는 '참조타입'이므로 incrementByTwo 는 runningTotal 값 획득을 통해 계속해서 증가시킬 수 있었다.
  • 함수나 클로저를 상수나 변수에 할당할 때마다 사실은 상수나 변수에 함수나 클로저의 참조를 설정하는 것이다.
  • 즉, incrementByTwo라는 상수에 클로저를 할당한다는 것은 클로저의 내용물, 즉 값을 할당하는 것이 아니라 해당 클로저의 참조를 할당하는 것이다.
  • 결국 클로저의 참조를 다른 상수에 할당해준다면 이는 두 상수가 모두 같은 클로저를 가리킨다는 뜻이다.

탈출 클로저

  • 함수의 전달인자로 전달한 클로저가 함수 종료 후에 호출될 때 클로저가 함수를 탈출(Escape) 한다고 표현한다.
  • 클로저를 매개변수로 갖는 함수를 선언할 때 매개변수 이름의 콜론(:) 뒤에 @escaping 키워드를 사용하여 클로저가 탈출하는 것을 허용한다고 명시해줄 수 있다.
    • 비동기 작업을 실행하는 함수들은 클로저를 컴플리션 핸들러 전달인자로 받아오는데, 비동기 작업으로 함수가 종료되고 난 후 호출할 필요가 있는 클로저를 사용해야 할 때 탈출 클로저(Escaping Closure)가 필요하다.
  • @escaping 키워드가 없으면 비탈출 클로저(Nonescape Closure)라고 보면 되며, 함수로 전달된 클로저가 함수의 동작이 끝난 후 사용할 필요가 없을 때 비탈출 클로저를 사용한다.
  • 클로저가 탈출할 수 있는 경우
    • 함수 외부에 정의된 변수나 상수에 저장되어 함수가 종료된 후에 사용할 경우.
    • 함수의 전달인자로 전달받은 클로저를 다시 반환(return) 할 경우
typealias VoidVoidClosure = () -> ()
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()

var closures: [VoidVoidClosure] = []

//closure 매개변수 클로저는 함수 외부의 변수에 저장될 수 있으므로 탈출 클로저.
func appendClosure(closure: @escaping VoidVoidClosure) {
    closures.append(closure)
}
  • 타입 내부 메서드의 매개변수 클로저에 @escaping 키워드를 사용하여 탈출 클로저임을 명시한 경우, 클로저 내부에서 해당 타입의 프로퍼티나 메서드, 서브스크립트 등에 접근하려면 self 키워드를 명시적으로 사용해야 한다.
    • 비탈출 클로저는 클로저 내부에서 타입 내부의 프로퍼티나 메서드, 서버스크립트 등에 접근할 때 self 키워드를 꼭 써주지 않아도 된다. (선택사항임)
func functionWithNoescapeClosure(closure: VoidVoidClosure) {
    closure()
}

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

class SomeClass {
    var x = 10
    func runNoescapeClosure() {
        functionWithNoescapeClosure { x = 200}
    }

    func runEscapingClosure() -> VoidVoidCLosure {
        return functionWithEscapingClosure { self.x = 100}
    }
}

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

let returnedClosure: VoidVoidClosure = instance.runEscapingClosure()
returnedClosure()
print(instance.x) // 100
profile
하루하루 꾸준히
post-custom-banner

0개의 댓글