🔥 옵셔널

1741자
22분

우리는 종종 값이 없는 상황을 마주하게 됩니다. 이럴 때 옵셔널을 사용하면 유용하지요. 옵셔널은 두 가지 가능성을 내포하고 있어요. 하나는 특정 타입의 값이 존재하는 경우이고, 이 경우에는 옵셔널을 통해 그 값에 접근할 수 있습니다. 다른 하나는 값이 전혀 없는 경우예요.

값이 없을 수 있는 상황의 예로 Swift의 Int 타입을 들 수 있겠네요. IntString 값을 Int 값으로 변환하려 시도하는 이니셜라이저를 가지고 있어요. 그런데 모든 문자열을 정수로 변환할 수 있는 건 아니에요. "123"은 숫자 123으로 변환이 가능하지만, "hello, world"는 해당하는 숫자 값이 없죠. 아래 예시는 이니셜라이저를 사용해 StringInt로 변환하려 시도하는 모습을 보여줍니다.

let possibleNumber = "123"
let convertedNumber = Int(possibleNumber)
// convertedNumber의 타입은 "Int?"
swift

위의 코드에서 사용된 이니셜라이저는 실패할 가능성이 있기에, Int가 아닌 optional Int를 반환하게 되는 거예요.

옵셔널 타입을 작성하려면 옵셔널이 포함하는 타입 이름 뒤에 물음표(?)를 붙이면 됩니다. 예를 들어, Int 옵셔널의 타입은 Int?가 되는 거죠. Int 옵셔널은 항상 어떤 Int 값을 포함하거나, 아니면 아무 값도 포함하지 않게 됩니다. Bool이나 String 값 같은 걸 포함할 순 없어요.

nil

옵셔널 변수를 값이 없는 상태로 설정하려면 특별한 값인 nil을 할당하면 됩니다.

var serverResponseCode: Int? = 404
// serverResponseCode는 404라는 실제 Int 값을 포함
serverResponseCode = nil
// 이제 serverResponseCode는 값을 포함하지 않음
swift

기본값을 제공하지 않고 옵셔널 변수를 정의하면, 그 변수는 자동으로 nil로 설정됩니다.

var surveyAnswer: String?
// surveyAnswer는 자동으로 nil로 설정됨
swift

옵셔널이 값을 포함하고 있는지 알아보려면 if 문을 사용해 옵셔널을 nil과 비교하면 돼요. 이 비교는 "같음" 연산자(==)나 "같지 않음" 연산자(!=)를 사용해 수행할 수 있어요.

옵셔널에 값이 있다면, nil과 "같지 않은" 것으로 간주됩니다.

let possibleNumber = "123"
let convertedNumber = Int(possibleNumber)
 
if convertedNumber != nil {
    print("convertedNumber는 어떤 정수 값을 포함하고 있습니다.")
}
// "convertedNumber는 어떤 정수 값을 포함하고 있습니다."가 출력됨
swift

nil은 옵셔널이 아닌 상수나 변수에는 사용할 수 없어요. 만약 여러분의 코드에서 상수나 변수가 특정 조건에서 값의 부재를 다뤄야 한다면, 그것을 적절한 타입의 옵셔널 값으로 선언하세요. 옵셔널이 아닌 값으로 선언된 상수나 변수는 절대 nil 값을 포함할 수 없음이 보장됩니다. 옵셔널이 아닌 값에 nil을 할당하려 하면, 컴파일 타임 에러가 발생할 거예요.

이렇게 옵셔널 값과 옵셔널이 아닌 값을 분리하면 어떤 정보가 누락될 수 있는지 명시적으로 표시할 수 있고, 누락된 값을 다루는 코드를 더 쉽게 작성할 수 있게 됩니다. 옵셔널을 실수로 옵셔널이 아닌 것처럼 처리하는 실수는 컴파일 시점에 에러를 발생시키기에 방지할 수 있어요. 값을 한 번 언래핑하고 나면, 그 값을 다루는 다른 코드들은 nil 체크를 반복할 필요가 없죠. 따라서 코드의 서로 다른 부분에서 같은 값을 반복 체크할 필요가 없게 됩니다.

옵셔널 값에 접근할 때는, 코드가 항상 nilnil이 아닌 경우 모두를 처리해야 합니다. 값이 없을 때 할 수 있는 몇 가지 방법이 있는데요, 다음 섹션들에서 설명하겠습니다.

  • 값이 nil일 때 그 값을 다루는 코드를 건너뛰기
  • nil 값을 전파하기 (즉, nil을 반환하거나 Optional Chaining에 설명된 ?. 연산자 사용하기)
  • ?? 연산자를 사용해 대체 값 제공하기
  • ! 연산자를 사용해 프로그램 실행을 중단하기

참고로 Objective-C에서 nil은 존재하지 않는 객체를 가리키는 포인터예요. 반면에 Swift에서 nil은 포인터가 아니에요. 그것은 특정 타입의 값의 부재를 나타내죠. 어떤 타입의 옵셔널도 nil로 설정될 수 있고, 객체 타입에만 국한되지 않아요.

옵셔널 바인딩

옵셔널 바인딩은 옵셔널에 값이 포함되어 있는지 확인하고, 만약 그렇다면 그 값을 임시 상수나 변수로 사용 가능하게 만드는 기능이에요. 옵셔널 바인딩은 if, guard, while 문과 함께 사용되어 옵셔널 내부의 값을 확인하고, 단일 동작의 일부로써 그 값을 상수나 변수로 추출할 수 있습니다. if, guard, while 문에 대한 더 자세한 정보는 Control Flow를 참고하세요.

if 문에서 옵셔널 바인딩을 작성하는 방법은 다음과 같아요.

if let <#constantName#> = <#someOptional#> {
   <#statements#>
}
swift

Optionals 섹션의 possibleNumber 예제를 강제 언래핑 대신 옵셔널 바인딩을 사용하도록 다시 작성해볼까요?

if let actualNumber = Int(possibleNumber) {
    print("\\"\(possibleNumber)\\" 문자열은 \(actualNumber)라는 정수 값을 가집니다.")
} else {
    print("\\"\(possibleNumber)\\" 문자열을 정수로 변환할 수 없습니다.")
}
// "123" 문자열은 123라는 정수 값을 가집니다."가 출력됨
swift

이 코드는 이렇게 읽힐 수 있겠네요.

"Int(possibleNumber)가 반환한 옵셔널 Int가 값을 포함하고 있다면, actualNumber라는 새로운 상수를 그 옵셔널이 포함하는 값으로 설정하라."

변환이 성공하면 actualNumber 상수는 if 문의 첫 번째 분기 내에서 사용 가능해집니다. 그것은 이미 옵셔널이 포함했던 값으로 초기화되었고, 해당하는 옵셔널이 아닌 타입을 가지게 되죠. 이 경우에 possibleNumber의 타입은 Int?이므로, actualNumber의 타입은 Int가 됩니다.

값을 포함하고 있던 원래의 옵셔널 상수나 변수에 접근한 후에는 더 이상 그것을 참조할 필요가 없다면, 새로운 상수나 변수에 같은 이름을 사용할 수 있어요.

let myNumber = Int(possibleNumber)
// 여기서 myNumber는 옵셔널 정수
if let myNumber = myNumber {
    // 여기서 myNumber는 옵셔널이 아닌 정수
    print("내 숫자는 \(myNumber)입니다.")
}
// "내 숫자는 123입니다."가 출력됨
swift

이 코드는 이전 예제의 코드처럼 myNumber에 값이 포함되어 있는지 확인하는 것으로 시작해요. myNumber에 값이 있다면, myNumber라는 이름의 새로운 상수의 값이 그 값으로 설정됩니다. if 문의 본문 내에서 myNumber를 작성하면 그 새로운 옵셔널이 아닌 상수를 참조하게 되죠. if 문 이전이나 이후에 myNumber를 작성하면 원래의 옵셔널 정수 상수를 참조하게 됩니다.

이런 종류의 코드가 매우 일반적이기 때문에, 옵셔널 값을 언래핑할 때 더 짧은 표기법을 사용할 수 있어요. 언래핑하려는 상수나 변수의 이름만 작성하면 됩니다. 새로운 언래핑된 상수나 변수는 암시적으로 옵셔널 값과 같은 이름을 사용하게 되죠.

if let myNumber {
    print("내 숫자는 \(myNumber)입니다.")
}
// "내 숫자는 123입니다."가 출력됨
swift

옵셔널 바인딩에는 상수와 변수 모두 사용할 수 있습니다. if 문의 첫 번째 분기 내에서 myNumber의 값을 조작하고 싶다면, if var myNumber라고 작성할 수 있고, 옵셔널에 포함된 값은 상수가 아닌 변수로 사용 가능해질 거예요. if 문의 본문 내에서 myNumber에 가하는 변경 사항은 오직 그 지역 변수에만 적용되며, 언래핑한 원래의 옵셔널 상수나 변수에는 적용되지 않아요.

하나의 if 문에 필요한 만큼의 옵셔널 바인딩과 불리언 조건을 콤마로 구분하여 포함시킬 수 있습니다. 옵셔널 바인딩의 값 중 어느 것이라도 nil이거나 불리언 조건 중 어느 것이라도 false로 평가되면, 전체 if 문의 조건은 false로 간주돼요. 다음의 if 문들은 서로 동등합니다.

if let firstNumber = Int("4"), let secondNumber = Int("42"), firstNumber < secondNumber && secondNumber < 100 {
    print("\(firstNumber) < \(secondNumber) < 100")
}
// "4 < 42 < 100"이 출력됨
 
 
if let firstNumber = Int("4") {
    if let secondNumber = Int("42") {
        if firstNumber < secondNumber && secondNumber < 100 {
            print("\(firstNumber) < \(secondNumber) < 100")
        }
    }
}
// "4 < 42 < 100"이 출력됨
swift

if 문에서 옵셔널 바인딩으로 생성된 상수와 변수는 if 문의 본문 내에서만 사용 가능해요. 이와는 대조적으로, guard 문으로 생성된 상수와 변수는 Early Exit에 설명된 대로 guard 문을 따르는 코드의 줄에서 사용 가능하답니다.

대체 값 제공하기

누락된 값을 처리하는 또 다른 방법은 nil-coalescing 연산자(??)를 사용하여 기본값을 제공하는 거예요. ??의 왼쪽에 있는 옵셔널이 nil이 아니라면, 그 값이 언래핑되어 사용됩니다. 그렇지 않으면 ?? 오른쪽의 값이 사용되죠. 예를 들어, 아래 코드는 이름이 지정되었다면 그 이름으로 누군가에게 인사하고, 이름이 nil이라면 일반적인 인사말을 사용합니다.

let name: String? = nil
let greeting = "안녕하세요, " + (name ?? "친구") + "님!"
print(greeting)
// "안녕하세요, 친구님!"이 출력됨
swift

??를 사용해 대체 값을 제공하는 것에 대한 더 자세한 정보는 Nil-Coalescing Operator를 참고하세요.

강제 언래핑

nil이 프로그래머 오류나 손상된 상태 같은 회복 불가능한 실패를 나타낼 때, 옵셔널의 이름 끝에 느낌표(!)를 추가하여 기본 값에 접근할 수 있어요. 이것을 옵셔널의 값을 강제 언래핑한다고 합니다. nil이 아닌 값을 강제 언래핑하면, 결과는 그 값의 언래핑된 값이 됩니다. nil 값을 강제 언래핑하면 런타임 오류가 발생하겠죠.

!는 사실상 fatalError(_:file:line:)의 더 짧은 표기법이에요. 예를 들어, 아래 코드는 두 가지 동등한 접근 방식을 보여줍니다.

let possibleNumber = "123"
let convertedNumber = Int(possibleNumber)
 
let number = convertedNumber!
 
guard let number = convertedNumber else {
    fatalError("숫자가 유효하지 않습니다.")
}
swift

위의 두 코드 버전 모두 convertedNumber가 항상 값을 포함하고 있다는 것에 의존하고 있어요. 코드의 일부로써 이 요구사항을 작성하면, 위의 접근 방식 중 하나를 사용하여, 코드가 런타임에 그 요구사항이 참인지 확인할 수 있게 해줍니다.

데이터 요구사항을 강제하고 런타임에 가정을 확인하는 것에 대한 더 많은 정보는 Assertions and Preconditions를 참고하세요.

암시적으로 언래핑된 옵셔널

위에서 설명한 대로, 옵셔널은 상수나 변수가 "값이 없음"을 허용함을 나타냅니다. 옵셔널은 if 문으로 값의 존재를 확인할 수 있고, 존재한다면 옵셔널 바인딩을 통해 그 옵셔널의 값에 조건부로 접근할 수 있죠.

때로는 프로그램의 구조상 옵셔널이 첫 번째로 값이 설정된 후에는 항상 값을 가질 것이 분명할 때가 있어요. 이런 경우에는 값에 접근할 때마다 옵셔널의 값을 확인하고 언래핑해야 하는 필요성을 제거하는 게 유용한데, 왜냐하면 그 값이 항상 존재할 거라고 안전하게 가정할 수 있기 때문이죠.

이런 종류의 옵셔널을 암시적으로 언래핑된 옵셔널이라고 정의합니다. 암시적으로 언래핑된 옵셔널을 선언할 때는 옵셔널로 만들고자 하는 타입 뒤에 물음표(String?) 대신 느낌표(String!)를 붙여요. 그러면 옵셔널을 사용할 때마다 이름 뒤에 느낌표를 붙일 필요가 없어집니다.

// 일반 옵셔널 선언과 사용
let optionalNumber: Int? = 42
let forcedNumber: Int = optionalNumber!
 
// 암시적으로 언래핑된 옵셔널 선언과 사용
let implicitlyUnwrappedNumber: Int! = 42
let normalNumber: Int = implicitlyUnwrappedNumber
 
swift

위 예제에서 implicitlyUnwrappedNumber은 암시적으로 언래핑된 옵셔널로 선언되었어요. normalNumberimplicitlyUnwrappedNumber을 할당할 때, 느낌표를 붙이지 않아도 암시적으로 언래핑이 됩니다. 그래서 별도의 언래핑 과정 없이 바로 사용할 수 있죠. 반면에 optionalNumber은 일반 옵셔널이라서 forcedNumber에 할당할 때 느낌표를 붙여서 강제 언래핑을 해야 해요.

암시적으로 언래핑된 옵셔널은 옵셔널의 값이 옵셔널이 처음 정의된 직후 즉시 존재함이 확인되고, 이후의 모든 지점에서 확실히 존재한다고 가정할 수 있을 때 유용해요. Swift에서 암시적으로 언래핑된 옵셔널의 주된 용도는 Unowned References and Implicitly Unwrapped Optional Properties에 설명된 것처럼 클래스 초기화 중에 사용되는 거예요.

변수가 이후의 시점에 nil이 될 가능성이 있다면 암시적으로 언래핑된 옵셔널을 사용하지 마세요. 변수의 수명 동안 nil 값을 확인해야 할 필요가 있다면 항상 일반 옵셔널 타입을 사용하세요.

암시적으로 언래핑된 옵셔널은 내부적으로는 일반 옵셔널이지만, 매번 그 옵셔널 값을 언래핑할 필요 없이 옵셔널이 아닌 값처럼 사용될 수도 있어요. 다음 예제는 명시적인 String으로서 래핑된 값에 접근할 때, 옵셔널 문자열과 암시적으로 언래핑된 옵셔널 문자열 사이의 동작 차이를 보여줍니다.

let possibleString: String? = "An optional string."
let forcedString: String = possibleString! // 명시적 언래핑 필요
 
 
let assumedString: String! = "An implicitly unwrapped optional string."
let implicitString: String = assumedString // 자동으로 언래핑됨
swift

암시적으로 언래핑된 옵셔널은 필요하다면 강제 언래핑될 수 있도록 옵셔널에 권한을 부여한다고 생각할 수 있어요. 암시적으로 언래핑된 옵셔널 값을 사용할 때, Swift는 먼저 그것을 일반 옵셔널 값으로 사용하려고 시도합니다. 옵셔널로 사용될 수 없다면 Swift는 그 값을 강제 언래핑하죠. 위의 코드에서, 옵셔널 값 assumedStringimplicitString에 그 값을 할당하기 전에 강제 언래핑되는데, 왜냐하면 implicitString은 명시적인 옵셔널이 아닌 String 타입을 가지고 있기 때문이에요. 아래 코드에서는 optionalString이 명시적인 타입을 가지고 있지 않아서 일반 옵셔널이 됩니다.

let optionalString = assumedString
// optionalString의 타입은 "String?"이고 assumedString은 강제 언래핑되지 않음
swift

암시적으로 언래핑된 옵셔널이 nil인데 여러분이 그것의 래핑된 값에 접근하려고 하면, 런타임 오류가 발생할 거예요. 결과는 느낌표를 작성해서 값을 포함하지 않는 일반 옵셔널을 강제 언래핑하는 것과 정확히 같아요.

일반 옵셔널을 확인하는 것과 같은 방식으로 암시적으로 언래핑된 옵셔널이 nil인지 확인할 수 있습니다.

if assumedString != nil {
    print(assumedString!)
}
// "An implicitly unwrapped optional string."이 출력됨
swift

단일 문장에서 값을 확인하고 언래핑하기 위해 옵셔널 바인딩과 함께 암시적으로 언래핑된 옵셔널을 사용할 수도 있어요.

if let definiteString = assumedString {
    print(definiteString)
}
// "An implicitly unwrapped optional string."이 출력됨
swift

이렇게 옵셔널에 대해 알아보았습니다. Steve McConnell의 '코드 컴플릿'에서처럼 예제 코드와 함께 단계별로 상세한 설명을 제공하려고 노력했어요. 옵셔널은 Swift에서 값의 부재를 안전하게 처리할 수 있게 해주는 강력한 기능이랍니다. 여러분의 코드에서 옵셔널을 현명하게 활용하시길 바랍니다!