반응형

iOS 개발자가 많이 하는 실수 - Dictionary에서 key 존재 여부 확인 없이 강제 언래핑(!)

기존에 강제 언래핑에서 얘기했던 부분이긴한데 Dictionary에서 생각보다 자주 발생하는 부분입니다.

Swift 초보자가 가장 자주 발생시키는 크래시 중 하나가 바로:

let value = dict["key"]!   // 여기가 터진다

형태의 코드입니다.

1. 문제 패턴

대표적인 코드 예시는 다음과 같습니다.

let json: [String: Any] = [
    "name": "Jun",
    "age": 20
]

let name = json["name"] as! String   // 있다고 가정
let city  = json["city"] as! String  // ❌ city 키가 없으면 크래시

실제 런타임에서는 다음과 같은 크래시가 발생합니다.

Fatal error: Unexpectedly found nil while unwrapping an Optional value

혹은 타입 캐스팅이 틀리면:

Could not cast value of type '...'

2. Dictionary 서브스크립트의 반환 타입

핵심은 이 한 줄입니다.

let value = dict["key"]

여기서 value의 타입은 Optional입니다.

// [Key: Value] 타입의 dictionary라면
dict["key"]   // => Value?

이것은 Swift 표준 라이브러리가 “해당 키가 없을 수 있다”는 것을

타입 레벨에서 명시적으로 표현하기 때문입니다.

그런데 여기에 강제 언래핑 !을 붙이면,

let value = dict["key"]!  // 키가 없으면 크래시

Optional이 가지고 있는 안전장치를 스스로 해제하는 셈이 됩니다.


3. 왜 이 실수가 실무에서 자주 터질까?

  1. 샘플 데이터에는 항상 존재하는 키
    • 테스트 JSON에는 항상 "city"가 들어있어서 문제가 안 보임
    • 실제 운영 환경에서는 해당 필드가 빠진 케이스가 존재
  2. 서버 스펙 변경 / 버전 차이
    • 서버 개발자가 필드 이름을 살짝 변경하거나 새 버전을 배포
    • 클라이언트는 구버전 처리를 유지하고 있다가 크래시
  3. 타입이 다르게 들어오는 경우
    • "age": "20" (String)으로 들어오는데
    • as! Int로 강제 캐스팅하여 크래시

4. 안전한 처리 패턴

4-1. Optional Binding (if let / guard let)

if let city = json["city"] as? String {
    print(city)
} else {
    print("도시 정보 없음")
}

혹은:

guard let city = json["city"] as? String else {
    print("도시 정보 없음")
    return
}
print(city)
  • 값이 있을 때만 안전하게 사용
  • 값이 없을 때의 처리 분기를 명확하게 할 수 있음

4-2. 기본값 제공 (?? 연산자)

UI에 기본 메시지를 보여주고 싶을 때 유용합니다.

let city = (json["city"] as? String) ?? "도시 정보 없음"

4-3. 타입 안정성을 위해 모델 + Codable 사용

사실 가장 좋은 방법은

Dictionary를 직접 파고들기보다 Codable을 사용하는 것입니다.

struct User: Decodable {
    let name: String
    let age: Int?
    let city: String?
}

JSON을 decode하고 나면:

let user = try decoder.decode(User.self, from: data)
print(user.city ?? "도시 정보 없음")
  • 키 누락, 타입 불일치는 decode 과정에서 에러 또는 nil로 처리
  • 강제 언래핑을 쓸 이유가 줄어듦

4-4. 딕셔너리 자체에 기본값을 줄 수 있는 API

Swift Dictionary는 기본값을 줄 수 있는 서브스크립트도 제공합니다.

let countByID = ["a": 1, "b": 2]
let countC = countByID["c", default: 0]   // 0
  • 키가 없으면 default 값 사용
  • Optional이 아니라 Non-Optional로 받기 때문에 코드가 간결해짐

5. 실무 스타일 예시

나쁜 예

func handle(json: [String: Any]) {
    let name = json["name"] as! String
    let age  = json["age"]  as! Int        // 타입/존재 보장 X
    let city = json["city"] as! String     // ❌ 크래시 위험
    print(name, age, city)
}

개선 예 (Dictionary 기반 그대로 쓴다면)

func handle(json: [String: Any]) {
    guard let name = json["name"] as? String else {
        print("이름 없음")
        return
    }

    let age  = json["age"]  as? Int
    let city = json["city"] as? String ?? "도시 정보 없음"

    print(name, age ?? -1, city)
}

더 좋은 예 (Codable 모델 기반)

struct User: Decodable {
    let name: String
    let age: Int?
    let city: String?
}

func handle(data: Data) {
    do {
        let user = try JSONDecoder().decode(User.self, from: data)
        print(user.name, user.age ?? -1, user.city ?? "도시 정보 없음")
    } catch {
        print("JSON decode 실패:", error)
    }
}

6. 정리

  • Dictionary 서브스크립트는 항상 Optional을 반환한다.
    → 키가 없을 수 있기 때문.
  • 여기에 !를 붙이면 언제라도 크래시 날 수 있는 지뢰 코드가 된다.
  • 안전한 접근 패턴:
    • if let / guard let + as?
    • ?? 기본값 제공
    • 가능하면 Codable 모델 사용하는 방식으로 전환
  • 원칙: > 외부 입력(서버, 파일, 사용자 입력 등)에 대해서는 절대 강제 언래핑을 쓰지 않는다.

 

 

 

반응형
Posted by 까칠코더
,