본문 바로가기

기능

[Swift]옵셔널(Optional)에 대해 알아보자!

주의 : Swift프로그래밍 3판 야곰책을 참고했으며 배우는 과정이기에 오탈자나 정확하지않는 정보가 있을 수 있습니다.

 

카테고리
- 옵셔널
- 옵셔널 사용
- 옵셔널 추출
- 옵셔널 바인딩
- 암시적 추출 옵셔널

 

옵셔널

옵셔널은 스위프트의 특정 중 하나인 안정성을 문법으로 담보하는 기능이다. 기존에 C언어나 Objective-C 에서는 찾아볼 수 없었던 콘셉트 이기도하다. 옵셔널은 단어 뜻 그대로 '선택적인', 즉 '값이 있을수도, 없을 수도 있음'을 나타내는 표현이다. 이는 '변수나 상수 등에 꼭 값이 있다는 것을 보장할 수 없다. 즉, 변수 또는 상수의 값이 nil일 수도 있다' 는 것을 의미한다.

그리고 옵셔널이 아닌 값을 철저히 다른 타입으로 인식하기 대문에 컴파일할때 바로 오류를 걸러낼 수 있다.

 

 

옵셔널사용

옵셔널 변수 또는 상수가 아니면 nil을 할당 할 수 없다 . "Int 타입에 0을 할당했다면 값이 없다는 의미일까요 ?"

 

그렇지않다. 0 도 하나의 값이며 ""로 빈문자열을 만들었다면 이 또한 빈문자열이라는 값이지, 값이 없는것이 아닙니다. 변수 또는 상수에 정말 값이 없을때만 nil로 표현합니다. 함수형 프로그래밍 패러다임에서 자주등장하는 모나드 개념과 일맥상통한다.

 

그래서 옵셔널의 사용은 많은 의미를 축약하여 표현하는 것과 같다. 옵셔널을 읽을때 해당 변수 또는 상수에 값이 있을 수도 없을 수 도 있다.

즉, 변수 또는 상수가 nil일 수도 있으므로 사용에주의하라 는 뜻으로 직관적으로 받아들일 수 있다.

값이 없는 옵셔널 변수 또는 상수에 강제로 접근하면 런타임오류가 발생합니다. 그렇게 되면 OS가 프로그램이 강제 종료시킬 확률이 매우높다.

var myName: String = "quokka"
//myName = nil // 오류발생
//'nil' cannot be assigned to type 'String'

nil은 옵셔널로 선언된 곳에서만 사용될 수 있습니다. 옵셔널 변수 또는 상수등은 데이터 타입 뒤에 물음표? 를 붙여 표현해준다

var maName: String? = "quokka"
print(maName)

 

사실 var myName: Optional<String>처럼 옵셔널을 조금 더 명확하게 써줄 수도 있습니다.

그러나 물음표를 붙여주는 것이 조금더 편하고 읽기도 쉽기 때문에 굳이 긴 표현을 사용하지는 않습니다.

 

옵셔널은 어떤 상황에 사용할까요 ?

왜 굳이 변수에 nil이 있음을 가정해야할까요 ?

이 질문에 답할 수 있는 예로 우리가 만든 함수에 전달되는 전달인자의 값이 잘못된 값일 경우 제대로 처리하지

못했음을 nil을 반환하여 표현하는 것을 들 수 있습니다. 물론 기능상 심각한 오류라면 별도로 처리해야겠지만,

간단히 nil을 반환해서 오류가 있음을 알릴 수도 있습니다. 또는 매개변수를 굳이 넘기지 않아도 된다는 뜻으로

매개변수의 타입을 옵셔널로 정의할 수도 있습니다.

스위프트 프로그래밍을 하면서 매개변수가 옵셔널일때는 '아, 이 매개변수에는 값이 없어도 되는구나'라는 것을

API문서를 보지않고도 알아야합니다. 이렇게 물음표 하나만으로 훌륭하고 암묵적인 커뮤니케이션을 완성했습니다.

switch를 통한 옵셔널 값의 확인

// guard let 을 사용한 옵셔널 바인딩 미리보기 
func takeOffTheOptionalWrapForSchoolType(value: School?) -> String {
    guard let unwrappedValues = value?.rawValue else {
        return "해당값은 존재하지 않습니다."
    }
    return unwrappedValues
}

func takeOffTheOptionalWrapForNumbersType(value: Numbers?) -> Int {
    if let unwrappedValues = value?.rawValue {
        return unwrappedValues
    } else {
        return 0
    }
}

enum School: String {
    case primary = "유치원"
    case element = "초등학생"
    case middle = "중학생"
    case high = "고등학생"
    case college = "대학"
    case univercity = "대학교"
    case graduate = "대학원"
}

enum Numbers: Int {
    case one = 1, two, three, four, five, six
}

print("=======옵셔널 랩핑을 벗기기 전=======")
let primary = School(rawValue: "유치원")
let graduate = School(rawValue: "석박사")

print(primary)
print(graduate)

let one = Numbers(rawValue: 1)
let seven = Numbers(rawValue: 7)

print(one)
print(seven)
// 옵셔널 랩핑을 해주는 기능을 만들어 보았다.
print("=======옵셔널 랩핑을 벗긴 후=======")
let unwrappedPrimary = takeOffTheOptionalWrapForSchoolType(value: primary)
let unwrappedGraduate = takeOffTheOptionalWrapForSchoolType(value: graduate)
print(unwrappedPrimary)
print(unwrappedGraduate)

let unwrappedOne = takeOffTheOptionalWrapForNumbersType(value: one)
let unwrappedSeven = takeOffTheOptionalWrapForNumbersType(value: seven)

print(unwrappedOne)
print(unwrappedSeven)

//=======옵셔널 랩핑을 벗기기 전=======
//Optional(WhatIsTheOptional.School.primary)
//nil
//Optional(WhatIsTheOptional.Numbers.one)
//nil
//=======옵셔널 랩핑을 벗긴 후=======
//유치원
//해당값은 존재하지 않습니다.
//1
//0

 

지금까지 전반적으로 변수나 상수 뒤에 데이터타입을 명시했습니다. 그러나 이 예제에서는

데이터 타입을 명시해주지 않고 타입추론 기능을 사용했습니다. 왜 그랬을까요 ? nil을 할당하는 경우가 생기기

때문이다. 컴파일러는 아마도 primary 및 graduate 상수의 데이터 타입을 School?이라고 추론했을 것이다.

또 one과 three상수 데이터 타입은 Numbers?라고 추론했을 것이다. 이때 원시 값이 열거형의 case에

해당하지 않으면 열거형 인스턴스 생성에 실패하여 nil을 반환하는 경우가 생깁니다. 앞에서 설명한 함수의

처리 실패 유형에 해당하는 것이죠. 옵셔널의 더 놀라운 점은 열거형으로 구현되어있다는 점입니다.

 

옵셔널의 정의를 한번 살펴볼까요?


public enum Optional<Wrapped> : ExpressibleByNilLiteral {
    case none
    case some(Wrapped)
    public init(_ some: Wrapped)
    // 중략....
}

옵셔널은 제네릭이 적용된 열거형입니다. ExpressiibleByNilLiteral 프로토콜을 따른다는 것도 확인할 수 있습니다.

여기서 알아야할 것은 옵셔널이 값을 갖는 케이스와 그렇지 못한 케이스 두가지로 정의 되어있다는 것입니다. 즉 nil일때는

none 케이스가 될것이고, 값이 있는 경우는 some케이스가 되는데, 연관값으로 Wraaped가 있습니다. 따라서 옵셔널에

값이 있으면 some의 연관 값인 Wrapped에 값이 할당됩니다. 즉, 값이 옵셔널 이라는 열거형의 방패막에 보호되어

래핑되어있는 모습이라는 겁니다.

 

옵셔널 자체가 열거형이기 때문에 옵셔널 변수는 switch 구문을 통해 값이 있고 없음을 확인 할 수 있습니다.

// switch를 통한 옵셔널 값의 확인

func checkOptionalValue(value optionalValue: Any?) {
    switch optionalValue {
    case .none:
        print("This Optional Variable is nil")
    case .some(let value):
        print("Value is \(value)")
    }
}

print(checkOptionalValue(value: 1))
print(checkOptionalValue(value: primary))
print(checkOptionalValue(value: "쿼카"))
// () 출력되는 이 소괄호는 뭐지?

var myeName: String? = "quokka"
print(checkOptionalValue(value: myeName))

myeName = nil
print(checkOptionalValue(value: myeName))

// 만일 여러 케이스의 조건을 통해 검사하고자 한다면 더욱 유용하게 쓰일 수도 있다. 그럴땐
// 세련되게 where 절과 병합해서 쓰면 더욱 좋다.

// switch를 통한 옵셔널 값의 확인

let numbers: [Int?] = [2, nil, -4, nil, 100]

for number in numbers {
    switch number {
    case .some(let value) where value < 0:
        print("Negative value!! \(value)")
    case .some(let value) where value > 10:
        print("Large value!! \(value)")
    case .some(let value):
        print("Value \(value)")
    case .none:
        print("nil")
    }
}

//Value 2
//nil
//Negative value!! -4
//nil
//Large value!! 100

그러나 단 하나의 옵셔널을 switch 구문을 통해 매번값이있는지 확인하는 것은 매우 불편할 것이다.

그래서 옵셔널 타입에서 값을 조금더 안전하고 편리하게 추출하는 방법에 대해 알아보자

 

옵셔널 추출

열거형의 some 케이스로 꼭꼭 숨어있는 옵셔널의 값을 옵셔널이 아닌 값으로 추출하는 옵셔널 추출방법에대해 알아보자

1. 강제추출

2. 옵셔널 바인딩

3. 암시적 추출 옵셔널

 

강제 추출

강제 추출 방식은 옵셔널의 값을 추출하는 가장 간단하지만 가장위험한 방법이다. 런타임 오류가 일어날 가능성이 가장 높기때문이다. 또

옵셔널을 만든의미가 무색해지는 방법이기도합니다. 옵셔널의 값을 강제 추출하려면 옵셔널 값의 뒤에 느낌표를 붙여주면 값을 강제로 추출하여 반환해준다. 만약 강제추출시 옵셔널에 값이 없다면, 즉 nil이라면 런타임오류가 발생합니다.

var myNameOptional: String? = "quokka"

//옵셔널이 아닌 변수에는 옵셔널 값이 들어갈 수 없다. 추출해서 할당해주어야함
var quokka: String = myNameOptional!
print(quokka)

myNameOptional = nil
//quokka = myNameOptional!
//print(quokka)
//런타임 오류 발생 Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value

// if 구문 등 조건문을 이용해서 조금더 안전하게 처리해볼 수 있다.
if myNameOptional != nil {
    print("My name is \(myNameOptional)")
} else {
    print("myNameOptional == nil")
}

런타임 오류의 가능성을 항상 내포하기때문에 옵셔널 강제 추출방식은 사용하는 것을 지양해야한다.

 

 

옵셔널 바인딩

if 구문을 통해 myNameOptional이 nil인지 먼저 확인하고 옵셔널 값을 강제 추출하는 방법은 다른 프로그래밍언어에서

NULL값을 체크하는 방식과 비슷하다. 앞서 설명한 것처럼 옵셔널을 사용하는 의미도 사라집니다. 그래서

스위프트는 조금 더 안전하고 세련된 방법으로 옵셔널 바인딩을 제공합니다

 

옵셔널 바인딩은 옵셔널에 값이 있는지 확인할 때 사용합니다. 만약 옵셔널에 값이 있다면 옵셔널에서 추출한

값을 일정 블록 안에서 사용할 수 있는 상수나 변수로 할당해서 옵셔널이 아닌 형태로 사용할 수 있도록

해줍니다. 옵셔널 바인딩은 if 또는 while 구문 등과 결합하여 사용할 수 있다.

 

옵셔널 바인딩을 사용한 옵셔널 값의 추출

// 옵셔널 바인딩을 사용한 옵셔널 값의 추출

var myNameOptional2: String? = "quokka"

//옵셔널 바인딩을 통한 임시 상수 할당
if let name = myNameOptional2 {
    print("My name is \(name)")
} else {
    print("myName == nil")
}

//My name is quokka


// 옵셔널 바인딩을 통한 임시 변수 할당
if var name = myNameOptional2 {
    name = "쿼카"
    print("My name is \(name)")
} else {
    print("my name == nil")
}

//My name is 쿼카

 

if구문을 실행하는 블록 안쪽에서만 name이라는 임시상수를 사용할 수 있다. 즉 , if블록밖에서는 사용할 수 없고

else 블록에서도 사용할 수 없습니다. 그렇기 때문에 위와 아래에서 모두 별도로 name을 사용했지만 충돌이 일어나지

않았다. 또 상수로 사용하지않고 변수로 사용하고 싶다면 if var를 통해 임시 변수로 할당할 수도 있다.

 

이번에는 if와 else블록만을 사용했지만 else if블록도 추가할 수 있다.

 

옵셔널 바인딩을 통해 한번에 여러 옵셔널의 값을 추출할 수도 있다. 쉼표(,)를 사용해 바인딩 할 옵셔널을 나열하면 된다.

단, 바인딩이라는 옵셔널 중 하나라도 값이 없다면 블록 내부의 명령문은 실행되지않는다.

 

 

암시적 추출 옵셔널

 

때때로 nil을 할당하고 싶지만, 옵셔널 바인딩으로 매번 값을 추출하기귀찮거나 로직상 nil때문에 런타임 오류가

발생하지 않을 것같다는 확신이 들때 nil을 할당해줄 수 있는 옵셔널이 아닌 변수나 상수가 있으면 좋을것입니다.

이때 사용하는 것이 바로 암시적 추출 옵셔널 입니다. 옵셔널을 표시하고자 타입뒤에 물음표를 사용했지만

암시적 추출 옵셔널을 사용하려면 타입뒤에 느낌표(!)를 사용해주면 됩니다.

 

암시적 추출 옵셔널로 지정된 타입은 일반값처럼 사용할 수 있으나, 여전히 옵셔널이기때문에 nil도 할당해줄 수 있다

그러나 nil이 할당되어 있을 때 접근을 시도하면 런타임 오류가 발생한다

* 때때로 nil을 할당하고 싶지만, 옵셔널 바인딩으로 매번 값을 추출하기귀찮거나 로직상 nil때문에 런타임 오류가

발생하지 않을 것같다는 확신이 드는 생각은 매우 위험한 생각이다. 실제 프로젝트용 프로그래밍 중에 오류가 생기지 않겠다라는

확신이 드는 순간은 드물것이다..

 

var myNameOptional4: String! = "quokka"
print(myNameOptional4)
myNameOptional4 = nil

// 암시적 추출 옵셔널도 옵셔널이므로 당연히 바인딩을 사용할 수 있다.
if let name = myNameOptional4 {
    print("My name is \(name)")
} else {
    print("myName == nil")
}
// myName == nil

//myNameOptional4.isEmpty // 오류!!

 

옵셔널을 사용할 때는 강제 추출 또는 암시적 추출 업셔널을 사용하기보단 옵셔널 바인딩 nil병합 연산자를 비롯해

뒤에서 배울 옵셔널 체이닝 등의 방법을 사용하는 편이 훨씬 안전하다. 또한 이렇게 하는편이 스위프트의 지향점에 부합함

 

포인트정리

- 옵셔널은 스위프트의 특정 중 하나인 안정성을 문법으로 담보하는 기능이다.

- 옵셔널은 단어 뜻 그대로 '선택적인', '값이 있을수도, 없을 수도 있음'을 의미하여 즉, 변수 또는 상수의 값이 nil일 수도 있다'

- 옵셔널이 아닌 값을 철저히 다른 타입으로 인식한다. 때문에 컴파일할때 바로 오류를 걸러낼 수 있다

- 0 도 하나의 값이며 ""로 빈문자열을 만들었다면 이 또한 빈문자열이라는 값이지, 값이 없는것이 아니다.

- var myName: Optional<String>처럼 옵셔널을 조금 더 명확하게 써줄 수도 있다.

- 값이 없는 옵셔널 변수 또는 상수에 강제(강제추출)로 접근하면 런타임오류가 발생한다.

   => 값이 없는데 강제로 값을 가져오려하기때문에  런타임오류가 발생하는 것임

- 매개변수의 타입을 옵셔널로 정의할 수도 있다.

- 인스턴스화할시 데이터 타입을 명시하지않고 값을 할당하면 컴파일러가 타입을 추론할 수 있다.

- 옵셔널 추출방식에는 3가지가 있다. 강제추출, 옵셔널바인딩, 암시적추출 옵셔널

- 옵셔널 바인딩에는 3가지가있다. switch, if let, guard let

- 쉼표(,)를 사용해 옵셔널 바인딩을 한번에 여러 옵셔널의 값을 추출할 수도 있다.

- 옵셔널은 제네릭으로 적용된 열거형타입이다.

- 암시적 추출 옵셔널은 nil이 할당되어 있을 때 접근을 시도하면 런타임 오류가 발생한다

 

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