본문 바로가기

Swift

Closure (클로저)

728x90

클로저에 대한 더 자세하고 정확한 정보는 공식 문서를 참고해주세요.

왜냐면 제 정보가 틀릴 수도 있으니까. 

 

클로저 

클로저의 의미

클로저란 간단하게 코드 블럭을 말한다.

이런 코드 블럭을 바탕으로 Swift만의 표현 방식을 작성할 수 있다.

 

보통 클로저를 익명함수라고 알고 있을 것이다.

클로저에는 크게 두 가지 종류가 있는데, 아래와 같다.

  • 이름이 있는 클로저 (= named closure)
  • 이름이 없는 클로저 (= unamed closure)

 

그동안에 우리가 자주 사용하는 이름이 있는 함수는 Named Closure이고 이를 우리는 함수라고 통칭하고 있었던 것이다. 

그리고 이름이 없는 함수인, 코드 블럭을 우리는 클로저라고 부른다.

 

클로저는 Named Clousre & Unnamed Closure 둘다 포함하지만, 보통 Unnamed Closure를 말한다!

 

Swift는 함수형 프로그래밍으로, Swift에서 함수는 일급 객체이다. 이는 곧 클로저 역시 일급 객체라는 것을 의미한다.

다시 말해, 클로저는 일급 객체로 변수, 상수 등으로 저장 가능하고 전달 인자로 사용할 수 있는 형태라는 것을 말한다.

 

일급 객체의 특징은 더보기를 통해서 확인할 수 있다.

더보기

일급 객체의 특징(조건)

  • 모든 일급 객체는 함수의 실질적인 매개변수가 될 수 있다.
  • 모든 일급 객체는 함수의 반환값이 될 수 있다.
  • 모든 일급 객체는 할당의 대상이 될 수 있다.
  • 모든 일급 객체는 비교 연산을 적용할 수 있다.

 

클로저의 표현식 

클로저 표현식을 알아보기 전에 클로저를 어떻게 사용할 수 있는지 예시 코드를 보면 아래와 같다.

// sum이라는 변수에 클로저를 할당하고 있다.
// 2개의 Int형 매개변수를 받아서 다시 Int를 반환하는 형태를 의미한다. 
var sum: (Int, Int) -> Int = { (a: Int, b: Int) -> Int in
	return a + b 
}

let sumResult: Int = sum(1, 2)

위의 코드를 보면. 다음과 같은 내용을 알 수 있다.

- 클로저 사용법을 통해서 클로저 식으로 데이터형을 선언하고 있다. 

- 클로저로 sum이라는 변수에 클로저를 할당하고 있는 모습을 볼 수 있다. 

 

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

{ (parameters) -> return type in
	코드 실행 구문
}

익명 함수이므로 func이라는 키워드는 작성하지 않는다. 또한, in을 기준으로 표현식을 나눠서 생각할 수 있다.

in을 기준으로 앞 부분은 클로저 헤더에 해당하고

in을 기준으로 뒷 부분은 클로저 바디에 해당한다.

(= 즉 헤더와 바디를 구분짓고 있는 것이 in이라는 키워드이다.)

 

클로저를 사용할 때 주의할 점이 있는데,

let nameClosure = { (name: String) -> String in
	return "안녕, 나는 \(name)이야."
}

이런 클로저를 할당한 상수 nameClosure가 있다고 할 때, 이를 사용하기 위해서는 

nameClosure("So Kyte")

라고 사용할 것을 알고 있는데, 일반적인 함수처럼 생각하여 name을 argument label이라고 생각할 수 있다.

nameClosure("Hu Ree")

그래서 이렇게 작성할 수 있지 않을까?라고 생각할 수 있는데 이와 같은 형태는 오류가 난다.

이유는 클로저에서는 argument label를 사용하지 않기 때문이다. 

 

일급 객체로서 클로저

클로저는 익명 '함수'이고 Swift에서 함수는 일급 객체이다. 그러므로 클로저 역시 일급 객체의 특성을 갖고 있다.

 

  1. 클로저를 변수, 상수에 대입할 수 있다.
  2. 함수의 파라미터 타입으로 클로저를 전달할 수 있다.
  3. 함수의 반환 타입으로 클로저를 사용할 수 있다.

 

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

클로저 역시 일급 객체이므로 변수, 상수에 대입할 수 있고 이 대입된 변수, 상수로 실행할 수 있다.

 

// 대입과 동시에 클로저를 작성할 수 있다.
let closure = { () -> () in
	print("클로저 실행")
}

// 새로운 변수, 상수에 대입할 수 있다.
let closure2 = closure
closure2()

 

함수에서의 일급 객체의 특성 (변수, 상수에 함수를 대입할 수 있다.)과 관련된 예시는 더보기를 통해서 확인할 수 있다.

더보기
func checkBankInformation(bank: String) -> Bool {
    let bankArray = ["우리", "국민", "신한"]
    return bankArray.contains(bank) ?  true : false
}

위와 같이 우리, 국민, 신한 은행이 포함되어 있는지 확인해주는 함수가 있다고 할 때

// 단지 함수만 대입한 것으로, 실행된 상태는 아니다. (= 함수 호출을 하지 않은 것이다.)
let checkAccount = checkBankInformation

// 호출방법 (-> 함수를 호출해야 실행이 된다.)
checkAccount("신한")

이렇게 변수, 상수에 함수를 대입할 수 있다.

여기서 말하는 대입은 함수를 실행을 해서 반환된 반환값을 대입하는 것을 말한다. 

 

그러므로 단순히 함수를 대입한

let checkResult = checkBankInformation(bank: "우리")
print(checkResult)

이런 상황의 경우 함수를 대입했다고 할 수 없다.


 

함수를 대입하는 또 다른 경우는 아래와 같다.

// Function Type: (String, Int) -> String
func hello(nickName: String, age: Int) -> String {
    return "저는 \(nickName), \(age)살 입니다."
}

이런 함수에 대해서 

let userInfo: (String, Int) -> String = hello
userInfo("후리스콜링스", 35)

userInfo를 호출해서 실행할 수 있다.

 

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

아래와 같이 함수를 파라미터로 전달 받는 함수가 있다고 할 때,

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

 

이렇게 클로저를 넘겨 함수 호출을 할 수 있다.

doSomething(closure: { () -> () in
	print("클로저 실행")
})

이 때, 좀 더 자세히 살펴보면 중괄호 블럭이 클로저로 작성된 부분이고 이것이 doSomething이라는 함수의 closure란 argument label의 parameter로 전달 된 것을 확인할 수 있다. 

 

함수에서의 일급 객체의 특성 (함수의 인자값으로 함수를 사용할 수 있다.)과 관련된 예시는 더보기를 통해서 확인할 수 있다.

더보기
func oddNumber() {
    print("홀수")
}

func evenNumber() {
    print("짝수")
}

func resultNumber(number: Int, odd: () -> (), even: () -> ()) {
    return number.isMultiple(of: 2) ? even() : odd()
}

이렇게 resultNumber의 매개변수로 받은 number에 대해서 2로 나눴을 때의 결과를 바탕으로 짝수인지 홀수인지 구분해주는 함수가 있다고 할 때, odd와 even의 경우 함수를 매개변수로 받고 있는 것을 확인할 수 있다.

// 매개변수로 함수를 전달
resultNumber(number: 32, odd: oddNumber, even: evenNumber)

 

그리고 매번 함수를 호출하는 것이 부담스럽면, 익명 함수(= 클로저)를 사용할 수 있다.

resultNumber(number: 9) {
    print("홀수")
} even: {
    print("짝수")
}

 

또 다른 예시로 계산하는 함수를 만들어보자.

func plus(a: Int, b: Int) -> Int {
    return a + b
}

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

func multiple(a: Int, b: Int) -> Int {
    return a * b
}

func divide(a: Int, b: Int) -> Int {
    return a / b
}

func calculate(operand: String) -> (Int, Int) -> Int {
    switch operand {
    case "+":
        return plus
    case "-":
        return minus
    case "*":
        return multiple
    case "/":
        return divide
    default:
        return plus
    }
}

 

그리고 이에 대해서

calculate(operand: "+")(3, 5)  // 함수 실행

 

let resultValue = calculate(operand: "*")
resultValue(4, 9)

이렇게 함수를 실행할 수 있다.

 

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

 

실제 값을 반환할 때 변수, 상수, 함수가 아닌 클로저를 반환할 수 있다.

func doSomething() -> () -> () {
	return { () -> () in
        print("안녕하세요?")
    }
}


let closure = doSomething()
closure() // "안녕하세요?"

 

함수에서의 일급 객체의 특성 (함수의 반환 타입으로 함수를 사용할 수 있다.)과 관련된 예시는 더보기를 통해서 확인할 수 있다.

더보기

계좌의 유무를 확인해주는 함수 2개가 작성하고,

func currentAccount() -> String {
    return "계좌 있음"
}

func noCurrentAccount() -> String {
    return "계좌 없음 "
}

 

checkBank의 반환 타입으로 지정을 하면 아래와 같다.

// 참고) 가장 왼쪽에 위치한 ->를 기준으로 오른쪽에 놓인 모든 타입은 반환 값을 의미한다.
func checkBank(bank: String) -> () -> String {
    let backArray = ["우리", "신한", "국민"]
    return backArray.contains(bank) ? currentAccount : noCurrentAccount // 여기서 함수를 호출한다고 할 수 없다.
}

 

그리고 함수를 변수에 넣고 호출하면

let sokyte = checkBank(bank: "우리") // 함수를 넣어주기만 하고 호출하지 않은 상태 
sokyte() // 함수 호출

 결과가 나오는 것을 확인할 수 있다. 

 

클로저 실행

클로저를 실행할 수 있는 방법은 크게 두 가지가 있다.

  • 클로저가 대입된 변수, 상수 호출하기
  • 클로저 직접 실행하기

 

클로저가 대입된 변수, 상수를 호출하는 방법은 

let closure = { () -> String in
    return "클로저가 실행됩니다?"
}

이런 식으로 클로저가 대입된 상수를 함수 호출 연산자 () 를 사용해서 실행하는 것이다.

 

클로저를 직접 실행하는 방법은

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

클로저를 소괄호로 감싸고 마지막에 함수 호출 연산자를 추가하면 된다. 

이렇게 클로저를 직접 실행하는 방법은 일회성으로 사용할 경우에 사용하면 된다.

 

클로저 경량 문법

클로저를 위와 같은 표현식으로 작성할 수도 있지만 다른 방법으로 작성할 수 있다.

 

크게 네 가지 방법으로 나눠서 작성할 수 있다.

  • 후행 클로저
  • 반환 타입 생략
  • 단축 인자 이름
  • 암시적 반환 표현

후행 클로저

클로저가 함수의 마지막 매개변수라면 그 이름을 생략해서 함수 소괄호 외부에 클로저를 구현할 수 있다.

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

var result: Int

위와 같이 두 개의 매개변수를 받아서 결과 값을 담는 함수를 선언하고 결과 값을 담을 result라는 Int형 변수를 선언하면,

 

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

이렇게 메소드에 left, right를 받아서 더한 값을 반환하는 클로저를 method에 넣어서 구현할 수 있다.

여기서 method는 함수의 마지막 전달 인자이므로 후행 클로저로 바꿔서 작성할 수 있다.

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

마지막 method 전달인자 이름을 생략하고 (a: 12, b: 12)로 마무리 한 뒤, { (Int, Int) -> Int } 클로저를 마지막으로 작성하면 같은 의미에 대해서 보다 경량화된 모습으로 작성할 수 있다.

 

반환 타입 생략

컴파일러 상에서 어떤 타입을 반환할 지 알고 있다면 클로저에서는 반환 타입을 명시하지 않아도 된다.

// (Int, Int) -> Int 를 반환한다고 명시하고 있으나,
result = calculate(a: 12, b: 12) { (left: Int, right: Int) -> Int in
    return left + right
}

// 컴파일 상에서 확인할 수 있다면 명시하지 않아도 된다.
result = calculate(a: 12, b: 12) { (left: Int, right: Int) in
    return left + right
}

주의할 점은 반환 타입 (위의 코드에서는 Int)은 생략 할 수 있지만 in 키워드는 생략할 수 없다.

 

단축 인자이름

클로저 상에서 매개변수 이름이 불필요하다면 단축 인자 이름을 활용 할 수 있다.

단축 인자 이름은 매개변수 순서대로 $0, $1, $2 .. 이렇게 표현하면 된다.

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

result = calculate(a: 12, b: 12) {
    return $0 + $1
}

두 개의 코드는 같은 결과 값, 같은 동작 방식으로 구현되는 표현법이다.

클로저의 축약 방법은 다양하지만 너무 많은 부분을 생략하면 협업 시 바로 이해하기 어려울 수도 있기 때문에 주의해야 한다.

(같은 이유로 협업 초반에 어디까지 축약할 것인지 논의하는 것도 좋다.)

'Swift' 카테고리의 다른 글

Access Control (feat. Framework)  (1) 2022.08.16
Attribute (@어쩌구)  (0) 2022.08.10
First-Class Citizen(일급 객체)  (0) 2022.08.09
Singleton Pattern  (0) 2022.08.05
프로토콜  (0) 2022.08.02