🍏 클로저(Closure)
- Swift에서 클로저는 코드 블럭을 변수처럼 저장하고 사용할 수 있는 기능이다.
- 함수와 유사하게 동작하지만 이름이 없고, 변수에 저장하거나 다른 함수의 파라미터로 넘길 수 있다.
- 클로저는 참조 타입(Reference Type)으로, Heap에 저장된다.
클로저의 표현식
// 클로저의 표현식
{ (매개변수) -> 반환 타입 in
실행 코드
}
in 키워드 앞은 입력값과 출력값 정의, 뒤는 실행될 코드다. in 키워드를 기준으로 나뉜다는걸 기억하면 쉽다.
간단 예제
let greeting = { (name: String) -> String in
return "Hello, \(name)!"
}
print(greeting("Mori")) // Hello, Mori!
축약 형태 (매개변수 없는)
let greeting = {
print("Hello, World!")
}
greeting() // Hello, World!
이처럼 클로저는 let 또는 var로 선언한 변수에 담아서 필요할 때 실행할 수 있다.
클로저 축약 문법
// 1. 전체 문법
let add = { (a: Int, b: Int) -> Int in return a + b }
// 2. 타입 추론
let add = { a, b in return a + b }
// 3. return 생략
let add = { a, b in a + b }
// 4. 축약 인자 이름 ($0, $1, ...)
let add = { $0 + $1 }
🍏 후행 클로저
후행 클로저(Trailing Closure)는 함수의 마지막 매개변수가 클로저일 경우, 해당 클로저를 괄호 바깥으로 빼서 더 간결하게 작성할 수 있도록 하는 문법이다.
클로저의 본문이 길어지면 가독성이 떨어지기 때문에, 후행 클로저를 사용하여 코드를 더 간결하고 읽기 쉽게 할 수 있다.
특히 UIKit의 애니메이션이나, SwiftUI의 뷰 선언 등에서 자주 사용된다.
// 일반 클로저 전달 방식
함수명(파라미터: { 클로저 })
// 후행 클로저 방식
함수명 { 클로저 }
위의 예시처럼, 마지막 파라미터가 클로저일 때 코드를 더 간결하게 만들기 위해 후행 클로저를 사용한다.
마지막 파라미터가 클로저가 아니라면 사용할 수 없다.
func test(closure: () -> Void, b: Int) { }
// ❌ 마지막이 클로저가 아니라서 후행 클로저 사용 불가
UIKit 후행 클로저 예시
// UIKit예시
UIView.animate(withDuration: 0.3, animations: {
print("애니메이션 시작")
})
// 후행 클로저 방식
UIView.animate(withDuration: 0.3) {
print("애니메이션 시작")
}
SwiftUI 후행 클로저 예시
// SwiftUI 예시
Button(action: {
print("버튼 눌림")
}) {
Text("Click!")
}
// 후행 클로저 방식
Button {
print("버튼 눌림")
} label: {
Text("Click!")
}
🍏 escaping vs non-escaping
@escaping은 “탈출하다”는 뜻으로, Swift에서 클로저는 기본적으로 non-escaping이다.
즉, 클로저는 함수 내부에서 실행되며, 함수가 종료되면 더 이상 사용되지 않는 것이 기본 동작이다.
하지만 클로저를 외부에 저장하거나, 나중에 실행하려는 경우에는 함수가 끝난 뒤에도 클로저가 실행될 수 있어야 하기 때문에, @escaping 키워드를 명시해야 한다.
오류 발생 예시
import UIKit
func esacpingClosure(closure: () -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
closure()
}
}
위의 예시는 함수가 끝나고 1초가 지난 후에 closure를 호출하기 때문에 오류가 발생한다.
@escaping 사용 예시
import UIKit
func esacpingClosure(closure: @escaping () -> Void) {
// 1초뒤에 코드블록을 실행하는 코드
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
closure()
}
}
esacpingClosure {
print("클로저 실행")
}
위처럼 함수가 종료된 이후에도 클로저를 실행할 수 있도록 하기위해 @escaping을 사용한다.
🧐 언제 @escaping이 필요할까?
- 비동기 처리 시 (async)
- 예: DispatchQueue.main.async, 네트워크 요청 등
- 클로저를 함수 외부에 저장할 때
- 예: 클로저를 배열에 저장하거나, 클래스의 속성으로 저장할 경우
- Completion Handler로 사용할 때
- 예: 네트워크 응답 등 비동기 작업이 완료된 후 실행되는 클로저
🍏 캡처(Capture)
클로저는 자신이 선언된 시점에 주변 변수나 상수를 기억하고, 나중에 그 값을 사용할 수 있다. 이걸 클로저 캡처 라고 부른다.
var number = 10
let printNumber = {
print(number)
}
number = 20
printNumber() // 20
🧠 메모리 관점에서 잠깐 보자면?
- Int, Struct, Array 등은 값 타입이며, 기본적으로 Stack 메모리에 저장된다.
- 하지만 클로저가 이러한 값을 나중에 참조하는 경우, Swift는 그 값을 Heap에 복사해서 클로저가 참조할 수 있도록 처리한다.
- 따라서 값 타입임에도 불구하고, 참조 타입처럼 최신 값이 출력되는 것이다.
즉, Int는 값 타입이지만 클로저 내부에서는 참조 방식처럼 동작하기 때문에 최신 값이 사용된다!
🍏 캡처 리스트(Capture List)
클로저는 외부 변수를 사용할 때 기본적으로 강한(strong)참조로 캡처한다. 이로 인해 메모리 누수나, 예상하지 않은 값 변경 문제가 발생할 수 있다.
이러한 문제를 해결하기 위해서 캡처 리스트를 사용하여, 메모리 관리 방식(weak, unowned)을 지정할 수 있다.
캡처 리스트 문법
let closure1 = { [캡처방식 변수명] in ... }
let closure2 = { [변수명] in ... }
캡처 리스트는 [캡처방식 변수명] 또는 [변수명] 형태로 작성된다.
{ [weak self] in ... }
{ [unowned user] in ... }
{ [user] in ... }
- weak: 참조 카운트를 올리지 않으며, 값이 해제되면 nil이 된다.
- unowned: 참조 카운트를 올리지 않지만, 값이 해제된 뒤 접근하면 크래시가 발생한다.
📍 참조 타입 (Reference Type)캡처
위에서 말했듯, 클로저가 클래스 인스턴스와 같은 참조 타입을 클로저가 캡처할 경우, 순환 참조로 인한 메모리 누수를 방지하기 위해 메모리 관리 방식(weak/unowned)을 고려해야한다.
class Dog {
var age = 1
}
var dog = Dog()
let closure = { [weak dog] in
print(dog?.age ?? -1)
}
📍 값 타입 (Value Type)캡처
클로저는 Int, Struct 등의 값 타입도 복사하지 않고 참조처럼 캡처하려고 한다. 즉, 클로저 안에서는 원본의 변화를 따라가며 최신 값을 사용하게 된다.
그렇기에 값 타입에서는 캡처 시점의 값을 확정적으로 쓰고 싶을 때 캡처 리스트가 필요하다.
참조처럼 캡처되는 예시
struct Person {
var name: String
}
var p = Person(name: "John")
let closure = {
print(p.name)
}
p.name = "Changed"
closure() // "Changed"
클로저는 외부 변수 p를 참조처럼 기억하고 있어서 변경된 최신의 값을 출력한다.
복사해서 사용하려면?
let closure = { [p] in
print(p.name)
}
p.name = "Changed"
closure() // "John"
캡처 리스트로 [p]를 명시하면, 그 시점의 값이 복사되어 클로저 안에 담을 수 있게 된다.
📌 Closure 전체 정리
- 클로저는 이름 없는 함수처럼 사용된다.
- 주변 변수나 상수를 기억하고 나중에 사용할 수 있다. 이를 클로저 캡처라고 한다.
- 클로저는 외부 변수를 기본적으로 strong 참조로 캡처함
- 비동기나 함수 외부에서 클로저를 사용할 경우 @escaping 키워드가 필요하다.
- 참조 타입은 메모리 누수 방지를 위해 [weak 변수], [unowned 변수] 를 지정해야 한다.
- 값 타입도 기본적으로는 참조처럼 캡처되므로. 값을 복사하고 싶으면 [변수] 형식으로 사용할 수 있다.
'🖋️ TIL Journal' 카테고리의 다른 글
06.05 (목) enumerated()가 제네릭에서 안 된다고..? (1) | 2025.06.05 |
---|---|
06.04 (수) 열거형(Enum)과 compactMap, flatMap (0) | 2025.06.04 |
05.30 (금) 실수로 날린 커밋, reflog로 되살리기 (0) | 2025.05.30 |
05.29 (목) 꽤나 자주 보이던 키워드들 간단 정리 (0) | 2025.05.29 |
05.28 (수) Convenience init과 프로퍼티 (0) | 2025.05.28 |