본문 바로가기
문법관련/Swift

클로저 (꼼꼼한 재은 씨의 Swift : 문법편 + babbab2)

by print_soo 2022. 5. 26.

출처: 꼼꼼한 재은 씨의 Swift : 문법편 + babbab2티스토리

클로저는 함수에 대해서 알고 배우는 편 좋기때문에 함수를 다 이해하고 살펴보자. 

 

 

Closure 란?

클로저란 보통 익명의 함수, 일회용 함수를 작성할 수 있는 구문이다. 

 

이전에 배운 함수또한 클로저에 속한다. 무슨 말이냐면 클로저는 아래의 사진과 같이 두 종류의 클로저가 있다. 

 

위의 사진에서 Named 클로저는 우리가 앞서 배운 아래의 코드와 같은 함수이다. (클로저이지만 그냥 함수라고 부를 뿐이다.)

 

func doSomething() {
    print("Somaker")
}

 

 

그리고 Unnamed 클로저는 앞으로 배울 Closure를 뜻한다. 

 

let closure = { print("Somaker") }

 

Swift에서 클로저라고 부르는 객체는 대부분 아래의 세가지 경우중 하나에 해당된다.

전역 함수
 : 이름이 있으며, 주변 환경에서 캡쳐할 어떤 값도 없는 클로저

중첩 함수
 : 이름이 있으며, 자신을 둘러싼 함수로부터 값을 캡처할 수 있는 클로저

클로저 표현식
 : 이름이 없으며 주변 환경으로부터 값을 캡쳐할 수 있는 경량 문법으로 작성된 클로저

 

 

 

 

클로저의 표현식

클로저의 표현식은 아래와 같다.

 

// -클로저의 표현식-

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

{ () -> () in 
	print("클로저가 실행됩니다.")
}

반환 값이 없는 경우에는 () 보다는 Void 로 명확히 표현해 주는 것이 좋다.
{ () -> Void in 
	print("클로저가 실행됩니다.")
}

 

클로저는 아래의 사진과 같이 클로저 헤드클로저 바디로 이루어져있다. 이 둘을 구분지어주는 것이 바로 in이라는 키워드이다. 

 

babbab2님 티스토리 사진

 

 

 

Closure의 예시

- 매개변수와 반환 타입이 둘다 없는 클로저

 

정확한 이해를 위해서 예제를 살펴보자. 

 

클로저는 익명인 함수이다. 따라서, Swift에서는 일급객체이기 때문에 상수에 클로저를 대입할 수 있다. 

(특히, 매개변수와 반환 타입이 둘다 없는 경우 아래와 같이 사용한다.)

 

let closure = { () -> () in
    print("Closure")
}

closure() // "Closure"

 

 

- 매개변수와 반환 타입이 둘다 없는 클로저

 

let closure = { (name: String) -> String in
    return "Hello, \(name)"
}

closure("junsu") // "Hello, junsu"

 

💡  왜 closure(name: "junsu")이 아니라 closure("junsu")로 호출할까?

함수에서는 매개변수가 인자의 이름 + 매개변수의 역할을 하지만 클로저에서는 매개변수는 오직 매개변수의 역할만 하기 때문.

 

 

 

일급객체로서 Closure

 

1 .클로저를 변수나 상수에 대입할 수 있다.

 

// 대입과 동시에 클로저 작성

let closure = { () -> () in
    print("Closure")
}

// 클로저 작성후 새로운 상수에 클로저 대입

let closure2 = closure

 

 

2. 함수의 매개변수 타입으로 클로저를 전달할 수 있다.

 

func doSomething(closure: () -> ()) {
    closure()
}

이렇게 함수를 매개변수로 전달 받는 doSomething이라는 함수가 있다. 이경우 파라미터로 함수를 넘겨주어도 되지만 아래처럼 클로저로 넘겨줘도 된다. 

 

// 1. 일반적인 방법으로 호출

func doSomething(closure: () -> ()) {
    closure()
}

func hello() {
    print("Hello!")
}

doSomething(closure: hello)

// Hello!

---------------------------------------

// 2. 클로저로 호출

func doSomething(closure: () -> ()) {
    closure()
}

doSomething(closure: { () -> () in
    print("Hello!")
})

// Hello!

 

초록색 영역이 클로저로 작성된 부분이고 closure이라는 인자이름의 매개변수로 전달된 것이다. 

따라서 doSomething이라는 함수에서 매개변수로 전달 받은 함수를 실행하면 클로저가 실행된다.

 

 

3. 함수의 반환 타입으로 클로저를 사용할 수 있다. 

 

func doSomething() -> () -> () {
    
    return { () -> () in
        print("Hello")
    }
}

let c = doSomething()
c()

 

 

 

 

 

 

클로저 실행하기

1. 클로저가 대입된 변수나 상수 호출하기 

let closure = { () -> String in
    return "Hello Sodeul!"
}

closure()

 

 

2. 클로저 직접 실행하기

({ () -> () in
    print("Hello Sodeul!")
})()

 

 


 

클로저를 간단하게 사용할 수 있는 경량 문법

 

1. 트레일링 클로저(Trailling Closure)

 

틀레일링 클로저란 함수의 마지막 매개변수가 클로저일 때, 이를 매개변수 형식이 아닌 함수 뒤에 붙여 작성하는 문법(인자 이름은 생략)

 

 

1 - 1 매개변수가 클로저 하나인 함수

 

아래와 같이 클로저 하나만 매개변수로 받는 함수는 아래와 같이 호출했다.

이렇게 클로저가 매개변수의 값 형식으로 함수 호출 구문 () 안에 작성되는 것을 우리는 Inine Closure이라고 부른다.

 

func doSomething(closure: () -> ()) {
    closure()
}

doSomething(closure: { () -> () in
    print("Hello!")
})

// Hello!

 

 

Inine Closure은 마지막 괄호도 }) 이런식으로 있어서 가독성이 떨어지고 해석도 복잡하다. 

이런 문제를 해결하기위해서 트레일링 클로저가 나왔는데 예제를 보자. 

 

func doSomething(closure: () -> ()) {
    closure()
}

doSomething () { () -> () in
    print("Hello!")
}

//여기서 좀더 생략해서 ()를 생략할 수 있다.

doSomething { () -> () in
    print("Hello!")
}

 

이렇게 함수의 가장 마지막에 클로저를 꼬리처럼 덧붙여 사용하는 것이 바로 트레일링 클로저이다. 

 

💡  트레일링 클로저의 핵심  💡

1. 파라미터가 클로저 하나일 경우

2. 인자 이름은 트레일링 클로저에서 생략됨 

 

 

1 - 2 매개변수가 여러개인 함수 

 

매개변수가 여러개인 함수에서는 마지막 매개변수의 클로저만 함수뒤에 붙여 쓸 수 있다. 

 

func fetchData(success: () -> (), fail: () -> ()) {
    //do something...
}

-------------------------------------------

// Inline Closure
 
fetchData(success: { () -> () in
    print("Success!")
}, fail: { () -> () in
    print("Fail!")
})

// Trailing Closure

fetchData(success:  { () -> () in
    print("Success!")
}) { () -> () in
    print("Fail!")
}

 

  매개변수가 여러개일 경우, 함수 호출 구문 ()을 마음대로 생략해서는 안된다.  ❌ 

 

 

 

 

2. 클로저의 경량 문법

아래의 함수가 있다고 생각해보자.

 

func doSomething(closure: (Int, Int, Int) -> Int) { 
    closure(1, 2, 3)
}

//함수 해석: 3개의 입력을 받고 1개의 출력을 하는 클로저를 매개변수로 받는 함수인데 이 클로저를 할당 받으면 클로저에 1, 2, 3을 넣는다.

 

이 함수는 매개변수라 받은 클로저를 실행하는데 그 클로저의 매개변수를 1, 2, 3이라는 숫자를 넘겨 주고 있다. 

이 함수를 호출하면 아래와 같다.

 

doSomething(closure: { (a: Int, b: Int, c: Int) -> Int in
    return a + b + c
})

// 매개변수 a, b, c이고 반환 타입은 Int인 클로저이고 실행구문은 a + b + c를 출력

 

위의 방식으로 하면 너무 헷갈리고 복잡하다 따라서 경량 문법을 사용해서 경량화 시켜보자.

 

 

경량화 방법(1) - 매개변수 타입과 반환 타입을 생략할 수 있다.

 

//생략 전
doSomething(closure: { (a: Int, b: Int, c: Int) -> Int in
    return a + b + c
})

//생략 후
doSomething(closure: { (a, b, c) in
    return a + b + c
})

 

 

경량화 방법(2) - 매개변수 이름은 Shortand Argument Names으로 대체하고 매개변수 이름과 in 키워드를 삭제한다.

 

// 생략 전
doSomething(closure: { (a, b, c) in
    return a + b + c
})

// 생략 후 
doSomething(closure: {  
    return $0 + $1 + $2
})

 

💡  매개변수의 이름을 Shortand Argument Names 바꾸다는 것이 뭔가?

매개변수 대신 $와 index를 이용해서 매개변수에 순서대로 접근하는 것이 Shortand Argument Names이다. 
따라서 a, b, c를 바꾼다면 $0, $1, $2로 바꾼 것이다. 

 

 

경량화 방법(3) - 단일 리턴문만 남을 경우 return도 생략한다.

 

단일 리턴문이란 아래와 같다.

 

doSomething(closure: {  
    return $0 + $1 + $2
})

 

이렇게 return 구문 하나만 남은 경우를 말한다. (출력 구문이 retrun 구문과 같이 있으면 제외)  이런 경우 아래와 같이 return 키워드도 생략할 수 있다.

 

doSomething(closure: {  
     $0 + $1 + $2
})

 

 

경량화 방법(4) - 클로저 매개변수가 마지막 매개변수라면 트레일링 클로저로 작성한다. 

 

doSomething() {  
     $0 + $1 + $2
}

 

 

경량화 방법(5) - ()에 값이 아무 것도 없다면 생략한다. (최종)

 

doSomething {  
     $0 + $1 + $2
}

 

 

 

 

3. @autoclosure

autoclosure란 매개변수로 전달된 일반 구문 & 함수를 클로저로 래핑(Wrapping)하는 것, 

(쉽게 말하면 autoclosure 속성이 붙어 있으면 일반 구문을 인자값으로 넣더라도 컴파일러가 알아서 클로저로 만들어서 사용한다는 것)

 

예제를 통해서 알아보자. 

우선 autoclosure는 매개변수 함수 타입 정의 바로 앞에다가 사용해야한다.

 

func doSomething(closure: @autoclosure () -> ()) {
}

 

위와 같이 만들었을 경우 closure라는 매개변수는 실제 클로저를 전달 받지는 않지만 클로저처럼 사용가능하다.

 

다만, 클로저와 다른 점은 실제 클로저로 전달하는 것이 아니기 때문에 매개변수 값을 넘기는 것처럼 ()를 통해서 구문을 넘겨줄 수 없다.

 

doSomething(closure: 1 > 2)

 

위의 구문에서 1 > 2는 일반 구문이지만 실제 함수 내에서는 아래와 같이 일반 구문을 클로저처럼 사용할 수 있다. (클로저를 래핑한 것이기 때문이다.)

 

func doSomething(closure: @autoclosure () -> ()) {
    closure()
}

 

❌  autoclosure를 사용할 때는 매개변수가 반드시 없어야한다. (반환타입은 상관없다.)  ❌ 

 

 

autoclosure의 특징- 지연된 실행

 

일반 구문은 원래 작성되자마자 실행되어야 하지만 autoclosure를 사용하면 함수 내에서 클로저를 실핼할 때까지 구문이 실행되지 않기 때문에 지연되어 실행한다는 특징이 있다. 

 

 

 

4. @escaping

우리가 지금까지 작성했던 클로저는 모두 non - escaping Closure이다. 

 

즉, 함수 내부에서 직접 실행하기 위해서만 사용해서 매개변수로 받은 클로저를 변수나 상수에 대입할 수 없고 중첩 함수에서 클로저를 사용할 경우, 중첩함수를 리턴할 수 없다.  

함수의 실행 흐름을 탈출하지 않아, 함수가 종료 되기전에 무조건 실행되어야 한다. 

 

💡  왜 클로저는 탈출불가(non escaping)일까?

탈출불가(non escaping)하게 관리함으로써 얻는 가장 큰 이점은  컴파일러가 코드를 최적화 하는 과정에서의 성능향상이다.
즉, 컴파일러가 더 이상 메모리 관리상의 지저분한 일들에 관여하지 않아도 된다는 뜻이다. 
이러한 이유로 클로저는 탈출불가(non escaping)이다.

 

따라서 아래의 경우 @escaping키워드를 사용해야한다.

1. 함수 실행을 벗어나서 함수가 끝난 후에도 클로저를 실행하고 싶은 경우
2. 중첩함수에서 실행 후 중첩 함수를 리턴하고 싶은 경우
3. 변수 상수에 대입하고 싶은 경우

 

사용방식은 아래와 같다. 

 

func doSomething(closure: @escaping () -> ()) {

}

 

위 처럼 작성하면 위 3개의 경우를 모두 충족 시킬 수 있다.

 

escaping 사용에서 주의할 점은 메모리 관련 부분이다.