[Swift] (기초) Closures / 클로저

2021. 8. 24. 21:23🍏/Swift 기초 공부

1. 클로저(Closure)란?

클로저는 이름이 없는 코드 블록으로, 함수처럼 동작하며 코드에서 값을 캡처하고 저장할 수 있습니다.

  • Swift 클로저의 세 가지 주요 유형:
    1. 전역 함수(Global Function): 이름이 있는 코드 블록이며 값을 캡처하지 않음.
    2. 중첩 함수(Nested Function): 이름이 있는 코드 블록이며 부모 함수의 값을 캡처할 수 있음.
    3. 클로저 표현식(Closure Expression): 이름 없는 클로저이며, 주변 컨텍스트의 값을 캡처할 수 있음.

클로저의 기본 문법

{ (매개변수 목록) -> 반환형 in 실행할 코드 }

예제

let add: (Int, Int) -> Int = { (a: Int, b: Int) -> Int in return a + b } 
let result = add(3, 5) // 결과: 8

2. 클로저의 캡처(Capture) 기능

캡처(Capture)는 클로저가 정의된 외부 컨텍스트의 변수나 상수를 저장하고 유지하는 기능입니다. 클로저는 이 변수나 상수의 참조를 유지하거나 복사본을 생성하여 사용 가능한 상태로 보존합니다.

예제

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

let incrementByTwo = makeIncrementer(incrementAmount: 2)
print(incrementByTwo()) // 2
print(incrementByTwo()) // 4
  • 클로저는 total과 incrementAmount를 캡처하여 함수가 종료된 후에도 값을 유지합니다.

3. @escaping 클로저와 non-escaping 클로저의 차이점

non-escaping 클로저

  • 클로저가 함수의 실행 흐름 내에서만 호출되고, 함수가 종료되면 사라지는 클로저입니다.
  • 기본 동작: Swift는 기본적으로 클로저를 non-escaping으로 처리합니다.

@escaping 클로저

  • 클로저가 함수 실행이 끝난 뒤에도 어딘가에 저장되어 호출될 가능성이 있는 경우 사용됩니다.
  • 힙 영역에 저장되며, 참조를 유지해야 하므로 참조 타입으로 동작합니다.
  • 보통 비동기 작업에서 사용됩니다.

비교 예제

func performNonEscaping(closure: () -> Void) {
    closure() // 함수 내에서만 실행됨
}

func performEscaping(closure: @escaping () -> Void) {
    DispatchQueue.global().async {
        closure() // 함수 종료 후 호출 가능
    }
}

주의사항

@escaping 클로저는 함수의 외부에서 참조되므로, 캡처 리스트를 사용하여 강한 참조 순환을 방지해야 할 수 있습니다.


4. 트레일링 클로저(Trailing Closure)

트레일링 클로저는 함수의 마지막 매개변수가 클로저일 경우, 클로저를 함수 호출 괄호 외부로 작성할 수 있는 문법입니다.

문법

함수이름(매개변수) { 클로저 코드 }

예제

func fetchData(completion: (String) -> Void) {
    completion("Data fetched successfully")
}

// 일반 클로저 표현
fetchData(completion: { message in
    print(message)
})

// 트레일링 클로저 표현
fetchData { message in
    print(message)
}

유용한 경우

  1. 클로저가 길어질 때 코드 가독성을 높일 수 있음.
  2. SwiftUI, Combine 등 클로저 기반 라이브러리에서 자주 사용.

다중 매개변수에서 트레일링 클로저

func performTask(priority: Int, action: () -> Void) {
    print("Priority: \(priority)")
    action()
}

performTask(priority: 5) {
    print("Performing task")
}

요약

  1. 클로저란: 함수명 없는 코드 블록으로, {} 안에 코드 작성.
  2. 캡처 기능: 외부 변수나 상수의 값을 저장하고 참조 유지.
  3. @escaping vs non-escaping:
    • @escaping: 함수 종료 후에도 클로저가 참조를 유지.
    • non-escaping: 함수 내에서만 실행.
  4. 트레일링 클로저: 클로저를 함수 호출 괄호 밖으로 빼서 가독성을 높이는 문법.

 

더보기

클로저(Closure) - 스위프트는 클로저 제공할 뿐만 아니라 함수를 1급 객체로 간주, 인자값으로 함수 자체를 전달하는 기능을 제공.

스위프트에서 클로저는 일회용 함수를 작성할 수 있는 구문.
일회용 함수란 한 번만 사용 할 구문들의 집합이면서, 그 형식은 함수로 작성되어야 하는 제약조건이 있을 때 만들어 사용할 수 있는 함수.
일회성을 가지므로 함수의 이름을 작성할 필요 없이 생락된다는 점에서 익명(Anonymous) 함수라고 부르기도 한다.

 클로저는 자신이 정의되었던 문맥(Context)으로부터 모든 상수와 변수의 값을 캡처하거나 레퍼런스를 저장하는 객체.
스위프트에서 클로저라고 부르는 객체는 대부분 다음 세 가지 경우 중 하나에 해당한다.

1. 전역 함수 - 이름이 있으며, 주변 환경에서 캡처할 어떤 값도 없는 클로저
2. 중첩 함수 - 이름이 있으며 자신을 둘러싼 함수로부터 값을 캡처할 수 있는 클로저
3. 클로저 표현식 - 이름이 없으며 주변 환경으로부터 값을 캡처할 수 있는 경량 문법으로 작성된 클로저

클로저 표현식

{ (매개변수) -> 반환 타입 in
	실행할 구문
}

let c = { () -> Void in
	print("Closure")
}
f() // "Closure"

== 

({ () -> Void in
	print("Closure")
})()

 

let c = {(s1: Int, s2: String) -> Void in
	print("s1:\(s1), s2:\(s2)")
}
c(1, "Closure")

==

({(s1: Int, s2: String) -> Void in
	print("s1:\(s1), s2:\(s2)")
})(1, "Closure")

 

- 클로저 표현식과 경량 문법

var value = [3,2,4,5,1]
func o(s1: Int, s2: Int) -> Bool {
	if s1 > s2 {
		return true
	} else {
		return false
	}
}
value.sort(by: o)
print(value) // [5, 4, 3, 2, 1]
// 함수
==

value.sort(by: {
	(s1: Int, s2: Int) -> Bool in
	if s1 > s2 {
		return true
	} else {
		return false
	}
})
// 클로저 표현
==

value.sort(by: {
	(s1: Int, s2: Int) -> Bool in
	return s1 > s2
})
// 상단 실행구문을 간단하게 요약 가능.
==

value.sort(by: {(s1: Int, s2: Int) in return s1 > s2})
// 한줄 표현 가능
// 클로저 표현식은 반환값의 타입 생략 가능.
// 반환 타입을 생략하면 컴파일러는 클로저 표현식의 구문을 해석하여 반환값을 찾고
// 이 값의 타입을 추론하여 클로저의 반환 타입을 정의합니다.
// '>' 비교구문 이므로 반환값을 Bool이라는 것을 컴파일러가 추론.
==

value.sort(by: {s1, s2 in return s1 > s2})
// 매개변수 타입 생략 가능.
// 이 매개변수의 타입 역시 컴파일러가 실제로 대입되는 값을 기반으로 추론.
==

value.sort(by: {return $0 > $1})
// 매개변수 자체 생략 가능.
// 매개변수가 생략되면 $0, $1, $2 같은 이름으로 할당된 내부 상수를 이용 가능.
// 이 값은 입력받은 인자값 순서대로 매칭.
==

value.sort(by: {$0 > $1})
// Bool값을 반환할 것을 컴파일러가 알고 있으며(sort 메소드의 인자값 타입을 통해서), 비교 연산자의 결과가
// if 구문과 같은 조건에서 사용되지 않은 점 역시 컴파일러가 반환 타입을 추론할 수 있기 때문에
// return 구문까지 생략.
==

value.sort(by:>)
// 연산자 함수(Operator Function)
// 연산자만을 사용하여 의미하는 바를 정확히 나타낼 수 있을때 사용.

 

 

 

더보기
// Swift - 클로저 기본
//
// 1. 클로저
// 클로저는 실행가능한 코드 블럭입니다.
// 함수와 다르게 이름정의는 필요하지는 않지만, 매개변수 전달과 반환 값이 존재 할 수 있다는 점이 동일합니다.
// 함수는 이름이 있는 클로저입니다.
// 일급객체로 전달인자, 변수, 상수 등에 저장 및 전달이 가능합니다.
//
// 2. 기본 클로저 문법
// 클로저는 중괄호 { }로 감싸져있습니다.
// 괄호를 이용해 파라미터를 정의합니다.
// -> 을 이용해 반환 타입을 명시합니다.
// "in" 키워드를 이용해 실행 코드와 분리합니다.
//
//{ (매개변수 목록) -> 반환타입 in
//    실행 코드
//}


// sum이라는 상수에 클로저를 할당
let sum: (Int, Int) -> Int = { (a: Int, b: Int) in
    return a + b
}

let sumResult: Int = sum(1, 2)
print(sumResult) // 3


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

let substract: (Int, Int) -> Int
substract = { (a: Int, b: Int) in
    return a - b
}

let divide: (Int, Int) -> Int
divide = { (a: Int, b: Int) in
    return a / b
}

func calculate(a: Int, b: Int, method: (Int, Int) -> Int) -> Int {
    return method(a, b)
}

var calculated: Int

calculated = calculate(a: 50, b: 10, method: add)

calculated = calculate(a: 30, b: 20, method: add)

print(calculated) // 60

calculated = calculate(a: 50, b: 10, method: substract)

print(calculated) // 40

calculated = calculate(a: 50, b: 10, method: divide)

print(calculated) // 5

//따로 클로저를 상수/변수에 넣어 전달하지 않고,
//함수를 호출할 때 클로저를 작성하여 전달할 수도 있습니다.

calculated = calculate(a: 50, b: 10, method: { (left: Int, right: Int) -> Int in
    return left * right
})

print(calculated) // 500
//:: 클로저 고급  - 다양한 클로저 표현 ::
//
//클로저는 아래 규칙을 통해 다양한 모습으로 표현될 수 있습니다.
//후행 클로저 : 함수의 매개변수 마지막으로 전달되는 클로저는 후행클로저(trailing closure)로 함수 밖에 구현할 수 있습니다.
//반환타입 생략 : 컴파일러가 클로저의 타입을 유추할 수 있는 경우 매개변수, 반환 타입을 생략할 수 있습니다.
//단축 인자 이름 : 전달인자의 이름이 굳이 필요없고, 컴파일러가 타입을 유추할 수 있는 경우 축약된 전달인자 이름($0, $1, $2...)을 사용 할 수 있습니다.
//암시적 반환 표현 : 반환 값이 있는 경우, 암시적으로 클로저의 맨 마지막 줄은 return 키워드를 생략하더라도 반환 값으로 취급합니다.

// 클로저를 매개변수로 갖는 함수 calculated(a:b:method:)와 결과값을 저장할 변수 result 선언
func calculate(a: Int, b: Int, method: (Int, Int) -> Int) -> Int {
    return method(a, b)
}

var result: Int

//1. 후행 클로저
//클로저가 함수의 마지막 전달인자일때, 마지막 매개변수 이름을 생략한 후 함수 소괄호 외부에 클로저를 구현할 수 있습니다.

result = calculate(a: 10, b: 10) { (left: Int, right: Int) -> Int in
    return left + right
}

print(result) // 20

//2. 반환타입 생략
//calculate(a:b:method:) 함수의 method 매개변수는 Int 타입을 반환할 것이라는 사실을 컴파일러도 알기 때문에 굳이 클로저에서 반환타입을 명시해 주지 않아도 됩니다. 대신 in 키워드는 생략할 수 없습니다
result = calculate(a: 10, b: 10, method: { (left: Int, right: Int) in
    return left + right
})

print(result) // 20

// 후행클로저와 함께 사용할 수도 있습니다
result = calculate(a: 10, b: 10) { (left: Int, right: Int) in
    return left + right
}

print(result) // 20

//3. 단축 인자이름
//클로저의 매개변수 이름이 굳이 불필요하다면 단축 인자이름을 활용할 수 있습니다. 단축 인자이름은 클로저의 매개변수의 순서대로 $0, $1, $2... 처럼 표현합니다.
result = calculate(a: 10, b: 10, method: {
    return $0 + $1
})

print(result) // 20


// 당연히 후행 클로저와 함께 사용할 수 있습니다
result = calculate(a: 10, b: 10) {
    return $0 + $1
}

print(result) // 20

//4. 암시적 반환 표현
//클로저가 반환하는 값이 있다면 클로저의 마지막 줄의 결과값은 암시적으로 반환값으로 취급합니다.
result = calculate(a: 10, b: 10) {
    $0 + $1
}

print(result) // 20

// 간결하게 한 줄로 표현해 줄 수도 있습니다
result = calculate(a: 10, b: 10) { $0 + $1 }

print(result) // 20

//**축약 전과 후 비교**/
//축약 전
result = calculate(a: 10, b: 10, method: { (left: Int, right: Int) -> Int in
    return left + right
})

//축약 후
result = calculate(a: 10, b: 10) { $0 + $1 }

print(result) // 20