본문 바로가기
스파르타코딩 클럽/기초

Swift | 클로저(Closure) 쉽게 이해하기?

by UDDT 2025. 4. 5.

클로저(Closure)

   Swift를 공부하면서 만나는 첫번째 보스는 클로저다
  개발할 때 많이 사용된다고 하기도 하고, 코드를 작성하면서 자주 보기도 한다.

Lv. 70 [클로저]

 
  대략 이런 느낌.....
 
  클로저를 쉽게 이해할 수 있는 방법이 있을까?
  사실 그런 방법은 없지만, 최대한 디테일하게 정리하면서 공부해보고자 한다.
 

 클로저가 뭔데?

클로저는 명명된 함수(이름을 가지고 있는 함수) 생성 없이 실행되는 코드 그룹이다.
'클로저 = 이름이 없는 함수'처럼 생각하라고 공식처럼 되어 있어서
함수의 하위 개념이 클로저인가 하고 생각할 수 있는데, 오히려 함수가 이름이 있는 클로저다다.

 
    어렵게 쓰여있지만,  "123"이 String Type인 것처럼,
  클로저도 Type이라 생각하면 된다. () -> () Type이다.
   * 물론 파라미터가 없고 리턴이 없을 때만 () -> ()

 클로저 기본 형태

// 함수의 형태
func 함수 이름(매개변수) -> 반환타입 {

}

// 클로저의 형태
{ (매개변수) -> 반환타입 in
  실행할 코드
}

  
    앞서 봤던 클로저의 기본 설명처럼,
   클로저는 func 키워드와 함수 이름이 없고, 
   반환 타입에 in 키워드와 실행할 코드를 가지고 있는 형태로 되어 있다.
 
   숫자를 입력하면 String Type으로 바꿔주는 함수가 있다.
 
    이 함수를 클로저로 바꿔주려면
  func 키워드와 함수 이름을 지운다음 남은 부분을 { } 안에 넣어주면 된다.

// 함수
func changeTypeString(number: Int) -> String {
	return "\(number)"
}

// 클로저
{ (number: Int) -> String in
	return "\(number)"
}

 

 클로저 캡처

   클로저는 값을 캡처할 수 있다.

  클로저는 주변 변수를 기억할 수 있고, 나중에 클로저가 실행될 때에도 그 값을 계속 사용할 수 있다.

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

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

    

    클로저로 리턴을 할 때 해당 값을 캡처해서 담아두면,

  함수를 실행할 때마다 count의 값을 기억하기 때문에, 지속적으로 값을 증가시킬 수 있음 (단 var로 선언했을 때만)

 

 클로저의 특징1 : 할당이 가능하다

   1-1. 변수에 할당하기
   이렇게 만들어진 클로저는 변수에 할당이 가능하다.

// 클로저
var typeChangeString = { (number: Int) -> String in
	return "\(number)"
}

 
    클로저의 호출은 함수 실행과 동일하게 할 수 있다.

// 함수
func changeTypeString(number: Int) -> String {
	return "\(number)"
}

changeTypeString(number: 10)

// 클로저
var typeChangeString = { (number: Int) -> String in
	return "\(number)"
}

typeChangeString(10)

 
    또한 변수에 클로저를 담으면 타입이 동일한 클로저로 값을 변경할 수 있다.

// 타입이 동일하면 변경이 가능하다 : 클로저 (Int) -> (String)
var typeChangeString = { (number: Int) -> String in
    return "\(number)"
}

typeChangeString = { (studentNumber: Int) -> String in
    return "\(studentNumber)"
}

// 타입이 같다면 함수를 클로저에 대입할 수도 있음
typeChangeString = changeTypeString

/*
typeChangeString의 Type은 (Int) -> String 이며,
changeTypeString의 Type 또한 (Int) -> String으로 
Type이 같이 때문에 대입할 수 있음
*/

 
   1-2. 매개변수에 할당하기
    함수의 매개변수에 클로저를 담을 수 있다.
    * completion Handler로 주로 사용(코드 실행이 완료되고 나서 무언가를 할 때)

// 함수의 매개변수로 함수를 받을 수 있음
func changeTypeString(change: () -> Void) {
    change()
}

// 실행할 함수
func change() {
    print("타입이 변경되었습니다.")
}

// 함수 호출
changeTypeString {
    change()
}

// 클로저를 변수에 저장
var typeChangeString = {
    print("타입이 변경되었습니다.")
}

// 함수의 매개변수로 클로저를 받을 수 있음(보통 이렇게 사용하지는 않고, 다음에 오는 코드처럼 사용)
changeTypeString (change: typeChangeString)

// 보통은 이렇게 사용한다 (Trailing Closure)
changeTypeString {
    typeChangeString()
}

// 함수의 매개변수로 클로저를 받으면 그때그때 코드를 다르게 실행할 수도 있음
func calculate(number1: Int, number2: Int, operation: (Int, Int) -> Void) {
    operation(number1, number2)
}

// 예를 들면, 이때는 더하기
calculate(number1: 3, number2: 5) { number1, number2 in
    print(number1 + number2)
}

// 이때는 빼기
calculate(number1: 6, number2: 10) { number1, number2 in
    print(number1 - number2)
}

// 클로저는 별도로 실행되는 것으로 클로저의 매개변수에 꼭 함수의 파라미터를 써줘야하는 것은 아님
calculate(number1: 6, number2: 10) { num1, num2 in
    print(num1 * num2)
}

 
  따라서 함수를 실행할 때 파라미터로 클로저를 받을 수 있는 특성을 이용해서,
 비동기 작업을 할 때 클로저를 자주 사용한다.

// 함수 실행 2초 후 코드 블럭에 있는 코드를 실행하기
        DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
            let navigationController = UINavigationController(rootViewController: rootVC)

            self.window?.rootViewController = navigationController
        }

 
    * completion Handler

// Completion Handler의 예시
func setupView() {
    let vc = UIViewController()
    vc.dismiss(animated: true, completion: (() -> Void)?)
}

// 예시 코드
func setUpView() {
    let vc = UIViewController()
    // 뷰가 사라지고나서 text를 변경하는 클로저를 실행할 수 있음
    vc.dismiss(animated: true) {
        changeText()
    }
}

// 텍스트를 변경해주는 클로저
var changeText = { 
	label.text = "텍스트가 바뀌었습니다."
}

 
   1-3. Return Type에 할당하기

// 클로저를 리턴하는 함수
func addClosure(first: Int) -> ((Int) -> Void) {
    return { second in
        let result = first + second
        print("result: \(result)")
    }
}

let closure = addClosure(first: 8)
closure(5)

 

 클로저의 특징2 : 축약이 가능하다

   - Swift Type 추론을 이용한 생략

// Int에서의 Type 추론
var number: Int = 123    // 이 코드는 아래와 동일한 코드
var number = 123         // 여기서 Int type을 명시해주지 않아도 Swift는 Type을 추론함

// Closure에서의 Type 추론 : Return Type을 명시해주지 않아도 Return하는 값이 있으면 Type 추론이 가능
var typeChangeString = { (number: Int) in
    return "\(number)"
}

 
   - 암시적 반환 : return 키워드 생략(return이 코드 블럭 내 유일한 코드일 때)

//함수에서의 생략
func changeTypeString(number: Int) -> String {
	return "\(number)"     // 여기서 return을 쓰지 않아도, return이 가능함
}

func changeTypeString(number: Int) -> String {
	"\(number)"     // 이렇게 하면 위와 동일한 코드
}

// 클로저에서의 생략
var typeChangeString = { (number: Int) in
	"\(number)"    // 마찬가지로 클로저에서도 return을 생략할 수 있음
}

// 위의 코드를 정리하여 아래의 코드처럼 작성 가능(다만, 이렇게 사용하지는 않음)
var typeChangeString = { (number: Int) in "\(number)" }

 

 클로저의 특징3 : 축약 인자를 가지고 있다

    Swift는 인라인 클로저에 $0, $1 등으로 대체하여 사용할 수 있는 축약 인자(Shorthand Argument Names를 제공한다.
  짧은 인수 이름의 수와 타입은 함수 타입에서 유추되며, 클로저 표현식 자체가 본문으로 구성되기 때문에 in 키워드의 생략이 가능하다

// 완전한 축약 형태
var typeChangeString = { "\($0)" }

// 파라미터에 해당하는 축약 인자가 있음
func calculate(number1: Int, number2: Int, add: (Int, Int) -> Void) {
    add(number1, number2)
}

calculate(number1: 3, number2: 5) {
    print($0 + $1)
}

 
  그러면 이러한 클로저의 특징은 어디에서 나온걸까?
클로저는 일급 시민 객체이기 때문에 위의 특징을 가지고 있다.
 

 일급 시민 객체(First-Class-Citizen)

    - 변수나 상수에 할당이 가능하다(Type이 있기 때문에)
       * 클로저나 함수도 () -> () 형태의 Type

// string이 Type이기 때문에 변수에 담을 수 있음
var stringNumber = "123"

// 마찬가지로 int도 Type이기 때문에 담을 수 있음
var number = 123

// 함수도 () -> () Type이므로 담을 수 있고,
var myFunction = printMyName

func printMyName() {
    print("당신의 이름은 홍길동입니다.")
}

myFunction()

// closure도 () -> () Type이기 때문에 담을 수 있음
var yourFunction = {
    print("당신의 이름은 고길동입니다")
}

 
   그렇다면 이런 형식도 가능하지 않을까?
   myFunction = yourFunction
  (이 부분은, 간단하게 클로저를 공부하려고 하는 사람은 아래의 방법으로 해결하고 넘어가면 된다.)

// 변수에 함수를 할당 : myFunction의 Type은 () -> ()
var myFunction = printMyName

func printMyName() {
    print("당신의 이름은 홍길동입니다.")
}

// 클로저의 타입 : yourFunction의 Type도 () -> ()
var yourFunction = {
    print("당신의 이름은 고길동입니다")
}

// myFunction에 클로저도 할당할 수 있겠다?
myFunction = yourFuntion    // 에러 발생

 
   myFunction = yourFunction을 하면 하단의 에러가 발생하고,

Converting non-sendable function value to '@Sendable () -> ()' may introduce data races

 
  이를 해결하는 방법은 myFunction의 타입을 명시적으로 () -> ()으로 선언해주면 된다.

// myFunction의 type을 명시적으로 () -> ()으로 선언
var myFunction: () -> () = printMyName

func printMyName() {
    print("당신의 이름은 홍길동입니다.")
}

// 클로저의 타입 : yourFunction의 Type도 () -> ()
var yourFunction = {
    print("당신의 이름은 고길동입니다")
}

// myFunction에 클로저 할당 가능
myFunction = yourFuntion    // 에러 해결

 
  * 위의 오류가 더 궁금하다면, 포스팅 맨 하단의 링크를 참고하면 된다.
 
    - 함수의 매개변수로 사용이 가능하다

// 함수의 매개변수로 String을 받을 수 있음
func changeTypeInt(strNumber: String) {
	print(strNumber)
}

// 위와 마찬가지로 함수의 매개변수로 함수도 받을 수 있음
func changeTypeString(change: () -> Void) {
    change()
}


// 실행할 함수
func change() {
    print("타입이 변경되었습니다.")
}

// 함수 호출
changeTypeString {
    change()
}

// 위와 마찬가지로 함수의 매개변수로 클로저도 받을 수 있는 것
func calculate(number1: Int, number2: Int, operation: (Int, Int) -> Void) {
    operation(number1, number2)
}

 
    - 다른 함수의 반환 값으로 사용이 가능하다

// 함수의 매개변수로 Int를 받고 Int를 리턴할 수 있음
func getNumber(number: Int) -> Int {
    return number
}

// 함수의 매개변수로 함수를 리턴할 수 있음
func makeGreeter() -> () -> Void {
    func greeter() {
        print("안녕하세요! 반갑습니다")
    }
    return greeter
}


let greetingFunction = makeGreeter() // 여기서 함수를 리턴 받음
greetingFunction()
// 안녕하세요 반갑습니다 출력


// 마찬가지로 함수의 매개변수로 클로저도 리턴할 수 있음
func addClosure(first: Int) -> ((Int) -> Void) {
    return { second in
        let result = first + second
        print("result: \(result)")
    }
}

let closure = addClosure(first: 8)
closure(5)
// result: 13

 

 Swift Docs 클로저

  [Swift Docs 클로저 - 영문 ver]
  https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures

 

Documentation

docs.swift.org

 
 
  [Swift Docs 클로저 - 한글 ver]
https://bbiguduk.gitbook.io/swift/language-guide-1/closures

 

클로저 (Closures) | Swift

명명된 함수 생성없이 실행되는 코드 그룹입니다. 클로저 (Closures) 는 코드에서 주변에 전달과 사용할 수 있는 자체 포함된 기능 블럭입니다. Swift의 클로저는 다른 프로그래밍 언어에서 클로저,

bbiguduk.gitbook.io

 

 오류 알아보기

Converting non-sendable function value to '@Sendable () -> ()' may introduce data races

    위 오류가 더 궁금하신 분은, 아래의 포스팅으로... (저와 같은 초보자는 누르지 마세요...)
 
    https://uddt.tistory.com/202

 

Swift | Converting non-sendable function value to '@Sendable () -> ()' may introduce data races 오류 이해하기

⎮ 주의    이 오류는 Swift5에서는 뜨지 않고, Swift6에서 뜬다.    (서치해보니, @Sendable 이라는 개념이 Swift 5.5부터 나온 것 같다)    아마 Swift 5에서는 경고로만 나오거나, 아예 나오지 않을 수

uddt.tistory.com

 
 

최근댓글

최근글

skin by © 2024 ttuttak