[iOS-Swift] 클로저 closure 심화

TIL 13일 차 - Swift 문법 클로저 심화

1. Escaping Closure

함수의 파라미터로 전달된 클로저는 함수 내부에서만 사용이 가능하지만,

함수가 종료된 후에도 실행해야 할 경우에는 escaping 키워드를 사용하면 됨

 

(1) Non-escaping (기본값)

func test(closure: () -> Void) {
  closure()   // 함수 실행 중에 바로 클로저 호출 가능
}

 

파라미터로 받은 클로저는 함수의 실행이 완료되기 전에 함수 내부에서 클로저를 사용해야 함

 

// 함수가 끝난 뒤에 클로저를 실행하려하면 에러 발생
// DispatchQueue.main.asyncAfter는 1초 뒤 실행이라 함수실행이 끝난뒤 클로저를 실행하려는 상황이 됨

func test(closure: () -> Void) {
  DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
    closure() // 여기서 에러
  }
}

 

파라미터로 받은 클로저는 다른 변수나 상수에 할당할 수 없음

//파라미터로 받은 클로저는 다른 변수나 상수에 할당 불가
func testEsacpingClosure(closure: () -> Void) {
	let newClosure: () -> Void = closure // Error 발생
}

 

 

(2) @escaping closure

- 앞의 경우 처럼 클로저를 함수의 종료 후에도 사용하려면 클로저의 타입 앞에 @escaping 키워드를 명시하면 됨

func testEscapingClosure(closure: @escaping () -> Void) {
    
    // 1초뒤에 코드 블록을 실행하는 코드
    DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
        closure() // 오류 발생 X, 하지만 스위프트 6에서는 오류 발생함
    }
}

testEscapingClosure {
    print("클로저 실행")
}

비동기적인 작업 후에 클로저를 호출할 때 주로 @escaping closure를 사용

- 서버에서 API를 호출하는 경우 서버의 응답을 기다린 후 데이터를 받아 처리해야 함

- 이때 서버에서 응답이 도착하기 도전에 함수가 먼저 종료될 수 있음

- 함수가 종료된 이후에도 클로저가 실행될 수 있도록 @escaping을 사용하여 서버의 응답을 받은 후에 클로저를 호출

 

 

2. 클로저 캡처 (closure capture)

클로저는 자신이 생성된 콘텍스트(자신이 생성된 코드블록) 내에 있는 변수나 상수에 접근할 수 있으며, 이를 클로저 캡처라 함

 

- 클로저 캡처는 참조 형식으로 값을 사용해, 값이 변경되면 캡처된 값도 변경된 값을 출력함

- 클로저가 선언된 뒤에 선언된 변수와 상수는 클로저 안에서 사용 불가함

struct Person {
    var name: String
    var age: Int
}

func testClosureCapture() {
    var person = Person(name: "JH", age: 24)
    let closure = {
        // 해당 클로저에서 person을 캡처하여 사용
        // value type이기때문에 나이 24를 복사하여 사용해 출력했을 경우에
        // 값이 24, 24가 나오지 않는 이유는 기본적으로 클로저 캡처는 참조형식이기 때문
        print(person.age)
    }
    closure() // 24 출력
    
    person.age = 25
    
    closure() // 25 출력
}

testClosureCapture()
// 클로저 캡처는 클로저 사용 전에 선언된 변수나 상수만 가능함

func testClosureCaptureError() {
    var a = 1
    
    let closure = {
        print(a)
        print(b) // 에러: 클로저 생성 후의 b는 캡처 불가
        // Closure captures 'b' before it is declared
    }
    var b = 1
}

 

 

3. 캡처 리스트

클로저가 주변 환경의 변수를 캡처할 때 메모리 관리를 명시적으로 제어할 수 있는 방법이다.

 

- value type의 값도 참조로 캡처하지만 value type으로 캡처 가능

- reference type은 참조 방식을 정할 수 있음

- [ ] 대괄호 안에 사용할 변수나 상수를 작성하여 캡처 리스트를 정할 수 있다.

 

(1) reference type 참조 방식 정하기

- 기본은 strong이기 때문에 강한 순환 참조가 발생할 수 있음

- 대괄호를 사용해 참조방식과 캡처할 변수(상수)의 이름을 작성하면 됨

- weak, unowned 사용 가능함

class Animal {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

func testClosureCaptureReference() {
    var animal = Animal(name: "모찌", age: 1)
    
    let closure = { [weak animal] in // [ ]
        print(animal?.age)}
    
    closure() // 출력 값: Optional(1)
    animal.age = 2
    closure() // 출력 값: Optional(2)
}

testClosureCaptureReference()

 

(2) value type으로 캡처하는 방법

- 대괄호를 사용하고 캡처할 변수(상수)를 작성하면 됨

struct Person {
    var name: String
    var age: Int
}

func testClosureCapture() {
    var person = Person(name: "JH", age: 24)
    var a = 1
    
    let closure = { [person] in
        print(person.age)
        print(a)
    }
    closure() // 출력 값: 24
    person.age = 25
    a = 2
    closure() // value type으로 캡처하였으므로 24 출력
}

testClosureCapture()

/* 출력값
24
 1
 24 -> person은 value type으로 복사를 하였기 때문에 값 변동 없음
 2 -> a는 참조캡처를 하였기 때문에 값 변동
 
 */