본문 바로가기
문법관련/Swift

옵셔널과 옵셔널 체인 (꼼꼼한 재은 씨의 Swift : 문법편)

by print_soo 2022. 4. 10.

옵셔널 체인을 배우기전 옵셔널에 대한 기본적인 지식을 학습해보자. (출처)

 

Swift에서는 프로그램의 안정성을 높이기 위해서 오류 대신 nil 값을 반환함으로써 개발자에게 문제가 있다는 사실을 알린다.

여기서 nil은 "값이 없음"을 의미한다. 

 

그렇다면 옵셔널이란 무엇일까?

 

옵셔널이란 값이 없을 수도 있는 상황에서 사용한다. 이때 값이 없다는 것은 nil을 의미하는데 이 nil을 반환하기 위해 필요한 것이 옵셔널이다. 오직 옵셔널 타입만이 nil을 반환할 수 있다. 따라서 오류 발생의 가능성이 조금이라도 있으면 옵셔널로 정의해야 한다. 


옵셔널 타입이란 무엇인가?

 

옵셔널은 nil이거나 nil이 아닌 값만 가질 수 있다.

- nil: 실행 과정에서 오류가 발생했을 때

- nil이 아닌 값: 오류가 발생하지 않았을 때 반환하려는 값이 옵셔널로 둘러싸인 형태의 값

 

자료형 뒤에 ? 를 붙여서 일반 자료형을 옵셔널 타입으로 바꿀 수 있다. 

var optionalValue: String? = "Swift"

옵셔널 타입의 변수에 일반 값을 할당할 때에는 위와 같이 일반 변수처럼 할당하면 알아서 옵셔널 객체 내부에 값이 대입된다. 

만약에 값을 할당하지 않으면 자동으로 nil로 초기화된다.


옵셔널 해제

옵셔널 타입은 연산을 지원하지 않기 때문에, 옵셔널 타입의 결과값으로만 할 수 있는게 없다. 따라서 옵셔널 언래핑을 통해서 옵셔널 객체를 해제해야 일반 타입의 값으로 사용할 수 있다.

<명시적 해제와 묵시적 해제>
- 명시적 해제
1. 강제 해제
2. 비강제 해제(옵셔널 바인딩)

- 묵시적 해제
1. 컴파일러에 의한 자동 해제
2. ! 연산자를 통한 자동 해제

명시적 해제 - 강제 해제

 

강제 해제는 옵셔널 타입의 값 뒤에 ! 만 붙여주면 일반 타입으로 강제 해제 된다. 강제 해제는 옵셔널 타입에 nil 값이 없다고 확실할 때만 사용해야한다. 그렇지 않으면 오류가 발생하게 된다.

var optionalInt: Int? = 10

print(optionalInt) // Optional(10)
print(optionalInt!) // 10

 

명시적 해제 - 옵셔널 바인딩

 

옵셔널 바인딩은 조건문 내에서 옵셔널 값을 일반 변수나 상수에 할당하는 구문을 사용하는 방식이다. 무조건 조건문에서만 사용해야 하고, 상수에 옵셔널 값을 대입한 결과는 true / false로 반환된다.  

 

강제 해제 연산자를 사용하지 않아도 옵셔널 값이 일반 변수나 상수에 할당되면서 자연스럽게 옵셔널 타입이 해제 되지만, 값이 nil이 더라도 값의 할당이 실패하여 결과값이 false로 반환된다. 

//if문 기본 구문
if let name: Type = OptionalExpression {
    Statements
}

//if문 예제
var myName: String? = "김준수"

if let name = myName { 
	print("내 이름은 \(name)")
} else {
	print("error!")
}
// 일반 상수에 옵셔널 타입 변수를 대입하면 자동 옵셔널 해제가 되는데 이때 값이 있다면 true 값을 반환해서 
// if 구문을 실행하지만 값이 없는 nil 상태라면 false 값을 반환해서 else 구문이 실행된다. 
-----------------------------------------------

//guard문
guard let name: Type = OptionalExpression else {
    Statements
    //바인딩에 실패할 경우 else블록이 실행된다.
}

//guard문 예제
var myName: String? = "김준수"

guard let myName = myName else { //이렇게 상수의 이름과 OptionalExpression의 이름을 동일하게 사용하는 경우가 대부분이다.
    print("erorr)"
}
print("내 이름은 \(myName)")

묵시적 해제 - 컴파일러에 의한 자동 해제

 

옵셔널 객체의 값을 비교 연산자를 사용하여 일반 타입과 비교하는 경우, 컴파일러에서 자동으로 옵셔널을 해제한다. 또한, 옵셔널 타입에 값을 할당할 때도 순수 리터럴만으로 정의할 수 있다. 

<!을 이용한 강제해제 후 값 비교>

let optInt1 = Int("123")

if (optInt1!) == 123 {
    print("optInt1 == 123")
} else {
    print("optInt1 != 123")
}

//실행결과: optInt1 == 123

------------------------------
<강제해제 없이 값 비교>

let optInt2 = Int("321")

if optInt2 == 321 {
    print("optInt2 == 321")
} else {
    print("optInt2 != 321")
}

//실행결과: optInt2 == 321

강제해제를 한 경우 123이지만 강제해제를 하지 않은 경우에는 Optional(321)이므로 둘은 다르다. 하지만 값을 비교할 때 컴파일러에서 자동으로 옵셔널을 해제해서 결과는 같다.

 

묵시적 해제 - ! 연산자를 통한 자동 해제

 

옵셔널 변수의 타입을 선언할 때 ? 연산자 대신에 ! 연산자를 사용하면 컴파일러가 알아서 옵셔널을 해제해준다. 묵시적 해제는 형식상 옵셔널로 정의해야 하긴하지만 실제 사용시 nil이 대입될 가능성이 없는 변수일 때만 사용한다. (주의! 옵셔널 강제 해제의 !과는 다르다.)

<? 연산자 사용>

var str: String? = "옵셔널인 스위프트"
print(str) // Optional("옵셔널인 스위프트")

--------------------------------------------

<! 연산자 사용>

var str: String! = "옵셔널인 스위프트"
print(str) // 옵셔널인 스위프트

이제 옵셔널 체인에 대해서 배워보자.

 

옵셔널 체인을 학습하기전 옵셔널 타입의 문제점을 알아보자.

 

옵셔널 타입은 nil을 대입할 수 있는 타입이기 때문에 옵셔널 타입을 사용할 때는 항상 if 구문이나 다른 방법을 통해서 값의 안정성 여부를 확인해야한다. 이 과정은 코드를 길게할 뿐만 아니라 굳이 값을 다시 확인해야 안전하게 사용할 수 있다는 점에서 옵셔널  타입을 사용할 필요가 있을까에 대한 의견도 많다. (아래 예제 참고)

 

struct Human {
    var name: String?
    var man: Bool = true
}

struct Company {
    var ceo: Human?
    var companyName: String?
}

var startup: Company? = Company(ceo: Human(name: "김대표", man: true), companyName: "iOS때문에")

// ceo의 nmae 프로퍼티를 참조해보자.

if let company = startup {
    if let ceo = company.ceo {
        if let name = ceo.name {
            print("우리회사 대표님의 이름은 \(name)입니다.")
        }
    }
}

 

따라서 이러한 문제를 해결하기위해서 제공되는 것이 바로 옵셔널 체인이다. 

 

[옵셔널 체인]

옵셔널 체인은 옵셔널 타입으로 정의된 값이 하위 프로퍼티나 메소드를 가지고 있을 때, 이 요소들을 if 구문을 쓰지 않고도 간결하게 사용할 수 있는 코드를 작성하기 위해서 만들어졌다.

 

옵셔널 체인의 기본 형태는 옵셔널 타입의 객체 바로 뒤에 ? 연산자를 붙여준다는 것이다. (예제 참고)

 

// 위의 옵셔널 타입의 문제점에서 다룬 예제를 더 간결한 코드로 작성해보겠다. 
struct Human {
    var name: String?
    var man: Bool = true
}

struct Company {
    var ceo: Human?
    var companyName: String?
}

var startup: Company? = Company(ceo: Human(name: "김대표", man: true), companyName: "iOS때문에")

// ceo의 nmae 프로퍼티를 참조해보자.

if let name = startup?.ceo?.name {
    print("우리회사 대표님의 이름은 \(name)입니다.")
}

 

맨 마지막 name은 ? 연산자가 사용되지 않았는데 그 이유는 맨 마지막 값 자체가 옵셔널 체인에 해당하지 않기 때문이다. 옵셔널 체인으로 처리할 수 있는 것은 하위 속성이나 메소드를 호출해야 할 때이다.

 

마지막 값은 다시 하위 속성이나 메소드를 호출하는 것이 아니라 직접 사용햐야 하는 값이기 때문에 옵셔널에 대한 검사가 필요하지만 값을 참조하는 것이 아니라 할당해야 한다면 옵셔널 체이닝으로 아래와 같이 간편하게 작성할 수 있다. 

 

startup?.ceo?.name = "iOSoo"

이때 startup이나 ceo의 값이 비어있다면 아무런 값도 할당되지 않은 채로 구문은 종료된다. (오류는 발생하지 않는다.)


옵셔널 체인에는 아래와 같은 특징이 있다.

 

1. 옵셔널 체인으로 참조된 값은 무조건 옵셔널 타입으로 반환된다.
- 옵셔널 체인 구문에서 마지막에 오는 값이 일반 타입이라도 옵셔널 체인을 통해 참조했다면 이 값은 옵셔널 타입으로 변경된다.

옵셔널 탕비으로 반환되는 이유는 옵셔널 체인이라는 구문 자체가 nil을 반환할 가능성을 내포하고 있기 때문이다.

print(startup?.ceo?.man)
// Optional(true)

옵셔널 체인의 흐름

 

 

2. 옵셔널 체인 과정에서 옵셔널 타입들이 여러 번 겹쳐 있더라도 중첩되지 않고 한번만 처리된다. 

- 옵셔널 처리를 여러번 해주면 아래와 같다고 생각할 수 있지만 전혀 아니다.

startup?.ceo?.name -> Optional(Optional("김대표")

옵셔널 타입을 몇 번 중첩하더라도 결국 반환할 수 있는 값은 nil 또는 정상값 두 개로 나누어 지므로 단순히 하나의 옵셔널 객체로 감싼 값일 뿐이다. 

 


지금까지 우리는 구조체와 클래스에 대해서 알아보았다. 구조체와 클래스는 Swift에서 핵심적인 내용이기 때문에 완벽하게 이해하고 다음 열거형과 익스텐션으로 넘어가야한다.