본 게시글은 The Ultimate Guide to Closures in Swift를 바탕으로 작성하였습니다.
Swift 클로저 사용을 위한 궁극의 가이드
이 튜토리얼은 Swift의 클로저(closure)를 여러분에게 상세히 안내해 줄 것입니다. 클로저란 여러분이 마치 변수에 값을 대입하거나 매개변수로서 전달하듯 소스 코드 안에서 주고 받을 수 있는 코드 블럭입니다. 클로저를 완전히 이해하는 것은 iOS 개발을 배우는 과정에서 중대한 부분입니다.
여러분이 옵셔널(optional)에 대해 이해하는 데 어려움이 있었다면 클로저를 이해하는 것은 더욱 큰 일이 될 지도 모릅니다. 하지만 걱정마시기 바랍니다. 클로저는 보기보다 해롭지 않습니다. 알고보면 매우 유용합니다.
이 튜토리얼에서는 다음과 같은 것을 설명할 것입니다.
- 클로저란 무엇이고 어떻게 사용할 것인가
- 클로저를 선언하고 호출하는 구문
- 클로저의 타입을 구성하고 해체하는 방법
- “클로징 오버(closing over)”란 무엇이고 캡처 리스트(capture list)는 어떻게 사용할 것인가?
- completion handler로서 클로저를 어떻게 사용할 것인가
준비되었다면 지금부터 시작하겠습니다.
클로저란 무엇이고 어떻게 사용할 것인가
클로저에 대한 Apple의 공식 설명은 다음과 같습니다.
클로저(closure)란 여러분의 소스 코드에서 전달될 수도 있고 사용될 수도 있는 독립적인 기능성 블록입니다(Closures are self-contained blocks of functionality that can be passed around and used in your code).
다르게 설명하면 클로저는 여러분이 변수에 대입시킬 수 있는 코드 조각입니다. 그 다음 함수와 같이 소스 코드 내 다른 곳으로 전달할 수 있습니다. 클로저를 받은 함수는 그 클로저를 호출하여 내부에 담긴 코드를 실행시킬 수 있습니다. 이 떄는 클로저가 마치 일반적인 함수처럼 역할을 합니다.
여러분도 알다시피 Swift 코드에서 변수는 정보를 보관하기 위해 있습니다. 그리고 함수(메소드)들은 작업을 실행할 수 있습니다. 여기에 클로저가 들어가게 되면 함수 구문을 변수에 보관하여 전달할 수도 있고, 어딘가에서는 그 구문을 실행할 수도 있습니다.
클로저를 이해하기 위해 비유를 들어 보면 다음과 같습니다.
(1) Bob이 Alice에게 지시사항을 주면서 이렇게 말합니다. “손을 흔들어 보시오!” Alice는 이 지시사항을 듣고 손을 흔듭니다. 이 때 손을 흔드는 행위는 함수라고 본다면 Bob은 함수를 직접적으로 호출한 것입니다.
(2) Alice는 본인의 나이를 쪽지에 적어서 Bob에게 건네줍니다. 이 쪽지는 변수이고, Alice는 어떤 데이터를 보관하기 위해 쪽지를 활용한 것입니다.
(3) Bob은 “손을 흔들어 보시오!”를 쪽지에 적어서 Alice에게 건네줍니다. Alice는 이 쪽지를 읽고 손을 흔듭니다. 이 때 쪽지에 적힌 이 지시사항을 ‘클로저”라 합니다.
(4) 비유하자면 훨씬 나중에 우리는 이 쪽지를 우편을 통해 전달할 수도 있고 Alice가 본인의 작업을 다 마친 후에 이 쪽지를 읽어보게끔 할 수도 있습니다. 이런 특징이 클로저가 갖는 이점 중 하나입니다.
이해가 되었는지요? Swift에서 클로저를 작성해 보겠습니다.
// Swift
let birthday = {
print("Happy Birthday!")
}
birthday()
위 코드에서 무엇이 일어나는지 보겠습니다.
첫 번째 줄에서 여러분은 클로저를 하나 정의하여 변수 birthday
에 대입하였습니다. 클로저는 중괄호 { }
사이에 둘러싸인 모양을 하고 있습니다. 함수와 비슷하게 생겼음을 확인할 수 있겠지요? 클로저는 변수에 대해 대입 연산자=
가 사용됨을 확인합니다.
마지막 줄에서 클로저는 호출됩니다. 상수의 이름인 birthday
에 괄호 ()
가 붙어서 호출 및 실행이 되는데, 이는 함수와도 비슷해 보입니다.
위 코드를 실행하면 클로저는 birthday()
로서 실행되고 "Happy birthday!"라는 print()
함수에 의해 문자열이 출력됩니다.
이 때 작성한 클로저의 타입 및 이 클로저를 보관하고 있는 birthday
의 타입은 () -> ()
입니다. 클로저의 타입에 대해서는 나중에 좀 더 설명합니다.
클로저에 매개변수를 추가해 보겠습니다. ‘매개변수(parameter)’란, 함수와 클로저를 위한 입력 값입니다. 함수에서 매개변수를 지정할 때와 마찬가지로 클로저도 이를 가질 수 있습니다.
// Swift
let birthday: (String) -> () = { name in
print("Happy birthday, \(name)!")
}
birthday("Arthur")
소스코드가 약간 더 복잡해졌습니다. 이를 하나씩 분해해가며 어떤 일들이 일어나는지 살펴보겠습니다.
(1) 이전과 같이 클로저를 선언하여 birthday
상수에 대입한 후 맨 마지막 줄에서 이 클로저를 호출합니다.
(2) 이 클로저는 String
타입의 매개변수 하나를 갖습니다. 그러므로 이 클로저의 타입은 (String) -> ()
으로 정의됩니다.
(3) 여러분은 클로저 안에서 이 매개변수 하나를 name
이라는 이름으로서 사용할 수 있습니다. 클로저가 호출될 때 매개변수에 대한 값도 지정되어야 합니다.
어떻게 이해가 되십니까? 필수적으로 가장 중요한 세 가지는 다음과 같습니다.
(1) 클로저 타입은 (String) -> ()
이다.
(2) 클로저의 표현식은 { name in ... }
이다.
(3) 클로저의 호출은 birthday()
이다.
Swift의 일반적인 함수(메소드)와는 다르게, 클로저의 매개변수에는 이름이 지정되지 않습니다. 위 예제처럼 String
과 같이 매개변수의 타입은 선언되더라도 그 이름은 지정되지 않고 있습니다.
클로저 표현식인 { name in ... }
부분에서 여러분은 클로저의 첫 번째 매개변수에 name
이라는 지역변수를 할당했습니다. 이 지역변수는 클로저 내용 안에서 매개변수에게 이름을 부여해줍니다. 지역변수의 이름은 여러분이 임의로 부여하고 상관없습니다.
사실 여러분은 매개변수에 굳이 이름을 부여하지 않아도 다음과 같이 $0
과 같은 ‘숏 핸드(shorthand)’를 사용할 수 있습니다.
// Swift
let birthday: (String) -> () = {
print("Happy birthday, \($0)!")
}
위 코드에서 birthday
에 보관되는 클로저는 하나의 매개변수를 가지고 있습니다. 이 클로저 안에서 숏 핸드 $0
는 첫 번째 매개변수를 참조하게 됩니다.
Swift에서 일반적으로 함수(메소드)의 매개변수에서는 이름이 명시되지만(이를 ‘argument label’이라 합니다), 여러분이 클로저를 호출할 때 어떤 매개변수에도 이름을 지정하지 않았습니다. 이것이 함수(메소드)와의 차이입니다. 이 예제의 실행 결과는 다음과 같습니다.
어떻게 작동되는 것인지 보이십니까? 우리는 이 클로저를 마치 birthday(_:)
라는 함수처럼 호출하였고 이 때 String
형 매개변수인 "Arthur"
를 전달하였습니다. 매개변수는 이름이 명시되지 않고 오로지 값만을 취합니다. 앞서 살펴보았듯이 이 매개변수는 클로저 안에서 지역변수에 할당됨으로써 그 클로저 안에서 이름을 갖게 될 것입니다.
여기까지 살펴본 내용을 요약하면 다음과 같습니다.
(1) 클로저는 소스 코드 안에서 전달이 가능한 한 블록의 코드 조각이다.
(2) 클로저는 하나 이상의 매개변수를 가질 수도 있고 매개변수가 없을 수도 있다.
(3) 모든 클로저는 타입을 가지고 있다.
다음 장에서는 클로저의 ‘타입(type)”에 대해 좀 더 자세히 살펴보겠습니다.