[Swift] Closure


Closure


클로저

클로저와 함수는 기능은 완전히 동일한데, 형태만 다르다고 생각하면 됨
(파이썬의 lambda / C언어의 Block)

  • 함수 : 이름이 있는 코드 묶음 -> 다른 코드가 함수 이름으로 호출하기 위함
    func myFunction() -> Int {
      // function
      return ...
    }
    
  • 클로저 : 이름이 없는 코드 묶음 -> 굳이 이름이 없어도 호출할 수 있는 형태로 사용 가능
    {() -> Int in
      // closer
      return ...
    }
    // 보통 컴파일러의 타입 추론이 가능한 경우 리턴형에 대한 표기를 생략하여 많이 사용한다.
    

스위프트는 함수를 일급 객체로 취급 (함수는 타입의 형태)

  • 함수를 변수에 할당할 수 있음
  • 함수를 호출할 때, 함수를 파라미터로 전달할 수 있음
  • 함수에서 함수를 반환할 수 있음

클로저의 사용

클로저는 함수를 실행할 때 전달하는 형태로 사용하기 때문에 이름이 필요 없음

클로저를 사용하는 이유
함수를 실행할 때 파라미터로 클로저 형태를 전달할 수 있음

  • 본래 정의 된 함수를 실행시키면서, 클로저를 사후적으로 정의가 가능하므로 활용도가 늘어남
  • 함수를 실행할 때 파라미터로 전달하는 함수를 콜백(Callback)함수라고 부름
func closureParamFunction(closure: () -> ()) {
    print("프린트 시작")
    closure()
}

func printSwiftFunction() {  // 함수를 정의
    print("프린트 종료")
}

// 클로저가 없이 파라미터로 함수를 넣을 떄
closureParamFunction(closure: printSwiftFunction)

// 함수를 실행할 때 파라미터로 클로저 형태를 전달
closureParamFunction(closure: { () -> () in
    print("프린트 종료")
})

클로저 문법 간소화

  • 문맥상에서 파라미터와 리턴밸류 타입 추론(Type Inference)
  • 싱글 익스프레션인 경우(한 줄 코드), 리턴을 안 적어도 됨(Implicit Return)
  • 아규먼트 이름을 축약(Shorthand Argements) -> $0, $1
  • 트레일링 클로저(후행 클로저) 문법: 함수의 마지막 전달 인자(아규먼트)로 클로저 전달되는 경우, 소괄호를 생략 가능
// 함수의 정의
func performClosure(param: (String) -> Int) {
    param("Swift")
}

// 1) 타입 추론(Type Inference)
performClosure(param: { (str: String) in
    return str.count
})

performClosure(param: { str in
    return str.count
})

// 2) 싱글 익스프레션인 경우(코드가 한줄인 경우), 리턴을 안 적어도 됨(Implicit Return)
performClosure(param: { str in
    str.count
})

// 3) 아규먼트 이름을 축약(Shorthand Argements)
performClosure(param: {
    $0.count  // 두번째 파라미터: $1 , 세번째 파라미터: $2
})

// 4) 트레일링 클로저
performClosure(param: {
    $0.count
})

performClosure() {
    $0.count
}

performClosure { $0.count }  // 한줄로 표현이 가능해짐

클로저 활용 예시

다양한 곳에서 활용 됨

// URLSession
URLSession(configuration: .default).dataTask(with: URL(string: "https://주소")!) { (data, response, error) in
    // 데이터 처리하는 코드
}
  
// Timer
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { (timer) in
    print("2초뒤에 출력하기")
}

// 앱 만드는 프레임 워크
class ViewController: UIViewController {
    // 뷰 컨트롤러
}

let vc = ViewController()

vc.dismiss(animated: true) {
    print("화면 닫기")
}

멀티플 트레일링 클로저

  • Swfit 5.3 부터 적용된 문법
// 여러개의 함수로 클로저를 파라미터로 사용할때
func multipleClosure(first: () -> (), second: () -> (), third: () -> ()) {
    first()
    second()
    third()
}

// 기존 방식에서는 마지막 클로저만 트레일링 클로저로 쓸 수 있어서 클로저의 경계에서 코드가 헷갈릴 가능성이 있었음
multipleClosure(first: {
    print("1")
}, second: {
    print("2")
}) {
    print("3")
}

// Swift 5.3 부터 적용된 멀티플 트레일링 클로저 문법을 통해 헷갈릴 가능성을 줄여 줌
multipleClosure {
    print("mutil-1")
} second: {
    print("mutil-2")
} third: {
    print("mutil-3")
}

클로저의 메모리 구조

클로저는 레퍼런스 타입 (참조 타입)

  • 주소를 메모리의 Stack 영역에 저장하고 값은 Heap 영역에 저장
  • 클로저(함수)가 실제 실행되는 건, 스택 프레임에서 동작

클로저의 캡처현상

  • 클로저를 변수에 할당하거나 클로저를 호출하는 순간, 클로저는 자신이 참조하는 외부의 변수를 지속적으로 사용해야 하기 때문에 캡쳐함 (클로저 외부 변수를 사용할 땐 캡쳐 현상을 유의하며 써야함)
// 중첩 함수로 이뤄져 있고 내부 함수 외부에 계속 사용해야하는 값이 있기 때문에 캡쳐 현상이 발생
// 클로저도 레퍼런스 타입이기 때문에 클로저(함수)를 변수에 저장하는 시점에 캡쳐
func calculateFunc() -> ((Int) -> Int) {
    var sum = 0
    func square(num: Int) -> Int {
        sum += (num * num)
        return sum
    }
    return square
}

// 변수에 저장하는 경우 (Heap 메모리에 유지)
var squareFunc = calculateFunc()

squareFunc(10)  // 100
squareFunc(20)  // 500
squareFunc(30)  // 1400  -> Heap 메모리가 유지되므로 결과가 중첩됨

// 변수에 저장하지 않는 경우 (Heap메모리에 유지하지 않음)
calculateFunc()(10)  // 100
calculateFunc()(20)  // 400
calculateFunc()(30)  // 900  -> Heap 메모리가 유지되지 않으므로 원하는 결과 출력

클로저와 관련된 애트리뷰트

@escaping 키워드

  • 원칙적으로 함수의 실행이 종료되면 파라미터로 쓰이는 클로저도 제거됨
  • @escaping 키워드는 클로저를 제거하지 않고 함수에서 탈출시킴 (함수가 종료되어도 클로저가 존재)
  • 클로저가 함수의 실행 흐름(스택 프레임)을 벗어날 수 있도록 함

클로저를 단순 실행 (non-escaping)

func performEscaping1(closure: () -> ()) {
    print("프린트 시작")
    closure()
}

performEscaping1 {
    print("프린트 종료")
}

클로저를 외부 변수에 저장 (@escaping 필요)

// --- 1) 함수의 내부에 존재하는 클로저를 외부 변수에 저장 ---
var aSavedFunction: () -> () = { print("출력") }

func performEscaping2(closure: @escaping () -> ()) {
    aSavedFunction = closure  // 클로저를 실행하는 것이 아니라  aSavedFunction 변수에 저장
}

aSavedFunction()  // 출력

// 함수가 종료되어도 클로저가 Heap 영역에 존재
performEscaping2(closure: { print("다르게 출력") })
aSavedFunction()  // 다르게 출력


// --- 2) GCD 비동기 코드 ---
func performEscaping1(closure: @escaping (String) -> ()) {
    var name = "홍길동"
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {   //1초뒤에 실행하도록 만들기
        closure(name)
    }
}

performEscaping1 { str in
    print("이름 출력하기: \(str)")
}

@autoclosure 키워드

  • 본래 함수 실행 시, 클로저 형태로 전달하지 않아도, 자동으로 클로저로 만들어 주는 키워드
    (파라미터가 없는 클로저만 사용 가능)
  • 기본적으로 autoclosure은 non-escaping 특성을 가지고 있음
  • 일반적으로 클로저 형태로 써도 되지만, 너무 번거로울 때 사용
  • 번거로움을 해결해 주지만, 실제 코드가 명확해 보이지 않을 수 있으므로 사용 지양 (애플 공식 문서)
    (잘 사용하지 않음, 코드를 읽기 위한 문법 정도로 생각)
func someFuction(closure: @autoclosure () -> Bool) {
    if closure() {
        print("참입니다.")
    } else {
        print("거짓입니다.")
    }
}

var num = 1

someFuction(closure: num == 1)  // 중괄호가 없는 형태이지만 클로저로 자동 인식

참고

Ellen Swift Class






© 2021. by hminkim