티스토리 뷰

스위프트

[Swift] - 클로저(Closure)

강철곰탱이 2024. 5. 1. 00:46

 

iOS 면접 준비를 위한 3탄이다. Swift에서 중요한 개념인 클로저에 대해 알아보자.

 

- 아래 링크 참고 

질문 리스트

1. Swift에서 클로저(Closure)란 무엇이며, 어떻게 사용하나요?
2. 클로저의 캡처 리스트(Capture List)는 어떤 역할을 하나요?
3. @escaping 클로저와 non-escaping 클로저의 차이점은 무엇인가요?
4. 트레일링 클로저(Trailing Closure) 문법은 언제 사용하면 좋나요?

1. 클로저란?

 

먼저 클로저의 개념에 대해 짚고 가보자!

 

클로저라고 하면 간단하게 익명함수를 뜻한다고 생각했었다.

하지만 자세히 알아보니 func 키워드를 이용해 이름을 붙여주는 함수들도 클로저라고 한다.

 

즉, 클로저는 두 가지 종류가 있다. => 1. Named Closure, 2. Unnamed Closure

 

아래와 같이 이름이 있는 함수는 

func find(){
	print("??")
}

 

바로 Named Closure이다. 

 

그리고, 다음과 같이 이름을 붙이지 않고 사용하는 함수를

let closure = { print("??") }

 

익명함수, 즉 Unnamed Closure이라고 한다. 

 

정리하자면 클로저는 Named Closure/Unnamed Closure 둘 다 포함하지만, 보통 Unnamed Closure를 말한다고 한다!!

 

 

1️⃣ 기본 클로저 

 

클로저는 코드의 여러 부분에서 사용할 수 있는 일정 기능을 하나의 블록으로 모아놓은 것을 말한다.

클로저는 보통 Unnamed Closure를 의미하므로 Unnamed Closure에 대해 설명하겠다.

 

func 키워드를 사용하지 않는다.

클로저는 다음과 같이 헤드와 바디로 이루어져 있고 이 둘을 구분지어주는 게 바로 in 키워드이다.

//closure head
{ (매개변수들)-> 반환타입 in 
	실행코드//closure body
}

 

  • 사용 예시
//클로저 사용 예시
var sum: (Int, Int) -> Int = { (a: Int, b: Int) -> Int in
	return a+b
}
result = sum(2,3)
print(result) //5

 

 

또한, 클로저는 1급 객체이기 때문에 변수 형태로 저장할 수 있고 함수의 파라미터로도 넘길 수 있다.

 

✏️ 1급 객체란?

1급 객체란 함수평 프로그래밍에서 사용하는 개념으로, 아래의 조건을 만족하는 객체를 말한다.

1. 변수나 메서드에 함수를 할당할 수 있다.
2. 함수 안에 함수를 매개변수로 담을 수 있다.
3. 함수가 함수를 반환할 수 있다.

 

 

1. 변수나 메서드에 함수를 할당할 수 있다.

// closure 상수에 할당
let closure = { (name: String) -> String in
	"Hi \(name)"
}

// 할당된 클로저 실행
closure("hhh")

 

 

2. 함수 안에 함수를 매개변수로 담을 수 있다.

// 상수에 클로저 할당
let closure = { (name: String) -> String in
	"Hello \(name)"
}

// 함수의 파라미터로 클로저를 전달받도록 정의
func greet(name: String, result: (String) -> String) {
	result(name)
}

// 함수 파라미터로 클로저 전달
// 리턴값 : Hello 이름
greet(name: "hhh", result: closure)

 

 

3. 함수에서 리턴이 가능하다.

// 함수의 리턴값으로 클로저 반환
func generateGreeting(greeting: String) -> (String) -> String {
    return { (name: String) -> String in
        return "\(greeting), \(name)!"
    }
}

// 리턴된 클로저를 변수에 할당
let greetWithHello = generateGreeting(greeting: "Hello")
let greetWithGoodMorning = generateGreeting(greeting: "Good morning")

// 클로저 호출
print(greetWithHello("Alice")) // 출력: Hello, Alice!
print(greetWithGoodMorning("Bob")) // 출력: Good morning, Bob!

 

2️⃣ Parameter와 Return Type 생략

 

 

parameter와 return type이 둘 다 없는 경우에는 아래와 같이 사용할 수 있다.

 

  • return type 없어도 생략 가능
  • return type 있어도 생략 가능
let closure = { () -> () in
    print("Closure")
}

 

 

3️⃣ Parameter와 Return Type 생략 x

 

 

parameter와 return type이 둘 다 있는 경우에는 아래와 같이 사용할 수 있다.

 

let closure = { (name: String) -> String in
	"Hi \(name)"
}

closure("hhh")

 

여기서 주의할 점은 함수처럼 호출할 때 "name"이라는 Argument Label을 넣으면 안 된다고 한다.

 

클로저에서는 Argument Label을 사용하지 않아, Parameter Name만을 의미!!

 

closure("hhh")
closure(name: "hhh")//오류 발생

 

 

 

언제 사용?

 

클로저를 사용하는 가장 일반적인 이유는 "기능을 저장"하는 것이다.

 

예를 들어, 다음과 같은 경우에 사용할 수 있다.

 

  1. 일정 시간 이후에 실행되어야 할 기능
  2. 애니메이션이 끝난 후 실행되어야 할 기능
  3. 다운로드가 끝난 후 실행되어야 할 기능

 


2. 클로저의 캡처 리스트

 

클로저에서 값을 캡처한다??

무슨 뜻인가 했는데 아래와 같이 정의할 수 있다.

 

클로저 안에서 외부의 변수나 상수를 캡처할 때 strong, weak, unowneded 등의 참조 강도를 명시해서 캡처해오는 방법을 말한다.

 

캡처리스트는 두 가지 타입으로 구분할 수 있다.

 

1. Value 타입

 

클로저가 생성될 시점에 값을 복사

 

  • closure가 생성될 때 값을 계속 유지하다가 closure가 실행될 때 사용된다.

 

2. Reference 타입

 

클로저가 호출되는 시점에 참조되어 사용된다.

 

  • closure가 생성될 때와 실행될 때의 값이 달라질 수 있다. 

 

=> 클로저는 기본적으로 Value/Reference 타입에 관계없이 Reference Capture를 한다.

 

 

클로저에서 값 캡처란?

 

값 캡처라는게 무슨 의미인지 알아보자.

 

"클로저란 내부함수와 내부 함수에 영향을 미치는 주변 환경을 모두 포함한 객체"

 

아래의 예제를 살펴보자.

func example() {
    var num = 10
    let closure = { print(num) }
}

// 함수 호출
example()

 

클로저 내부에서 외부 변수인 num이라는 변수를 사용하여 출력하고 있으며, num의 값을 클로저 내부적으로 저장하고 있다.

 

이것을 클로저에 의해 num의 값이 캡처되었다고 표현한다!

 

 

클로저에서 Reference Capture

 

위에서 설명했듯이 클로저는 Value/Reference 타입에 관계없이 Reference Capture를 한다.

 

func example() {
    var num: Int = 0
    print("num #1 = \(value)")
    
    let closure = {
        print("num #3 = \(value)")
    }
    
    value = 10
    print("num #2 = \(value)")
    closure()
}

// 함수 호출
example()

 

위와 같은 함수가 있을 때 호출하면 결과가 어떻게 나올까

 

정답은 다음과 같다.

num #1 = 0
num #2 = 10
num #3 = 10

 

클로저는 num이라는 외부 변수를 클로저 내부에서 사용하므로, num이라는 변수를 참조한다.

 

Referense 타입은 클로저가 호출되는 시점에 참조되어 사용된다고 하였으므로, 클로저를 실행하기 전에 num이라는 값을 외부에서 변경하면 위와 같이 변경된 num의 값이 나온다.

 

 

만약 클로저 내부에서 num의 값을 변경하면 어떻게 나올까?

 

func example() {
    var num: Int = 0
    print("num #1 = \(num)")
    
    let closure = {
        num = 10
        print("num #3 = \(num)")
    }
    
    closure()
    print("num #2 = \(num)")
}

 

클로저 외부에 있는 num의 값도 변경되어서 결과는 똑같이 나오게 된다.

 

num #1 = 0
num #2 = 10
num #3 = 10

 


3. @escaping/@non-escaping 클로저

 

🟡 @non-escaping 클로저

 

: 함수 내에서만 호출되고 함수가 종료되기 전에 클로저가 실행되는 경우

 

클로저가 함수의 범위를 벗어날 수 없으므로, 함수가 종료된 후에도 클로저를 호출할 수 없다.

대부분 클로저를 즉시 실행하거나 동기적으로 호출하는 경우에 사용한다.

 

class SyncOperation {
    // @non-escaping 클로저를 사용하는 함수 예제
    func syncOperation(closure: () -> Void) {
        // 동기 작업 수행
        closure() // 클로저 호출
    }
}

// 클래스 인스턴스 생성
let syncOp = SyncOperation()

// 함수 호출
syncOp.syncOperation {
    print("Synchronous operation completed")
}

 

✅ 작동 순서

1. SyncOperation 클래스 인스턴스 생성
2. syncOperation 함수 호출, 이때 프린트 구문(클로저)이 syncOperation의 매개변수 값으로 할당된다.
3. syncOperation 함수 내부에 전달된 클로저를 호출하면서 "Synchronous operation completed"이 출력
4. syncOperation 함수 실행 완료

 

syncOperation함수가 완전히 종료되기 전에 프린트 구문(클로저)는 반드시 실행되어야 한다.

 

 

 

🟡 @escaping 클로저

 

: 함수의 실행이 종료된 후에도 클로저가 호출될 수 있는 경우 

즉, 함수의 실행 흐름에 상관없이 실행되는 클로저

 

함수 내에서 비동기적인 작업 수행하고 이후에 클로저가 실행되는 경우에 주로 사용하며, 클로저가 함수를 벗어나서 호출되기 때문에 함수가 종료된 후에도 클로저가 유효한 상태를 유지해야 한다.

 

AsyncOperation을 다음과 같이 구현했다고 하자.

 

class AsyncOperation {
	
    var name: () -> Void = {}
    
    func asyncOperation(value: () -> Void) {//error
        name = value
        value()
    }
}

 

asyncOperation 함수의 value 클로저 값을 name 변수에 할당하려고 한다.

하지만, swift에서는 함수의 매개변수로 전달된 클로저는 해당 scope을 벗어날 수 없으므로 할당할 수 없다!

 

여기서 사용할 수 있는 것이 바로 @escaping 클로저다.

코드를 아래와 같이 고쳐보았다.

class AsyncOperation {
	
    var name: () -> Void = {}
    
    static let shared = AsyncOperation()

    func asyncOperation(value: @escaping() -> Void) {
        name = value
        value()
    }
}

AsyncOperation.shard.asyncOperation {
    print("hhh")
}

let nameCheck = AsyncOperation.shared.name
nameCheck()//"hhh" 출력

 

외부에서 asyncOperation 함수를 호출하면, name 변수에 값이 잘 할당되는 것을 확인할 수 있다!!

 

 

✅ 작동 순서

1. asyncOperation 함수 호출, 이때 프린트 구문(클로저)이 asyncOperation의 매개변수 값으로 할당
2. name 프로퍼티에 value라는 클로저를 할당하고, 클로저 내부의 코드 실행하여 "hhh" 출력
4. nameCheck에 AsyncOperation의 name 프로퍼티를 저장
5. nameCheck()로 "hhh" 출력

 

@non-escaping 클로저는 컴파일러가 클로저이 실행이 언제 종료되는지 알아 객체의 라이프 사이클을 효율적으로 관리할 수 있다.

하지만, @escaping은  함수 밖에서 실행되기 때문에 함수 밖에서도 실행되는 것을 보장해줘야 한다.

지속적인 참조 과정을 거치기 때문에 순환참조가 발생하여 메모리 누수가 발생할 수 있기 때문!!

 

즉, 성능과 최적화를 위해 필요할 때만 @escaping을 사용하는게 가장 효율적이라고 한다.

 

 


4. 후행 클로저(Trailing Closure)

 

후행 클로저는 클로저를 매개변수로서 사용할 때 이용된다.

주의할 점! => 클로저는 꼭 마지막 변수이어야만 한다 

 

 

일반적인 클로저와 후행 클로저의 차이

 

🟡 문법적인 차이

 

일반: 인자로 받는 함수 호출 구문 내부에 인라인으로 작성된다.

후행: 클로저를 함수 호출 구문 외부에 작성할 수 있다.

 

🟡 매개변수 목록

 

일반: 매개변수 목록과 반환타입을 명시해야 한다.

후행: 매개변수 목록과 반환타입을 생략할 수 있으며, 컴파일러가 타입 추론을 통해 자동으로 추론한다.

 

🟡 중괄호 사용

 

일반: 여러 줄의 코드를 작성하기 위해서는 클로저를 중괄호 {} 안에 작성해야 한다. 

후행: 여러 줄의 코드를 작성할 때에도 중괄호 {}를 사용할 수 있지만, 생략하고 한 줄짜리 표현식 형태도 가능하다.

 

 

예시를 통해 이해해보자. 

이름을 인자로 받아 인사를 출력한 후에 클로저를 실행하는 greet 함수를 살펴보자.

greet 함수의 마지막 파라미터가 클로저이므로 인자를 생략할 수 있다!

 

  • 일반적인 클로저
// 함수 정의
func greet(name: String, closure: () -> Void) {
    print("Hello, \(name)!")
    closure()
}

// 일반적인 클로저를 사용하여 함수 호출
greet(name: "Bob", closure: {
    print("Nice to meet you!")
})

 

 

greet 함수를 호출할 때 인자로 클로저를 인라인으로 작성한다.

 

  • 후행 클로저
// 함수 정의
func greet(name: String, closure: () -> Void) {
    print("Hello, \(name)!")
    closure()
}

// 후행 클로저를 사용하여 함수 호출
greet(name: "Alice") {
    print("Nice to meet you!")
}

 

 

greet 함수의 인자로 전달되는 클로저가 함수 호출 괄호 외부에 작성된다.

 

언제 사용?

 

후행 클로저를 사용하면 함수 호출 구문이 더 간결하고 읽기 쉬워지며, 코드의 가독성이 향상된다.

 

주로 후행 클로저를 사용하는 경우는 다음과 같다.

1. 함수의 마지막 인자로 전달되는 클로저가 길거나 복잡한 경우
2. 여러 개의 인자를 가진 함수 호출 구문이 클로저를 포함할 때 가독성을 높이기 위해
3. 함수 호출이 클로저를 인자로 받는 경우에 호출 구문이 더 명확해질 때

 

 

또한, 비동기 작업을 수행하는 함수배열을 변환하는 메서드 등에서 후행 클로저를 많이 사용한다고 한다.

 

 


참고

 

https://babbab2.tistory.com/83

https://zeromin-code.tistory.com/110

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함