Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swift에서 클로저(Closure)란 무엇이며, 어떻게 사용하나요? #34

Open
kmh5038 opened this issue May 7, 2024 · 1 comment
Assignees

Comments

@kmh5038
Copy link
Member

kmh5038 commented May 7, 2024

  • 클로저의 캡처 리스트(Capture List)는 어떤 역할을 하나요?
  • @escaping 클로저와 non-escaping 클로저의 차이점은 무엇인가요?
  • 트레일링 클로저(Trailing Closure) 문법은 언제 사용하면 좋나요?
@kmh5038 kmh5038 self-assigned this May 7, 2024
@kmh5038
Copy link
Member Author

kmh5038 commented May 8, 2024

1. 정의

클로저는 사용자의 코드 안에서 전달되어 사용할 수 있는 로직을 가진 중괄호 구분된 코드의 블럭입니다. 클로저는 두 가지 종류가 있습니다.
이름이 있는 함수(Named Closure), 익명함수(Unnamed Closure)
우리가 보통 부르는 함수는 이름이 있는 함수(Named Closure)로 클로저라 부르지 않지만 사실은 클로저입니다.

func thinkBig() {
	print("Closure")
}

우리가 평소에 부르는 클로저는 익명함수(Unnamed Closure)를 클로저라 부릅니다.

let thinkBig = { print("Closure") }

2. 후행 클로저(trailing closure)

후행 클로저는 클로저 표현을 간소화 하는 방법입니다.
클로저를 좀 더 쓰기 편하게, 보기 편하게 하기 위한 방법입니다. 함수의 마지막 파라미터가 클로저일 때, 이를 파라미터 값 형식이 아닌 함수 뒤에 붙여 작성하는 문법입니다. 이때, Argument Label은 생략됩니다.

func closureFunc(closure: () -> ()) {
	// 함수 내부에서 클로저 실행
}

이런 코드를

closureFunc {
    // 클로저의 내용을 여기에 작성
}

이렇게 간략하게 표현하여 가독성을 향상 시킬 수 있습니다.


3. 캡처 리스트(Capture List)

캡처 리스트값 타입 일때는 클로저 내부에서 클로저 외부의 값을 참조할 때 참조하는 값이 변경되면 클로저 내부에서도 참조하는 값 또한 바뀌게 되므로 이를 방지하고자 주로 사용되고, 참조 타입 일때는 클로저의 강한 참조 순환 문제를 해결하기 귀해 사용됩니다.


3-1. 값(Value) 타입 캡처 리스트

클로저 내부에서 외부의 값을 참조할 때 의 예시입니다.

var n = 0

var numberPrint = {
    print("반환값: \(n) 입니다.")   // 클로저 내부에서 외부 변수 사용
}

numberPrint()   // 반환값: 0 입니다.


n = 10
numberPrint()   // 반환값: 10 입니다.


n = 100
numberPrint()   // 반환값: 100 입니다.

위 코드처럼 클로저 내부에서 외부 변수를 캡처를 하는 경우인데 변수의 주소값을 힙(Heap) 영역에 저장하기 때문에 값이 변경되면 클로저 내부에도 변경이된다.


캡처 리스트에 의한 캡처의 예시입니다.

var n = 0

var numberPrint = { [n] in  // 캡처 리스트 구현
    print("반환값: \(n) 입니다.")   // 클로저 내부에서 외부 변수 사용
}

numberPrint()   // 반환값: 0 입니다.


n = 10
numberPrint()   // 반환값: 0 입니다.


n = 100
numberPrint()   // 반환값: 0 입니다.

이 경우는 외부 변수 값 자체를 힙(Heap) 영역에 저장하여 외부 변수에 다른 값을 할당하더라도 내부 값이 변경되지않습니다.


3-2. 참조(Reference) 타입의 캡처 리스트

강한 참조가 문제가 되는 예시를 설명하겠습니다.

class Person {
    var name: String
    var run: (()->Void)?
   
    init(name: String) {
        self.name = name
    }
   
    func runClosure() {
        run = {
            print("\(self.name)이 달리고 있습니다.")
        }
    }
   
    deinit {
        print("\(self.name) 메모리에서 제거되었습니다.")
    }
}

func doSomething() {
    var phang: Person? = Person(name: "이창준")  // phang 인스턴스 생성 (phang RC 1증가)
    phang?.runClosure()  // 클로저(run)가 메모리의 Heap 영역에 생성
}

doSomething() 
  1. doSomthing() 함수 작동
  2. phang 인스턴스 생성 (phang RC 1 증가)
  3. phang 인스턴스가 runClosure() 작동
  4. runClosure() 함수에 의해 클로저(run)가 메모리의 Heap 영역에 생성
    (클로저(run) RC 1 증가)
  5. runClosure() 함수에서 클로저(run)가 phang을 지목하여 참조하고 있음
    (phang RC 1 증가)
  6. doSomething() 함수의 실행이 종료 (phang RC 1감소)

결과 : 최종적으로 phang 인스턴스의 카운트 1, 클로저(run)의 카운트 1 인스턴스와 클로저가 강한 참조 사이클을 유지하고 있기 때문에 소멸자가 작동하고 있지 않습니다.


메모리 누수를 해결한 예시입니다.

class Person {
    var name: String
    var run: (()->Void)?
   
    init(name: String){
        self.name = name
    }
       
    func runClosure() {
        run = { [weak self] in
            print("\(self?.name)이 달리고 있습니다.")
        }
    }
   
    deinit {
        print("\(self.name) 메모리에서 제거되었습니다.")
    }
}

func doSomething() {
    var phang: Person? = Person(name: "이창준")  // phang 인스턴스 생성 (phang RC 1증가)
    phang?.runClosure()  // 클로저(run)가 메모리의 Heap 영역에 생성
}

doSomething()  // 이창준 메모리에서 제거되었습니다.
  1. doSomething() 함수 작동
  2. phang 인스턴스 생성 (phang RC 1 증가)
  3. phang 인스턴스가 runClosure() 함수 작동
  4. runClosure() 함수에 의해 클로저(run)가 메모리의 Heap 영역에 생성 (약한 참조에 의해 클로저(run) RC 0 증가)
  5. runClosure() 함수에서 클로저(run)가 phang 지목하여 참조하고 있음 (phang RC 1 증가)
  6. doSomething() 함수의 실행이 종료, 함수가 종료됨에 따라 phang 인스턴스가 메모리에서 제거되기 때문에 클로저(run) 또한 메모리에서 제거된다. (phang RC 1 감소)

결과 : 최종적으로 phang 인스턴스의 카운트 0, 클로저(run)의 카운트0 이기때문에 소멸자 작동

4. escaping, non-escaping 클로저

4-1. non-escaping 클로저

func thinkBig(completion: () -> ()) {
	completion()
}
thinkBing {
	print("study hard")
 }

이런 코드처럼 우리가 일반적으로 아무런 키워드 없이 파라미터로 받는 클로저를 모두 non-escaping 클로저라 부릅니다.


func thinkBig(completion: () -> ()) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
        completion()
    }
}

같은 코드를 3초 뒤에 실행하고 싶어 위와 같이 수정을 하면

이런 에러가 발생합니다. 그 이유는 이름 그대로 탈출이 불가능한 클로저이기 때문에 함수의 "흐름"을 탈출 하지 않는 클로저 이기 때문입니다.
한 마디로 함수가 종료되고 나서 클로저가 실행될 수 없고, 함수가 종료되기 전에 클로저가 사용되어야합니다.

class example {
	var property: (() -> ())
    
    func closureFunc(_ closure : () -> ()) {
    	self.property = closure  // error
    }
}

따라서 위와 같이 함수 외부의 변수에 값을 할당하는 이런 경우도 에러가 발생합니다.


4-2. escaping 클로저

non-escaping 클로저에서 함수의 흐름을 벗어날 수 없던 경우에 @escaping 키워드를 붙여 함수를 탈출하여 사용할 수 있습니다.

func sodeul(completion: @escaping () -> ()) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
        completion()
    }
}

이렇게 사용을하면 이 클로저는 함수의 실행 흐름에서 벗어나도 상관 없이 실행되는 클로저이기 때문에 에러가 발생하지 않습니다.

class example {
	var property : (() -> Void)?
    
	func closureFunc(_ closure : @escaping () -> Void ) {
		self.property = closure 
    }
}

이 에시 또한 함수 외부의 변수에 할당을 해주고싶을때 @escaping 키워드를 사용하여 사용할 수 있습니다.

escaping 클로저는 이러한 특징 때문에 비동기작업을 시행할 때 주로 사용이 많이됩니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant