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

윤여송·2023년 8월 7일
0

Swift

목록 보기
20/28
post-thumbnail

옵셔널 체이닝(Optional Chaining)

언래핑 없이 옵셔널 값의 멤버에 접근합니다.

옵셔널 체이닝(Optional chaining)은 현재 nil 일 수 있는 옵셔널 인 프로퍼티, 메서드, 그리고 서브 스크립트를 조회하고 호출하기 위한 프로세스 입니다. 옵셔널에 값이 포함되어 있으면 프로퍼티, 메서드, 또는 서브 스크립트는 호출에 성공합니다. 옵셔널이 nil이면 프로퍼티, 메서드, 또는 서브 스크립트 호출은 nil을 반환합니다. 여러 조회는 함께 연결될 수 있고 체인에 어느 부분이라도 nil이면 전체 체인은 실패합니다.

Note
Swift에서 옵셔널 체이닝은 Objective-C에서 메시징 nil과 유사하지만 모든 타입에 대해 동작하고 성공 또는 실패 여부를 확인할 수 있습니다.

강제 언래핑의 대안으로 옵셔널 체이닝(Optional Chaining as an Alternative to Forced Unwrapping)

프로퍼티, 메서드 또는 서브 스크립트를 호출하려는 옵셔널 값 뒤에 물음표 (?)를 배치하여 옵셔널 체이닝을 지정합니다. 이것은 값에 강제 언래핑을 하기 위해 옵셔널 값 뒤에 느낌표(!)를 배치하는 것과 유사합니다. 이것의 주요 차이점은 옵셔널이 nil일 때 옵셔널 체이닝은 실패하는 반면에 강제 언래핑은 런타임 에러가 발생합니다.

옵셔널 체이닝은 nil 값에 대해 호출될 수 있다는 사실을 반영하기 위해 조회하는 프로퍼티, 메서드 또는 서브 스크립트가 옵셔널 값이 아닌 값을 반환하더라도 항상 옵셔널 값으로 반환합니다. 옵셔널 반환 값으로 옵셔널 체이닝 호출이 성공(반환된 옵셔널 체이닝에 값이 포함됨)했는지 실패(반환된 옵셔널 값은 nil) 했는지 확인할 수 있습니다.

특히 옵셔널 체이닝 호출의 결과는 예상되는 반환값과 동일한 타입이지만 옵셔널로 래핑됩니다. 일반적으로 Int 로 반환하는 프로퍼티는 옵셔널 체이닝을 통해 접근하면 Int?를 반환합니다.

다음의 코드들은 옵셔널 체이닝이 강제 언래핑과 어떻게 다른지 보여주고 성공여부를 확인할 수 있도록 합니다.

첫번째, PersonResidence 라는 2개의 클래스는 정의되어 있습니다.

class Person {
	var residence: Residence?
}

class Residence{
	var numberOfRooms = 1
}

Residence 인스턴스는 기본값이 1numberOfRooms라는 Int 프로퍼티를 가지고 있습니다. Person인스턴스는 Residence? 타입의 옵셔널 residence 프로퍼티를 가지고 있습니다.

let john = Person()

값에 강제 언래핑을 하기 위해 residence 뒤에 느낌표를 배치하여 사람의 residencenumberOfRooms 프로퍼티를 접근하면 residence 값이 없기 때문에 런타임 에러가 발생합니다.

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

John.residencenil 값이 아니고 roomCount에 방의 적절한 숫자를 포함한 Int로 설정하면 위의 코드는 정상동작 합니다. 그러나 residencenil 이면 이 코드는 항상 런타임 에러가 발생합니다.

옵셔널 체이닝은 numberOfRooms의 값에 접근하기 위한 대안으로 제공합니다. 옵셔널 체이닝을 사용하기 위해 느낌표 위치에 물음표를 사용합니다.

if let roomCount = john.residence?.numberOfRooms {
	print("John's residence has \(roomCount) room(s).")
} else {
	print("Unable to retrieve the nubmer of rooms.")
}
//Prints "Unalbe to retrieve the number of rooms."

이것은 Swift가 옵셔널 residence 프로퍼티를 "체인"하고 residence가 존재하면 numberOfRooms 값을 조회하도록 합니다.

numberOfRooms 접근하기 위한 시도는 실패할 수 있으므로 옵셔널 체이닝은 Int? 타입이나 "옵셔널Int"의 값을 반환합니다. 위 예제에서 처럼 residencenil일 경우 numberOfRooms 접근이 불가능한 사실을 반영하기 위해 옵셔널 Intnil입니다. 옵셔널 Int는 언래핑 한 정수에 옵셔널 바인딩으로 접근되고 roomCount 상수에 옵셔널이 아닌 값을 할당합니다.

numberOfRooms가 옵셔널 Int가 아니더라도 마찬가지입니다. 옵셔널 체인으로 조회 된다는 것은 numberOfRooms 호출은 항상 Int대신 Int?를 반환한다는 의미입니다.

더이상 nil 값을 갖지 않기 위해 john.residenceResidence 인스턴스를 할당할 수 있습니다.

john.residence = Residence()

john.residencenil이 아닌 실제 Residence 인스턴스를 포함합니다. 이전 처럼 같은 옵셔널 체이닝으로 numberOfRooms에 접근하면 기본 numberOfRooms 값인 1Int?를 반환할 것입니다.

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)

하나 이상의 레벨 깊이인 프로퍼티, 메서드, 그리고 서브 스크립트를 호출하기 위해 옵셔널 체이닝을 사용할 수 있습니다. 타입 호환되는 복잡한 모델 내에서 하위 프로퍼티로 내려갈 수 있으며 해당 하위 프로퍼티에 프로퍼티, 메서드, 그리고 서브스크립트에 접근 가능한지 확인할 수 있습니다.

아래의 코드는 여러 레벨 옵셔널 체이닝의 예를 포함하여 몇몇의 후속 예제에서 사용할 4개의 모델 클래스를 정의합니다.
이 클래스는 관련 프로퍼티, 메서드, 그리고 서브 스크립트를 가지는 RoomAddress 클래스를 추가하여 위의 PersonResidence 모델을 확장합니다.

class Person{
	var residence: Residence?
}

Residence 클래스는 이전보다 더 복잡합니다. 이제 Residence 클래스는 [Room] 타입의 빈 배열을 가지고 초기화 되는 rooms라는 변수 프로퍼티를 정의합니다.

class Residence{
	var rooms: [Room] = []
    var numberOfRooms: Int{
    	return rooms.count
    }
    subscript(i: Int) -> Roomg{
    	get {
        	return rooms[i]
        }
        set {
        	rooms[i] = newValue
        }
    }
    func printNumberOfRooms(){
    	print("The number of rooms is \(numberOfRooms)")
    }
    var address: Address?
}

Residence 버전은 Room 인스턴스의 배열을 저장하기 때문에 numberOfRooms 프로퍼티는 저장된 프로퍼티가 아닌 계산된 프로퍼티로 구현됩니다. 계산된 numberOfRooms 프로퍼티는 rooms 배열에서 count 프로퍼티의 값을 반환합니다.

rooms 배열에 접근하는 짧은 구문을 위해 Residencerooms 배열에 요청된 인덱스로 방에 접근을 제공하는 읽기-쓰기 서브 스크립트를 제공합니다.

Residence 는 주택에 있는 방의 갯수를 출력하는 printNumberOfRooms라는 메서드도 제공합니다.

마지막으로 ResidenceAddress? 타입을 가지는 address 라는 옵셔널 프로퍼티를 정의합니다. 이 프로퍼티에 대한 Address 클래스 타입은 아래에 정의되어 있습니다.

rooms 배열에 사용된 Room 클래스는 name 이라는 프로퍼티와 적절한 방 이름을 프로퍼티에 설정하는 초기화 구문을 가진 클래스 입니다.

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

이 모델의 마지막 클래스는 Address 라 합니다. 이 클래스는 String? 타입의 3개의 옵셔널 프로퍼티를 가집니다. buildingNamebuildingNumber인 첫번째, 두번째 프로퍼티는 주소의 부분으로 특정 건물을 식별하기 위한 대안책입니다. 세번째 프로퍼티인 street는 주소에 대한 도로의 이름으로 사용됩니다.

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과 연결된 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.")
}
// Prints "Unable to retrieve the number of rooms."

John.residencenil 이기 때문에 옵셔널 체이닝은 이전과 같이 실패를 호출합니다. 옵셔널 체이닝을 통해 프로퍼티의 값을 설정할 수 있습니다.

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

이 예제에서 john.residencenil이기 때문에 john.residenceaddress 프로퍼티에 설정하기 위한 시도는 실패 할 것입니다.

이 할당은 = 연산자의 우항의 코드는 평가되지 않으므로 옵셔널 체이닝의 일부입니다. 이전 예제에서 상수에 접근하는 것은 어떠한 영향도 없기 때문에 someAddress가 평가되지 않는다는 것을 쉽게 파악할 수 없습니다. 아래의 리스트는 같은 할당을 수행하지만 주소를 생성하기 위해 함수를 사용합니다. 이 함수는 값을 반환하기 전에 "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() 함수가 호출되지 않음을 알 수 있습니다.

옵셔널 체이닝을 통한 함수 호출(Calling Methods Through Optional Chaining)

옵셔널 값의 메서드를 호출하고 메서드 호출이 성공적인지 확인하기 위해 옵셔널 체이닝을 사용할 수 있습니다. 해당 메서드가 반환값을 정의하지 않아도 사용할 수 있습니다.

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

이 메서드는 반환 타입이 지정되지 않았습니다. 그러나 반환 타입이 없는 함수와 메서드는 Void의 암시적 반환 타입을 가지고 있습니다. 이것은 ()의 값 또는 빈 튜플을 반환한다는 의미입니다.

옵셔널 체이닝을 사용하여 옵셔널 값에 대해 메서드를 호출하면 반환값은 옵셔널 타입이기 때문에 메서드의 반환 타입은 Void가 아닌 Void? 입니다. 메서드가 반환값을 정의하지 않았어도 printNumberOfRooms() 메서드 호출이 가능한지 if 구문을 사용하여 확인할 수 있습니다. 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."

옵셔널 체이닝을 통해 프로퍼티를 설정하려는 경우에도 마찬가지입니다. 위의 예제는 residence 프로퍼티가 nil이지만 John.residence에 대해 address 값을 설정 하려고 합니다. 옵셔널 체이닝을 통해 프로퍼티를 설정하려는 모든 시도는 nil과 비교하여 프로퍼티에 값이 성공적으로 설정되었는지 확인할 수 있는 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 addresss."

옵셔널 체이닝을 통한 서브 스크립트 접근(Accessing Subscripts Through Optional Chaining)

옵셔널 값의 서브 스크립트에서 값을 조회하고 설정하고 해당 서브 스크립트 호출이 성공했는지 확인하기 위해 옵셔널 체이닝을 사용할 수 있습니다.

Note
옵셔널 체이닝을 통해 옵셔널 값의 서브 스크립트에 접근할 때 물음표는 서브 스크립트의 대괄호 전에 위치합니다. 옵셔널 체이닝 물음표는 항상 옵셔널 표현구 부분의 바로 다음에 위치합니다.

아래의 예제는 Residence 클래스에 정의된 서브 스크립트를 사용하여 john.residence 프로퍼티에 rooms 배열의 첫번째 방 이름을 조회합니다. 현재 john.residencenil 이므로 서브 스크리브 호출은 실패합니다.

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

residence가 현재 nil이므로 서브 스크립트 설정은 실패합니다.
rooms 배열에 하나 이상의 Room 인스턴스 가지고 john.residence에 실제 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)

서브 스크립트가 Swift의 Dictionary 타입의 키 서브 스크립트와 같이 옵셔널 타입의 값을 반환하는 경우 옵셔널 반환값을 연결하기 위해 서브 스크립트의 닫는 대괄호 뒤에 물음표를 추가합니다.

var testScores = ["Dave": [86, 82, 94], "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]

위의 예제는 String키를 Int 값 배열에 매핑하는 2개의 키-값 쌍을 포함하는 testScores 라는 딕셔너리를 정의합니다. 이 예제는 "Dave" 배열에 첫번째 항목에 91을 설정하고 "Bev" 배열에 첫번째 항목에 1을 더하고 "Brian" 키에 대한 배열에 첫번째 항목에 값을 설정하기 위해 옵셔널 체이닝을 사용합니다. testScores 딕셔너리는 "Dave""Bev"에 대한 키를 가지고 있으므로 첫번째 두번째 호출은 성공합ㄴ디ㅏ. testScores 딕셔너리는 "Brian"에 대한 키를 가지고 있지 않으므로 세번째 호출은 실패합니다.

여러 수준의 체인연결(Linking Multiple Levels of Chaining)

여러 수준의 옵셔널 체이닝을 연결하여 모델 내에서 프로퍼티, 메서드 그리고 서브 스크립트로 깊게 접근할 수 있습니다. 그러나 여러 수준의 옵셔널 체이닝은 반환된 값에 더 많은 수준의 옵션성을 추가하지 않습니다.
다른 말로 표현하면:

  • 조회하려는 타입이 옵셔널이 아니면 옵셔널 체이닝 때문에 옵셔널이 됩니다.
  • 조회하려는 타입이 이미 옵셔널이면 체이닝 때문에 더 많은 옵셔널이 되지 않습니다.

따라서 :

  • 옵셔널 체이닝으로 Int 값을 조회하려고 하면 사용된 체이닝의 수준과 상관없이 항상 Int?가 반환됩니다.
  • 유사하게 옵셔널 체이닝으로 Int? 값을 조회하려고 하면 사용된 체이닝의 수준과 상관없이 항상 Int?가 반환됩니다.

아래의 예제는 jhonresidence 프로퍼티에 addresss 프로퍼티에 street 프로퍼티를 접근합니다. 여기에서는 residenceaddress 프로퍼티를 통해 연결하기 위해 2단계 수준의 옵셔널 체인이 사용되며 둘 다 옵셔널 타입입니다.

if let johnsStreet = john.residence?.address?.street{
	print("John's street name is \(johnsStreet).")
} else {
	print("Unable to retrieve the address.")
}
// Pritns "Unable to retrieve the address."

john.residence 의 값은 현재 유효한 Residence 인스턴스를 가지고 있습니다 .그러나 john.residence.address의 값은 현재 nil입니다. 이것 때문에 john.residence?.address?.street을 호출하면 실패합니다.

위의 예에서는 street 프로퍼티의 값을 조회하려고 합니다. 이 프로퍼티의 타입은 String? 입니다. 따라서 john.residence?.address?.street의 반환값은 2단계 옵셔널 체이닝이 프로퍼티의 옵셔널 타입에 적용되었지만 String?입니다.
john.residence.address에 대한 값으로 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."

이 예제에서 john.residence의 값은 현재 유효한 Residence 인스턴스 이므로 john.residenceaddress 프로퍼티에 값을 설정하는 것이 가능합니다.

옵셔널 반환값으로 메서드 체이닝(Chaining on Methods with Optinal Return Values)

이전 예제는 어떻게 옵셔널 체이닝을 통해 옵셔널 타입의 프로퍼티의 값을 조회해야 하는지 보여줍니다. 옵셔널 체이닝은 또한 옵셔널 타입의 값을 반환하는 메서드를 호출하고 필요하다면 메서드의 반환값을 연결하기 위해 사용할 수 있습니다.
아래의 예제는 옵셔널 체이닝을 통해 Address 클래스의 buildingIdentifier() 메서드를 호출합니다. 이 메서드는 String? 타입의 값을 반환합니다. 위에서 설명 했듯이 옵셔널 체이닝 이후 이 메서드 호출의 반환 타입은 String?입니다.

if let buildingIdentifier = john.residnece?.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"."

Note
위의 예제에서 연결하려는 옵셔널 값이 buildingIdentifier() 메서드 자체가 아닌 buildingIdentifier() 메서드의 반환값이므로 소괄호 뒤에 옵셔널 체이닝 물음표를 위치시킵니다.

profile
y_some__velog

0개의 댓글