모바일앱/Swift

클로저 톺아보기

GeekCode 2022. 11. 27. 12:51
반응형

Closure 톺아보기

클로저는 Named Closure와 Unnamed Closure로 나뉜다.

우리가 흔하게 사용하는 함수는 Named Closure다.

둘다 클로저에 포함되지만 우리가 통상적으로 부르는 클로저는 Unnamed Closure를 말한다.

// 1. Named Closure
func doSomthing() {
  print("do something!!!")
}

// 2. UnnamedClosure
let doSomething = { print("do something!!!") }

결국 클로저와 함수는 그게 그것이기 때문에 1급객체의 성질을 가지고 있다.

즉 클로저도 아래의 성질을 갖고있다.

  1. 변수나 상수에 대입할 수 있다.
  2. 함수의 인자값으로 클로저를 전달할 수 있다.
  3. 함수의 반환값으로 클로저를 사용할 수 있다.

클로저 표현식

unnamed Closure , 익명함수 (이하 클로저로 설명) 는 헤드 부분과 바디부분으로 나누어진다.

이걸 구분하는게 in이라는 키워드이다.

스크린샷 2022-11-25 오전 9 00 59

클로저의 사용법

함수의 사용법


// 기본형
func doSomething(firstParam: String, secondParam: String) -> Void {
}

// 위 함수에서 Void 생략
func doSomething(firstParam: String, secondParam: String) {
}

// 파라미터가 없다면?
func doSomething() {
}

클로저의 사용법

파라미터와 리턴타입의 유무에 따라 조금씩 다르다.

// ParamX, ReturnTypeX
let doSomething = { () -> () in
    print("someThing")
}
// 사용
domething()

// ParamO, ReturnTypeO
let doSomething = { (firstParam: String ) -> String in
  return "Something is\(firstParam)"  
}
// 사용
doSomething("work") // doSomething( firstParam: "work")   //ERROR!!!!
// Something is work

1급객체의 특징

1. 변수나 상수에 대입할 수 있다.

클로저 자체를 변수에 대입하며 동시에 작성이 가능하다.

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

let closure2 = closure

2. 함수의 파라미터 값으로 클로저를 전달할 수 있다.

클로저의 타입이 () -> () 와 같을 때

파라미터의 타입을 명시하는 부분에 명시한다.

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

위 경우처럼 함수를 파라미터로 받는 함수가 있다.

이걸 아래처럼 파라미터 안에 클로저를 넣어줘도 된다.

스크린샷 2022-11-25 오후 12 34 53

표시된 부분이 파라미터 안에 들어간 클로저다.

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

클로저의 타입이 () -> () 와 같을 때,

  1. 함수의 반환타입에 명시해 준다.
  2. return 에 클로저를 넣는다.
func doSomething() -> () -> () {

    return { () -> () in
            print("Hello world!")
    }
}

클로저 실행하기

  1. 대입된 변수나 상수로 호출하기
let closure = { () -> String in
    return "Hello world!"
}

closure()
  1. 클로저를 직접 호출하기

    클로저 자체를 ()로 감싸고, 호출구문()를 한번더 추가한다.

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

1. 후행클로저 Trailing Closure

  • 함수의 마지막 파라미터가 클로저일 때, 이 클로저를 파라미터 값 형식이 아니라 함수 뒤에 붙여서 작성할 수 있다.
  • Argument Label은 생략한다.

파라미터가 클로저 하나인 함수

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

위 함수를 호출하려면 함수의 호출구문 ()안에 아래와 같이 해야했다.

위 함수를 호출할 때 아래와 같이 작성한다. 이렇게 파라미터 의 값형식으로 쓰인 클로저를 Inline Closure라고 부른다.

// 함수의 파라미터 값안에 있는 클로저 Inline Closure
doSomething(closure: { () -> () in
    print("Hello!")
})

각 괄호들 }) 때문에 가독성이 떨어져서 해석이 쉽지않다.

이럴때, 클로저를 파라미터 값형식이 아니라 함수의 마지막에 꼬리처럼 덧붙여서 아래처럼 사용할 수 있다.

// 함수호출구문 안에 파라미터값으로 있던 클로저가 꼬리처럼 붙었다.
doSomething () { () -> () in
    print("Hello!")
}
  1. 파라미터가 클로저 하나여서 후행클로저가 가능하다
  2. closure라는 Argument Label이 생략됐다.

여기서 파라미터가 클로저 하나일경우엔 호출구문도 생략가능하다

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

파라미터가 여러개인 함수

아래와 같이 첫 번째 파라미터로 success라는 클로저를 받고,

두 번째 파라미터로 fail이라는 클로저를 받는 함수가 있다.

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

위 함수를 Inline Closure로 호출하면 아래와 같다.

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

역시 가독성이 훌륭하진않다. 이것을 탈출클로저로 작성한다면 마지막 클로저는 함수뒤로 옮길 수 있다.

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

그러면 success의 파라미터 값은 inlineClosure, fail의 파라미터 값은 후행클로저로 호출이 가능하다.

fetchData(success: { () -> () in

print("Success!")

}) { () -> () in

print("Fail!")

}

하지만 이렇게 파라미터가 여러개일 경우에는 파라미터를 넘겨줘야하기 때문에 함수 호출 구문()을 생략할 수 없다.

클로저 경량화

클로저는 헤드부분 () → () in 여러종류의 괄호때문에 살짝 가독성이 떨어진다. 그래서 어려워보인다. 그래서 문법을 최적화해서 클로저를 단순하게 쓸 때 사용하는 것이 경량화 문법이다.

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

위 함수는 파라미터값으로 클로저를 갖고 있다. 이걸 호출할 때, Inline Closure로 호출하면 아래처럼 작성할 수 있다.

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

이걸 경량문법으로 바꿔보자

1. 파라미터의 타입과 리턴타입을 생략할 수 있다.

위에서 파라미터에 쓰인 : Int와 in 좌측에 있던 → Int 처럼 기입된 타입들을 아래와 같이 생략하여 작성가능하다.

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

2. Parameter Name은 Shortand Argument Name으로 대체하고, 동시에 in키워드를 삭제한다.

Shortand Argument Name이란 클로저 내에서 파라미터 이름대신 $를 사용해서 첫번째일경우 $0, 두번째 파라미터일경우 $1를 사용하는 것을 말한다.

위 1번 예제의 함수를 아래와 같이 바꿀 수 있다.

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

3. 단일 리턴문일 경우, return도 생략한다.

클로저 내부에 코드가 return 구문 하나뿐이라면 아래와 같이 생략가능하다.

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

4. 마지막 파라미터 값이 클로저라면 후행클로저로 작성한다.

위의 예시는 단일 파라미터지만 마지막 파라미터의 값이 클로저라면 파라미터안에 있는 클로저를 뒤로 뺄 수 있다.

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

또한 이미 후행클로저는 함수의 파라미터가 클로저 하나라면 함수 호출구문() 도 생략가능하다.

doSomething {  
     $0 + $1 + $2
}

2. @autoClosure

파라미터로 전달된 일반구문 과 함수를 클로저로 래핑(wrapping)하는 것을 말한다.

@autoClosure 어노테이션은 파라미터의 함수타입정의 바로앞에 붙인다.

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

위와 같이 작성하면 이제 closure라는 파라미터는 실제로 클로저를 전달받진 않지만 클로저처럼 사용할 수 있게된다.

클로저와 다른 점은 실제 클로저를 전달하는 것이 아니기 때문에 파라미터로 값을 넘기는 것처럼 ()를 통해 구문을 넘겨줄 수가 있다.

doSomething(closure: 1 > 2)

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

이 때 주의할 점은 파라미터가 없어야만한다. 리턴타입은 상관없다.

autoClosure는 지연된 실행을 할 수 있다.

일반구문은 작성되자마자 실행된다. 하지만 autoClosure로 작성하면 함수 내에서 클로저를 실행할 때까지 구문이 실행되지않는다. 함수가 실행되는 시점에 구문을 클로저로 만들기 때문이다.

autoClosure는 아직 공부가 더필요하다.

다음에 보강할 것!!

3. 탈출클로저 @escaping

지금까지 사용한 클로저들은 대부분 함수내부에서 직접실행할 때 사용하는 클로저 들이었다.

중첩함수를 리턴할 수 없고 어쩌고 저쩌고 하는 이유들이 있지만. 가장 중요한것은

내가 원하는 시점에 원하는 로직을 사용할 수 있도록 조절할 수 있다는 것이다.

이것 만으로도 엄청난 매력을 가진 클로저다.

사용방법

클로저의 파라미터 타입 앞에 @escaping 을 달아준다.

// 구현
func doSomething(closure: @escaping () -> ()) {
}

// 사용
doSomething {
    closure()
}

사용할 땐 내가 원하는 상황, 시점에 후행 클로저 안에서 @escaping을 붙인 파라미터를 호출하면된다.

iOS에서 흔하게 사용하는 클로저

후행클로저

Alert을 구현하다보면 마주치는 후행클로저이다.

confirmAction에 담긴 버튼을 클릭했을 때, 이후에 실행되는 클로저 이다.

let alert = UIAlertController(title: "", message: "종료하시겠습니까.", preferredStyle: .alert)
let confirmAction = UIAlertAction(title: "확인", style: .default) { _ in
    self.dismiss(animated: true)
}

alert.addAction(confirmAction)
self.present(alert, animated: false, completion: nil)

completion / Completion handler

위와 똑같은 방법인데 특별한 시점을 위해 자주 사용하는 탈출클로저이다.

함수를 구현할 때, 파라미터명을 completion 이나 completion handler로 자주 작성한다.

주로 저녁함수를 빼서 사용하거나, API통신할 때 특정시점을 위해 사용한다.

아래와 같이 이미지를 다운로드 성공, 실패할 때 UIImage를 전달할 때도 사용한다.

public struct FileDownloader {

    /// 이미지 다운로드 함수
    /// - Parameters:
    ///   - url: 이미지 URL
    ///   - completionHandler: true : 이미지 다운로드 성공, false : 이미지 다운로드 실패(with UIImage())
    static public func downloadImage(fromUrl url: String, completionHandler: @escaping (Bool, UIImage) -> Void) {
        guard let imageUrl = URL(string: url),
              let imageData = try? Data(contentsOf: imageUrl),
              let image = UIImage(data: imageData) else {
            ErrLog("Convert url to Data OR data to Image FAIL")
            completionHandler(false, UIImage())
            return
        }
        completionHandler(true, image)
    }

}

아래와 같이 사용한다.

스크린샷 2022-11-25 오후 3 03 19

API를 이용할 때도 많이 사용한다.

성공이나 실패를 탈출클로저에 담아 사용가능하다.

func getDataReturnData(url: String, successHandler: @escaping (_ resultData: Data?)-> Void, errorHandler: @escaping (_ error: Error)-> Void) -> Void {

    let session = URLSession.shared
    if let reqUrl = URL(string: url) {
    session.dataTask(with: reqUrl) { (data, response, error) in
        if error != nil {
              errorHandler(error!)
        } else {
            guard let resultdata = data else { 
                  successHandler(nil)
             return 
            }
            successHandler(resultData)
        }
    }.resume()

+++++

정말 다양하게 사용이 가능하다.

작성하다보니 생각보다 별거 아니라는 생각이 들었는데

어렵다 ㅋㅋㅋ… 특히 autoClosure는… 저만 못 봤나요.. 언제쓰지….

반응형