[Swift] 옵셔널(Optional) 완전 정복하기: 개념부터 옵셔널 바인딩까지

2021. 8. 24. 13:51Swift, iOS Foundation

옵셔널(Optional)은 Swift의 언어적 특징을 가장 잘 나타내 주는 부분이다.

애플의 Swift는 안전성을 굉장히 중시하는 언어로
프로그래머가 행할 수 있는 실수를 문법 차원에서 미연에 방지할 수 있도록 되어 있는데,
그 일환으로 옵셔널(Optional)이라는 문법, guard구문, 오류처리, 강력한 타입 통제 같이 강력한 통제 수단을 활용해서 프로그래머가 코드를 작성하도록 Swift는 유도하고 있다.

오늘은 이 중에서 가장 많이 쓰린다고 볼 수 있는 옵셔널(Optional)에 대해서 알아보려 한다.

 

Apple Developer Documentation

 

developer.apple.com

 

1️⃣ 옵셔널(Optional)은?


옵셔널(Optional)은 "값이 있을 수도 있고 없을 수도 있는 경우"를 나타낸다.

값이 없을 수도 있다는 말이 무슨 말이지? 라고 생각할 수 있으니 한 가지 예시를 들어보겠다.

서버로부터 사용자의 정보를 받아 화면에 그려야하는 경우를 생각해보자.   

데이터를 문제없이 받아오는 경우에는 상관없겠지만, 만약 데이터를 받아올 때 문제가 생겨 받아오지 못하는 경우에는 레이아웃을 잡는 작업 혹은 더 나아가 해당 데이터를 참조하고 있는 경우에는 참조할 데이터가 없어지는 중대한 오류가 발생하게 되는 것이다.

이렇게 값이 들어올 수도, 들어오지 않을 수도 있는 경우에 발생할 수 있는 오류를 방지하기 위해 Swift에서는 옵셔널(Optional)이라는 것을 사용한다. 

옵셔널을 표시하는 방법은 아주 간단하게 데이터 타입(Data Type) 옆에 `?`를 붙여주기만 하면 된다.

또한 참고로, Swift에서 값이 들어오지 않는 경우는 "nil"이라는 키워드를 사용해 표현하게 된다!

var number: Int           // Int형 변수
var optionalNumber: Int?  // Int Optional형 변수 (위 Int형과는 다른 데이터 타입)

number = nil              // [에러 발생] Int형에는 값이 들어오지 않는 경우를 처리할 수 없다.
optionalNumber = nil      // 옵셔널 변수에는 값이 들어오지 않는 경우를 처리할 수 있다.
optionalNumber = 100      // 물론, Int형도 저장할 수 있다.

print(optionalNumber)     // Optional(100) 출력

 

위 코드에서는 Int형 number 변수와
Int 뒤에 "?"를 붙인 Int Optional형 optionalNumber 변수를 각각 선언해주었다.

number 변수는 그냥 Int형이기 때문에 반드시 값을 넣어주어야 한다.
만약, 값이 존재하지 않는다는 nil 키워드를 이 변수에 넣어줄 경우 에러가 발생하게 될 것이다.

반면, optionalNumber 변수는 옵셔널이므로 Int형 데이터가 들어와도, 들어오지 않아도 된다.
그래서 nil을 대입해도, 정수 100을 대입해도 이 코드는 모두 정상적으로 작동할 것이다.

단, 100이 들어있는 optionalNumber 변수를 print 했을 때,   
일반 100이 출력되는 것이 아니라, Optional(100)이 출력되는 것을 확인할 수 있다. (일반 100과는 분명 다른 자료형이다.)
이것은 값을 출력 했음에도 "100이 있을 수도 없을 수도 있는 상태"를 나타내고 있기 때문인데,   
그렇기 때문에 우리가 정상적으로 이 값을 사용하기 위해서는, 옵셔널에 들어있는 "값을 꺼내는 과정"이 반드시 필요하다.

이 과정을 언래핑(Unwrapping)이라고 부른다.   

언래핑에는 두 가지 방법이 있다.   
하나는, 옵셔널을 강제로 부수어 값을 꺼내 버리는 (과격한) 강제 언래핑 방식.   
또 다른 하나는, 값이 있는지 체크한 다음 값이 있을 경우에만 (친절하게) 동작을 하는 옵셔널 바인딩 방식으로 나누어진다.

 

2️⃣ 옵셔널 값을 꺼내기 위한 방법 (1): 강제 언래핑


먼저, 강제 언래핑은 "!"를 사용한다.
(강제 언래핑의 과격함을 !로 표현했다고 생각하자)

옵셔널에 들어 있던 값을 "!"를 사용해서 꺼낸다면, 옵셔널을 깨부수고 원래 데이터를 가져올 수 있다.


"!"만 붙이면 되는 강제 언래핑 방식이 쉽고, 간단해 보이는데 위험하다는 이유는 뭘까?

옵셔널을 깨부수고 정상적으로 원래 데이터를 가져올 수 있는 경우는 옵셔널 안에 값이 들어있는 경우에 한정해서이다.

그럼 만약, 옵셔널 안에 값이 들어있지 않다면?
변수 안에는 값이 담겨있지 않으니 당연히 그 값을 접근하려고 하면 에러를 발생시킬 것이다.
이렇게 되면, 원래 안전성을 중시하는 Swift의 노력이 필요 없어지는 것을 의미하며, 프로그래머가 행할 수 있는 실수를 막을 방법이 없어지게 된다.

그러니깐 강제 언래핑 사용은 되도록 "지양"하도록 하자.

// 강제 언래핑 작동
optionalNumber = 100
number = optionalNumber!

// 강제 언래핑 에러
optionalNumber = nil
number = optionalNumber!    // 값이 비어있기 때문에 런타임 오류가 발생 (빌드는 가능)

 

3️⃣ 옵셔널 값을 꺼내기 위한 방법 (2): if문을 사용한 옵셔널 바인딩


과격했던(?) 강제 언래핑과 다르게 옵셔널 바인딩은 부드러운(?) 방식을 사용한다.

가장 기본적으로 사용하는 옵셔널 바인딩은 if-let 구문을 사용해 옵셔널에 값이 있는지 없는지를 먼저 확인하고,   
만약 값이 있을 경우에는 해당 구문에서 지정한 (임시) 상수 혹은 변수에 할당해 이후 코드에서 사용하게 되는 방식이다.
아래 예시 코드의 경우, optionalNumber 안에 Int값이 있을 경우 number라는 임시 변수에 값이 할당되어 조건문 블록 안에서 자유롭게 사용할 수 있는 모습을 보인다.

이때 중요한 점은 number라는 변수는 임시 변수이므로, 반드시 조건문 블록 안에서만 사용할 수 있다는 점이다!

if let number: Int = optionalNumber {
    print(number)  // 옵셔널 바인딩 되어서 정상 출력
} else {
    print("The value is nil")
}
print(number)    // [에러 발생] number라는 상수는 임시 상수이므로 if-let 구문 안에서만 사용 가능하다.

 

4️⃣ 옵셔널 값을 꺼내기 위한 방법 (3): guard문을 사용한 옵셔널 바인딩


guard문을 이용해서도 옵셔널 바인딩을 해줄 수 있다.

'경비원, 문지기, 보호하다' 등의 의미를 가진 guard는 실제로 코드에서도 비슷한 역할을 한다.
guard라는 구문 안에서 조건을 검사하고, 조건과 다른 값이 전달되었을 경우 특정 실행 구문을 빠르게 종료하는 용도로 사용된다.
(말 그대로 값이 올바른지 아닌지 검사하는 모습이 아파트 주민이 맞는지 아닌지 검사하는 경비원의 모습과 유사하다.)

if문은 "해당 조건에 부합하는 경우에 코드 블록 부분이 실행되는" 조건 제어 문법이었다면, guard은 "조건과 다른 값이 전달"되었을 경우 코드 블록 부분이 실행된다는 점이 가장 큰 차이점이라고 볼 수 있다.

💡 guard문을 사용할 때 기본적으로 알아두어야 할 사항

1. guard 조건이 true일 경우에만 guard 구문 다음으로 위치한 코드가 실행된다.
2. 조건이 false인 경우에 실행될 else 구문을 반드시 함께 써야 한다.
3. else 구문에 작성할 코드는 반드시 return, break, continue, throw 같은 제어 변경 구문(Control Transfer Statements)이 포함되어야 한다.

아래는 위의 if let을 이용한 옵셔널 바인딩과 같은 역할을 하는 guard-let 옵셔널 바인딩 코드 예시이다.

guard let number = optionalNumber as? Int else { return }
print(number)

 

5️⃣ 그래서 if문과 guard문의 차이점은 무엇이고, 각각은 언제 사용하는게 좋은건데?


1. 가장 크게는 상수(let)의 사용범위가 다르다.

if-let 같은 경우에는 해당 구문이 true일 때를 블록 안에다 작성하기 때문에, 이 블록을 벗어나 상수를 사용하는 것이 불가능하다!
반면, guard-let은 해당 구문이 true이면 문제없이 코드가 진행되는 방식이다.
그러다 보니, guard문은 해당 상수(or 변수)를 메서드 내에서 지역 상수처럼 자유롭게 사용할 수 있다는 장점이 있다.

즉, 상수의 활용범위가 훨씬 넓은 guard-let 구문이 if-let 구문보다 자주 쓰이게 될 것이다.

2. else의 필수 사용 여부에서 차이가 난다.

if-let은 앞에서도 말했지만, 해당 조건이 "참인 경우"를 강조하는 문법이기 때문에 else 구문을 꼭 필수로 써주지 않아도 된다.

반면, guard는 해당 조건이 "거짓인 경우" 동작을 벗어나게끔 하는 문법이기 때문에 else 구문을 필수로 써줘야 하며, else 구문 안에는 동작을 종료시키기 위한 구문 return, break, continue, throw 같은 명령어를 꼭 써줘야 한다.

즉, else문에서 작성할 내용이 없거나 제어문 전환 명령어(return, break, continue, throw)를 사용할 수 없는 경우에는 guard-let 구문 대신, if-let 구분을 반드시 써야할 것이다.