옵셔널 체이닝(optional chaining)
은 프로퍼티, 메소드, 서브스크립트를 nil
이 될 수 있는 옵셔널로 호출하고 질의하는 것이다. 만약 옵셔널이 값을 포함하고 있다면 성공적으로 호출된다. 옵셔널이 nil
이면 nil
을 반환한다. 여러 질의를 함께 연결할 수 있고, 연결에서 어느 하나라도 nil
이면 전체 연결은 실패한다.
Swift의 옵셔널 체이닝은 Objective-C의
nil
메시징과 유사하지만, Swift는 어떤 타입에서든 작동하고 성공/실패를 확인할 수 있다.
옵셔널 값 뒤에 물음표(?)를 붙임으로써 옵셔널 체이닝을 지정한다. 옵셔널 값 뒤에 느낌표(!)를 붙여서 값을 강제 언래핑 하는 것과 유사하다. 옵셔널 체이닝은 옵셔널이 nil
이면 실패하지만, 강제 언래핑은 런타임 에러를 발생시킨다는 게 주요한 차이점이다.
옵셔널 체이닝이 nil
값을 호출할 수 있기 때문에 옵셔널 체이닝 호출의 결과는 항상 옵셔널 값이 되어야 한다. 질의를 던진 프로퍼티, 메소드, 서브스크립트의 결과 값이 옵셔널이 아닌 값을 반환하더라도 그렇다. 옵셔널 체이팅 호출이 성공했는지 혹은 nil
때문에 실패했는지를 확인하기 위해 옵셔널 반환 값을 사용할 수 있다.
옵셔널 체이닝 호출은 예상되는 결과 값과 동일한 타입에 옵셔널이 붙여진 값을 반환한다. 옵셔널 체이닝을 통하면 보통 Int
를 반환하는 프로퍼티는 Int?
를 반환한다.
다음 몇 가지 코드는 옵셔널 체이닝이 강제 언래핑과 어떻게 다른지를 보여 준다.
첫 번째로, Person
과 Residence
두 클래스를 정의한다.
class Person {
var residence: Residence?
}
class Residence {
var numberOfRooms = 1
}
Residence
인스턴스는 기본 값이 1인 하나의 정수 프로퍼티 numberOfRooms
를 갖고 있다. Person
인스턴스는 Residence?
타입의 residence
옵셔널 프로퍼티를 가진다.
만약 새로운 Person
인스턴스를 만들면, residence
프로퍼티는 기본적으로 nil
로 초기화된다.
let john = Person()
만약 john
의 residence
에 있는 numberOfRooms
에 접근하려고 한다면, 값을 강제 언래핑 하기 위해 residence
뒤에 붙어있는 느낌표가 런타임 에러를 발생시킬 것이다. 언래핑 할 residence
가 없기 때문이다.
let roomCount = john.residence!.numberOfRooms
// this triggers a runtime error
위의 코드는 john.residence
가 nil
이 아니고 roomCount
가 정수 값을 포함하고 있어야 성공한다. 하지만 residence
는 nil
이기 때문에 항상 런타임 에러가 발생한다.
옵셔널 체이닝은 numberOfRooms
값에 접근하는 대안을 제공한다. 옵셔널 체이닝을 사용하기 위해 느낌표 대신 물음표를 붙인다.
if let roomCount = john.residence?.numberOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
// Prints "Unable to retrieve the number of rooms."
이는 Swift에게 옵셔널 residence
프로퍼티의에 대해 "체인"을 하도록 하고, residence
가 있는 경우 numberOfRooms
의 값을 검색하도록 한다.
numberOfRooms
에 접근하려는 시도가 잠재적으로 실패할 수 있기 때문에 옵셔널 체이닝은 Int?
타입의 값을 반환한다. residence
가 nil
이라면 Int?
는 nil
이 된다. 이 옵셔널 Int
는 정수를 언랩하는 옵셔널 바인딩을 통해 접근되고, roomCount
변수에 옵셔널이 아닌 값을 할당한다.
numberOfRooms
이 옵셔널이 아닌 Int
인 경우에도 마찬가지다. 옵셔널 체인을 통해 질의된다는 사실은 numberOfRooms
에 대한 호출이 항상 Int
대신 Int?
를 반환한다는 것을 뜻한다.
john.residence
에 Residence
인스턴스를 할당할 수 있고, 더 이상 nil
이 아니게 된다.
john.residence = Residence()
john.residence
는 이제 Residence
인스턴스를 포함하고 있고, 만약 옵셔널 체이닝으로 numberOfRooms
에 접근한다면 1인 Int?
값을 반환할 것이다.
if let roomCount = john.residence?.numberOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
// Prints "John's residence has 1 room(s)."
옵셔널 체이닝을 한 레벨보다 깊은 프로퍼티, 메소드, 서브스크립트를 호출하는 데 사용할 수 있다. 이것은 상호 연괸된 타입의 복잡한 모델 내 서브프로퍼티로 깊게 파고들 수 있도록 한다. 이러한 서브프로퍼티의 프로퍼티, 메소드, 서브스크립트에 접근할 수 있는지 확인할 수 있다.
아래의 코드는 네 가지 모델 클래스를 정의한다.
Person
클래스는 전과 같은 방식으로 정의한다.
class Person {
var residence: Residence?
}
Residence
클래스는 전보다 복잡하다. [Room]
타입의 빈 배열로 초기화되는 rooms
프로퍼티를 클래스에서 정의한다.
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript(i: Int) -> Room {
get {
return rooms[i]
}
set {
rooms[i] = newValue
}
}
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}
var address: Address?
}
이 버전의 Residence
가 Room
인스턴스의 배열을 저장하기 때문에 numberOfRooms
프로퍼티는 계산 프로퍼티로 구현된다.
rooms
배열에 축약하여 접근하기 위해 읽기 전용 서브스크립트를 제공한다. Address?
타입의 프로퍼티인 address
는 옵셔널 프로퍼티로 선언한다.
rooms
배열에서 사용되는 Room
클래스는 name
프로퍼티 하나를 갖는 간단한 클래스다.
class Room {
let name: String
init(name: String) { self.name = name }
}
Address
클래스는 세 개의 String?
타입의 옵셔널 프로퍼티를 갖고 있다.
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier() -> String? {
if let buildingNumber = buildingNumber, let street = street {
return "\(buildingNumber) \(street)"
} else if buildingName != nil {
return buildingName
} else {
return nil
}
}
}
Address
클래스는 String?
타입을 반환하는 buildingIdentifier()
메소드를 갖고 있다. 이 메소드는 buildingName
이 값을 갖고 있는지 확인하고 반환한다. street
도 값을 갖고 있다면 두 프로퍼티를 연결하여 반환한다. 이외에는 nil
을 돌려 준다.
옵셔널 체이닝을 사용해 옵셔널 값을 갖는 프로퍼티에 접근하고 프로퍼티에 대한 접근이 성공적인지 확인할 수 있다.
새로운 Person
인스턴스를 생성하고 numberOfRooms
프로퍼티에 접근을 시도한다.
let john = Person()
if let roomCount = john.residence?.numberOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
// Prints "Unable to retrieve the number of rooms."
john.residence
가 nil
이기 때문에 옵셔널 체이닝 호출은 이전처럼 실패했다.
옵셔널 체이닝을 통해 프로퍼티의 값을 설정할 수 있다.
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
john.residence?.address = someAddress
john.residence
의 address
프로퍼티를 설정하는 것은 실패한다. john.residence
가 nil
이기 때문이다.
이 할당은 옵셔널 체이닝의 일부이다. 할당 연산자(=)의 오른쪽 항은 실행되지 않는다. 위 예씨에서 someAddress
에 접근할 때 어떤 사이드 이펙트도 발생하지 않기 때문에 someAddress
가 평가되지 않는다는 걸 알아채기가 쉽지 않다. 아래 코드는 동일한 할당이지만, address
를 만들기 위해 함수를 사용한다. 함수는 할당연산자의 우항이 평가되는 경우 값을 반환하기 전에 “Function was called”를 출력한다.
func createAddress() -> Address {
print("Function was called.")
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
return someAddress
}
john.residence?.address = createAddress()
아무것도 출력되지 않기 때문에 createAddress()
가 호출되지 않았음을 알 수 있다.
옵셔널 체이닝을 사용해 옵셔널 값에 대한 메소드를 호출하고, 메소드가 성공했는지 여부를 판단할 수 있다. 메소드가 반환 값을 정의하지 않은 경우에도 가능하다.
Residence
클래스의 printNumberOfRooms()
메소드는 numberOfRooms
의 현재 값을 출력한다.
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}
이 메소드는 반환 값을 지정하지 않았다. 하지만 반환 타입이 없는 함수와 메소드는 암시적으로 Void
타입을 반환한다. 빈 튜플인 ()
을 반환한다는 뜻이다.
만약 옵셔널 체이닝으로 옵셔널 값에 이 메소드를 호출한다면, 메소드의 반환 값은 Void
가 아니라 Void?
가 된다. 옵셔널 체이닝을 통해 호출했을 때 반환 값은 항상 옵셔널 타입이기 때문이다. 이는 if
문을 사용해 printNumberOfRooms()
을 호출하는 게 가능한지 확인할 수 있도록 한다. 메소드가 반환 값을 정의하지 않았더라도 말이다.
if john.residence?.printNumberOfRooms() != nil {
print("It was possible to print the number of rooms.")
} else {
print("It was not possible to print the number of rooms.")
}
// Prints "It was not possible to print the number of rooms."
if (john.residence?.address = someAddress) != nil {
print("It was possible to set the address.")
} else {
print("It was not possible to set the address.")
}
// Prints "It was not possible to set the address."
옵셔널 체이닝을 통해 프로퍼티에 값을 설정하려고 시도한 경우와 동일하다.
옵셔널 체이닝을 사용해 옵셔널 값에 대한 서브스크립트에 값을 설정하고, 검색하는 시도를 할 수 있다. 서브스크립트 호출이 성공적이었는지 확인할 수도 있다.
만약 옵셔널 체이닝을 통해 옵셔널 값의 서브스크립트에 접근할 때는 서브스크립트의 대괄호 전에 물음표를 붙인다.
아래 예시는 john.residence
프로퍼티의 rooms
배열 중 첫 번째 방의 이름을 검색한다. Residence
클래스에 정의된 서브스크립트를 사용한다. john.residence
가 현재 nil
이기 때문에 서브스크립트는 실패한다.
if let firstRoomName = john.residence?[0].name {
print("The first room name is \(firstRoomName).")
} else {
print("Unable to retrieve the first room name.")
}
// Prints "Unable to retrieve the first room name."
서브스크립트에서 호출하는 옵셔널 체이닝 물음표는 john.residence
뒤, 대괄호 앞에 위치한다. john.residence
가 옵셔널 값이기 때문이다.
비슷하게, 옵셔널 체이닝과 서브스크립트를 통해 새 값을 설정할 수 있다.
john.residence?[0] = Room(name: "Bathroom")
residence
가 nil
이기 때문에 이는 실패한다.
만약 실제 Residence
인스턴스에 rooms
배열에 하나 이상의 Room
인스턴스를 가진 john.residence
를 만들고 할당한다면, 옵셔널 체이닝으로 rooms
배열에 있는 아이템에 접근하기 위해 Residence
서브스크립트를 사용할 수 있다.
let johnsHouse = Residence()
johnsHouse.rooms.append(Room(name: "Living Room"))
johnsHouse.rooms.append(Room(name: "Kitchen"))
john.residence = johnsHouse
if let firstRoomName = john.residence?[0].name {
print("The first room name is \(firstRoomName).")
} else {
print("Unable to retrieve the first room name.")
}
// Prints "The first room name is Living Room."
만약 서브스크립트가 옵셔널 타입의 값을 반환한다면, 대괄호 뒤에 물음표를 붙인다.
var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]
testScores["Dave"]?[0] = 91
testScores["Bev"]?[0] += 1
testScores["Brian"]?[0] = 72
// the "Dave" array is now [91, 82, 84] and the "Bev" array is now [80, 94, 81]
프로퍼티, 메소드, 서브스크립트에 더 깊게 들어가기 위해 여러 단계에 걸쳐 옵셔널 체이닝을 연결할 수 있다. 하지만 여러 단계의 옵셔널 체이닝은 반환 값에 더 많은 단계의 optionality
를 추가하지는 않는다.
다시 말하자면,
그러므로,
Int
를 검색할 경우 Int?
가 반환된다. 몇 단계의 연쇄를 거치든 상관 없다.Int?
를 검색하면 Int?
가 반환된다. 여러 단계를 거쳤다고 해서 Int?????
와 같이 반환되지는 않는다.아래 예시는 john
의 residence
프로퍼티 안에 있는 address
프로퍼티의 street
프로퍼티에 접근하는 것을 보여 준다. 여기에는 두 단계의 옵셔널 체이닝이 사용됐다.
if let johnsStreet = john.residence?.address?.street {
print("John's street name is \(johnsStreet).")
} else {
print("Unable to retrieve the address.")
}
// Prints "Unable to retrieve the address."
john.residence
은 현재 유효한 값을 포함하고 있다. 하지만 john.residence.address
가 현재 nil
이기 때문에 john.residence?.address?.street
를 호출하는 것은 실패한다.
street
프로퍼티의 타입이 String?
이기 때문에 john.residence?.address?.street
의 반환 값은 항상 String?
이다.
만약 실제 Address
인스턴스를 john.residence.address
에 설정하고 street
프로퍼티에 값을 할당하면, 다중 레벨 옵셔널 체이닝을 통해 street
프로퍼티에 접근할 수 있다.
let johnsAddress = Address()
johnsAddress.buildingName = "The Larches"
johnsAddress.street = "Laurel Street"
john.residence?.address = johnsAddress
if let johnsStreet = john.residence?.address?.street {
print("John's street name is \(johnsStreet).")
} else {
print("Unable to retrieve the address.")
}
// Prints "John's street name is Laurel Street."
이전 예시는 옵셔널 체이닝을 통해 옵셔널 타입의 프로퍼티 값을 검색하는 방법을 보여 줬다. 옵셔널 타입 값을 반환하는 메소드를 호출하고 그 메소드의 반환 값에 연쇄하기 위해 옵셔널 체이닝을 사용할 수 있다.
아래 예시는 Address
클래스의 buildingIdentifier()
메소드를 옵셔널 체이닝을 통하여 호출한다. 이 메소드는 String?
타입의 값을 반환한다. 위에서 묘사한 것처럼, 옵셔널 체이닝 이후 이 메소드 호출의 결과 타입 역시 String?
이다.
if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
print("John's building identifier is \(buildingIdentifier).")
}
// Prints "John's building identifier is The Larches."
만약 메소드의 반환 값에서 추가적인 옵셔널 체이닝을 하고자 할 경우, 메소드의 소괄호 뒤에 물음표를 붙인다.
if let beginsWithThe =
john.residence?.address?.buildingIdentifier()?.hasPrefix("The") {
if beginsWithThe {
print("John's building identifier begins with \"The\".")
} else {
print("John's building identifier does not begin with \"The\".")
}
}
// Prints "John's building identifier begins with "The"."
위의 예시에서 옵셔널 체이닝 물음표를 소괄호 뒤에 표기했다.
buildingIdentifier()
메소드 그 자체가 아니라 메소드가 반환하는 결과 값이 옵셔널 값이기 때문이다.