반응형

Swift에서 옵셔널 기본값: 중첩 if let vs a ?? b

옵셔널을 안전하게 다루는 방법은 여러 가지가 있습니다. 그중 중첩 if let nil‑병합 연산자 ?? 는 가장 자주 쓰입니다. 이 문서는 두 방식의 의도 차이·가독성·성능 특성과 함께, 실무에서 바로 쓸 수 있는 예제를 정리했습니다.

 

1. 한눈에 요약

상황 권장 패턴 이유
“값이 있으면 쓰고, 없으면 기본값을 쓰고 싶다” a ?? default 간결·표현적, 기본값 표현은 지연 평가(필요할 때만 계산)
다단계 옵셔널 체이닝 + 기본값 user?.team?.name ?? "Unknown" 스코프 오염 없이 한 줄 해결
값이 있으면 추가 검증/분기가 필요 if let / guard let 분기/로깅/early‑return 등 제어 흐름에 적합
여러 값 중 첫 번째 non‑nil선택 a ?? b ?? c 오른쪽 결합, 자연스러운 폴백 체인
실패를 nil로 흡수한 뒤 기본값 try? decode(...) ?? fallback try? + ??조합이 명확
성능 민감 + 기본값 계산이 무거움 a ?? expensive() expensive()는 a가 nil일 때만실행됨

핵심: 단순 “기본값 폴백”은 ??가 표준. 분기/로깅/조기 종료가 필요하면 if let/guard let을 사용하세요.

 

2. 기본 형태 비교


2.1 a ?? b (nil‑병합 / 기본값 폴백)

let displayName = user.nickname ?? user.fullName ?? "Guest"
  • 왼쪽이 non‑nil이면 그 값을 그대로 반환, nil이면 오른쪽을 평가합니다.
  • b는 지연(lazy) 평가됩니다. (왼쪽이 non‑nil이면 b는 계산되지 않음)

2.2 중첩 if let

let name: String
if let nick = user.nickname {
    name = nick
} else if let full = user.fullName {
    name = full
} else {
    name = "Guest"
}
  • 로깅, 메트릭 집계, 얼럿 등 부가 동작이 필요할 때 적합합니다.

 

3. 체이닝과 결합 예제


3.1 옵셔널 체이닝 + 기본값

let city = profile?.address?.city ?? "N/A"

3.2 try? + 기본값

let model = (try? JSONDecoder().decode(Model.self, from: data)) ?? .empty

3.3 첫 번째 non‑nil 선택 (오른쪽 결합)

let token = runtimeOverride ?? cachedToken ?? keychain.token ?? ""

3.4 사전 조회 + 기본값

let n = counts["apple"] ?? 0          // Dictionary 서브스크립트는 Optional 반환

3.5 빈 문자열/컬렉션을 nil 취급하고 기본값 주기

extension Optional where Wrapped == String {
    var nilIfEmpty: String? { self?.isEmpty == true ? nil : self }
}
let title = inputTitle.nilIfEmpty ?? "제목 없음"

 

4. ?? 의 지연 평가(lazy evaluation)

오른쪽 피연산자는 필요할 때만 평가됩니다.

func fetchFromNetwork() -> String { print(network); return remote }
let value1: String? = local
let result1 = value1 ?? fetchFromNetwork() // network 출력 없음

let value2: String? = nil
let result2 = value2 ?? fetchFromNetwork() // network 출력됨


- 기본값 계산이 **무거운 연산**일 때??`는 특히 유리합니다.

 

5. if let / guard let 을 선택해야 하는 경우


5.1 값 유무에 따라 행동이 달라지는 분기

if let img = cache[id] {
    log.info("cache hit")
    imageView.image = img
} else {
    log.info("miss, fetch")
    imageView.image = try await loader.fetch(id)
}

5.2 조기 종료(Early Exit)와 검증

func submit(_ form: Form?) {
    guard let form, form.isValid else {
        toast("입력을 확인하세요")
        return
    }
    send(form)
}

 

6. 가독성·유지보수 관점 가이드

  • 단순 값 선택 ??  표현을 값 중심으로 작성
  • 제어 흐름(로깅/분기/리턴/throw)이 개입되면 if let/guard let
  • 삼항 연산자와 섞일 때는 괄호로 의도를 명확히
  • 길어지면 중간 변수를 도입하여 단계 분리

 

7. 실무 예제 모음


7.1 포맷팅과 기본값

let priceText = formatter.string(from: price as NSNumber) ?? "—"

7.2 SwiftUI 바인딩 값 폴백

Text(user.nickname?.uppercased() ?? "GUEST")

7.3 환경 변수/설정 오버라이드

let endpoint = ProcessInfo.processInfo.environment["API_ENDPOINT"]
    ?? UserDefaults.standard.string(forKey: "api_endpoint")
    ?? "https://api.example.com"

7.4 URL 생성 실패 시 폴백

let url = URL(string: raw) ?? URL(string: "https://example.com")!

7.5 매핑 실패 시 기본 모델

let item = source.first(where: { $0.id == id }) ?? Item.placeholder

 

8. 성능 관점 메모

  • ?? 와 간단한 if let  실제로 매우 비슷합니다. 차이가 있다면 일반적으로 ?? 쪽이 더 간결하고 분기 오버헤드가 적은 표현을 유도합니다.
  • 중첩 if let 은 다단계 분기/로깅에서는 필수지만, 단순 폴백이라면 가독성·유지보수 측면에서 불리할 수 있습니다.
  • 항상 Release(-O) 빌드에서 측정하세요. 디버그 빌드는 최적화가 꺼져 있습니다.

 

9. 흔한 실수와 교정

1) 기본값 계산을 미리 해두는 실수

let d = expensiveDefault() // 🚫 항상 계산됨
let name = user.name ?? d // 불필요한 비용



교정

let name = user.name ?? expensiveDefault() // ✅ 필요할 때만

2) 빈 문자열/배열을 nil과 혼동

let shown = (title ?? "Untitled") // title == "" 인 경우도 "Untitled"를 원했나요?


교정

let shown = (title?.isEmpty == false ? title! : "Untitled")
// 또는 위의 nilIfEmpty 헬퍼 활용

3) 불필요한 중첩 if let

if let a = a { result = a } else { result = b } // 🚫 장황
let result = a ?? b // ✅ 간결

 

10. 사용 예제

import Foundation

func measure(_ name: String, _ block: () -> Void) {
    let t1 = CFAbsoluteTimeGetCurrent(); block(); let t2 = CFAbsoluteTimeGetCurrent()
    print(name, ":", t2 - t1, "sec")
}

let N = 200_000
let arr: [String?] = (0..<N).map { $0 % 3 == 0 ? nil : "v\($0)" }

measure("??") {
    _ = arr.map { $0 ?? "d" }
}
measure("if let") {
    _ = arr.map { s -> String in
        if let s { return s } else { return "d" }
    }
}

 

11. 정리

  • 기본값 폴백 ??가 기본값.
  • 분기/로깅/조기 종료가 필요하면 if let/guard let.
  • ??의 오른쪽은 지연 평가되므로, 비용이 큰 기본값 계산에 유리.
  • 빈 값 처리(예: "", [])는 nil과 구분하고, 필요하면 유틸(예: nilIfEmpty)로 명시하세요.

 

반응형
Posted by 까칠코더
,