Swift/Swift 가이드

[Swift] 공식 문서 - 옵셔널 체이닝(Optional Chaining)

유정주 2022. 7. 28. 00:02
반응형

새로 배운 점

  • 옵셔널 체이닝은 2계층 이상의 프로퍼티나 메서드, 서브스크립트를 호출할 때에도 사용할 수 있습니다.
  • 옵셔널 체이닝을 사용하여 옵셔널 값에 대해 메서드를 호출하고 그 메서드 호출이 성공했는지 확인할 수 있습니다.
  • 이 방법은 메서드의 반환값이 정의되지 않은 경우에도 사용할 수 있습니다.
  • 메서드의 리턴 값에 추가 옵셔널 체이닝을 사용하고 싶다면, 물음표를 메서드의 괄호 뒤에 적으면 됩니다.

 

Optional Chaining

옵셔널 체이닝(Optional Chaining)은 현재 nil일 수도 있는

옵셔널 프로퍼티, 메서드, 서브스크립트를 참조하고 호출하는 과정입니다.

만약 옵셔널이 값을 가지고 있다면 프로퍼티, 메서드, 서브스크립트는 성공적으로 호출됩니다.

만약 nil이라면 nil을 반환합니다.

다중 질의도 함께 chaining 될 수 있으며, 만약 하나라도 nil이라면 전체 chain이 실패합니다.

 

Optional Chaining as an Alternative to Forced Unwrapping

옵셔널 체이닝은 호출하고자 하는 프로퍼티, 메서드, 서브스크립트가 nil이 아닌 경우,

옵셔널 값 뒤에 물음표를 붙여서 옵셔널 체이닝을 지정할 수 있습니다.

 

이는 옵셔널 값을 강제로 언래핑하는 느낌표를 붙이는 것과 비슷합니다.

가장 큰 차이점은 옵셔널이 nil일 때 옵셔널 체이닝은 nil을 리턴하지만,

강제 옵셔널 언래핑은 런타임 에러를 트리거합니다.

 

옵셔널 체이닝이 호출되어 nil을 반환할 수 있기 때문에 옵셔널 체이닝의 결과는 항상 옵셔널 값이어야 합니다.

이러한 옵셔널 값을 사용하여 옵셔널 체이닝이 성공했는지 아니면 nil을 반환했는지 확인할 수 있습니다.

옵셔널 체이닝의 결과는 보통 예상되는 반환값과 동일한 타입이지만, 옵셔널로 래핑됩니다.

따라서 일반적으로 Int를 반환하는 속성은 옵셔널 체이닝을 통해 참조할 때 Int?를 반환합니다.

 

아래 코드들은 옵셔널 체이닝과 강제 언래핑의 차이점을 보여줍니다.

먼저 Person과 Residence 클래스를 정의합니다.

class Person {
    var residence: Residence?
}
 
class Residence {
    var numberOfRooms = 1
}

Person 인스턴스는 옵셔널 Residence 타입 프로퍼티를 갖습니다.

 

새로운 Person 인스턴스를 생성하면 이 인스턴스의 residence 프로퍼티는 nil로 초기화됩니다.

let john = Person()

만약 이 사람의 residence의 numberofRooms 프로퍼티를 강제 언래핑한다면

언랩(unwrap)할 residence 값이 없기 때문에 런타임 에러가 발생합니다.

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

위의 코드에서 john.residence가 nil이 아니라면 성공했을 것이고, roomCount의 값은 Int값으로 설정될 것입니다.

그러나 위 코드는 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 프로퍼티를 chain하고,

만약 residence 값이 존재할 경우에 numberOfRooms의 값을 반환하도록 합니다.

numberOfRooms에 참조하려는 시도가 실패할 수 있기 때문에 옵셔널 체이닝은 Int? 타입의 값을 반환합니다.

위의 예시처럼 residence가 nil일 때, 이 옵셔널 Int 또한 nil이 됩니다.

numberOfRooms가 옵셔널이 아니어도 Int?를 리턴합니다.

 

Residence 인스턴스를 john.residence에 할당하면 nil이 아니게 됩니다.

john.residence = Residence()

john.residence는 이제 실제 Residence 인스턴스를 가집니다.

만약 전과 같이 동일한 옵셔널 체이닝으로 numberOfRooms에 참조하려고 시도하면,

numberOfRooms의 값을 가지고 있는 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)."

 

Defining Model Classes for Optional Chaining

옵셔널 체이닝은 2계층 이상의 프로퍼티나 메서드, 서브스크립트를 호출할 때에도 사용할 수 있습니다.

이는 상호 연관된 복잡한 모델에서 하위 속성으로 들어가서 해당 속성의

프로퍼티, 메서드, 서브스크립트에 참조할 수 있는지 여부를 확인할 수 있습니다.

 

아래 코드는 다중 옵셔널 체이닝을 포함해 네 가지 클래스를 정의합니다.

class Person {
    var residence: Residence?
}
 
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 클래스는 빈 배열로 초기화 되는 rooms 프로퍼티가 존재합니다.

연산 프로퍼티, 서브스크립트를 이용해 Room을 전달하고 관리합니다.

 

rooms 배열에 사용되는 Room 클래스는 다음과 같습니다.

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

마지막 Address 클래스는 3개의 옵셔널 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 클래스는 buildingNumber와 street이 있다면 붙여서 리턴하고,

buildingNumber만 있으면 buildingNumber를 리턴합니다.

둘 다 없으면 nil을 리턴합니다.

 

Accessing Properties Through Optional Chaining

위에서 설명한 것처럼 옵셔널 체이닝을 사용해 프로퍼티의 옵셔널 값에 액세스하고

액세스가 성공했는지 확인할 수 있습니다.

 

이제 위에서 정의한 클래스로 새로운 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.")
}

john.residence는 nil이기 때문에 옵셔널 체이닝을 실패합니다.

 

옵셔널 체이닝을 통해 프로퍼티의 값 대입을 시도할 수 있습니다.

let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
john.residence?.address = someAddress

아직 john의 residence는 nil이므로 값 대입은 실패합니다.

 

이 할당은 옵셔널 체이닝의 일부입니다.

즉, = 연산자 오른쪽에 있는 코드는 체크되지 않습니다.

아래 코드는 위와 동일한 할당을 수행하지만 address 인스턴스를 생성합니다.

func createAddress() -> Address {
    print("Function was called.")
 
    let someAddress = Address()
    someAddress.buildingNumber = "29"
    someAddress.street = "Acacia Road"
 
    return someAddress
}
john.residence?.address = createAddress()

위 코드를 실행해보면 createAddress()가 실행되지 않고, 출력도 하지 않습니다.

 

Calling Methods Through Optional Chaining

옵셔널 체이닝을 사용하여 옵셔널 값에 대해 메서드를 호출하고

그 메서드 호출이 성공했는지 확인할 수 있습니다.

이 방법은 메서드의 반환값이 정의되지 않은 경우에도 사용할 수 있습니다.

 

Residence 클래스의 printNumberOfRooms()는 현재 numberOfRooms의 값을 출력합니다.

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

이 메서드에 반환값은 없습니다.

그러나 반환값이 없는 함수나 메서드는 암시적으로 Void 타입의 리턴 타입을 갖습니다.

이 메서드는 빈 튜플인 ()값을 리턴한다는 것을 의미합니다.

 

만약 이 메서드를 옵셔널 체이닝으로 호출하면, 메서드의 리턴 타입은 Void?가 됩니다.

이는 if 구문을 사용하여 printNumberOfRooms()를 성공적으로 호출했는지 확인할 수 있습니다.

따라서 printNumberOfRooms 호출로부터 리턴값을 nil과 비교하여 호출이 성공적인지 확인할 수 있습니다.

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

 

Accessing Subscripts Through Optional Chaining

옵셔널 체이닝을 사용하여 옵셔널 값에 대한 서브스크립트로 값을 탐색하거나

설정하고 해당 서브스크립트 호출이 성공했는지 확인할 수 있습니다.

 

아래 예제는 Residence 클래스에 정의된 서브스크립트를 사용하여 john.residence 프로퍼티의 rooms 배열을 탐색합니다.

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

아직 residence는 nil이기 때문에 위의 시도도 실패합니다.

 

만약 Residence 인스턴스를 생성하고 1개 이상의 Rooms 인스턴스를

rooms 배열에 추가하여 john.residence에 할당하면,

Residence 서브스크립트를 사용해 rooms 배열에 참조할 수 있습니다.

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

 

Accessing Subscripts of Optional Type

만약 서브스크립트가 옵셔널 타입의 값을 반환한다면 물음표는 서브스크립트의 괄호 뒤에 적어야 합니다.

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]

 

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

위 코드는 john.residence?.address?.street에 접근합니다.

여기서는 2단계의 옵셔널 체이닝이 사용됩니다.

john.residence는 nil이 아니므로 성공하고 john.residence.address는 nil이므로 호출이 실패합니다.

 

위 예시에서 street 프로퍼티 타입은 옵셔널 String입니다.

따라서, john.residence?.address?.street의 반환 값 또한 String? 입니다.

 

이제 Address 인스턴스를 생성하고 할당하여, 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."

 

Chaining on Methods with Optional Return Values

위 예제에서 옵셔널 체이닝을 통해 어떻게 옵셔널 타입의 프로퍼티 값에 접근하는지 알아봤씁니다.

프로퍼티 뿐만 아니라 옵셔널 타입의 값을 반환하는 메서드에 대해서도

옵셔널 체이닝을 사용할 수 있습니다.

 

아래 예제 코드는 Address 클래스의 buildingIdentifier()를 옵셔널 체이닝을 통해 호출합니다.

이 메서드는 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 doesn't begin with \"The\".")
    }
}
// Prints "John's building identifier begins with "The"."

 

참고

https://docs.swift.org/swift-book/LanguageGuide/OptionalChaining.html

반응형