[6편] 클로저 — 이름 없는 함수 블록, @escaping, 캡처 리스트

🤖 이 글은 Claude Code(AI)가 작성합니다. | 시리즈 목차 | 이전: 5편

5편에서 함수를 변수에 담을 수 있다는 것을 배웠습니다. 클로저(closure)는 그것의 확장입니다. 이름을 붙이지 않고, 그 자리에서 바로 정의해서 넘길 수 있는 함수 블록입니다.

Swift에서 map, filter, sort 같은 메서드를 쓸 때, 비동기 작업의 완료 콜백을 넘길 때, 버튼의 동작을 정의할 때 — 모두 클로저를 씁니다. 클로저를 이해하지 않으면 Swift 코드의 절반을 읽을 수 없습니다.


클로저 기본 문법

클로저는 { } 안에 파라미터, 반환 타입, 본문을 모두 씁니다.

{ (파라미터) -> 반환타입 in
    본문
}

5편에서 함수로 만들었던 덧셈을 클로저로 쓰면 이렇습니다.

// 함수 방식
func add(_ a: Int, _ b: Int) -> Int { a + b }

// 클로저 방식
let add = { (a: Int, b: Int) -> Int in
    return a + b
}

add(3, 4)  // 7

타입은 컨텍스트에서 추론될 때 생략할 수 있고, 표현식이 하나면 return도 생략됩니다.

let add: (Int, Int) -> Int = { a, b in a + b }

후행 클로저와 축약 문법

클로저를 함수의 마지막 인자로 넘길 때는 괄호 밖으로 꺼내 쓸 수 있습니다. 이것을 후행 클로저(trailing closure)라고 합니다.

let numbers = [3, 1, 4, 1, 5, 9]

// 일반 방식
let sorted = numbers.sorted(by: { (a: Int, b: Int) -> Bool in
    return a < b
})

// 후행 클로저
let sorted = numbers.sorted { (a: Int, b: Int) -> Bool in
    return a < b
}

// 타입 추론으로 생략
let sorted = numbers.sorted { a, b in a < b }

// $0, $1 축약 인자
let sorted = numbers.sorted { $0 < $1 }

// 연산자 함수로 더 짧게
let sorted = numbers.sorted(by: <)

모두 완전히 동일한 코드입니다. 상황에 따라 적절한 수준의 축약을 씁니다. 너무 줄이면 읽기 어려워지므로, 한 줄에 다 들어오고 의미가 명확할 때만 $0, $1 축약을 씁니다.

3편에서 봤던 map, filter도 클로저를 받는 함수입니다.

let numbers = [1, 2, 3, 4, 5]

let doubled = numbers.map { $0 * 2 }       // [2, 4, 6, 8, 10]
let evens   = numbers.filter { $0 % 2 == 0 } // [2, 4]
let total   = numbers.reduce(0) { $0 + $1 }  // 15

값 캡처

클로저는 자신이 정의된 스코프의 변수를 캡처(capture)합니다. 함수가 끝난 뒤에도 그 변수를 계속 참조할 수 있습니다.

func makeCounter() -> () -> Int {
    var count = 0
    let increment = {
        count += 1
        return count
    }
    return increment
}

let counter = makeCounter()
print(counter())  // 1
print(counter())  // 2
print(counter())  // 3

makeCounter()가 반환되고 나서도 클로저 incrementcount를 살아있게 유지합니다. 클로저가 count를 캡처했기 때문입니다. Python의 클로저나 JavaScript의 클로저와 동일한 개념입니다.


@escaping — 클로저가 함수보다 오래 사는 경우

클로저를 함수 인자로 받을 때, 그 클로저가 함수 실행이 끝난 뒤에도 살아남아야 한다면 @escaping을 표시합니다.

var completionHandlers: [() -> Void] = []

func register(handler: @escaping () -> Void) {
    completionHandlers.append(handler)  // 함수 종료 후에도 handler가 살아남음
}

가장 흔한 사례는 비동기 작업의 완료 콜백입니다.

func fetchData(completion: @escaping (String) -> Void) {
    DispatchQueue.global().async {
        // 백그라운드에서 작업
        let result = "데이터"
        DispatchQueue.main.async {
            completion(result)  // 함수 반환 이후에 호출됨
        }
    }
}

@escaping이 없는 클로저(non-escaping)는 함수 내에서만 실행되고 사라집니다. Swift가 기본을 non-escaping으로 둔 이유는 성능 최적화와 메모리 안전성 때문입니다. 탈출하지 않는다는 보장이 있으면 컴파일러가 더 적극적으로 최적화할 수 있습니다.


캡처 리스트와 [weak self]

클로저가 클래스 인스턴스를 캡처하면 강한 참조(strong reference)가 생깁니다. 그 인스턴스가 클로저를 소유하고, 클로저가 다시 그 인스턴스를 참조하면 순환 참조(retain cycle)가 생겨 메모리 누수가 발생합니다.

class NetworkManager {
    var onComplete: (() -> Void)?

    func start() {
        onComplete = {
            print(self.description)  // self를 강하게 캡처
            // NetworkManager → onComplete → 클로저 → NetworkManager 순환
        }
    }
}

이를 막기 위해 캡처 리스트로 약한 참조를 씁니다.

class NetworkManager {
    var onComplete: (() -> Void)?

    func start() {
        onComplete = { [weak self] in
            guard let self = self else { return }
            print(self.description)
        }
    }
}

[weak self]는 "self를 약하게 캡처한다"는 선언입니다. 약한 참조는 인스턴스가 해제되면 자동으로 nil이 됩니다. 그래서 guard let self = self로 먼저 nil 여부를 확인합니다.

언제 [weak self]가 필요한가:

  • 클로저가 @escaping이고
  • 클로저가 self를 참조하고
  • self가 그 클로저를 (직접 또는 간접으로) 소유하고 있을 때

세 조건이 모두 해당되면 순환 참조 가능성이 있으니 [weak self]를 씁니다. 메모리 관리는 12편에서 ARC와 함께 더 자세히 다룹니다.


다른 언어와 비교

Python JavaScript Swift
익명 함수 lambda x: x*2 x => x*2 { x in x*2 } 또는 { $0*2 }
값 캡처 있음 있음 있음
캡처 방식 제어 없음 없음 캡처 리스트 [weak self]
탈출 여부 명시 없음 없음 @escaping
후행 클로저 문법 없음 없음 있음

핵심 요약

  • 클로저는 이름 없는 함수 블록이다. { 파라미터 in 본문 } 형태로 쓴다.
  • 함수의 마지막 인자가 클로저면 괄호 밖으로 꺼내는 후행 클로저 문법을 쓸 수 있다.
  • $0, $1은 클로저의 첫 번째, 두 번째 인자를 가리키는 축약 표현이다.
  • 클로저는 자신이 정의된 스코프의 변수를 캡처한다. 함수가 끝난 뒤에도 그 변수를 유지한다.
  • @escaping은 클로저가 함수 반환 이후에도 살아남는다는 표시다. 비동기 콜백에 많이 쓰인다.
  • 순환 참조를 막으려면 [weak self] 캡처 리스트를 쓴다.

다음 편은 7편 — 옵셔널: nil을 타입 시스템으로 다루기입니다. Swift에서 가장 낯선 개념이지만, 이것을 이해하면 Swift가 왜 "안전한 언어"인지 실감할 수 있습니다.

🤖 Generated with Claude Code

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 항목은 *(으)로 표시합니다