Notice
Recent Posts
Recent Comments
Link
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

일왓록(日What錄)

[Swift] 옵셔널(Optional)을 알아보자 본문

iOS/Swift

[Swift] 옵셔널(Optional)을 알아보자

일왓 2023. 8. 1. 22:59

 

프로젝트를 진행하면서 코드를 작성하거나 Swift로 작성된 코드를 보다보면 뜬금 없는 ? 라던지 ! 라던지 이러한 Optional을 정말 많이 접하게 된다. 이러한 Optional 개념 때문에 컴파일 오류가 뜨는 경험을 아주 많이 했었다. 그럴 때마다 그냥 인터넷 검색해서 어떻게든 해결하면 이번엔 런타임 오류... 그래서 차라리 이럴바에 다른 언어처럼 런타임 오류만 나게 해주면 안되나... 이런 생각을 하기도 했었다.

공부를 했으면 됐을 것을...ㅎㅎ; 이렇게 필자를 아주 귀찮게 했던 옵셔널(Optional)에 대해서 알아보자.


옵셔널의 개요

Swift는 안전성을 굉장히 강조하는 언어다. 옵셔널은 그 안전성을 높이기 위해 도입된 스위프트만의 새로운 개념이다.

제모오옥은 Swift로 하겠습니다... 근데 이제 안전을 곁들인,,,

당장 구글에 Swift라고 쳐서 나오는 애플 공식 홈페이지 첫 화면에 떡하니 써져 있다. 그 밑으로 내려보면 바로 옵셔널에 대한 설명도 있다.

 

또 다른 안전 기능은 기본적으로 Swift 객체가 결코 nil이 될 수 없게 하는 것입니다. 실제로 Swift 컴파일러는 컴파일 시 오류가 있는 nil 객체를 만들거나 사용할 수 없도록 합니다. 이렇게 하면 코드를 훨씬 깔끔하고 안전하게 작성할 수 있으며 앱에서 거대한 카테고리의 런타임 충돌을 방지할 수 있습니다. 하지만 nil이 유효하고 적절한 경우도 있습니다. 이러한 경우를 위해 Swift는 선택 사항이라는 혁신적인 기능을 제공합니다.

여기서 말하는 선택사항이라는 것이 옵셔널(Optional)을 말하는 것이다.

 

여기서 nil은 값이 없음을 의미하는데, 순수하게 아무 것도 없는 공란과 같은 것이다. 마치 타 언어들의 Null 또는 null 과 같은 개념이다. 

nil이 필요한 경우는 꽤 많다. 예를 들어 특정 변수가 nil이라면 할당을 한다거나 하는 조건문을 작성하고 싶을 때 처럼 말이다. 예가 적절한

지는 모르겠지만...?!

 

"Optional은 어떠한 객체(변수, 상수, 클래스 무엇이든)든지 간에 nil이 될 수 없게 하겠지만 nil이 필요한 경우가 있으니, 선택권을 주겠다." 라는 것이 Optional의 개념인 것이다.

 

 

그래서 래핑된(Wrapped) 값 또는 값이 없음(nil)을 나타내는 값의 타입을 Optional이라고 부른다.


옵셔널의 사용

 

Optional | Apple Developer Documentation

A type that represents either a wrapped value or the absence of a value.

developer.apple.com

 

사실 개요를 조금 추상적으로? 복잡하게 들어갔지만 사용하는 시기는 간단하다고 본다.

내가 사용하고 싶은 객체가 상황에 따라서 값이 존재할지도, 존재하지 않을수도 있다 하면 사용하면 된다.

사용법은 간단하다!

타입 뒤에 '?' 를 붙이거나 Optional<타입> 으로 객체를 선언해주면 된다.

참고로, 어떤 타입이든 옵셔널로 선언할 수 있다. 근데 그렇다고 해서 별도로 존재하는 자료형은 아니다 기본 자료형들에 대응하는 옵셔넌 타입인 것이다. 즉, Int와 Int?의 타입이 따로 존재하는 것이 아닌 "타입이 Int인데 값이 있을 수도 있도 없을수도 있어" 단순히 그 표현이다.

let shortForm: Int? = Int("42")
let longForm: Optional<Int> = Int("42")

이렇게 옵셔널로 선언해야지만 nil을 사용할 수 있다!

이렇게 선언하지 않고 일반적으로 선언한 타입에 nil을 할당하면 컴파일 에러가 발생해서 빌드 안된다.

 

근데 여기서 끝나면 얼마나 좋을까....

여기서 끝나지 않는다...

아까 말했던 것 처럼, 래핑(Wrapping) 이라는 개념이 있다.

var nickname: String?

print(nickname)

nickname = "고래밥"

print(nickname) //Optional()...

 

다음과 같이, 값이 없는 경우에는 nil이지만 nil이 아닌 값이 할당 되었을 때에는 옵셔널 타입으로 한번 감싸져서 나온다.

마치 박스 안에 담긴 물건이라고 생각하면 된다!

이와 같이, 옵셔널 객체(상자)로 다시 한 번 감싼 형태를 옵셔널 래핑(Optional Wrapping)이라고 한다.

 

래핑된 값은 우리가 할당 했던 원래의 타입처럼 사용할 수가 없다.

아까의 박스 안에 담긴 물건이라는 예시를 다시 가져오면, 박스 안에 담긴 물건이 폭탄(nil)인지, 제대로된 물건인지 확인하는 절차가 필요하다.  그래서 조심스럽게 박스를 확인하고 제대로 된 값이면 활용하는 방식으로 접근을 해야한다.

 

그래서 이처럼 우리가 이 값을 제대로 활용하기 위해서 값을 확인하고 꺼내는 것을 옵셔널 언래핑(Optional Unwrapping)이라고 한다.


 

옵셔널 언래핑

옵셔널 언래핑은 크게 두가지 방법으로 나뉜다.

명시적 해제 묵시적 해제
강제 해제 컴파일러에 의한 자동해제
비강제 해제 ! 연산자를 사용한 자동해제

 

강제 해제(Unconditional Unwrapping, Forced Unwrapping)

강제 해제는 Forced Unwrapping 혹은 Unconditional Unwrapping이라고 불린다.

공식 문서에 따르면 옵셔널 인스턴스가 값을 가지고 있는 것을 확신할때, 강제 해제 연산자(forced unwrap operator)인 변수 뒤에 '!'를 붙여 언래핑하는 방법을 말한다.

 

When you’re certain that an instance of Optional contains a value, you can unconditionally unwrap the value by using the forced unwrap operator (postfix !). For example, the result of the failable Int initializer is unconditionally unwrapped in the example below.

 

//Case. 1
let number = Int("7")!
print(number)
// 결과 
// 7


//Case. 2
var number2: Int? = 7
print("일반 출력: \(number2)")
print("강제 해제 출력: \(number2!)")

//결과
//일반 출력: Optional(7)
//강제 해제 출력: 7

 

Int(String) 생성자는 옵셔널 정수값을 반환한다. 

따라서 number 상수는 Optional(7)이 되겠지만 강제 해제 연산자(forced unwrap operator) ! 가 붙어 언래핑된 값 7이 할당되게 된다.

number 2도 Optional(7)이 할당되었지만, 문자열 보간법(String intepolation)에서 강제 해제 연산자가 붙으면서 언래핑 된 것을 확인할 수 있다.

Case 1의 경우에는 할당을 할때부터 언래핑하여 값을 할당한 것이고, Case 2의 경우에는 옵셔널로 두다가 값을 사용하고자 할때 언래핑을 진행 한 것이다. 어느 상황에서든 사용할 수 있기 때문에 필요할 때 사용하면 되지만!!

 

위에서 언급했듯이 무조건 값이 있다고 확신하는 상황에 사용하는 방법인 만큼 만약 nil인 상태에서 강제 해제를 하게 되면 런타임 오류가 발생하게 되니 주의해서 사용해야한다.

 

즉, 무조건 값에 무조건 값이 있을 수 밖에 없는 상황에서 사용하는 방법이라고 생각하면 된다.

 

 

비강제 해제(Optional Binding)

Optional Binding(옵셔널 바인딩)이라고 불리는 비강제 해제 방법은 조건문 내에서 조건식 대신 옵셔널 값을 변수나 상수에 할당하는 구문을 사용(Binding)하는 방식을 말한다.

공식문서의 해석으로는 if let문, guard let문, switch문을 사용하여 래핑된 옵셔널 인스턴스를 새로운 변수로 조건적인 할당(bind)을 진행하는 것이라고 나온다.

 

To conditionally bind the wrapped value of an Optional instance to a new variable, use one of the optional binding control structures, including if let, guard let, and switch.

 

옵셔널 값을 대입한 결과는 true/false로 리턴되기 때문에 안전한 옵셔널 해제가 가능하다.

let number = Int("7")

if let newNumber = number {
	print("옵셔널 언래핑 성공")
} else {
	print("값이 nil입니다.")
}

//또는

func test(){
	let number = Int("7")
    
    //false일때만 실행하여 새로운 분기를 일으키는 조건문 -> guard문
    //newNumber에 담길 number가 nil이면 false를 반환
    guard let newNumber = number else {
    	print("값이 nil입니다.")
        return
   }
   
}

위와 같은 방식으로 조건식을 통해 상수/변수에 래핑된 옵셔널 인스턴스를 할당한 뒤, 사용하는 방식인데 if문과 guard문의 특성을 활용하여 원하는 흐름대로 if let, guard let을 사용하면 된다. 

 

근데 여기서 필자는 한가지 의문점이 생겼는데, if let, guard let 대신 if var, guard var를 사용하면 안되나? 였다.

결론은 사용해도 된다.

필요에 따라 변수로 선언해서 사용해도 된다. 옵셔널 언래핑도 정상적으로 수행된다.

그럼 왜 애플 공식문서도 그렇고 대부분의 문서들에서 if let/guard let문만 적어 놓을까?

 

⚠️ 필자의 개인적 의견⚠️

1. if let / guard let은 snippets에 저장되어있을 만큼 용도가 확실하다.

 

snippets에 if let / guard let의 설명을 보면 옵셔널 바인딩을 위한 조건문으로 쐐기가 박혀 있다.

재밌는 점은 if let의 설명을 보면 상수 또는 변수로 사용할 수 있다고 하는데, 그 말은 즉슨 let을 var로 변환해도 된다는 얘기다.(let만 허용할 것이라면 상수라고만 적혀 있을테니까...)

 

2. 옵셔널 언래핑을 위해서 사용하는 바인딩인데 굳이 변수를 쓸 필요가 있을까?

결국에 if let / guard let을 사용하는 이유는 옵셔널 언래핑을 위해서 래핑되어 있는 옵셔널 인스턴스를 할당시키면서 해제 시키는 것이다.

즉, 값을 변경할 필요가 없다는 것이다. 아니, 어쩌면 값을 변경시키는게 의도에 다른 것이라고 볼 수도 있을 것이다.

그렇기 때문에 고정적인 값을 할당하는 let이 더 의도에 맞고 보편적인 것이다.

 

 

컴파일러에 의한 옵셔널 자동해제(Forced-unwrapping)

말 그대로 컴파일러가 자동적으로 옵셔널 래핑을 해제 시켜주는 것이다.

직접 예제를 보는 것이 이해가 빠르다

let number = Int("7") //Optional(7)로 초기화

if number == 7{
    print("number == 7")
} else {
    print("number != 7")
}


if number! == 7{
    print("number == 7")
} else {
    print("number != 7")
}

number는 옵셔널 래핑이 되어있는 상태라서 7과 비교를 하게 되면 잘못 된게 아닌가? 하는 생각이 들 수도 있다.

하지만 막상 실행해보면 결과는 똑같다.

이게 바로 컴파일러가 자동적으로 옵셔널을 해제 해주는 경우다.

옵셔널 래핑된 값과 일반 타입을 비교하고자 하면 컴파일러는 알아서 Optional을 해제 하여 비교 연산을 진행 해주는 것이다.

 

대표적으로 값을 비교하는 상황에서 많이 쓰인다.

 

재밌는 것은 다음 4가지가 다 참이라는 것이다.

number == 7
number == Optional(7)
number! == 7
number! == Optional(7)

만약 컴파일러가 자동해제 해주지 않으면 매번 강제 해제 연산자를 사용해야되니 애플이 개발자의 편의를 생각해준게 아닌가 싶다.

 

 

 

옵셔널의 묵시적 해제(Implicitly Unwrapped Optional)

묵시적 해제는 옵셔널이긴 하지만 값을 사용할 때에는 자동으로 옵셔널이 해제 되어 언래핑 과정이 필요 없는 방식이다.

사실상 옵셔널이 아닌 일반 타입처럼 사용하면 된다는 것이다. 

컴파일러가 자동으로 옵셔널을 해제해 준다는 점에서 위의 자동 해제와 유사하지만, 자동 해제가 비교 연산이나 값의 할당 등 일부 구문에 한정되는 것과 달리 묵시적 해제는 옵셔널 변수를 미리 선언해주어야 한다는 차이점이 있다.

 

 

옵셔널 변수를 선언 할때 ? 연산자 대신 ! 연산자를 사용해주면 된다.

var number: Int? = 7

var nuber2: Int! = 7

여기서 연산을 진행해보자

! 연산자로 선언한 옵셔널 Int타입의 경우는 일반 변수처럼 사용해도 연산이 잘 진행된 반면, ? 연산자로 옵셔널 Int타입을 선언한 경우에는 강제 해제 연산자를 사용해야지만 연산이 가능하다.

 

굉장히 편리한데 싶은데 사용조건이 생각보다 어렵다.

형식상 옵셔널로 정의해야하지만, 실제로 사용할 때에는 절대 nil 값이 대입될 가능성이 없는 변수일 때 사용해야 된다는 것인데, 예제를 보면 이해가 더 빠를 것이다.

var number: Int! = Int("777")

"777"을 Int로 변환하면 성공적으로 변환될 것이다. 하지만 Int()의 반환값은 옵셔널이기 때문에 옵셔널 Int타입으로 할당을 해야한다.

그렇기 때문에 이와 같은 경우에 형식상 옵셔널로 정의해야하지만, 실제로 사용할 때에는 절대 nil 값이 대입될 가능성이 없는 변수인 것이다.

 

 

 

nil 병합 연산자(nil coalescing Operator)

nil 병합 연산자는 ?? 연산자를 사용하여 옵셔널 언래핑을 수행하거나 nil일 때 할당할 기본 값을 지정하는 것을 말한다.

사용법은 다음과 같다

 

Use the nil-coalescing operator (??) to supply a default value in case the Optional instance is nil. Here a default path is supplied for an image that is missing from imagePaths.
 

 

사용법은 다음과 같다

let newNumber2 = number ?? 10
print(newNumber2)

위처럼 number 값이 존재할때는 number의 값을 할당하지만

위와 같이 nil일 경우에는 뒤에 적어 놓은 값을 할당한다.

재밌는 점은 연쇄적으로 사용하는 것도 가능하다.

 

 

 

 

⚠️이 글은 필자가 새싹, 서적(꼼꼼한 재은씨의 스위프트), 블로그 등을 토대로 습득한 내용을 정리해보면서 다시 공부하는 글입니다. 개념을 습득하는 과정 중 잘못된 개념이 있을 수 있으니 그 점에 유의하시길 바라며, 잘못된 내용은 피드백을 해주시면 감사드리겠습니다.⚠️