Swift 정리 - 14. 옵셔널 체이닝


Swift의 옵셔널 체이닝에 대해 알아보자.


Swift.org:Optional Chaining

Optional Chaining

옵셔널 체이닝은 nil일 수 있는 프로퍼티, 메서드, 서브스크립트를 호출하거나 사용하는 절차다.

  • 옵셔널이 값을 가지고 있다 : 프로퍼티, 메서드, 서브스크립트 호출이 성공
  • 옵셔널이 nil : 프로퍼티, 메서드, 서브스크립트 호출이 nil을 리턴한다.

이런 쿼리들을 여러 번 엮어서 사용할 수 있는데, 한 곳에서라도 nil이라면 전체 체인의 결과가 nil이 된다.

Forced Unwrapping 대신 Optional Chaning

옵셔널이 nil이 아닐 경우 이 값을 불러오길 원할 때 옵셔널 값 뒤에 물음표를 붙여 옵셔널 체이닝을 표현한다. 이는 옵셔널 값을 강제로 unwrapping라기 위해 느낌표를 붙이는 것과 비슷하다. 이 둘의 차이는 옵셔널 체이닝은 옵셔널이 nil일 경우 아름답게(?) 종료되지만, 강제 옵셔널 추출의 경우 옵셔널 값이 nil이면 런타임 에러가 발생한다.

옵셔널 체이닝이 nil 값에 호출될 수 있기 때문에, 프로퍼티, 메서드, 서브스크립트의 리턴 값이 옵셔널이 아닌 값이더라도 옵셔널 체이닝 호출의 결과는 항상 옵셔널 값이다.

class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}

let john = Person()

let roomCount = john.residence!.numberOfRooms
// this triggers a runtime error

// 옵셔널 `residence` 프로퍼티에 체인을 걸고, 만약 존재한다면 값을 추출한다는 것을 의미한다.
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 = Residence()

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)."

Optional Chaning을 위한 모델 클래스 정의하기

옵셔널 체이닝을 더 깊이 있게 사용해보자. 서로 관련 있는 복잡한 모델들의 프로퍼티에 접근하고, 이런 프로퍼티들의 또 다른 프로퍼티, 메서드, 서브스크립트에 접근할 수 있는지 확인할 수 있다.

아래 코드는 멀티레벨 옵셔널 체이닝의 예제를 보여줄 4개의 모델 클래스를 정의한다.

Person class

class Person {
    var residence: Residence?
}

Residence class

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?
}

Room class

class Room {
    let name: String
    init(name: String) { self.name = name }
}

Address class

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
        }
    }
}

Optional Chaining으로 프로퍼티 접근하기

위에서도 봤듯이 옵셔널 프로퍼티에서 값을 추출할 수 있는지 확인하기 위해 옵셔널 체이닝을 사용할 수 있다.

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."

프로퍼티의 값을 설정한느 것도 옵셔널 체이닝을 통해 할 수 있다.

let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
// john.residence가 nil이기 때문에 someAddress를 할당할 수 없다.
john.residence?.address = someAddress

Optional Chaning으로 메서드 호출하기

printNumberOfRooms() 메서드는 아래와 같다.

func printNumberOfRooms() {
    print("The number of rooms is \(numberOfRooms)")
}

옵셔널 값의 메서드를 옵셔널 체이닝으로 호출할 때, 메서드의 리턴 타입은 Void가 아닌 Void?가 된다. 메서드 자체가 리턴하는 값이 없더라도 if문에서 메서드를 호출할 수 있는지를 확인할 수 있다.

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."

옵셔널 체이닝을 통해 값을 설정하는 것도 마찬가지다. 옵셔널 체이닝을 통해 프로퍼티의 값을 설정하려는 것은 Void? 타입의 값을 리턴하고, 이를 통해 프로퍼티의 값이 성공적으로 할당됐는지를 확인할 수 있다.

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."

Optional Chaining으로 서브스크립트 접근하기

옵셔널 값에 서브스크립트를 사용해서 값을 가져오거나 설정할 때도 옵셔널 체이닝을 사용할 수 있고, 이 호출이 성공적으로 일어났는지도 확인할 수 있다.

옵셔널 체이닝을 통해 옵셔널 값에 서브스크립트를 호출하게 되면 서브스크립트 괄호 이전에 물음표를 붙여야 한다. 옵셔널 체이닝의 물음표는 항상 옵셔널인 표현 바로 뒤에 붙어야 한다.

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?[0] = Room(name: "Bathroom")

Multiple Levels of Chaining

여러 겹으로 중첩된 프로퍼티, 메서드, 서브스크립트에 접근하기 위해 여러 레벨의 옵셔널 체이닝을 사용할 수 있다. 참고로 접근하는 프로퍼티의 타입이 옵셔널이 아니더라도 옵셔널 체이닝을 통해 추출한 값은 옵셔널 타입이 된다.

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."

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."

Early Exit

guard 문은 if 같이 표현의 bool 값에 따라 특정 문장을 실행하거나 실행하지 않는다. guard문은 조건이 참임을 확실히 하고 싶을 때 사용한다. 조건이 참이면 guard 문 이후의 문장이 실행된다. guard문은 항상 뒤에 else가 따라오는데, else 뒤의 괄호에 속하는 코드는 조건이 거짓일 때 실행된다.

func greet(person: [String: String]) {
    guard let name = person["name"] else {
        return
    }

    print("Hello \(name)!")

    guard let location = person["location"] else {
        print("I hope the weather is nice near you.")
        return
    }

    print("I hope the weather is nice in \(location).")
}

greet(person: ["name": "John"])
// Prints "Hello John!"
// Prints "I hope the weather is nice near you."
greet(person: ["name": "Jane", "location": "Cupertino"])
// Prints "Hello Jane!"
// Prints "I hope the weather is nice in Cupertino."

조건이 만족하지 않으면 else 괄호 안의 코드가 실행된다고 했는데, 여기에서는 guard 문이 위치한 코드 블럭에서 빠져나오는 문장이 필요하다. 따라서 guard문은 자신을 감싸는 코드 블럭이 있을 때만 쓸 수 있다. 이 코드 블럭을 빠져나오는 것은 return, break, continue, throw, fatalError 등을 통해 이를 할 수 있다.

guard는 옵셔널 바인딩의 역할도 할 수 있다. guard 문에 위치한 옵셔널 바인딩에 값이 있다면 조건이 참이라고 여겨지고, 아닐 경우 else 부분 내의 코드가 실행된다. 만약 값이 있다면 옵셔널 바인딩 된 상수나 변수를 guard 문 이후에 지역상수, 지역변수 처럼 사용할 수 있다.

위 코드 예시에서 guard 부분을 다시 보자. 옵셔널 바인딩을 통해 값이 있는지 확인했고, 바인딩 된 location 상수는 guard 문 이후에 지역 상수처럼 사용되고 있다.

guard let location = person["location"] else {
    print("I hope the weather is nice near you.")
    return
}

조건을 더 구체적으로 추가하고 싶으면 쉼표로 추가조건을 표시하면 된다. 쉽표로 추가된 조건들은 AND(&&)로 추가된 것과 같은 효과를 띈다.

guard let location = person["location"], location == "한국" else {
    print("I hope the weather is nice near you.")
    return
}