※ 본 포스팅은 유튜브 Swift Programming Tutorial 강의를 기반으로 제가 직접 정리한 Swift 문법에 대한 글입니다.
Swift에 대해 처음 배워나가는 과정을 블로그에 정리하였습니다. 기본적으로 다른 프로그래밍 언어를 다룬 적이 있기에 제가 아는 부분은 생략될 수 있습니다. 최대한 틀린 부분이 있지 않게 적겠지만 혹시 잘못된 부분이 있으면 알려주세요!
< Closures >
Closure는 일종의 익명 function(함수)로서 코드 블록을 감싸고, 변수나 상수로 할당하거나 함수의 매개변수로 전달할 수 있는 기능입니다.
1. Closure의 구조
클로저의 기본 구조는 다음과 같습니다.
{ (매개변수) -> 반환 타입 in
// 클로저의 실행 코드
}
함수는 독립적으로 이름을 가지고 호출될 수 있지만, 클로저는 변수나 상수에 할당될 수 있습니다. (익명의 함수)
let add: (Int, Int) -> Int
= { (lhs: Int, rhs: Int) -> Int in
lhs + rhs
}
위의 코드는 클로저를 `add`라는 상수에 할당한 코드입니다. 필수적인 요소는 아니지만 위의 코드에서는 클로저 타입임을 명시했습니다. `(Int, Int) -> Int` 이후 { } 중괄호 안에 매개변수와 반환 타입 (클로저의 시그니처)를 적고 실행 코드를 적으면 됩니다.
클로저의 매개변수와 반환타입을 정의한 후 `in` 키워드를 사용하여 클로저의 구현 코드 블록이 시작합니다. 즉 `in`키워드를 기준으로 클로저의 시그니처와 실제 동작을 구분합니다.
여기서 lhs와 rhs는 내부적으로 이용되는 이름입니다. 실제로 해당 `add`상수를 불러올 때 이런 식으로 사용하게 됩니다.
add(20, 30)
그렇기에 함수와 달리 외부 매개변수 이름을 명시하지 않고 매개변수 값만 들어갑니다.
2. 클로저의 활용 - 함수 안의 클로저
클로저는 Swift에서 다양하게 사용됩니다. 하나의 예시로 함수 안에 클로저를 정의하여 사용하는 경우가 있습니다. 이는 함수 내에서 특정한 작업을 수행하는데 필요한 클로저를 직접 정의하여 활용하는 방식입니다. 이에 대해 설명하기 위해 useless 한 function을 보여드리겠습니다.
func customAdd(
_ lhs: Int,
_ rhs: Int,
using function: (Int, Int) -> Int
) -> Int {
function(lhs, rhs)
}
위 코드를 보시면 `customAdd`라는 함수에는 3개의 매개변수가 있음을 알 수 있습니다.
1. 외부이름 생략된 lhs(내부이름을 가진 매개변수)
2. 외부이름 생략된 rhs(내부이름을 가진 매개변수)
3. 외부이름이 using 이고 내부이름이 function인 클로저
즉 `customAdd`함수는 세 개의 매개변수를 이용하여 작업을 수행하고, 세 번째 매개변수로 전달된 클로저를 호출하여 결과를 반환합니다.
3. Trailing Closure Syntax (후행 클로저)
trailing closure란 위 2번처럼 함수 안에서 매개변수로 클로저가 활용되는 곳에서 사용할 수 있는 문법입니다.
클로저가 함수의 마지막 인자로 전달되는 경우, 클로저를 괄호 `( )` 밖으로 이동하여 더 간결하게 표현할 수 있습니다.
위의 코드와 연결된 예시를 통해 설명드리겠습니다.
customAdd(20, 30, using: {(lhs: Int, rhs: Int) -> Int in
lhs + rhs
})
원래 함수의 세 번째 매개변수인 closure을 사용하려면 위와 같이 `using`이라는 지정된 외부 매개변수 이름을 사용해서 써야 합니다. 하지만 Trailing Closure Syntax에 의해 이는 아래와 같이 적을 수 있습니다.
customAdd(20, 30) {(lhs: Int, rhs: Int) -> Int in
lhs + rhs
}
이렇게 함수의 괄호 ( )를 닫은 다음 클로저 { }를 적어주는 형태입니다.
이처럼 함수 호출의 마지막 인자가 클로저인 경우, (마지막인 경우에만 해당됩니다) 클로저를 소괄호 밖에 작성하고 함수 호출 괄호 내부에는 위와 같이 Int 인자 두 개만 존재하는 것처럼 작성할 수 있습니다.
4. 클로저의 간결한 표현식 ( + Swift의 타입추론)
클로저 코드도 생략해서 쓸 수 있는 표현식이 있습니다. 다만 큰 프로젝트에서는 타입추론에 시간이 걸리기 때문에 지양하는 것이 좋습니다. 현재 배우는 과정에서는 간결하게 쓸 수 있는 표현식에 대해 알아보겠습니다.
위의 `customAdd` 함수를 보시면 이미 함수를 정의할 때 두 개의 클로저의 Int 매개변수와 Int 반환타입을 명시해 뒀기 때문에 아래와 같이 Int 데이터 타입을 생략해도 컴파일러가 타입추론이 가능합니다.
customAdd(
20,
30
) { (lhs, rhs) in
lhs + rhs
}
이를 더 간결하게 표현할 수 있는 표현식이 있습니다. 아래의 코드를 한 번 보겠습니다.
customAdd(20,30) { $0 + $1 }
아예 변수 정보와 `in`키워드를 지우고 이렇게 표현해도 위와 똑같은 표현식이 됩니다.
Swift에서는 $ 기호를 가지고 각각 인자들의 인덱스를 $와 함께 표시할 수 있습니다. 그렇기에 여기서 $0은 0번 인덱스 인자, 즉 lhs를 의미하고 $1은 1번 인덱스 인자, 즉 rhs를 의미합니다.
이렇게 간결한 표현식으로 클로저를 나타낼 수 있지만 간결해지면 간결해질수록 컴파일러가 데이터 타입을 추론하는 게 어려워지니 주의해서 사용해야 합니다.
5. 클로저의 활용 - 정렬 함수
`ages`라는 정수 배열을 정렬하는데 클로저가 어떻게 활용되는지 코드를 통해 살펴보겠습니다.
let ages = [30, 20, 19, 40]
ages.sorted(by: {(lhs: Int, rhs: Int) -> Bool in
true
})
// result : [40, 19, 20, 30]
`sorted(by:) ` 함수의 인자로 클로저가 전달되며, 배열의 요소를 비교하여 정렬 순서를 결정하는 역할을 합니다.
해당 정렬은 클로저 구문안이 항상 true로 설정되어 있습니다. 저기서 항상 true인 경우는 첫 번째 요소가 두 번째 요소보다 정렬되어야 할 경우 true를 반환해야 한다는 정의에 따라 정렬알고리즘을 사용해 요소들이 정렬되었습니다. 이렇게 정렬한 결과는 우리가 아는 오름차순, 내림차순 정렬을 표현하지 않습니다. 그러나 이 true구간에 다른 조건을 부여하면 오름차순, 내림차순 정렬이 가능합니다.
ages.sorted(by: {(lhs: Int, rhs: Int) -> Bool in
lhs > rhs
})
// result: [40, 30, 20, 19]
위의 코드는 내림차순 정렬을 보여줍니다. 반대로 `lhs < rhs`이면 오름차순 정렬이 됩니다. 위에서 설명했던 4번 간결한 표현식으로 이를 아래와 같이 줄일 수 있습니다.
ages.sorted(by: { $0 > $1 })
// result: [40, 30, 20, 19]
너무 훅 간결해진 기분이 들 수 있습니다. 하지만 차근차근 생략된 순서를 생각해 보시면 (여기서는 그 과정을 하나하나 짚진 않았습니다) 구문 자체는 명확한 표현식임을 알 수 있습니다.
사실 sorted(by:) 함수는 여기서 더 간결하게 표현할 수 있는 방법도 있습니다. 바로 연산자 함수를 이용하는 겁니다. 연산자 함수(Operator function)는 연산자(operator)를 사용하여 기능을 수행하는 함수입니다.
예를 들어 우리가 아는 비교 연산자인 `>`의 동작을 나타내는 연산자 함수를 살펴보겠습니다.
func > (lhs: Int, rhs: Int) -> Bool {
lhs > rhs
}
연산자 함수의 설명을 위해 든 예시입니다. 이 함수는 실제로는 자기 자신을 재귀적으로 호출하고 있기 때문에 무한 루프가 발생합니다. 여기서 말하고자 했던 것은 이전 sorted(by:) 다음의 `(lhs: Int, rhs:Int) -> Bool` 이 sorted(by:) 함수와 연산자 함수의 시그니처가 똑같은 function signature이라는 점입니다. 그렇기에 연산자만을 사용하여 의미하는 바를 정확히 표현할 수 있다면 연산자 함수를 통해 더 간결한 표현식을 만들 수 있습니다.
ages.sorted(by: >) // descending
ages.sorted(by: <) // ascending
6. 클로저의 활용 - 함수를 클로저로 전달
func add10To( value: Int) -> Int {
value + 10
}
func add20To( value: Int) -> Int {
value + 20
}
func doAddition(
on value: Int,
using function: (Int) -> Int
) -> Int {
function(value)
}
doAddition(
on: 20,
using: add10To(value:)
)
// result : 30
doAddition(
on: 20,
using: add20To(value:)
)
// result : 40
함수를 클로저로 사용할 때에는 함수 이름만 사용하는 대신 함수 이름을 클로저의 값으로 전달할 수 있습니다.
위의 코드를 보시면 doAddition 의 두 번째 매개변수는 using이라는 외부이름과 function이라는 내부이름을 가진 클로저입니다.
해당 함수를 사용한 인스턴스를 보면 using: 에 add10To(value:) 함수를 적음으로서 클로저에 함수를 전달할 수 있음을 보여줍니다. 이처럼 Swift에서는 클로저와 함수 간의 유연한 상호작용이 가능합니다.
여기까지 Closures 에 대한 정리였습니다. 다음 Swift 편은 Structures 에 대한 이야기로 돌아오겠습니다.
강의는 freeCodeCamp 채널의 영상인데, Vandad라는 분이 진행하는 7시간 분량의 full tutorial for beginners입니다. 해당 강의는 프로그래밍 언어를 Swift로 처음 접하는 사람들에게는 다소 어려울 수 있다고 생각합니다. 저는 기초적인 부분의 디테일한 중요성을 깊게 알려주는 것 같아서 강의를 택했습니다. 영어 강의이며 영어자막 또는 자동번역 기능이 제공되기에, 혹시 프로그래밍 언어를 다룬 적이 있는 분들 중 Swift에 대해 새롭게 배우고자 한다면 강추드립니다!
해당 강의 영상은 아래에 걸어두겠습니다. 혹시 틀린 부분이나 추가하고 싶은 부분이 있으면 알려주세요.
강의 전체를 시리즈로 적을 수 있도록 노력하겠습니다.
<이전글>
2023.05.08 - [TIL/Swift] - [Swift] Swift 기본 문법 정리 1. Variables
2023.05.15 - [TIL/Swift] - [Swift] Swift 기본 문법 정리 2. Operators & If-else
2023.05.23 - [TIL/Swift] - [Swift] Swift 기본 문법 정리 3. Functions