본문 바로가기

문법

[Swift] 옵셔널 체이닝(Optional Chaning)과 빠른종료(guard) 알아보기

주의 : 영문서 번역 중 오역이 있을 수 있으며 정확하지않는 정보가 있을 수 있습니다.

 

옵셔널체이닝과 빠른종료
- 옵셔널 체이닝
- 빠른종료
- 포인트 정리
- 궁금증

 

# 옵셔널체이닝

값이 중첩된 형태를 띄어야 제몫을 발휘하는 친구이며 옵셔널을 이해하지 못한다면 스위프트의 절반도 이해하지 못한것과 마찬가지이다.

옵셔널 체이닝이 없다면 옵셔널은 정말로 귀찮고 또 귀찮은 존재일 수 밖에 없다. 이번 장에서는 옵셔널을 좀더 편리하게 사용할 수

있는 옵셔널 체이닝과 빠른 종료 문법에 대해 알아보자 

Optional Chaning

해석)

옵셔널 체이닝이란 현재 값이 nil일 수 있수도 있는 옵셔널에 대한 메서드나 프로퍼티와 서브스크립트를 캐묻거나 호출하기위한 방식이다.

만일 옵셔널타입에 값이 포함되 있으면 프로퍼티나 메서드 혹은 서브스크립트를 호출하고 만약 값이 nil이거나 없을 경우 nil을 반환한다.

동시에 여러개를 캐물을 수도 있다. 그리고 여러개를 캐묻는 도중 하나의 nil의 값이 있을경우 값 확인을 실패하여 nil을 반환한다.

 

NOTE

느낌표(!)

물음표 대신 느낌표를 사용할 수 도 있는데 이는 옵셔널에서 값을 강제 추출하는 효과과 있다. 물음표를 사용하는 것과 가장 큰 차이점은 값을 강제로 추출하기때문에 값이 없을 경우에 런타임오가 발생한다. 또 다른점은 옵셔널에서 값을 강제추출해 반환하기떄문에 반환값이 옵셔널이 아니라는 점이다. 하지만 정말 100% nil이 아니라는 확신을 하더라도 사용을 지양하는편이 좋다.

 

옵셔널 체이닝을 알아보는 클래스를 설계해보자

 

사람의 주소 정보 표현 설계

class Room {            //호실
    var number: Int     // 호실 번호
    
    init(number: Int) {
        self.number = number
    }
}

class Building {        // 건물
    var name: String    // 건물 이름
    var room: Room?     // 호실 정보
    
    init(name: String) {
        self.name = name
    }
}

struct Address { //주소
    var province: String    //광역시/도
    var city: String        //시/군/구
    var street: String      //도로명
    var building: Building? //건물
    var detailAddress: String? //건물 외 상세 주소
    
    init(province: String, city: String, street: String) {
        self.province = province
        self.city = city
        self.street = street
    }
}

class Person { //사람
    var name: String        //이름
    var address: Address?   //주소
    
    init(name: String) {
        self.name = name
    }
}

 

사람의 정보를 표현하기 위해 Person클래스를 설계했다. Person클래스는 이름이 있으며 주소를 옵셔널로 갖는다. 주소 정보는 Address 구조체로 설계했다. 주소에는 광역시/도, 시/군/구, 도로명이 필수며, 건물 정보가 있거나 건물이 아니면 상세주소를 기재할 수 있도록 했다. 건물정보는 Building 클래스로 설계했다. 건물은 이름이 있으며 호실의 정보를 갖는다. 호실 정보는 Room클래스로 설계했으며 각 호실은 번호를 갖는다.

 

사람의 인스턴스를 생성해보자

 

quokka 인스턴스 생성

let quokka = Person(name: "quokka")

quokka이 사는 호실 번호를 알고싶다. 옵셔널 체이닝과 강제추출을 사용하여 프로퍼티에 접근해보면 같은 결과를 볼수 있다.

 

옵셔널 체이닝 문법

let quokkaRoomViaOptionalChaning: Int? = quokka.address?.building?.room?.number

//let quokkaRoomViaOptionalUnwraping: Int = quokka.address!.building!.room!.number
// 오류발생

 

quokka에는 아직 주소, 건물, 호실정보가 없다. quokkaRoomViaOptionalChaning상수에 호실 번호를 할당하려고 옵셔널 체이닝 도중 nil이 반환된다. 그러나 quokkaRoomViaOptionalUnwraping상수에 호실 번호를 할당할 때는 강제 추출을 시도했기 때문에 nil인 address 프로퍼티에 접근하려 할 때 런타임 오류가 발생합니다.

 

옵셔널 바인딩 사용

let quokka2 = Person(name: "quokka2")

var roomNumber: Int? = nil

if let quokkaAddress: Address = quokka2.address {
    if let quokkaBuilding: Building = quokkaAddress.building {
        if let quokkaRoom: Room = quokkaBuilding.room {
            roomNumber = quokkaRoom.number
        }
    }
}

if let number: Int = roomNumber {
    print(number)
} else {
    print("Can not find room number")
}

// 옵셔널 체이닝 사용
let quokka3 = Person(name: "quokka3")

if let roomNumber = quokka3.address?.building?.room?.number {
    print(roomNumber)
} else {
    print("Can not find room number")
}

완전히 똑같은 결과를 내놓지만, 코드의 간결함과 분량은 꽤 차이가 크다. 그런데 재미있는 점은 옵셔널 체이팅 코드가 옵셔널 바인딩 기능과 결합했따는 점이다. 옵셔널 체이닝의 결괏값은 옵셔널 값이기 때문에 옵셔널 바인딩과 결합할 수 있다.

 

- 체이닝으로 값을 확인하는 도중 하나라도 값이 없으면 nil이 할당된다.

 

코드에 바로 아래를 작성하면 아직 quokka의 address프로퍼티가 없으며 그 하위의 building프로퍼티도 room프로퍼티도 없다. 그렇기 때문에 옵셔널 체이닝은 동작 도중에 중지 될 것이다. number 프로퍼티는 존재조차 하지 않으므로 505가 할당되지 않는 것이 물론이다.

 

옵셔널 체이닝을 통한 값 할당

quokka3.address = Address(province: "서울시", city: "용산구", street: "이태원동")
quokka3.address?.building = Building(name: "퀔캌")
quokka3.address?.building?.room = Room(number: 0)
quokka3.address?.building?.room?.number = 505

print(quokka3.address?.building?.room?.number)//Optional(505)

옵셔널 체인에 존재하는 프로퍼티를 실제로 할당해준 후 옵셔널 체이닝을 통해 값이 정상적으로 반환되는 것을 확인할 수 있다. 옵셔널 체이닝을 통해 메서드와 서브스크립트 호출도 가능하다. 서브스크립트는 인덱스를 통해 값을 넣고 빼올 수 있는 기능이다. 아직 서브스크립트에 대해 배우지는 않았지만 차후에 사용하게 되므로 기억해두자.

 

먼저, 옵셔널 체이닝을 통한 메서드 호출이다. 호출 방법은 프로퍼티 호출과 동일하다. 만약 메서드의 반환타입이 옵셔널이라면 이또한 옵셔널 체인에서 사용가능하다

 

예제로 살펴보자

 

옵셔널 체이닝을 통한 메서드 호출

struct Address2 {
    var province: String        //광역시/도
    var city: String            //시/군/구
    var street: String          //도로명
    var building: Building?     //건물
    var detailAddress: String?  //건물 외 상세주소
    
    init(province: String, city: String, street: String) {
        self.province = province
        self.city = city
        self.street = street
    }
    
    func fullAddress() -> String? {
        var restAddress: String? = nil
        
        if let buildingInfo = self.building {
            restAddress = buildingInfo.name
        } else if let detail = self.detailAddress {
            restAddress = detail
        }
        
        if let rest: String = restAddress {
            var fullAddress: String = self.province
            
            fullAddress += " " + self.city
            fullAddress += " " + self.street
            fullAddress += " " + rest
            
            return fullAddress
        } else {
            return nil
        }
    }
    
    func printAddress() {
        if let address: String = self.fullAddress() {
            print(address)
        }
    }
}

 

서브스크립트의 옵셔널 체이닝을 살펴보자

 

옵셔널 체이닝을 통한 서브스크립트 호출

let optionalArray: [Int]? = [1, 2, 3]
optionalArray?[1]

let optionalArray2: [Int?] = [nil, 1]
optionalArray2[1]

var optionalDictionary: [String: [Int]]? = [String: [Int]]()
optionalDictionary?["numberArray"] = optionalArray
optionalDictionary?["numberArray"]?[2] // 3

 

 

# 빠른종료

빠른종료(Early Exit)의 핵심 키워드는 guard이다. guard 구문은 if구문과 유사하게 Bool타입의 값으로 동작하는 기능이다. guard 뒤에 따라붙는 코드의 실행결과가 true일 때 코드가 계속실행된다. if 구문과는 다르게 guard구문은 항상 else구문이 뒤에 따라와야 한다. 만약 guard 뒤에 따라오는 Bool 값이 false라면 else의 블록 내부 코드를 실행하게 되는데 이때 else구문의 블록 내부에는 꼭 자신보다 상위의 코드 블록이 종료하는 코드가 들어가게 된다. 그래서 특정 조건에 부합하지 않다는 판단이 되면 재빠르게 코드의 블록의 실해을 종료할 수 있다. 이렇게 현재 코드 블록을 종료할때는 return, break, continue, throw등의 제어문 전환 명령을 사용한다. 또는 fatalError()와 같은 함수나 메서드를 호출할 수도 있다.

 

guard 구문을 사용하면 if코드를 훨씬 간결하고 읽기 좋게 구성할 수 있다. if 구문을 사용하면 예외사항을 else블록으로 처리해야하지만

예외사항만을 처리하고 싶다면 guard구문을 사용하는 것이 훨씬 간편하다.

해석)

가드 문 조건의 선택적 바인딩 선언에서 값이 할당된 모든 상수 또는 변수는 가드 문의 엔클로징 범위의 나머지 부분에 사용할 수 있다.

 

같은 역할을 하는 if 구문과 guard 구문

// if 구문을 사용한 코드
for i in 0...3 {
    if i == 2 {
        print(i)
    } else {
        continue
    }
}

// guard 구문을 사용한 코드
for i in 0...3 {
    guard i == 2 else {
        continue
    }
    print(i)
}

Bool 타입의 값으로 guard 구문을 동작시킬 수 있지만 옵셔널 바인딩의 역할도 할 수 있다. guard뒤에 따라오는 옵셔널 바인딩 표현에서 옵셔널의 값이 있는 상태라면 guard 구문에서 옵셔널 바인딩된 상수를 guard구문이 실행된 아래 코드부터 함수내부의 지역상수처럼 사용할 수 있다.

 

guard 구문의 옵셔널 바인딩 활용

func great(_ person: [String: String]) {
    guard let name: String = person["name"] else {
        return
    }
    
    print("Hello \(name)")
    
    guard let location: String = person["location"] else {
        print("I hope the weather is nice near you")
        return
    }
    print("I hope the weather is nice in \(location)")
}

var personInfo: [String: String] = [String: String]()
personInfo["name"] = "Jenny"

great(personInfo)
// Hello Jenny!

// I hope the weather is nice near you

personInfo["location"] = "Korea"

great(personInfo)
// Hello Jenny!
// I hope the weather is nice in Korea

 

guard를 통해 옵셔널 바인딩 된 상수 great(_:) 함수 내에서 지역상수처럼 사용된것을 볼 수있다. 그러면 우리가 옵셔널 체이닝에서 작성했던 코드를 조금더 발전시켜보겠다. 작성했던 Address 구조체의 fullAddress() 메서드를 조금 수정해보겠다.

 

 메서드 내부에서 guard 구문의 옵셔널 바인딩 활용

func fullAddress() -> String? {
    var restAddress: String? = nil

    if let buildingInfo: Building = self.building {
        restAddress = buildingInfo.name
    } else if let detail = self.detailAddress {
        restAddress = detail
    }

    guard let rest: String = restAddress else {
        return nil
    }

    var fullAddress: String = self.province
    fullAddress += " " + self.city
    fullAddress += " " + self.street
    fullAddress += " " + rest

    return fullAddress
}

 

기존에 사용했던 if let 바인딩보다는 조금 더 깔끔하고 명료하게 사용할 수 있다. 조금 더 구체적인 조건을 추가하고 싶다면 쉼표로 추가 조건을 나열해주면 된다. 추가된 조건은 Bool타입 값이어야 한다. 또 쉼표로 추가된 조건은 AND 논리연산과 같은 결과를 준다. 즉. 쉼표를 &&로 치환해도 같은 결과를 얻을 수 있다.

 

guard 구문에 구체적인 조건을 추가

func enterClub(name: String?, age: Int?) {
    guard let name: String = name, let age: Int = age, age > 19,
          name.isEmpty == false else {
              print("You are too young to enter the Club")
              return
          }
    print("Welcome \(name)")
}

guard 구문의 한계는 자신을 감싸는 코드 블록, 즉 return , break, continue, throw 등의 제어문 전환 명령어를 쓸 수 없는 상황이라면 사용이 불가능하다는 점이다. 함수나 메서드, 반복문 등 특정 블록 내부에 위치하지 않는다면 사용이  제한된다.

 

guard 구문이 사용될 수 없는 경우

let first: Int = 3
let second: Int = 5

guard first > second else {
    // 여기에 들어올 제어문 전환 명령은 딱히 없다. 오류!
}

 

포인트정리

- 옵셔널 체이닝은 간단하게 nil인지 아닌기 확인하거나 옵셔널타입의 여러값들을 체이닝으로 값을 확인해볼 수 있다.

- if let로 생성된 상수는 {} 중괄호내에서만 사용할 수 있지만 guard let은 본인을 클로징하고있는 범위까지 더넓게 사용할 수 있다.

- !강제추출은 값이 없을 경우 런타임오류를 발생시킨다.

- guard문은 else구문 내에 return, break, throw, continue 등의 명령어를 꼭 명시해줘야하며 함수나 메서드 반복문등 특정 블록내부에 위치하게되면 사용이 제한된다.

- guard문은 else구문 내에 fatalError()와 같은 메서드나를 호출할 수 있으며 예외처리에 적합하다.

 

Reference

- 야곰책 3판 Swift 프로그래밍

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

- https://docs.swift.org/swift-book/ReferenceManual/Statements.html