TIL: 옵셔널(Optional) 타입

Royce·2025년 3월 16일

Swift 문법

목록 보기
19/63

옵셔널(Optional) 타입

  • Swift의 옵셔널(Optional) 타입은 값이 있을 수도 있고 없을 수도 있는 상태를 표현할 수 있는 특수한 타입이다
  • 옵셔널을 이해하면 Swift에서 안전하게 nil 값을 다루고, 앱이 충돌하지 않도록 코드를 작성할 수 있다

옵셔널(Optional) 타입의 기본 개념

옵셔널이란?

  • Swift에서는 변수나 상수가 nil 값을 가지지 못한다
  • 즉, 다음 코드는 오류를 발생시킨다
var name: String = "Royce"
name = nil  // ❌ 오류 발생! Swift에서는 nil을 허용하지 않음
  • Swift에서는 값이 없을 수도 있는 경우, 옵셔널 타입을 사용해야 한다
  • 옵셔널 타입은 ? 를 사용하여 정의한다
var name: String? = nil // ✅ 옵셔널 타입이므로 nil 할당 가능

옵셔널의 정식 표현 방법

  • Swift에서 옵셔널 타입을 선언하는 방법은 두 가지가 있다

1. 축약형 표현(? 사용)

var number: Int? = 7
  • Int? ➡️ "옵셔널 Int 타입" 을 의미하며, 값이 nil 이 될 수 있다

2. 정식 표현 (Optional<Type> 사용)

var number: Optional<Int> = 7
  • Optional<Int> ➡️ Int? 와 같은 의미
  • Optional<T> 는 Swift의 제네릭(Generic) 열거형이며, 내부적으로 some(T) 또는 none 을 지정한다
    즉, 다음 두 개의 코드는 완전히 동일한 코드이다
var number1: Int? = 7
var number2: Optional<Int> = 7
  • 하지만 일반적으로 ? 를 사용하는 축약형을 더 많이 사용한다

옵셔널 값을 출력할 때 Optional(값) 이 표시되는 이유

  • 옵셔널 값을 그대로 출력하면, Swift는 해당 변수가 옵셔널 타입임을 명확히 나타내기 위해 Optional(값) 형식으로 출력한다.
var optionalNumber: Int? = 7
print(optionalNumber) // 출력: Optional(7)

이유

  • optionalNumberInt? 타입이므로 Swift는 이를 일반 Int 값이 아니라 옵셔널(Int) 이라는 점을 명확히 하기 위해 Optional(7) 과 같이 출력한다
  • 옵셔널 타입이기 때문에, 값이 있을 수도 있고 없을 수도 있는 상태를 유지한다

옵셔널 타입끼리의 연산

  • Swift에서 옵셔널 값끼리는 직접 연산할 수 없다
  • 즉, 다음과 같은 코드는 컴파일 오류가 발생한다
var a: Int? = 5
var b: Int? = 3
var result = a + b  // ❌ 오류 발생! (Value of optional type 'Int?' must be unwrapped)

이유

  • 옵셔널(Int?) 는 실제 값이 nil 일 수도 있기 때문에, Swift는 직접 연산을 허용하지 않는다
  • Int? 는 실제 Int 값이 아닌 Optional<Int> 타입의 래퍼(wrapper) 이므로, Int 타입과 다르게 취급된다
  • 따라서, 옵셔널을 먼저 언래핑(unwrapping)하여 실제 값(Int)으로 변환해야 한다

옵셔널 값의 추출 방법 : Optional Unwrapping

  • 옵셔널 변수의 값을 사용하려면 옵셔널 값을 추출(unwrapping) 해야 한다

1. 강제 언래핑(Forced Unwrapping)

  • 옵셔널 변수 뒤에 ! (느낌표)를 붙이면 강제 언래핑이 가능하다
  • 단, 값이 nil 이면 런타임 오류(Crash)가 발생하므로 조심해야 한다
var optionalNumber: Int? = 7
print(optionalNumber!) // ✅ 7

‼️ optionalNumbernil 인 경우

var optionalNumber: Int? = nil
print(optionalNumber!) // ❌ 런타임 오류 발생! (Fatal error: Unexpectedly found nil)

⚠️ 강제 언래핑은 위험하므로 가능하면 사용을 피하는 것이 좋다

강제 언래핑을 사용하는 경우

  • 옵셔널 변수에 값이 반드시 존재한다고 확신할 때만 사용해야 한다
  • 예를 들어, 특정 옵셔널 값이 nil 이 아님을 100% 보장하는 경우
var alwaysHasValue: Int! = 42
print(alwaysHasValue) // ✅ 42

2. if 문을 이용해 nil 여부 확인 후 강제 언래핑

  • 강제 언래핑을 사용할 때 if 문을 사용하여 nil 이 아님을 확인한 후 언래핑하면 좀 더 안전한 코드 작성이 가능하다
var optionalNumber: Int? = 10

if optionalNumber != nil {
    print(optionalNumber!) // ✅ 10 (안전한 강제 언래핑)
} else {
    print("값이 없습니다.")
}
  • 이 방식은 nil 일 경우 언래핑을 하지 않기 때문에 런타임 오류를 방지할 수 있다
    하지만 여전히 강제 언래핑(!)을 사용해야 하므로, 옵셔널 바인딩을 사용하는 것이 더 권장된다

3. 옵셔널 바인딩 (Optional Binding)

  • if let 또는 guard let 을 사용하여 안전하게 옵셔널 값을 추출한다

(1) if let 을 사용한 옵셔널 바인딩

var optionalNumber: Int? = 7

if let number = optionalNumber {
    print("숫자: \(number)") // ✅ "숫자: 7"
} else {
    print("값이 없습니다.")
}
  • 옵셔널 값이 있을 때만 특정 동작을 실행하고 싶을 때 사용한다
  • 예를 들어, 사용자가 입력한 값이 nil 이 아닐 때만 실행할 경우

(2) guard let 을 사용한 옵셔널 바인딩

  • guard let 은 함수나 특정 블록을 빠져나가야 하는 경우 유용하다
func printNumber(_ number: Int?) {
    guard let unwrappedNumber = number else {
        print("값이 없습니다.")
        return
    }
    print("숫자: \(unwrappedNumber)")
}

printNumber(7)   // ✅ "숫자: 7"
printNumber(nil) // ✅ "값이 없습니다."
  • 값이 없으면 즉시 함수를 종료하고 싶을 때 유용하다

4. 닐 병합 연산자(Nil-Coalescing Operator, ??)

  • 닐 병합 연산자(??)는 옵셔널 값이 nil 일 경우 기본값을 제공하는 가장 간단한 방법이다
  • 옵셔널 값이 nil 일 경우 기본값을 제공해야 하는 경우와 기본적으로 값이 설정되지 않을 때 대체할 값을 사용할 때 사용한다
var optionalNumber: Int? = nil
let number = optionalNumber ?? 0
print(number) // ✅ 0

?? 연산자의 사용 방법

  1. 기본값 제공
var username: String? = nil
let displayName = username ?? "Guest"
print(displayName) // ✅ "Guest"
  1. 연산을 수행할 때 기본값 제공
var a: Int? = nil
var b: Int? = 5
let sum = (a ?? 0) + (b ?? 0)
print(sum) // ✅ 5
  1. 중첩 사용 가능
var firstName: String? = nil
var lastName: String? = "Kim"
let fullName = (firstName ?? lastName) ?? "Unknown"
print(fullName) // ✅ "Kim"

옵셔널 체이닝(Optional Chaining)

  • 옵셔널 체이닝(Optional Chaining)은 ?. 연산자를 사용하여 옵셔널 값이 포함된 속성이나 메서드에 안전하게 접근하는 방법이다
  • 옵셔널 체이닝을 사용하면 옵셔널 값이 nil 인 경우 자동으로 nil 을 반환하고, 옵셔널 값이 존재하면 그 값의 속성이나 메서드에 접근하여 프로그램이 충돌하지 않도록 한다

옵셔널 체이닝을 사용하지 않는 경우 (기본 접근 방식)

class Person {
    var address: Address?
}

class Address {
    var city: String = "Seoul"
}

let person = Person()

// 일반적인 접근 방식 (안전하지 않음)
print(person.address!.city) // ❌ 런타임 오류 발생 (Fatal error: Unexpectedly found nil)
  • person.addressnil 이므로, address!.city 를 강제 언래핑하면 런타임 오류(Crash)가 발생한다

옵셔널 체이닝을 사용한 안전한 접근

class Person {
    var address: Address?
}

class Address {
    var city: String = "Seoul"
}

let person = Person()

// 옵셔널 체이닝을 사용하여 안전하게 접근
print(person.address?.city) // ✅ nil (오류 없이 실행됨)
  • person.addressnil 이므로, address?.citynil 을 반환하지만 오류 없이 실행된다

옵셔널 체이닝의 활용 예제

1. 옵셔널 프로퍼티 접근

class Person {
    var address: Address?
}

class Address {
    var city: String = "Seoul"
}

let person = Person()

// 옵셔널 체이닝을 사용하여 프로퍼티 접근
if let city = person.address?.city {
    print("도시는 \(city)입니다.")
} else {
    print("주소 정보가 없습니다.") // ✅ 출력
}

-person.addressnil 이면 citynil 이 되므로, else 블록이 실행된다


2. 옵셔널 메서드 호출

class Person {
    var address: Address?
}

class Address {
    func fullAddress() -> String {
        return "Seoul, South Korea"
    }
}

let person = Person()

// 옵셔널 체이닝을 사용한 메서드 호출
if let address = person.address?.fullAddress() {
    print(address)
} else {
    print("주소 정보가 없습니다.") // ✅ 출력
}
  • person.addressnil 이므로 fullAddress() 가 실행되지 않고 nil 반환

3. 옵셔널 체이닝을 사용한 서브스크립트 접근

class School {
    var students: [String]?
}

let school = School()
school.students = ["Alice", "Bob", "Charlie"]

// 옵셔널 체이닝을 사용한 배열 접근
if let firstStudent = school.students?.first {
    print("첫 번째 학생은 \(firstStudent)입니다.")
} else {
    print("학생 명단이 없습니다.") // ✅ 출력되지 않음
}
  • studentsnil 이면 students?.firstnil 이지만, 오류 없이 실행된다

옵셔널 체이닝의 동작 방식

옵셔널 체이닝은 연속적으로 연결 가능

  • 옵셔널 체이닝은 여러 단계로 연결할 수 있다
class Company {
    var ceo: CEO?
}

class CEO {
    var name: String = "Steve Jobs"
    var office: Office?
}

class Office {
    var address: String = "Cupertino, California"
}

let apple = Company()

// 옵셔널 체이닝을 사용한 다중 속성 접근
if let officeAddress = apple.ceo?.office?.address {
    print("회사 주소: \(officeAddress)")
} else {
    print("회사 주소 정보가 없습니다.") // ✅ 출력
}
  • ceonil이므로, ceo?.office?.address전체가 nil이 되고 else 블록이 실행된다

옵셔널 체이닝과 기본값 제공

  • 옵셔널 체이닝과 닐 병합 연산자(??)를 함께 사용하면 nil일 경우 기본값을 제공할 수 있다
let companyAddress = apple.ceo?.office?.address ?? "주소 정보 없음"
print(companyAddress) // ✅ "주소 정보 없음"
  • 옵셔널 체이닝 결과가 nil이면 "주소 정보 없음"이 기본값으로 설정된다

옵셔널 체이닝과 강제 언래핑 비교

옵셔널 체이닝은 값이 nil이면 연산이 중단된다

  • 옵셔널 체이닝을 사용하면 중간 단계 값이 nil이면 이후 연산이 중단되므로 주의해야 한다
class User {
    var profile: Profile?
}

class Profile {
    var bio: String?
}

let user = User()
user.profile?.bio = "iOS Developer" // ✅ 실행되지만 아무런 효과 없음 (bio는 설정되지 않음)

profilenil이면 bio에 값을 설정할 없다

➡️ 해결 방법: 옵셔널 바인딩(if let)을 사용하여 nil 여부를 먼저 확인


옵셔널 체이닝과 함께 사용하면 좋은 기능

옵셔널 체이닝 + 닐 병합 연산자(??)

  • 옵셔널 체이닝을 사용할 때 nil이 반환될 경우 기본값을 제공하는 것이 좋다
let bio = user.profile?.bio ?? "소개 없음"
print(bio) // ✅ "소개 없음"

옵셔널 체이닝 + 옵셔널 바인딩(if let)

  • 옵셔널 체이닝을 통해 값을 가져온 후, 옵셔널 바인딩으로 안전하게 사용 가능하다
if let bio = user.profile?.bio {
    print("사용자 소개: \(bio)")
} else {
    print("소개 정보가 없습니다.")
}

함수와 옵셔널 타입

함수에서 옵셔널 타입을 사용하는 이유

Swift에서는 함수가 값을 반환하지 못할 수도 있는 경우를 고려해야 한다
예를 들어:

  • 사용자가 찾는 데이터가 없을 수 있다 ➡️ nil 반환 가능
  • 연산이 실패할 가능성이 있다 ➡️ nil 반환 가능
  • 잘못된 입력값이 주어질 수 있다 ➡️ nil 반환 가능

이럴 때 함수의 반환 타입을 옵셔널로 설정하면 nil을 반환할 수 있다


기본값을 nil로 설정

func search(query: String, category: String? = nil) {
    let selectedCategory = category ?? "전체"
    print("'\(query)'을(를) \(selectedCategory)에서 검색합니다.")
}

search(query: "Swift") // ✅ "'Swift'을(를) 전체에서 검색합니다."
search(query: "iPhone", category: "전자제품") // ✅ "'iPhone'을(를) 전자제품에서 검색합니다."

옵셔널을 매개변수로 받는 함수

매개변수를 옵셔널로 선언

  • 함수의 매개변수를 옵셔널로 선언하면 인자를 전달하지 않거나 nil을 전달할 수 있다
func greet(name: String?) {
    if let unwrappedName = name {
        print("안녕하세요, \(unwrappedName)님!") // ✅ "안녕하세요, Royce님!"
    } else {
        print("이름이 없습니다.") // ✅ "이름이 없습니다."
    }
}

greet(name: "Royce") // ✅ "안녕하세요, Royce님!"
greet(name: nil) // ✅ "이름이 없습니다."

기본값을 제공하는 경우(?? 닐 병합 연산자 사용)

  • nil일 경우 기본값을 사용하도록 설정 가능
func greet(name: String?) {
    let finalName = name ?? "Guest" // `nil`이면 "Guest" 사용
    print("안녕하세요, \(finalName)님!")
}

greet(name: "Royce") // ✅ "안녕하세요, Royce님!"
greet(name: nil) // ✅ "안녕하세요, Guest님!"

옵셔널 체이닝을 활용한 매개변수 처리

  • 옵셔널 타입의 속성을 직접 사용할 때 ?.을 사용하면 안전한 접근 가능
class User {
    var nickname: String?
}

func welcome(user: User?) {
    print("환영합니다, \(user?.nickname ?? "Guest")님!") 
}

let user1 = User()
user1.nickname = "Royce"

welcome(user: user1) // ✅ "환영합니다, Royce님!"
welcome(user: nil) // ✅ "환영합니다, Guest님!"

옵셔널을 반환하는 함수

기본적인 옵셔널 반환 함수

func findUser(id: Int) -> String? {
    let users = [1: "Royce", 2: "Steve"]
    return users[id] // 없는 ID일 경우 자동으로 `nil` 반환
}

let user = findUser(id: 1)
print(user) // ✅ Optional("Alice")
  • 사용자가 존재하면 이름을 반환하고, 없으면 nil 반환

옵셔널 바인딩을 활용한 안전한 값 사용

if let userName = findUser(id: 1) {
    print("사용자: \(userName)") // ✅ "사용자: Alice"
} else {
    print("사용자를 찾을 수 없습니다.")
}
  • 옵셔널 바인딩(if let)을 사용하여 nil인지 확인 후 안전하게 사용 가능

닐 병합 연산자를 활용한 기본값 제공

let userName = findUser(id: 3) ?? "Guest"
print("사용자: \(userName)") // ✅ "사용자: Guest"
  • 값이 없으면 "guest"를 기본 값으로 사용

옵셔널을 반환하는 함수의 활용 예제

숫자를 문자열로 변환하는 함수 (Int -> String)

func convertToString(number: Int?) -> String {
    return number?.description ?? "값 없음"
}

print(convertToString(number: 10)) // ✅ "10"
print(convertToString(number: nil)) // ✅ "값 없음"
  • 옵셔널 체이닝(?.)과 ??를 활용하여 nil 처리 가능

문자열을 정수로 변환하는 함수 (String -> Int)

func stringToInt(_ str: String) -> Int? {
    return Int(str) // 변환 실패 시 `nil` 반환
}

print(stringToInt("123")) // ✅ Optional(123)
print(stringToInt("Swift")) // ✅ nil
  • 입력이 숫자가 아니면 nil 반환
profile
iOS 개발자 지망생

0개의 댓글