클로져(closures)에서 좌절한 사람을 위한 글


함수라고 알고 있던 클로져를 공부하면서 기록합니다.
어느 순간 이해가 안되는 코드들이 클로져로 이루어져있다는 것을 알게된다음 정리와 적용과정이 필요하다는 것을 알고 정리해 놓습니다.

클로저란?

클로저는 코드에 전달되어 사용할 수있는 독립적인 기능 블록입니다.
Swift의 클로저는 C 및 Objective-C의 블록 및 다른 프로그래밍 언어의 람다와 유사합니다. 라는 정의가 공식문서에 적혀있습니다.
하지만 이 개념은 이해가 가지 않았고, 개인적으로는 이름이 없는 함수이고 축약을 위한 다른 규칙들이 있다라고 이해하고 있습니다.

실제 저는 아래의 단계에 따라서 클로져를 공부하고 있더라구요. 비슷하거나 아니면 이에 해당하시는 분들은 미래를 예측 해 보시기 바랍니다.

  1. swift 문법을 공부합니다. 그리고 한 단원은 클로져입니다. 그래서 공부했습니다.
  2. 나는 클로져를 알고 있는데, 고차 함수인, map, fitler, reduce를 잘 몰라서 공부하다 보니까 클로져까지 왔습니다.
  3. 위에까지 다 공부해서 클로져를 알고 있다고 생각했는데 @escaping completeHandler: (() -> ())? 를 만나서 공부하다 보니 클로져까지 다시 오게 되었습니다.
  4. 이 전 단계를 다 공부하고 왔는데 아래와 같은
    request { (result) in
       completed(.success(data))
    }
    
    코드를 보고 막혔다가 공부하러 왔습니다.

한 2개월에 한 단계씩 올라간 것 같습니다. 물론 순서대로는 아니지만 생각보다 한번의 예제나 하나의 실습으로 익혀지지 않는 것은 확실한 것 같네요.

클로저의 문법

간단한 함수의 생김새와 사용방법

let name = "Leeo"
func printName() {
  print(name)
}

printName() // Leeo

간단한 클로져의 생김새

let name = "Leeo"
let printName = {
  print(name)
}
printName() // Leeo

함수같이 생겨서 구조를 뜯어보았습니다.

// 함수
func 함수이름(파라미터) -> 리턴타입 {
    (코드 블럭)
}

// 클로저
{ (파라미터) -> 리턴타입 in
    (코드 블럭)
}

인자가 있는 클로져와 함수의 비교

// 함수
func introduce(name: String, age: Int) {
    print("안녕 내 이름은 \(name) 나는 \(age)살 이야")
}

introduce(name: "Leeo", age: 19) // 안녕 내 이름은 Leeo 나는 19살 이야

// 클로저
let introduceClosure: (String, Int) -> Void = { (name, age) in
    print("안녕 내 이름은 \(name) 나는 \(age)살 이야")
}
introduceClosure("Leeo",19) // 안녕 내 이름은 Leeo 나는 19살 이야

클로져 축약과정

아래의 예시를 들어 함수를 사용하는 방법과 클로져의 사용을 정리해 보겠습니다.

  1. 함수를 사용한모습
func squared(i: Int) -> Int{ i * i }

squared(i: 5) // 25

squared 함수를 만들어 놓고, 5를 입력하면 25를 반환합니다.
리스트를 만들어서 하나씩 입력 해보겠습니다.

  1. 함수와 클로저 사용
let array = [1,2,3,4,5]

// 함수를 매개변수로 전달
let arrayFunctionUsed = array.map(squared)
print(arrayFunctionUsed) // [1, 4, 9, 16, 25]

// 클로저를 매개변수로 전달
let arrayClosureUsedA = array.map({(i: Int) -> Int in return i * i })
print(arrayClosureUsedA) // [1, 4, 9, 16, 25]

map을 이용해 인자를 받아 처리한다면 함수를 대입해서 사용할 수 있습니다.
하지만 한번만 사용할 함수라면 작성하는게 귀찮고 코드만 길어질 뿐이겠죠? 이럴경우 클로져를 대입해줍니다.

  1. 타입의 생략
// 이미 배열의 타입이 Int이므로 param도 당연히 Int
let arrayClosureUsedB = array.map({(i) -> Int in return i * i })
print(arrayClosureUsedB)
// [1, 4, 9, 16, 25]

array에서 넘어올 타입은 Int 이기 때문에 입력받은 타입은 반드시 Int 입니다. 함수의 정의와 다르게, 입력받을 타입을 추측할 수 있다는게 신선했습니다.

  1. 반환 탑입을 생략
// Int타입을 받아서 연산하기 때문에 결과는 Int이기 때문
let arrayClosureUsedC = array.map({(i) in return i * i })
print(arrayClosureUsedC)
// [1, 4, 9, 16, 25]

i의 타입을 알고 있는 상태에서 i*i의 반환 타입을 명시하지 않아도 Int임을 알 수 있습니다. 그래서 생략가능합니다.

  1. return 키워드 생략
let arrayClosureUsedD = array.map({(i) in i * i })
print(arrayClosureUsedD)
// [1, 4, 9, 16, 25]

클로져가 반환하는 값이 있다면, 마지막 줄의 결과값은 암시적으로 반환값 취급해줍니다. 그러므로 return키워드를 생략할 수 있습니다.

  1. 파라미터 구문 생략
let arrayClosureUsedE = array.map({ $0 * $0 })
print(arrayClosureUsedE)
// [1, 4, 9, 16, 25]

첫번째 단축인자는 $0으로 표기, 두번째 단축인자는 $1로 표기합니다.

중간 결론

함수를 사용할 때 보다 클로저를 쓰면 어떤 장점이 있을까요? 예시와 같이 제곱하는 기능을 한번만 사용한다면, 함수로 구현해 메모리 낭비를 할 필요가 없습니다. 또한 코드상에서 한번만 쓰이는 코드가 사라지니 가독성에서도 이득을 볼 수 있습니다.

후행 클로저

클로저가 함수의 마지막 전달인자라면 마지막 매개변수 이름을 생략한 후에 함수 외부에 클로저를 구현할 수 있습니다.

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

입력 받은 두 수를 계산해주는 함수를 선언하고, 계산 방법을 인자로 전달받는 함수로 설명 해보겠습니다.

func calculate(a: Int, b: Int, method: (Int, Int) -> Int) -> Int {
    return method(a, b)
}
print(calculate(a: 3, b: 4, method: add)) // 7

함수 전달 인자 중 마지막 전달 인자가 클로저라면 마지막 매개변수 이름을 생략한 후 함수 소괄호 외부에 중괄호로 구분해 클로저를 구현할 수 있습니다.

var result: Int
result = calculate(a: 3, b: 4) { (left: Int, right: Int) -> Int in
    return left + right
}
print(result) // 7

클로저 함수와 비교

클로저는 함수와 비교했을 때 뚜렷한 차이점들이 있습니다.

  1. 클로저는 이름이 없습니다.
  2. 클로저는 매개변수에 라벨값이 없습니다.
  3. 클로저는 기본인자 값이 없습니다.
  4. 클로저는 인라인의 형태로 작성될 수 있습니다.

고차함수

고차함수는 함수의 인자로 다른 함수를 받는 함수를 가리킵니다. 예를 들면 아래와 같은 모습입니다

func printSum(a: Int, b: Int, printer:(Int) -> Void) {
    printer(a+b)
}

printSum(a: 3, b: 4) { result in
    print(result)
}

a와b를 입력받아서 더하고, 그 더한 값을 출력해주는 printer는 Int를 입력받는 클로져 입니다. 이런식으로 함수를 인자로 입력받는 함수를 고차함수라고 합니다.

많이 쓰이는 고차함수인 map, reduce, filter에 대해 알아봅시다.

map

콜렉션 내부의 데이터를 순회하여 새로운 콜렉션을 생성

import Foundation
// --------------------------------------
var prices = [1.50, 10.00, 4.99, 2.30, 8.19]
// --------------------------------------

var arrayForSalePrices: [Double] = []
for price in prices {
  arrayForSalePrices.append(price * 0.9)
}
arrayForSalePrices

let salePrices = prices.map { $0 * 0.9 }
salePrices

let priceLabels = salePrices.map { (price) in
  String(format: "%.2f", price)
}

가격이 담긴 리스트가 있을 때, 10% 할인 된 가격이 필요할 경우의 에를 들어보겠습니다.
for를 사용했을 때 새로운 리스트를 만들고 그 안에 10%할인된 가격을 리스트에 넣어줍니다.
하지만 map을 사용했을 때 코드가 더 간결해지는 것을 볼 수 있습니다.

reduce

콜렉션 내부의 데이터를 하나로 통합

// --------------------------------------
let ozmaGrades = [60, 96, 87, 42]
// --------------------------------------
let totalGrade = ozmaGrades.reduce(0){ (total, grade) -> Int in
    total + grade
}
totalGrade

점수가 담긴 배열의 모든값을 0으로 초기화 된 total 변수에 모두 더해 최종적으로 1개의 값만 남깁니다.

filter

콜렉션 내부의 데이터 중 조건에 맞는 요소들로 이루어진 새로운 콜렉션 생성합니다.

// --------------------------------------
let arrayOfDwarfArrays = [
  ["Sleepy", "Grumpy", "Doc", "Bashful", "Sneezy"],
  ["Thorin", "Nori", "Bombur"]
]
// --------------------------------------

let dwarvesAfterM = arrayOfDwarfArrays.flatMap { dwarves -> [String] in
  dwarves.filter { $0 > "M" }
}.sorted()

주어진 이름에서 M보다 알파벳 순으로 늦은 이름들만 남겨 새로운 콜렉션을 생성합니다.

결론

좀 더 간결한 코딩을 위해 사용하는 클로져를 이해하기 위해서는 원래의 형태가 어떻게 생겼는지 파악하고 어떻게 축약되었는지를 익혀 함수형 프로그래밍의 구조가 눈에 익숙하게 만들어야 합니다.

참고 : https://nightohl.tistory.com/entry/Swift-%ED%81%B4%EB%A1%9C%EC%A0%80Closure

댓글

이 블로그의 인기 게시물

[IOS] AppDelegate는 뭐하는 녀석이지?

[git] git의 upstream과 origin 헷갈리는 사람 손!

[git] Github 이슈 라벨(issue labels)