Ios closure cover

Swift의 클로저 및 고차 함수 이해하기

iOS 개발자 밋업인 Let us go에서 클로저와 Swift의 고차함수라는 주제로 많은 호응을 얻은 강연입니다.

소개

이전에 Objective-C를 사용하면서 fuckingblocksyntax.com 사이트를 방문한 분들이 있을지 모르겠습니다. 블럭 사용법이 난해해서 저런 이름을 붙인 것 같은데요. 이제 Swift가 등장하자 fuckingclosuresyntax 사이트도 생겼습니다. Objective-C의 블럭과 Swift의 클로저는 iOS 프레임워크에서 사용하는 것은 큰 차이가 없어 보일 수 있습니다. 하지만 Closure 문법을 잘 살펴보면 내부적으로 변화가 있을 뿐만 아니라, 다른 용도로 많이 사용할 수 있음을 파악할 수 있습니다.

Closure

클로저란 코드의 블럭이자, 일급 객체로 완벽한 역할을 할 수 있습니다. 일급 객체란 전달 인자로 보낼 수 있고, 변수/상수 등으로 저장하거나 전달할 수 있으며, 함수의 반환 값이 될 수도 있습니다. 실제 우리가 알고 있는 함수는 클로저의 한 형태로, 이름이 있는 클로저입니다.

클로저에는 다양하게 파생된 문법이 있습니다. 사전 지식이 없는 상태에서 이 문법을 보면 좀 당황스러울 수 있는데요. 네 가지 정도의 형태의 클로저를 살펴보겠습니다.

말씀드리기 전에 클로저의 기본 형태를 먼저 볼까요?


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

클로저의 기본 형태는 위 코드와 같습니다. 여기서 in이라는 키워드가 사용되는 이유는 정의부와 실행부를 분리하기 위해서입니다.

Cocoa Touch의 클로저

앞에 매개 변수가 없는 경우에는 앞서 본 in 키워드도 생략할 수 있습니다. UIAlertAction에서 클로저를 많이 볼 수 있습니다.

let action = UIAlertAction(title: String?, style: UIAlertActionStyle, handler ((UIAlertAction) -> Void)?)

let action = UIAlertAction(title: "OK", style: .default) {
  (UIAlertAction) in
  // code
}

let action = UIAlertAction(title: "OK", style: .default) {
  (action) in
}

이런 개발 뉴스를 더 만나보세요

UIAlertAction을 자동 완성해가는 모습입니다. 엔터를 치면 자동완성되므로 실제로 사용할 때는 맨 마지막 코드와 같은 모습으로 쓰는데, 축약되고 생략되는 과정에서 어떤 단계를 거치는지 생각해본 분은 거의 없을 겁니다. 이번 강연에서는 이 과정에 초점을 맞춰 보겠습니다.

매개 변수, 반환 타입 생략 가능

클로저는 함수의 매개 변수로 넘어가는 용도로 가장 많이 쓰이게 됩니다.


func sorted(by areInIncreasingOrder: (E, E) -> Bool) -> [E]
// (Type, Type) -> Type
 
 


Swift 표준 라이브러리의 sorted를 예로 들어보겠습니다. Type과 Type을 가지고 Type을 반환해주는 형태의 함수로 클로저를 받고 있습니다.


func backwards(left: String, right: String) -> Bool {
  print("(left) (right) 비교중")
  return left > right
}

let names: [String] = ["hana", "eric", "yagom", "kim"]
let reversed: [String] = names.sorted(by: backwards) print(reversed) 

sorted 함수는 매개 변수로 함수를 넘길 수 있으므로 이 함수를 만들어 봤습니다. 비교군에서 왼쪽이 크게 되면 왼쪽이 먼저 나오게 하는 함수를 만들고 실행시켰습니다. 그냥 함수를 넘겼지만 동작하는 이유는 함수가 클로저의 일종이기 때문입니다. 하지만 이처럼 함수를 미리 만들어두고 쓰는 경우는 드무므로 클로저로 넘기도록 바꿔보겠습니다.


let reversed: [String]

reversed = names.sorted (by: { (left: String, right:
String) -> Bool in
  return left > right})
print(reversed) // ["yagom", "kim", "hana", "eric"]
 
 


아까의 형태와 유사하지만 이름이 없는 클로저가 안쪽으로 들어오는 것을 볼 수 있습니다. in 키워드로 정의부와 실행부를 나누고 반환했습니다. 이제부터 이 클로저를 여러 가지 단계로 축약해 보겠습니다.

후행 클로저


// 후행 클로저(Trailing Closure) 사용
let reversed: [String] = names.sorted() { (left: String,
right: String) -> Bool in
  return left > right
} 

// sorted(by:) 메서드의 소괄호까지 생략 가능 
let reversed: [String] = names.sorted { (left: String,
right: String) -> Bool in
  return left > right
} 
 

어떤 함수나 메서드의 맨 마지막으로 클로저를 받아오면 중첩되는 괄호를 생략해서 위 예제의 후행 클로저와 같은 모습으로 바뀌게 됩니다. 또한, 메서드의 소괄호까지도 생략할 수 있습니다.

매개 변수 타입과 반환 타입 생략 가능

// 클로저의 매개 변수 타입과 반환 타입을 생략
let reversed: [String] = names.sorted { (left, right) in
  return left > right
} 

클로저의 매개 변수 타입과 반환 타입을 생략해서 표현할 수 있습니다. sorted라는 메서드에서 타입을 알고 있기 때문입니다.

return 키워드 생략 및 축약된 전달 인자 이름 사용 가능


// 단축 인자 이름 사용
let reversed: [String] = names.sorted {
  return $0 > $1
} 

// 암시적 반환 표현 사용
let reversed: [String] = names.sorted { $0 > $1 } 

앞서 left, right로 지정했던 매개 변수 이름도 생략하고 단축 인자를 사용할 수 있습니다. 내부에 한 줄만 있다면 return 키워드 없이도 반환 타입이 됩니다. 이 원리를 안다면 외부 라이브러리에 $0, $1이 나오더라도 하나씩 풀어서 보면 이해가 쉬워질 겁니다. 개인적으로는 협업 상황에서는 생략을 적당한 수준까지만 하는 것을 권장합니다.

고차 함수(Higher-order function)

사실 클로저 문법을 앞서 설명해 드린 이유는 고차 함수를 설명하기 위해서입니다. 고차 함수란 하나 이상의 함수를 인자로 취하거나 함수를 결과로 반환하는 함수입니다. 이제부터 Swift의 고차 함수인 map, filter, reduce에 대해서 알아보겠습니다.

map

ios-closure-map

map은 컬렉션 내부의 기존 데이터를 변형해서 새로운 컬렉션을 생성하는 함수입니다. 내부의 자료에 변형을 가하기 위해 함수의 각 요소에 함수를 적용해서 새로운 컬렉션을 만들어줍니다. 잘 사용하면 for 문을 사용하지 않고도 작업할 수 있습니다.


let numbers: [Int] = [0, 1, 2, 3, 4]
var doubledNumbers: [Int] = [Int]()
var strings: [String] = [String]()

// for for number in numbers {
  doubledNumbers.append(number * 2)
  strings.append("(number)")
}

print(doubledNumbers) // [0, 2, 4, 6, 8]
print(strings) // ["0", "1", "2", "3", "4"]

이제 같은 로직으로 map을 적용해 보겠습니다.


// map 메서드 사용
doubledNumbers = numbers.map({ (number: Int) -> Int in
  return number * 2
})
strings = numbers.map({ (number: Int) -> String in
  return "(number)"
})

print(doubledNumbers) // [0, 2, 4, 6, 8]
print(strings) // ["0", "1", "2", "3", "4"]


map을 사용하면 numbers라는 배열에 map을 통해 함수를 적용해서 요소 하나하나에 변형을 가해줘서 기존 요소에 2배를 한 새로운 배열이 만들어집니다.


// 매개변수, 반환 타입, 반환 키워드 (return) 생략
// 후행 클로저
doubledNumbers = numbers.map { $0 * 2 }
print(doubledNumbers) // [0, 2, 4, 6, 8]
 
 


위 내용에 앞서 말한 생략 기법을 사용하면 좀 더 가독성이 높아질 수 있습니다.

filter

다음으로 컨테이너 내부의 값을 걸러서 추출하는 filter 함수에 대해 알아보겠습니다.


let numbers: [Int] = [0, 1, 2, 3, 4, 5]
var evenNumbers: [Int] = [Int]()

// for 구문 사용
for number in numbers {
  if number % 2 != 0 { continue }
  evenNumbers.append(number)
}

print(evenNumbers) // [0, 2, 4]


짝수를 골라내기 위해 for 문을 사용하면 위와 같은 모습입니다.

let numbers: [Int] = [0, 1, 2, 3, 4, 5]
let evenNumbers: [Int] = numbers.filter { (number: Int) -> Bool in return number % 2 == 0 }
print(evenNumbers) // [0, 2, 4]


짝수를 골라내기 위해 for 문을 사용하면 위와 같은 모습입니다.

let oddNumbers: [Int] = numbers.filter {
  $0 % 2 != 0
}
print(oddNumbers) // [1, 3, 5]
 
 
 


앞서와 마찬가지로 한 줄로 축약할 수 있습니다. 다만 map과는 용도만 달라질 뿐입니다.

reduce

reduce는 컨테이너 내부의 컨텐츠를 하나로 통합해줍니다.


let numbers: [Int] = [2, 8, 15]
var sum: Int = 0

for number in numbers {
  sum += number
}

print(sum) // 25


컬렉션의 모든 내용을 합쳐주는 기능을 해줍니다. for 문으로는 위와 같은 모습입니다.


let numbers: [Int] = [2, 8, 15] // 0

// 초깃값이 0이고 정수 배열의 모든 값을 더함
let sum: Int = numbers.reduce(0, { (first: Int, second:
Int) -> Int in
  print("(first) + (second)")
  return first + second
})

print(sum) // 25 !

sum이라는 let 상수에 값을 더하는데, 뒤에 값을 바꾸는 로직이 없으므로 상수를 사용할 수 있습니다. 앞서 for 문에서는 var 변수로 선언해야 하기 때문에 나중에 의도하지 않게 값을 변화할 여지가 있으므로 고차원 함수로 사용하면 이런 실수를 줄일 수 있다는 장점이 있습니다.


let numbers: [Int] = [2, 8, 15]

// 초깃값이 0이고 정수 배열의 모든 값을 뺌
let subtract: Int = numbers.reduce(0, { (first: Int,
second: Int) -> Int in
  print("(first) - (second)")
  return first - second
})

print(subtract) // -25
 
 



// 초깃값이 3이고 정수 배열의 모든 값을 더함
let sumFromThree = numbers.reduce(3) { $0 + $1 }
print(sumFromThree) // 28


/* var sum: Int = 3
for number in numbers {
  sum += number
}
*/

초깃값을 0이 아닌 3으로 지정할 수도 있습니다.

정리

크게 어려운 내용이 아닌 기초적인 내용이지만 클로저와 고차 함수를 사용해서 보다 Swift스럽게 코딩하는 데 도움이 될 것 같습니다. 전체 슬라이드는 아래를 참조하세요.


Let us go는 iOS 개발자로 일하시는 분, 이제 막 시작한 분, 그리고 이제 시작하려 하는 분 모두가 모여 서로 가지고 있는 지식을 공유하며 소통하고 서로 어울려 친해질 수 있는 편한 자리입니다.

본 영상과 글은 Let us go의 비디오 스폰서인 Realm에서 제공합니다. 모바일 개발자가 더 나은 앱을 더 빠르게 만들도록 돕는 Realm 모바일 데이터베이스Realm 모바일 플랫폼을 통해 핵심 로직에 집중하고 개발 효율을 높여 보세요! 공식 문서에서 단 몇 분 만에 시작할 수 있습니다. 또한 Realm 홈페이지에서는 모바일 개발자를 위한 다양한 최신 기술 뉴스와 튜토리얼을 제공하고 있으니 즐겨찾기하고 자주 들러 주세요!

다음: Swift 시작부터 RxSwift까지 #5: RxSwift 시작하기

General link arrow white

컨텐츠에 대하여

이 컨텐츠는 저자의 허가 하에 이곳에서 공유합니다.

조성규

iOS 개발자이자 교육자로 활동하고 있습니다. Swift와 iOS 그리고 컴퓨터과학에 관심이 많습니다.

4 design patterns for a RESTless mobile integration »

close