Swift Optional(옵셔널)

마이노·2024년 3월 14일
2

15주 글쓰기 🐣

목록 보기
4/10
post-thumbnail
post-custom-banner

Optional

옵셔널에 대해 알아봅시다.

swift의 옵셔널은 Wrapping된 값이나 값이 없음을 나타내는 형식이에요. 이름만 들어도 선택적으로 무언가 고를 수 있다고 생각해보면 골라도되고 안골라도 되는거잖아요? 비슷한 기능을 수행한다고 보시면 될 것 같아요.

이를 swift에서는 후행에 ?로 나타내주어 옵셔널로(있을수도~ 없을수도) 만들 수 있습니다.

여기 다음과 같은 프로퍼티가 있다고 해보겠습니다.

// 1. 나이: 일반
var age: Int

// 2. 전화번호: 옵셔널
var phoneNumber: String?

어느 사이트에 가입하기 위해 필수적으로 입력해야 하는 값이 ‘나이’ , ‘전화번호’는 굳이 입력하지 않아도 되는 값이라고 가정해보겠습니다.

age = nil

phoneNumber = nil

유저가 깜빡하고 나이를 입력하지 않았습니다. 그렇다고해서 nil을 할당할 수 있을까요?

🚨 'nil' cannot be assigned to type 'Int’

불가능합니다! 값이 없으면 안되는 타입이기 때문입니다. 따라서 nil을 Int Type에 할당할 수없다고 에러를 뱉게 됩니다.


Optional 열거형 살펴보기

public enum Optional<Wrapped> : ExpressibleByNilLiteral {

    case none
    case some(Wrapped)

    public init(_ some: Wrapped)
}

Optional은 Wrapped 라는 제네릭 타입을 받고 있어요. 그리고 ExpressibleByNilLiteral 프로토콜을 채택하고 있습니다. ExpressibleByNilLiteral는 Optional 유형만 준수하기 때문에 다른 목적으로의 사용은 권장되지 않는다고 합니다.

정리하자면 Optional은 Wrapped라는 제네릭 타입을 받는데, 이 Wrapped는 nil이 될 수 있다는 것을 암시합니다.

두개의 case를 살펴볼께요.

none은 값이 없을 때 일반적으로 사용합니다. swift에서는 일반적으로 명시적인 .none 열거형 케이스를 사용하기 보다는 nil을 사용합니다.

두번째 some 입니다. Wrapped로 저장된 값입니다. 하지만 말그대로 아직 래핑되어있기 때문에 값은 있지만 아직은 옵셔널 값입니다.

이를 코드로 어떻게 표현할 수 있을까요?

// 값이 있는 Optional Int Type
var a: Int? = 1
var b = Optional.some(1)
var c: Optional<Int> = .some(1)
var d: Optional<Int> = 1

먼저 값이 있는 옵셔널 값입니다. 두번째 열거형 케이스와 같이 some(Wrapped)로 사용할 수 있어요.

a를 살펴보면 가장 보편적으로 사용하는 방법이라고 할 수 있습니다. 타입을 지정해주고 값을 할당해주는 방법이에요.

b는 Optional의 some case의 연관값을 이용해서 만들 수 있습니다. 곧바로 some에 접근해서 연관값에 1을 할당해줌으로써 Wrapped Type은 Int임을 타입추론할 수 있겠네요.

c,d는 타입을 지정할 때 Optional과 타입을 같이 지정하는 방법입니다. 두가지의 다른점은 곧바로 열거형의 Wrapped를 넣어주는 방법과 바로 할당하는 방법으로 나눠서 볼 수 있습니다.

결국에는 a,b,c,d 모두 똑같은 동작을 하고 있으며, 표현이 조금씩 다른 것을 알 수 있습니다.

var e = Optional<Int>.none
var f: Optional<Int> = .none
var g: Optional<Int> = nil
var h: Int? = nil
var i: Int?

none case도 여러가지 형태로 나타낼 수 있습니다. 방금 말씀드린 것과 같이 .none을 사용하기 보다는 코드에서 일반적으로 ?를 붙이는 형태로 많이 사용합니다. 자주 사용하는 형태로는 h와 i가 있겠네요!

var a: Int? = 1
print(a) //Optional(1)

var b: Int?
print(b) // nil

이제 옵셔널 값을 출력해봅시다. 래핑된 값이 찍히지 않으신가요? 이대로는 어디에도 사용할 수 없습니다. 어떻게 해결할까요? 옵셔널 바인딩으로 해결할 수 있습니다.


옵셔널 바인딩 (Optional Binding)

우선 옵셔널을 강제로 확인하려면 어떻게 할까요? !를 사용해서 벗겨내면 됩니다.

var a: Int? = 10

print(a!) // 10

하지만 이런경우는요?

var b: Int?

print(b!) // ❌ fatal Error

!를 강제언래핑(Force unwrapping)이라고 하는데요, 강제로 옵셔널을 추출하는 방법입니다. 이 방법은 값이 없을 때 사용한다면 에러를 뱉게 됩니다. 이를 해결하기 위해 안전하게 값을 꺼내오는 방법이 바로 옵셔널 바인딩 입니다.

  1. if let, if var

조건문을 통해 상수,변수에 할당이 되면 옵셔널을 벗겨낼 수 있습니다.

var value: Int? = 10

// 1. 상수에 할당
if let unWrap = value {
    print(unWrap)
}

// 2. 내자신에 조건문걸기
if let value {
    print(value)
}

// 결과
// 10
// 10

if let을 통해 벗겨내는것은 동일합니다. 1번방법과 2번방법은 각각 조금씩 다른데요, 기존에는 1번방법을 사용했었습니다. Swift언어가 버전업이 되면서 자기자신에게도 사용할 수 있습니다. 큰 차이는 변수를 하나 만들어서 새로 사용하는지, 기존의 프로퍼티를 활용하는지에 대한 차이가 있습니다.

var value: Int? = 10

if var unWrap = value {
    unWrap = 5
    print(unWrap)
}

if var value {
    value = 5
    print(value)
}

// 결과
// 5
// 5

if var와 if let과 차이점은 새로 만든 변수를 수정할 수 있는지의 여부만 다릅니다. 내 자신이라면 변수를 생성하지 않고 새로운 값을 할당할 수 있습니다.

2.guard let

var value: Int? = 10
var nilDateValue: Date?

func doSomething<T>(_ input: Optional<T>) {
    guard let input else {
        print("걸려버렸당")
        return
    }
    
    print("value: \(input)")
}

doSomething(value)
doSomething(nilDateValue)

// 결과
// value: 10
// 걸려버렸당

guard let은 코드의 스코프를 걸어버려 더이상 진행하지 못하게 막아버릴 수 있습니다.

guard let에 input을 통해 할당이 되었다면 guard문 밖에서 input을 사용할 수 있습니다. 반면, 할당되지 않는다면 nil이기 때문에 리턴됩니다.

다시말해 해당함수에서 사용 예를 살펴보면 사전에 input이 nil이라면 하단까지 코드의 범위가 닿지않고 print()를 출력하고 리턴됩니다.

3.≠ nil

var value: Int? = 10

if value != nil { print(value!) }

어찌보면 제일 간단한 방법일 수 있어요. 직접적으로 nil이 아닐때라면 강제로 !를 사용해 강제언래핑을 할 수 있습니다.

4.nil-coalescing

let value = Int("앙~인트타입") ?? 100

Int initializer를 통해 값을 생성해주면서 시작합니다. 여기까지만 보면 value의 타입은 Optional Int입니다. 사용자로 하여금 Int가 아닌 값이 들어올 수 있기 때문입니다.

이를 ?? 를 붙여줌으로써 100이 Optional fail을 대체하게 되어 결과적으로 value에는 100이 담기게 됩니다.


옵셔널 체이닝(Optional Chaining)

현재 nil일 수 있는 옵션에 대해 호출하는 프로세스입니다.

값에 optional이 포함되어 있다 하더라도 우선 속성이나 메서드 등의 첨자(.)호출을 성공시키게 됩니다.

여기서 옵션이 nil이면 속성, 메서드, 첨자 호출이 nil을 반환합니다. 이를 함께 체인으로 연결할 수 있으며, 체인에 있는 링크가 nil인 경우 전체 체인이 정상적으로 실패합니다.

class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}

Person은 옵셔널 프로퍼티를 가지고 있습니다. 이 Residence 클래스에는 값을 가지고 있는 프로퍼티가 하나 보이네요.

let john = Person()

객체를 하나 만들어준 후

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

할당을 시켜보면 에러가 발생합니다. residence의 값을 넣지 않았기 때문에 하위 속성에 접근할 수 없습니다.

그렇다면 옵셔널로 설정하면 하위 값에 접근할 수 있을까요?

let successNil = john.residence?.numberOfRooms

print(successNil)
// 결과
// nil

없습니다! 여전히 residence가 없기 때문이죠. 접근하고자 하는 속성의 상위값이 할당되어 있지 않다면 그 하위값들은 전부다 nil로 연쇄적으로 바뀝니다. 따라서

john.residence = Residence()

let successNil = john.residence?.numberOfRooms
print(successNil)

if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")
}

// 결과
// Optional(1)
// John's residence has 1 room(s).

이렇게 값을 할당해주고 출력을하면 정상적으로 Optional로 감싸진 값을 만날 수 있습니다. 이를 해결하기 위한 방법으로는 위에서 언급한 옵셔널 바인딩을 통해 원하는 값에 접근할 수 있어요.


궁금증들

  1. 암시적 언래핑

    @IBOulet weak var label: UILabel!

    UIKit에서 많이 볼 수 있는데요 해당 label이 실제로 호출이 될 때면 값이 있다는 것이 확인되게 됩니다.

    즉 처음에는 nil이지만 해당 값에 접근할 때 무조건 값이 존재하게 되며 이를 매번 언래핑하는 방법보다는 암시적으로 언래핑을 하게 됩니다.

    let value: String! = "안녕하세요"
    let value2 = value

    만약 다음과 같은 코드가 있다면 value2는 String일까요?

    Optional String입니다.

    value에 초기값을 할당하지 않고 nil을 준다면 어떻게 될까요?

    var value: String!
    value = nil
    
    print(value)
    print(value!)
    // 결과
    // nil

    fatal error가 발생하지 않습니다.

    암시적 언래핑은 동작 순서가 있기 때문인데요,

    옵셔널을 적용할 수 있다면 옵셔널이 먼저 적용됩니다. 그렇지 않은 상황이라면 언래핑된 값이 사용됩니다.

    위에 상황에서는 옵셔널을 적용할 수 있는 상황이기 때문에 nil이 됩니다. 한번더 강제언래핑을 한다면 그제서야 에러가 발생합니다.

  2. let과 함께 사용하는 Optional

    let userName: String?

    다음과 같이 사용하는 코드를 보신적이 있으신가요? 보통 네트워크응답 모델을 만들 때 종종 보셨을 것 같아요. 주요한 점은 userName의 값을 변경할 수 있습니다.

    let Optional은 init중에 값이 할당되어야 하기 때문입니다. 아직 nil인지 다른값이 들어오는지 확인이 필요하기 때문에 할당할 수 있습니다.

    let userName: String?
    userName = "mino"
    print(userName)
    
    // 결과
    // Optional("mino")
    ----------------------
    let userName: String?
    userName = "ss"
    userName = "dd" // Immutable value 'userName' may only be initialized once

    상수에 nil을 할당하는 경우는 어떤경우일까요?

    이 속성을 의도적으로 비워두었다는 표시와 동일합니다.

지금까지 옵셔널을 알아보았는데요! 워낙 익숙하게 사용하던 내용들이라 술술 포스팅이 될 것 같았지만 아니더라구요🥲 글을 작성하며 몰랐던 점들이 의외로 많이 있었다는 것... 배움에는 끝이..없다..

profile
아요쓰 정벅하기🐥
post-custom-banner

4개의 댓글

comment-user-thumbnail
2024년 3월 14일

Optional을 let으로 선언하는 방식은 completionHandler를 호출하는 경우에도 유용하게 쓸 수 있습니다

아래 aFunction은 개발자가 깜빡하면 컴파일러가 알려주는 데 반해
bFunction은 handler 호출을 깜빡해도 컴파일러가 잡아주지 못하는 경우가 있죠..

enum ManyCase{
    case caseA
    case caseB
    case caseC
    case caseD
}

func aFunction(aCase: ManyCase, handler: (String?) -> ()) {
    let param: String?
    
    switch aCase {
    case .caseA:
        print("business logic")
        param = "caseA"
    case .caseB:
        print("business logic")
        param = "caseB"
    case .caseC:
        print("business logic")
        param = "caseC"
    case .caseD:
        print("business logic")
        // 컴파일 에러로 깜빡한 것을 잡아낼 수 있음
    }
    
    handler(param) // Constant 'param' used before being initialized
}

func bFunction(aCase: ManyCase, handler: (String?) -> ()) {
    switch aCase {
    case .caseA:
        print("business logic")
        handler("caseA")
    case .caseB:
        print("business logic")
        handler("caseB")
    case .caseC:
        print("business logic")
        handler("caseC")
    case .caseD:
        print("business logic")
        // Handler 호출하는 거 까먹어도 컴파일 에러 안남
    }
}
1개의 답글
comment-user-thumbnail
2024년 3월 15일

이전에 옵셔널관련해서 공부하다가 헷갈렸던 부분이있었는데 참고삼아 남겨놓습니다

옵셔널 체이닝의 경우
let a = b?.c?.e 인데 만약에 e라는 변수가 c라는 객체내애서 non optional로 선언된 경우에도
a의 타입이 optional이라고하더라고요! 헷갈릴수도있는 부분이라서 추가로 남겨놓습니다:)

1개의 답글