Swift 언어가 가지고 있는 가장 큰 특징 중 하나가 Optional 입니다.
직역하면 '선택적인' 이라는 뜻인데요. 값이 있을 수도 있고, 없을 수도 있는 상태, 즉 값이 있는지 없는지 확신할 수 없는 상태 입니다.
예를 들어보겠습니다. "가나다" 라는 문자열은 값이 존재한다고 할 수 있죠. 그럼 "" 는 값이 없는 것일까요? 아닙니다 ""도 엄연히 문자열이고, 값이 없다기보다는 빈 값이죠.
값이 없는 것은 Swift 에서는 nil 이라고 표현합니다. (왜 nil...? null 이라 안하고...?)
마찬가지로 빈 배열이나 빈 딕셔너리도 nil 이 아니고 비어있는 것 뿐입니다. 배열과 딕셔너리의 경우에도 값이 없는 경우 nil 로 표현합니다.
모든 값에 nil 을 넣을 수 있는 것은 아닙니다.
var number: Int = nil // error
var optionalNumber: Int? = nil // ok
값이 있을 수도 있고, 없을 수도 있는 변수를 선언할 때 타입 어노테이션에 ?를 붙여야 합니다.
이렇게 정의한 변수를 Optional이라 하고, 값을 초기화 하지 않으면 nil이 저장됩니다.
Kotlin의 Nullable 과 매우 유사하네요.
Optional로 정의한 변수는 일반 변수와는 다릅니다.
예를들어 다음과 같은 경우 오류가 발생합니다.
var optionalNumber: Int? = 1
var number: Int = optionalNumber // error
Optional 이 아닌 일반 Int 타입 변수에 Optional Int 타입 변수를 저장하려하고 있습니다.
number 입장에서는 optinalNumber 변수가 nil 이 있을지 실제 값이 있을지 명확하지 않은 상태에서 값을 초기화하려고 하고 있으니 안전하지 않은 접근이죠. 따라서 Swift 컴파일러는 안전을 보장하기 위해 위와 같은 접근을 제한하고 있습니다.
그렇다면, 어떻게 해야 옵셔널 변수에 nil이 아닌 실제 값을 가져올 수 있을까요?
이때 사용하는 것이 '옵셔널 바인딩' 입니다.
옵셔널 바인딩은 옵셔널의 값이 존재하는지 검사하고, 존재한다면 그 값을 다른 변수에 대입해줍니다. "if let" 또는 "if var"를 사용하는데요. 옵셔널 바인딩을 통해 값이 존재한다면 if 문 안으로 들어가고, 아니면 그냥 통과하게 됩니다.
var optionalNumber: Int? = 1
if let number = optionalNumber { // optinalNumber의 값이 존재하면
print(number) // 다음 출력문을 실행합니다.
}
// nil이면 그냥 통과
하나의 if 문에서 콤마(,) 로 구분하여 여러 옵셔널 바인딩을 할 수 있습니다.
이 때, 모든 옵셔널의 값이 존재해야만 if 문 안으로 진입할 수 있습니다.
var optionalName: String? = "이은재"
var optionalEmail: String? = "dldmswo1209@gmail.com"
if let name = optionalName,
let email = optionalName {
// name 과 email 모두 값이 존재
print("이름 : \(name) 이메일 : \(email)")
}
옵셔널 바인딩을 할 때, 콤마(,)로 조건을 지정할 수도 있습니다.
콤마 이후의 조건절은 옵셔널 바인딩 이후에 실행됩니다.
즉, 옵셔널이 벗겨진 실제 값을 가지고 조건절을 실행합니다.
var optionalAge: Int? = 22
if let age = optionalAge, age >= 20 {
// age 값이 존재하고, 20 이상
print("성인 입니다")
}
위 코드는 아래와 동일합니다.
var optionalAge: Int? = 22
if let age = optionalAge{
if age >= 20{
print("성인 입니다")
}
}
옵셔널로 선언된 배열이 '빈 배열' 인지 확인하려면 어떻게 해야 할까요?
nil 이 아니면서 빈 배열인지 확인하면 될 것 입니다.
let optionalArray: [String]? = []
var isEmptyArray = false
if let array = optionalArray, array.isEmpty {
isEmptyArray = true
} else {
isEmptyArray = false
}
print(isEmptyArray)
이 때 옵셔널 체이닝을 사용하면 위 코드를 아래와 같이 간결하게 작성할 수 있습니다.
print(optionalArray?.isEmpty == true)
위와 아래 코드 둘 다 출력 결과는 true 입니다.
옵셔널 체이닝은 다음과 같이 옵셔널의 속성에 접근할 때, 옵셔널 바인딩하는 과정을 ? 키워드로 줄여주는 역할을 합니다.
optionalArray?.isEmpty 까지 실행 했을 때 반환되는 값은 Optional(Bool)로 Optional Boolean 값 입니다. isEmpty 의 반환 값은 Bool인데 옵셔널 체이닝으로 인해 Bool? 로 바뀐 것이죠.
따라서 반환되는 값의 경우의 수로 nil, true, false 가 있습니다.
그렇기 때문에 값이 실제로 true 인지 확인하기 위해 ==true 를 해주어야 합니다.
옵셔널을 사용할 때마다 옵셔널 바인딩 하는 것이 바람직합니다. 하지만, 개발을 하다보면, 값이 분명히 존재할 것임에도 불구하고, 옵셔널로 사용해야하는 경우가 있는데요. 이 때, 옵셔널에 nil 이 아닌 값이 존재한다는 가정하에 ! 키워드를 사용해 값에 접근할 수 있습니다.
var optionalName: String? = "eun jae"
print(optionalName!)
하지만, 예상과는 다르게 옵셔널 변수에 nil 이 있는 경우 ! 를 사용해서 강제로 옵셔널을 벗긴다면, 컴파일 에러가 발생하기 때문에 ! 키워드를 사용해서 옵셔널 값에 접근할 때는 주의가 필요합니다.
var optionalName: String?
print(optionalName!) // error!!
Fatal error: Unexpectedly found nil while unwrapping an Optional value
런타임 에러가 발생하면 iOS 는 앱을 강제 종료 시키므로 주의해야 합니다.
코틀린의 Nullable 변수를 강제로 접근할 때 !! 키워드를 사용하는 것과 동일하죠.
그래서 안드로이드 개발할 때에도 !! 키워드는 지양하는 편입니다.
Swift 역시 웬만하면, 옵셔널 바인딩을 통해 접근하는 것이 안전할 것 같습니다.
옵셔널을 정의할 때 타입 어노테이션 뒤에 ? 대신 !를 를 붙이면, ImplicitlyUnwrappedOptional 라는 옵셔널로 정의됩니다. 직역하면 암묵적으로 벗겨진 옵셔널 입니다.
var optionalName: String! = "은재"
print(optionalName)
옵셔널 벗기기와 마찬가지로 값이 없는데 접근을 시도하면 런타임 에러가 발생합니다.
출력결과는 Optional("은재") 입니다.
Swift 2 버전에서는 "(email)"과 같이 문자열을 포맷팅하면 devxoul@gmail.com이 나왔으나, Swift 3 버전부터는 ImplicitlyUnwrappedOptional을 문자열 포맷팅 할 경우 Optional("devxoul@gmail.com")로 포맷팅되니 주의해서 사용해야 합니다. Swift 3 버전부터 ImplicitlyUnwrappedOptional을 일반 Optional과 거의 동일하게 취급했기 때문이랍니다...
Swift2 버전에서는 값에 바로 접근할 수 있는 장점이라도 있었지 이제는 그렇지도 않으니 사용할만한 이유를 모르겠네요.
게다가 런타임 에러가 발생할 수 있는 여지가 있는 방식을 굳이 사용할 것 같진 않습니다.
웬만하면 옵셔널 바인딩을 통해 런타임 에러를 피하는 것이 좋겠습니다.