Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

옵셔널 체이닝 #52

Open
simoniful opened this issue Oct 29, 2022 · 0 comments
Open

옵셔널 체이닝 #52

simoniful opened this issue Oct 29, 2022 · 0 comments

Comments

@simoniful
Copy link
Owner

simoniful commented Oct 29, 2022

옵셔널 체이닝(optional chaining)은 프로퍼티, 메소드, 서브스크립트를 nil이 될 수 있는 옵셔널로 호출하고 쿼리하는 과정
만약 옵셔널이 값을 포함하고 있다면 성공적으로 호출하고, 옵셔널이 nil이면 nil을 반환
여러 쿼리문을 함께 연결할 수 있고, 연결에서 어느 하나라도 nil이면 전체 연결은 실패

Swift의 옵셔널 체이닝은 Objective-C의 nil 메시징과 유사하지만,
Swift는 어떤 타입에서든 작동하고 성공 / 실패를 확인 가능

강제 언래핑의 대안으로써 사용되는 옵셔널 체이닝

옵셔널 값 뒤에 물음표(?)를 붙임으로써 옵셔널 체이닝을 지정
옵셔널 값 뒤에 느낌표(!)를 붙여서 값을 강제 언래핑 하는 것과 유사
옵셔널 체이닝은 옵셔널이 nil이면 실패하지만, 강제 언래핑은 런타임 에러를 발생시킨다는 게 주요한 차이

옵셔널 체이닝이 nil 값을 호출할 수 있기 때문에
옵셔널 체이닝 호출의 결과는 항상 옵셔널 값이 되어야 한다
쿼리를 던진 프로퍼티, 메소드, 서브스크립트의 결과 값이 옵셔널이 아닌 값이 아니라도 옵셔널로 래핑되어 반환
해당 옵셔널 반환 값이 nil 인지 여부에 따라서 옵셔널 체이닝이 성공 / 실패 여부를 확인 가능하기에 if let 구문으로 활용 가능

옵셔널 체이닝 호출은 예상되는 결과 값과 동일한 타입에 옵셔널이 래핑된 값을 반환
ex. 옵셔널 체이닝을 통하면 보통 Int를 반환하는 프로퍼티는 Int?를 반환

다음 몇 가지 코드는 옵셔널 체이닝이 강제 언래핑과 어떻게 다른지 예시

// Person 클래스를 정의
class Person {
    // 인스턴스는 Residence? 타입의 residence 옵셔널 프로퍼티를 소유
    var residence: Residence?
}

// Residence 클래스를 정의
class Residence {
    // 인스턴스는 기본 값이 1인 하나의 정수 프로퍼티 numberOfRooms을 소유
    var numberOfRooms = 1
}

// 만약 새로운 Person 인스턴스를 만들면, residence 프로퍼티는 기본적으로 nil로 초기화
let john = Person()

// john의 residence에 있는 numberOfRooms에 접근 시도
// 강제 해제를 위한 !로 인하여 런타임 에러 발생
// john.residence가 nil이 아니고 roomCount가 정수 값을 포함하고 있어야 성공
// 하지만, residence는 nil이기 때문에 항상 런타임 에러가 발생

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

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

옵셔널 체이닝은 numberOfRooms 값에 접근하는 대안을 제공
옵셔널 체이닝을 사용하기 위해 느낌표 대신 물음표 사용
Swift에게 옵셔널 residence 프로퍼티의에 대해 "체인"을 시도하도록 하고,
residence가 있는 경우 numberOfRooms의 값을 쿼리하도록 한다
numberOfRooms에 접근하려는 시도가 잠재적으로 실패할 수 있기 때문에 옵셔널 체이닝은 Int? 타입의 값을 반환

residence가 nil이라면 Int?는 nil이며, 해당 옵셔널 Int는 Int로 언랩하는 옵셔널 바인딩을 통해 접근
roomCount 변수에 옵셔널이 아닌 값을 할당

numberOfRooms이 옵셔널이 아닌 Int인 경우, 해당 옵셔널 체인을 통해 쿼리된다는 건
numberOfRooms에 대한 호출이 항상 Int 대신 Int?를 반환한다는 것을 의미
john.residence에 Residence 인스턴스를 할당할 수 있고, 더 이상 nil이 아니게 된다

// john.residence는 이제 Residence 인스턴스를 포함하게 되고,
// 만약 옵셔널 체이닝으로 numberOfRooms에 접근한다면 1인 Int? 값을 반환
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)."

옵셔널 체이닝을 위한 모델 클래스 정의

옵셔널 체이닝을 한 레벨보다 깊은 프로퍼티, 메소드, 서브스크립트를 호출하는 데 사용 가능
이것은 상호 연괸된 타입의 복잡한 모델 내 서브 프로퍼티로 깊게 파고들 수 있도록 한다
이러한 계층의 프로퍼티, 메소드, 서브스크립트에 접근 여부도 확인도 가능

// Person 클래스를 정의
class Person {
    var residence: Residence?
}

// Residence 클래스를 정의
class Residence {
    // [Room] 타입의 빈 배열로 초기화되는 rooms 프로퍼티 소유
    var rooms = [Room]()

    // numberOfRooms 프로퍼티는 rooms에 대한 연산을 기반으로 하도록 계산 프로퍼티로 구현
    var numberOfRooms: Int {
        return rooms.count
    }

    // rooms 배열에 축약하여 접근하기 위해 읽기 전용 서브스크립트를 제공
    subscript(i: Int) -> Room {
        get {
            return rooms[i]
        }
        set {
            rooms[i] = newValue
        }
    }

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

    // Address? 타입의 프로퍼티인 address는 옵셔널 프로퍼티로 선언
    var address: Address?
}

// Room 클래스를 정의
// Residence 클래스의 rooms에서 사용하기 위한 모델 구성
class Room {
    let name: String
    init(name: String) { self.name = name }
}

// Address 클래스를 정의
// Residence 클래스의 address에서 사용하기 위한 모델 
// 세 개의 String? 타입의 옵셔널 프로퍼티를 소유
class Address {
    var buildingName: String?
    var buildingNumber: String?
    var street: String?

    // String? 타입을 반환하는 buildingIdentifier() 메소드
    // buildingName이 값을 갖고 있는지 확인하고 반환
    // treet도 값을 갖고 있다면 두 프로퍼티를 연결하여 반환
    // 이 외에는 nil 반환
    func buildingIdentifier() -> String? {
        if let buildingNumber = buildingNumber, let street = street {
            return "\(buildingNumber) \(street)"
        } else if buildingName != nil {
            return buildingName
        } else {
            return nil
        }
    }
}

옵셔널 체이닝을 통해 프로퍼티에 접근하기

옵셔널 체이닝을 통해 프로퍼티 값의 설정을 시도
'강제 언래핑의 대안으로써 사용되는 옵셔널 체이닝' 파트에서 설명된 대로
옵셔널 체이닝을 사용하여 옵셔널 값인 프로퍼티에 접근하고
해당 프로퍼티에 대한 접근이 성공적인지 확인할 수 있다

위에서 구성한 클래스를 활용하여 접근 해보자

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

새로운 Person 인스턴스를 생성하고 numberOfRooms 프로퍼티에 접근을 시도
john.residence가 nil이기 때문에 옵셔널 체이닝 호출은 이전처럼 실패

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

Address 클래스의 인스턴스 someAddress를 구성하고 프로퍼티들을 할당한 뒤
john.residence의 address 프로퍼티에 someAddress를 할당하는 것은 실패한다. 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 클래스를 정의
class Residence {
    // [Room] 타입의 빈 배열로 초기화되는 rooms 프로퍼티 소유
    var rooms = [Room]()

    // numberOfRooms 프로퍼티는 rooms에 대한 연산을 기반으로 하도록 계산 프로퍼티로 구현
    var numberOfRooms: Int {
        return rooms.count
    }

    // rooms 배열에 축약하여 접근하기 위해 읽기 전용 서브스크립트를 제공
    subscript(i: Int) -> Room {
        get {
            return rooms[i]
        }
        set {
            rooms[i] = newValue
        }
    }

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

    // Address? 타입의 프로퍼티인 address는 옵셔널 프로퍼티로 선언
    var address: Address?
}

printNumberOfRooms() 메소드는 반환 값을 지정하지 않았다
하지만, 반환 타입이 없는 함수와 메소드는 암시적으로 Void 타입을 반환
즉, 빈 튜플인 ()을 반환

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가 아니라 Void?다
옵셔널 체이닝을 통해 호출 했을 때 반환 값은 항상 옵셔널 타입이기 때문
메소드가 반환 값을 정의하지 않았더라도 if문을 사용하여,
printNumberOfRooms()을 호출하는 게 가능한지 확인할 수 있도록 구성 가능하다

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

옵셔널 체이닝을 통해 프로퍼티에 값을 설정하려고 시도한 경우와 동일하다
위의 옵셔널 체이닝을 통한 프로퍼티 접근 예제에서는
residence 프로퍼티가 nil인 경우에도 john.residence에 대한 주소 값을 설정하려고 시도한다
옵셔널 체이닝을 통해 프로퍼티를 설정하려고 하면 nil과 비교하여
프로퍼티가 성공적으로 설정되었는지 확인할 수 있는 Void? 유형의 값이 반환되게 된다

옵셔널 체이닝을 통해 서브스크립트에 접근하기

옵셔널 체이닝을 사용해 옵셔널 값에 대한 서브스크립트에 값을 설정하고, 검색하는 시도를 할 수 있다
서브스크립트 호출이 성공적이었는지도 확인 가능

만약 옵셔널 체이닝을 통해 옵셔널 값의 서브스크립트에 접근할 때는 서브스크립트의 대괄호 전에 물음표를 붙인다

아래 예시는 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."

1) 옵셔널 타입의 서브스크립트에 접근하기

만약 서브스크립트가 Swift의 딕셔너리 타입의 키 첨자와 같은 옵셔널 타입의 값을 반환한다면,
대괄호 뒤에 물음표를 붙여서 선택적 반환 값을 옵셔널 체인으로 연결한다

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]

위의 예제는 testScores라는 딕셔너리 타입의 변수 정의하는데,
여기에는 문자열 키에 Int 값의 배열을 매핑하는 두 개의 Key-Value 쌍이 포함되어 있다
해당 예제에서 옵셔널 체이닝을 사용하여 "Dave" 배열의 첫 번째 항목을 91로 설정하고,
"Bev" 배열의 첫 번째 항목을 1씩 증가시키며,
"Brian" 키에 대한 배열의 첫 번째 항목을 설정하려고 시도
testScores 딕셔너리에는 "Dave" 및 "Bev"에 대한 키가 포함되어 있기 때문에 처음 두 번의 호출이 성공
testScores 딕셔너리에는 "Brian"에 대한 키가 포함되어 있지 않기 때문에 세 번째 호출이 실패

체이닝의 다중 레벨 연결

프로퍼티, 메소드, 서브스크립트에 더 깊게 들어가기 위해 여러 단계에 걸쳐 옵셔널 체이닝을 연결 가능
하지만, 여러 단계의 옵셔널 체이닝은 반환 값에 더 많은 단계의 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() 메소드 그 자체가 아니라 메소드가 반환하는 결과 값이 옵셔널 값이기 때문

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant