Swift의 클로저(Closure)복습

sonny·2025년 2월 2일
0

TIL

목록 보기
118/133

Swift에서 클로저(Closure)독립적인 코드 블록으로, 함수와 유사하게 사용할 수 있다.

하지만 함수와 달리 이름이 없으며, 다른 변수나 함수의 인자로 전달할 수도 있다.

클로저는 강력한 기능을 제공하지만, 메모리 관리 문제(특히 강한 참조 순환 문제)를 유발할 수 있다.

이번 글에서는 클로저의 기본 개념부터 메모리 관리까지 깊이 있게 살펴본다.

1. 클로저의 기본 문법

클로저는 함수와 비슷한 방식으로 작성되지만, 더 간결한 문법을 제공한다. 기본적인 클로저의 형태는 다음과 같다.

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

예제를 통해 살펴보자.

let greet = { (name: String) -> String in
    return "Hello, \(name)!"
}

print(greet("Swift")) // "Hello, Swift!"

위 코드에서 greet 변수는 클로저를 저장하고 있으며, 이를 호출하면 함수처럼 동작한다.

1.1 클로저 표현식 간소화

Swift는 클로저의 문법을 최적화하여 더 간결하게 작성할 수 있도록 지원한다.

1) 매개변수와 반환 타입 생략

Swift는 타입 추론을 제공하므로, 컴파일러가 타입을 유추할 수 있다면 생략할 수 있다.

let greet = { name in
    "Hello, \(name)!"
}

print(greet("Swift"))

2) $0, $1과 같은 단축 인자 사용

단일 표현식인 경우 return을 생략할 수 있으며, 매개변수를 $0, $1 등의 단축 인자로 사용할 수도 있다.

let multiply = { $0 * $1 }
print(multiply(3, 4)) // 12

3) 후행 클로저(Trailing Closure)

함수의 마지막 매개변수로 클로저를 전달하는 경우, 후행 클로저(Trailing Closure) 문법을 사용할 수 있다.

func perform(action: () -> Void) {
    action()
}

// 일반적인 클로저 전달 방식
perform(action: {
    print("클로저 실행")
})

// 후행 클로저 사용
perform {
    print("후행 클로저 실행")
}

2. 클로저의 캡처(Capturing Values)

클로저는 실행될 때 외부 변수나 상수의 값을 캡처할 수 있다. 이를 클로저의 캡처(Capturing Values)라고 한다.

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

let incrementByTwo = makeIncrementer(amount: 2)
print(incrementByTwo()) // 2
print(incrementByTwo()) // 4
print(incrementByTwo()) // 6

위 코드에서 total 변수는 makeIncrementer 함수가 끝난 후에도 계속 유지된다. 이는 클로저가 total캡처하고 있기 때문이다.

3. 클로저와 메모리 관리

Swift는 자동 참조 카운트(ARC, Automatic Reference Counting)를 사용하여 메모리를 관리한다. 하지만 클로저를 사용할 때, 강한 참조 순환(Strong Reference Cycle)이 발생할 수 있다.

3.1 강한 참조 순환 문제

클로저가 클래스 인스턴스를 캡처할 경우, 강한 참조가 유지되어 메모리 누수(Memory Leak)가 발생할 수 있다.

class Person {
    var name: String
    var greeting: (() -> Void)?

    init(name: String) {
        self.name = name
    }

    func setGreeting() {
        greeting = {
            print("Hello, my name is \(self.name)")
        }
    }
}

var person: Person? = Person(name: "Alice")
person?.setGreeting()
person = nil // 메모리 해제되지 않음 (메모리 누수 발생)

여기서 self.name을 클로저가 캡처하면서 self가 강하게 참조된다. 따라서 person = nil을 실행해도 메모리가 해제되지 않는다.

3.2 [weak self][unowned self]를 활용한 해결

이 문제를 해결하려면 weak 또는 unowned를 사용하여 약한 참조(Weak Reference)로 캡처해야 한다.

class Person {
    var name: String
    var greeting: (() -> Void)?

    init(name: String) {
        self.name = name
    }

    func setGreeting() {
        greeting = { [weak self] in
            guard let self = self else { return }
            print("Hello, my name is \(self.name)")
        }
    }
}

var person: Person? = Person(name: "Alice")
person?.setGreeting()
person = nil // 메모리 정상 해제

[weak self] vs [unowned self]

  • [weak self] : 선택적(optional) 참조를 사용하여, 인스턴스가 해제되면 nil이 됨.
  • [unowned self] : 비선택적(non-optional) 참조를 사용하며, 인스턴스가 해제되었는데 접근하면 런타임 오류 발생.

일반적으로 [weak self]가 더 안전한 선택이다.

4. 클로저의 활용

4.1 비동기 처리

클로저는 네트워크 요청, 애니메이션, GCD(Grand Central Dispatch)에서 비동기 처리를 수행하는 데 자주 사용된다.

func fetchData(completion: @escaping (String) -> Void) {
    DispatchQueue.global().async {
        sleep(2) // 네트워크 요청 시뮬레이션
        let data = "데이터 로딩 완료"
        
        DispatchQueue.main.async {
            completion(data)
        }
    }
}

fetchData { result in
    print(result) // "데이터 로딩 완료"
}

4.2 SwiftUI에서의 활용

SwiftUI에서는 뷰 업데이트를 위한 핸들러로 클로저를 자주 활용한다.

struct ContentView: View {
    @State private var text = "Hello"
    
    var body: some View {
        VStack {
            Text(text)
            Button("변경") {
                text = "클로저 활용!"
            }
        }
    }
}

결론

Swift의 클로저는 코드 블록을 변수처럼 다룰 수 있도록 해 주며, 함수형 프로그래밍을 지원하는 강력한 기능이다. 하지만 클로저의 값 캡처로 인해 메모리 누수가 발생할 수 있으므로 [weak self][unowned self]를 적절히 사용해야 한다.

profile
iOS 좋아. swift 좋아.

0개의 댓글

관련 채용 정보