🟣 nil이 왜 이렇게 유별난가
다른 언어에서 null은 그냥 아무 참조에나 들어갈 수 있는 값이었다. 그래서 값이 없는데 접근하려다 터지는 사고를 늘 조심해야 했다. Swift를 처음 잡았을 때 변수 타입 뒤에 붙는 물음표가 낯설었다. String과 String?이 왜 다른 타입인지, nil을 넣으려면 왜 굳이 물음표를 붙여야 하는지 이해가 안 갔다.
정리하고 나서야 이게 Swift의 핵심 안전장치라는 걸 알았다. Swift는 값이 없을 수 있다는 사실 자체를 타입에 새겨 넣는다. 값이 없을 수 있으면 옵셔널 타입이고, 없을 수 없으면 일반 타입이다. 이 구분 덕분에 "값이 없는데 썼다"는 사고를 컴파일러가 미리 막는다. 이 장은 그 장치를 다룬다.
🟣 옵셔널은 값을 담는 상자다
String?은 문자열이 아니다. 문자열이 들어 있거나(some) 비어 있는(none) 상자다. Swift에서 옵셔널은 실제로 이렇게 생긴 열거형이다.
enum Optional<Wrapped> {
case none // 값 없음 (nil)
case some(Wrapped) // 값 있음
}String?은 Optional<String>의 축약 표기일 뿐이다. nil은 특별한 포인터가 아니라 그냥 .none 케이스다. 그래서 옵셔널 값을 바로 쓸 수 없다. 상자 안에 값이 있는지부터 확인하고 꺼내야 한다.
let name: String? = "Lody"
// print(name.count) 컴파일 에러. 상자를 바로 쓸 수 없다이 강제가 처음엔 번거로웠는데, 곧 고마워졌다. 값이 없을 가능성을 컴파일러가 무시하지 못하게 막아 주기 때문이다. 옵셔널이 아닌 타입은 절대 nil이 아님이 보장되므로, 그 값은 마음 놓고 쓸 수 있다.
🟣 안전하게 꺼내기: 옵셔널 바인딩
상자에서 값을 꺼내는 가장 안전한 방법이 옵셔널 바인딩이다. "값이 있으면 그걸 상수에 담고 이 블록을 실행하라"는 구문이다. if let이 기본형이다.
let name: String? = "Lody"
if let name = name {
print(name.count) // 여기서 name은 String. 안전하게 쓴다
} else {
print("이름이 없다")
}if let name = name은 상자에 값이 있을 때만 그 값을 꺼내 새 상수 name에 담는다. 그 블록 안에서 name은 옵셔널이 아닌 순수 String이다. 값이 없으면 else로 간다. Swift 최신 문법에서는 같은 이름이면 if let name처럼 오른쪽을 생략할 수 있다.
🚀 guard let과 이른 반환
if let은 성공 경로가 중괄호 안으로 들어가서, 조건이 여러 개 겹치면 들여쓰기가 계단처럼 깊어졌다. 이걸 뒤집는 게 guard let이다. "값이 없으면 지금 함수를 빠져나가라"고 먼저 선언하고, 통과하면 그 아래를 평평하게 쓴다.
func greet(_ name: String?) {
guard let name else {
print("이름이 없다")
return // 없으면 여기서 함수 종료
}
// 이 아래로는 name이 String으로 보장된다
print("안녕, \(name)")
print(name.count)
}guard let으로 꺼낸 값은 if let과 달리 블록 밖, 즉 함수의 나머지 전체에서 유효하다. 실패를 위로 걷어내고 본문을 평평하게 유지하는 이 형태가 실무에서 훨씬 읽기 좋았다. 함수 앞머리에서 전제 조건을 다 걸러내고 본론을 쓰는 흐름이 자연스러웠다.
🟣 옵셔널 체이닝: 실패하면 통째로 nil
옵셔널 안의 프로퍼티를 또 타고 들어가야 할 때가 있다. 중간에 하나라도 nil이면 전체가 nil이 되게 하는 게 옵셔널 체이닝이다. 물음표를 붙여 "값이 있으면 계속, 없으면 여기서 멈추고 nil"이라고 표시한다.
class Person { var address: Address? }
class Address { var city: String? }
let person: Person? = Person()
let city = person?.address?.city // 타입은 String?person?에서 person이 nil이면 뒤는 아예 평가하지 않고 전체가 nil이 된다. address?도 마찬가지다. 중간 어디서 끊겨도 크래시 없이 결과가 nil로 나온다. 그래서 최종 결과 타입은 항상 옵셔널이다.
이걸 옵셔널 바인딩 없이 손으로 풀면 if let이 세 겹으로 중첩된다. 옵셔널 체이닝은 그 중첩을 한 줄로 접으면서도 안전성은 그대로 지킨다.
🟣 기본값과 강제 언래핑
옵셔널이 nil일 때 대신 쓸 기본값을 정하고 싶으면 ?? 연산자를 쓴다. 왼쪽이 값이 있으면 그 값을, 없으면 오른쪽 기본값을 준다.
let name: String? = nil
let display = name ?? "손님" // "손님"??의 결과는 옵셔널이 아니다. 어느 쪽이든 값이 정해지기 때문이다. 화면에 보여줄 문자열처럼 "없으면 이걸로"가 명확한 자리에 잘 맞는다.
반대편 극단에 강제 언래핑 !가 있다. 상자를 확인 없이 억지로 열어 값을 꺼내는데, 만약 nil이면 그 자리에서 앱이 크래시한다. null 사고를 막으려고 옵셔널을 도입했는데 !는 그 안전장치를 스스로 꺼 버리는 셈이라, 입문기에 습관적으로 붙였다가 여러 번 크래시를 냈다.
!가 정당한 경우는 좁다. 논리상 이 시점엔 절대 nil일 수 없다고 확신할 수 있고, 만약 nil이면 그건 고쳐야 할 프로그래머의 실수인 상황뿐이다. 그 외에는 if let, guard let, ??로 안전하게 처리하는 게 맞다. 확신이 없으면 !를 쓰지 않는다는 원칙 하나로 크래시가 크게 줄었다.
🟣 정리
- 옵셔널은 값이 든 상자다.
String?은Optional<String>이라는 열거형으로,some(값 있음)과none(nil) 중 하나다. 값이 없을 수 있다는 사실이 타입에 새겨져 있다. - 옵셔널은 바로 쓸 수 없고 꺼내야 한다.
if let은 성공 경로를 블록 안에,guard let은 실패를 먼저 걷어내고 본문을 평평하게 둔다. 꺼낸 값의 유효 범위가 다르다. - 옵셔널 체이닝(
?.)은 중간에 하나라도nil이면 크래시 없이 전체를nil로 만든다. 중첩된 옵셔널 접근을 한 줄로 접으면서 안전성을 지킨다. ??는nil일 때 쓸 기본값을 주고 결과를 옵셔널이 아니게 만든다.!는 확인 없이 강제로 여는 위험한 연산으로,nil이면 크래시한다.- 3장의
weak참조가 옵셔널인 이유가 여기서 만난다. 대상이 사라지면nil이 되기 때문이다. 면접에서 옵셔널을 물으면 열거형이라는 정체와if let,guard let, 체이닝,??를 상황별로 구분해 답하면 깊이가 드러난다. 특히 "언제!를 써도 되는가"에 답할 수 있으면 좋다.
여기까지가 입문기에 가장 헷갈렸던 Swift의 기본기였다. 함수를 값으로 다루는 것에서 시작해, 값 타입과 참조 타입의 복사 차이, 참조 타입의 수명을 세는 ARC와 순환참조, 그리고 값의 부재를 타입으로 다루는 옵셔널까지 하나의 흐름으로 이어졌다. 네 주제는 따로 노는 문법이 아니라, 캡처가 순환을 부르고 약한 참조가 옵셔널을 부르듯 서로 맞물려 있었다. 이 기본기가 이후 SwiftUI와 동시성 같은 더 깊은 주제를 읽는 바닥이 됐다.