06.02 (월) Closure.. 난 너가 밉다..

2025. 6. 2. 23:20·🖋️ TIL Journal
-->

 

🍏 클로저(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이 필요할까?

  1. 비동기 처리 시 (async)
    • 예: DispatchQueue.main.async, 네트워크 요청 등
  2. 클로저를 함수 외부에 저장할 때
    • 예: 클로저를 배열에 저장하거나, 클래스의 속성으로 저장할 경우
  3. 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
'🖋️ TIL Journal' 카테고리의 다른 글
  • 06.05 (목) enumerated()가 제네릭에서 안 된다고..?
  • 06.04 (수) 열거형(Enum)과 compactMap, flatMap
  • 05.30 (금) 실수로 날린 커밋, reflog로 되살리기
  • 05.29 (목) 꽤나 자주 보이던 키워드들 간단 정리
MoriOS
MoriOS
기억하기 위해 기록하는 공간 🖋️
  • MoriOS
    MoriOS
    MoriOS
  • 전체
    오늘
    어제
    • 분류 전체보기 (51) N
      • 📌 Swift (10)
      • 📱 iOS (4)
      • 💡 Algorithm (1)
      • ❕Data structure (4)
      • 🪙 Python (0)
      • ⚙️ Git (2)
      • 🖋️ TIL Journal (27) N
      • 📝 Etc (3)
  • 블로그 메뉴

    • GitHub
  • 인기 글

  • 태그

    Optional
    GitHub
    mark:
    swift 연관값
    swift optional
    한 줄 주석
    swift
    swift 열거형
    convenience init
    중첩 주석
    후행클로저
    swift 원시값
    innerloop
    ios
    weak
    swift 중복 없는 랜덤 숫자
    Components
    편의 이니셜라이저
    TiL
    클로저란?
    static
    네비게이션 주석
    깃허브 복구
    문서화 주석
    Split
    구문 레이블
    주석 활용
    깃허브 로컬 복구
    enumerated()
    트레일링 클로저
  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
MoriOS
06.02 (월) Closure.. 난 너가 밉다..
상단으로

티스토리툴바