궁금한 내용을 검색해보세요!
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
서근 개발노트
티스토리에 팔로잉
SWIFT/Grammar

Swift : 기초문법 [ Optional 옵셔널 - Unwrapping ]

서근
QUOTE THE DAY

-
Written by SeogunSEOGUN

반응형

본 게시글은 yagom님과Zedd0202님의 게시글을 참고하여 작성되었습니다.

 

옵셔널

Optional : "선택적인" - 평소 생활에서 '옵션'이라고 하면 있어도 되고 없어도 되는 느낌처럼 Swift에서도 마찬가지입니다.

 

옵셔널? 값이 있을 수도, 없을 수도 있어! 그니까, 변수나 상수가 nil일 수도 있다는 뜻!

 

그렇다면 Swift에서 옵셔널이 왜 필요할까요? 

 

코드에서 많이 봐오던 '?' / '!' 기호가 바로 옵셔널 기호입니다. 예제를 보면 nil값은 Int형 타입에 할당될 수 없다고 컴파일 오류가 납니다. 

var test : Int
test = nil        //nil cannot be assigned to type 'Int'

Swift에서는 기본적으로 변수를 선언할 때 non-optional 즉 어떠한 '값'을 변수에게 주어야 합니다.

 

Swift에서는 Int형으로 선언한 변수는 무조건 '정수 타입'이 들어가야 합니다. 하지만 예제에는 nil이 들어갔습니다. '이건 정수 타입이 아니야!' 하고 컴파일을 하지 않는 것이죠. Int타입에는 Int타입만 들어올 수 있습니다.

 

코딩을 하다 보면 어떠한 변수에 값이 안 들어갈 수도 있죠? 즉 우리는 변수 안에 값이 확실히 있다는 것을 보장할 수 없으면 Optional을 사용해야 합니다. 이 옵셔널을 사용하려면 아래와 같이 수정해줘야 합니다.

var test : Int?
test = nil

Int앞에 '?' 를 붙였는데 오류가 사라진 것을 확인할 수 있습니다. '!' 를 붙여도 마찬가지입니다. 옵셔널 기호를 사용했기 때문에 이 변수 안에는 값이 있을 수도, 없을 수도 있다는 것을 알려준 셈이죠.

 

옵셔널이라는 타입의 정의로 이동해서 보자면

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

이런 식으로 선언이 되어있는데, 여기서 눈치챘겠죠?!  옵셔널은 = 열거형이다! 그리고 제네릭으로 선언되어있기 때문에 어떤 타입으로도 받을 수 있구나! 

 

none : nil 값이 없다.

some(Wrapped): 값이 있다.

 

열거형의 방패  = 옵셔널 = 보호되어 래핑 되어있음!

 

옵셔널이 열거형이기 때문에 case의 연관 값으로도 비교가 가능하겠죠?

if name == .some("Mijin") { } 
if name == .nil { }

 

또! 옵셔널이 열거형이기 때문에 가능한 것이 바로! switch 구문을 이용해서 값이 있고 없음을 확인할 수 있다는 것입니다.

	
func isOptional(value OptionalValue: Any?) {
    switch OptionalValue {
    case .none:
        print("이 옵셔널은 값이 없습니다.")
    case .some(let someValue):
        print("이 옵셔널의 값은 \(someValue) 입니다.")
    }
}

var name: String? = "서근"
isOptional(value: name) // 이 옵셔널의 값은 서근 입니다.

name = nil
isOptional(value: name) // 이 옵셔널은 값이 없습니다.

만약 여러 케이스의 조건을 통해 검사하고 싶으면? Where을 사용하면 됩니다!

let numbers: [Int?] = [nil, 100, -10, nil, 6, 20]

for item in numbers {
    switch item {
    case .some(let someValue) where someValue > 50:
        print("큰 값: \(someValue)")
    case .some(let someValue) where someValue < 0:
        print("음수 : \(someValue)")
    case .some(let someValue):
        print("양수: \(someValue)")
    case .none:
        print("nil")
    }
}

/*
 nil
 큰 값: 100
 음수 : -10
 nil
 양수: 6
 양수: 20
*/

근데 고작 한 개의 옵셔널을 가지고 이렇게 복잡하게 확인하는 게 너무 불편합니다. 그래서 이 옵셔널 타입을 안전하고 편하게 추출하는 방법이 이 있습니다. 

옵셔널 추출

옵셔널의 값을 옵셔널이 아닌 값으로 추출하는 방법을 알아볼게요!

 

Optional변수에서 값을 가져오는 방법에 대해 Zedd님이 작성한 글을 가져와서 쉽게 같이 알아보도록 합니다! 쉽게 설명해주신 Zedd님께 감사드립니다!

옵셔널 변수에서 값을 가져오는 방법 

일단 상자 하나를  떠올려 보면, 그동안 선언해 온 변수가 왼쪽 상자 안에 들어있지 않은 것이라고 생각하면 되고, Optional 변수는 오른쪽 상자 안에 값이 들어가 있다고 생각해 봅시다. 

 

왼: 일반적인 변수 선언 / 오 : 옵셔널 변수

 

상자 안에는 값이 있을 수도, 없을(nil) 수도 있습니다. 만약 변수가 옵셔널로 되어있으면, 상자 하나를 만듭니다.

 

? 와 ! 의 차이점

일단 옵셔널로 변수를 선언해줬으니 Xcode는 상자 하나를 만듭니다.

만약 그곳에 ?가 붙으면 Xcode는 상자에 '노크'를 하게 됩니다.

 

'안에 누구 계십니까?' 만약 상자 안에 값이 있으면 '응 나 30이라는 값을 가지고 있어' 하고 왼쪽처럼 '30'을 얻게 되는 것이죠.

 

하지만 상자를 열었는데 아무것도 없으면 nil 을 반환해 주게 되는 것이죠.

 

 

 

예제를 하나 보게 되자면

var someValue : Int? = 30
var value = someValue

someValue뒤에 ? 가 붙었기 때문에

'someValue에 정수가 들어갈 수도 있지만 nil이 들어갈 수도 있겠구나'

하고 생각할 수 있습니다. 그런데 value라는 애한테 someValue를 넣어줬죠? 이때 value 타입은 옵셔널일까요, 아닐까요? 정답은 value 타입은 옵셔널 타입입니다.

 

var value = someValue의 의미는 value는 옵셔널 타입이고 Int 데이터형을 가질 수 있는 변수입니다. (Int형 값을 가질 수도, 안 가질 수도 있다) 만약 value에 타입을 좀 더 명확히 명시해보면 어떨까요?

var someValue : Int? = 30
//Value of optional type 'Int?' must be unwrapped to a value of type 'Int'
var value : Int = someValue

Value of optional type 'Int?' must be unwrapped to a value of type 'Int' 라며 컴파일 오류가 나는 것을 확인할 수 있죠?

 

우리는 지금

'value라는 애는 Int형 데이터밖에 가질 수 없어!'

라고 명시해준 것인 셈이죠. 그런데 거기에 옵셔널 타입을 가지는 someValue를 넣어줬으니 당연히 valuesomeValue를 받아들이지 못합니다. 왜냐면.. nil값이 올 수도 있는데 Int 타입이라고 해줬으니 말이죠. 즉 아직 노크를 안 한 상태라고 할 수 있습니다. 이렇게 생각 하지면 됩니다.

'IntInt? 는 다른 타입이구나' 

 

⭐️ Optional과 Optional이 아닌 것은 다른 타입입니다.

강제 추출

!는 언래핑(Unwrapping)이라고 불립니다. 노크 따위 하지 않고 강제로 값을 꺼내버립니다. 상자 안에 값이 있던 말던 그냥 일단 깨 부지고 값을 가져오겠다는 것이죠. 운 좋게 값이 있을 수도 있고, 아닐 수도 있겠네요. 근데 이건 가장 위험한 방법이에요

 

 

아까 썼던 예제를 다시 한번 볼게요

var someValue : Int? = 30
//Value of optional type 'Int?' must be unwrapped to a value of type 'Int'
var value : Int = someValue

이렇게 하면 오류가 났었죠?  someValue 뒤에 !를 붙여보겠습니다. 그럼 오류가 사라졌습니다. 왜 오류가 없어졌을까요? 왜냐면 강제로 상자를 부시고 값을 꺼냈기 때문입니다.

var someValue : Int? = 30
var value : Int! = someValue

 

 

someValue안에는 30이라는 값을 줬었고, 그것을 깨부수었더니 운이 좋게도 Int형 데이터 30이 들어있었던 거죠. 그래서 Optional이 아닌 Int형의 Value타입에 잘 들어갈 수 있었던거죠.

 

상자를 깨부수고, 값을 꺼내고, 그 값(정수)을 value에 넣어준 코드이니 value입장에서는 아무 문제가 없죠. valueInt형 데이터만 받을 수 있는데, 원하는 데로 Int형 데이터(30)를 주었으니까요. 그런데 만약 상자를 부셨는데 안에 값이 없다면?

var someValue : Int? = nil
var value : Int = someValue!

(someValueOptional타입이니 당연히 nil이 들어갈 수 있습니다.)

 

저렇게 코드를 입력하면 컴파일 오류는 나지 않지만 nil이라는 곳에 접근을 하려고 하니 내부적으로 오류가 납니다.

 

스위프트 언어 가이드에 나와있는 내용을 보면

 

 

느낌표 !를 사용하여 값이 존재하지 않는 옵셔널 값에 접근하려 시도하면 런타임 에러가 발생합니다. 느낌표를! 사용하여 강제 언랩 핑을 하기 전에는 항상 옵셔널 값이 nil이 아니라는 것을 확실히 해야 합니다.

 

nil이 아니라는 것이 확실하지도 않은 상태에서 !를 남용하여 쓰게 되면 오류가 날 가능성이 높아지겠죠?

 

또한 !Optional이기 때문에 초기화할 때 값을 요구하지 않습니다. 초기화를 안해주면?와 마찬가지로 nil이 들어갑니다.

var name: String? = "서근"

// 만약 값을 var Seogun: String = name 으로 해준다면 런타입 오류가 생깁니다.
// Value of optional type 'String?' must be unwrapped to a value of type 'String'
// 즉, 옵셔널이 아닌 변수에는 옵셔널 값이 들어갈 수 없습니다.
// var Seogun: String = name ( X )
var Seogun: String = name!

name = nil
Seogun = name //name이 nil인데 변수 Seogun의 name을 강제로 언래핑해젔기 때문에 nil값이므로 컴파일 오류!

위에 처럼 강제 추출하는 방법도 있지만 좀 더 안전하게 If 문을 사용해서 처리해줄 수도 있습니다.

var name: String? = "서근"

var Seogun: String = name!

if name != nil {
    print("저의 이름은 \(name) 입니다.")
} else {
    print("이 값은 nil 입니다.")
}

// 저의 이름은 Optional("서근") 입니다.

이렇게 런타임 오류의 가능성이 있기 때문에 강제로 언래핑 하는 것은 비추천!


이제 Optional 변수의 값을 어떻게 가져올 수 있을까요? 

 

첫 번째로 옵셔널 바인딩(Optional Binding)과, 옵셔널 체이닝(Optional Chaining)이 있습니다.

 

그리고 강제 언랩핑(Forced Unwrapping) 즉 ! 이 있습니다.

 

일단 옵셔널 바인딩과 옵셔널 체이닝에 대해서 알아보도록 하겠습니다.

옵셔널 바인딩(Optional Binding)

옵셔널 바인딩은 If let(또는 If var) 구문과 같이 사용합니다. 즉 먼저 체크해준다 라고 생각하면 됩니다.

 

그니깐, 옵셔널에 값이 있는지 없는지 확인하고 값이 있으면 옵셔널이 아닌 형태로 바꿔줄게! 

 

라고 보면 됩니다.

 

nil인지 아니면 값이 있는지 경우에 따라 결과를 다르게 하고 싶다면 옵셔널 바인딩을 사용하면 됩니다.

func printName(_name : String) {
   print(_name)
 }
 
var myName: String? = nil
//myName 상자에 값이 있으면 name에 myName을 넣어주고 조건을 실행해
if let name = myName {
   printName(_name: name)
 }

이렇게 코드를 작성하고 런 해보면 아무것도 프린트되지 않는 것을 확인할 수 있습니다. 이유는 myNamenil 이기 때문이죠. if let  구문은

 

'myName 상자에 값이 있으면 name myName을 넣어주고 조건을 실행해'

 

라는 것입니다.

 

하지만 myName 상자에는 nil이 있어서 if let 구문은 작동을 멈추고 실행을 하지 않는 것입니다. 값이 있을 때만 값이 바인딩되기 때문이죠. 그렇기에 옵셔널 바인딩 이라고 부른답니다.

 

그렇기에 if let 구문을 통해 nil인 경우와 아닌 경우를 안전하게 대비할 수 있습니다.

var height: Int? = 170
if let value = height {
  if value >= 160 {
    print("옵셔널 실행")
  }
}  //print "옵셔널 실행"

var height : Int? = 170
// ','을 통해서 '&&' 효과를 줄 수 있습니다.
//value 는 height이고 value가 160보다 크거나 같다면 조건 실행
if let value = height, value >= 160 {
    print("옵셔널 실행")
}
  //print "옵셔널 실행"

두 구문 다 실행 결과는 같습니다.

 

예시

var name: String? = nil

if let unwrapped = name {
    print("\(unwrapped.count) letters")
} else {
    print("Missing name.")
}

nameString이 포함되어 있으면 일반 String으로 unwrapping 되지 않은 상태로 내부에 넣어지고, 조건 내에서 카운트 프로퍼티를 읽을 수 있습니다. 또는 namenil이면 else 코드가 실행됩니다.

 

if 또는 while 구문 등과 결합하여 사용할 수도 있습니다.

var myName: String? = "서근"

// 옵셔널 바인딩을 통한 임시 상수 할당

if let name = myName {
    print("저의 이름은 \(name) 입니다")
} else {
    print("이 값은 nil 입ㄴ디ㅏ.")
}

// 옵셔널 바인딩을 통함 임시 변수 할당

if var name = myName {
    name = "미진"
    print("저의 이름은 \(name) 입니다.")
} else {
    print("이 값은 nil 입니다.")
}

/*
 저의 이름은 서근 입니다.
 저의 이름은 미진 입니다.
 
*/

위 코드에서 If 구문을 실행하는 블록 안에서만 name이라는 상수를 사용할 수 있고 그 밖에서는 사용할 수 없습니다(else 블록 포함).

 

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

var Seogun: String? = "서근"
var Mijin: String? = nil

if let name = Seogun, let yourName = Mijin {
    print("저의 이름은 \(name) 이고, 제 친구는 \(yourName) 입니다.")
} else {
    print("이 옵셔널 값은 nil 입니다.")
}
// yourName에 바인딩 되지 않으므로 실행되지 않음
// 이 옵셔널 값은 nil 입니다.

Mijin = "미진"

if let name = Seogun, let yourName = Mijin {
    print("저의 이름은 \(name) 이고, 제 친구는 \(yourName) 입니다.")
} else {
    print("이 옵셔널 값은 nil 입니다.")
}

// 저의 이름은 서근 이고, 제 친구는 미진 입니다.

옵셔널 바인딩은 옵셔널 체이닝과 환상의 짝꿍인데, 이 옵셔널 체이닝에 대해서는 간단하게만 알아보고 나중에 따로 자세히 다룰게요.

암시적 추출 옵셔널 

nil을 할당해주고 싶은데 런타임 오류는 발생할 거 같지는 않고 매번 값을 추출 하기는 귀찮다! 할 때 사용하는 것이 바로 암시적 추출 옵셔널입니다. 

 

키워드 타입 뒤에!

 

옵셔널 이기 때문에 nil을 할당할 수는 있지만 접근을 시도하면 런타임 오류가 발생합니다.

var Seogun: String! = "서근"
print(Seogun) // Optional("서근")


Seogun = nil
if let name = Seogun {
    print("저의 이름은 \(name) 입니다")
} else {
    print("nil")
}

// nil

Seogun.isEmpty // 오류!

근데 옵셔널을 사용할 때는 이 암시적 추출 옵셔널을 사용하기보다는 옵셔널 체이닝을 사용해서 좀 더 안전하게 사용하는 것이 좋습니다 :)

옵셔널 체이닝 (Optional Chaining)

이름처럼 chain으로 이어져 있다고 생각하면 됩니다.

 

옵셔널 체이닝은 하위 property에 옵셔널 값이 있는지 연속적으로 확인하면서, 중간에 하나라도 nil이 발견된다면 nil이 반환되는 형식입니다.

 

스위프트 언어 가이드에 나와있는 내용을 보면

class Person {
    var residence: Residence? 
    
/* residence라는 변수가 Residence 클래스를 상속받고 있습니다. 동시에 옵셔널이네요
밑에서 Person타입의 인스턴스가 만들어지면 residence변수의 초기값은 nil이 되겠네요. */

}

class Residence {
    var numberOfRooms = 1
}

let seogun = Person()

/* Person타입의 인스턴스가 seogun으로 만들어졌습니다. 
seogun의 프로퍼티로 class에서 선언한 residence가 있죠.

하지만 residence변수는 Residence클래스를 옵셔널 타입으로 상속받고 있기 때문에 

residence에는 값이 있을수도, 또는 없을수도 있죠.

옵셔널 타입은 따로 초기화를 하지 않으면 nil로 초기화가 됩니다.
그러면 현재 seogun의 residence의 값은 nil이겠네요! */

//seogun의 residence가 값이 있다면 조건문을 실행하고, 아니라면 else를 실행한다.
if let roomCount = seogun.residence?.numberOfRooms {

    print("seogun's residence has \(roomCount) room(s).")
    
} else {
   print("Unable to retrieve the number of rooms.")
}


//print = "Unable to retrieve the number of rooms."
 

 

 

읽어주셔서 감사합니다🤟

 

 

 

 


잘못된 내용이 있으면 언제든 피드백 부탁드립니다.


서근


위처럼 이미지 와 함께 댓글을 작성할 수 있습니다.