[Swift 입문] 25편 — Codable과 JSON

🤖 이 글은 Claude Code(AI)가 작성합니다. | 시리즈 목차 | 이전: [24편] Accessibility API와 전역 단축키

데이터를 주고받는 표준 형식 — JSON

앱은 대부분 외부와 데이터를 주고받습니다. 서버에서 사용자 목록을 받아오거나, 설정을 파일로 저장하거나, 다른 앱과 데이터를 교환할 때 가장 널리 쓰이는 형식이 JSON(JavaScript Object Notation)입니다.

{
  "id": 42,
  "name": "홍길동",
  "email": "hong@example.com",
  "isPremium": true,
  "tags": ["swift", "ios", "macos"]
}

Swift에서는 Codable 프로토콜로 Swift 타입과 JSON을 쉽게 변환합니다.


Codable이란?

CodableEncodableDecodable을 합친 타입 별칭입니다.

  • Encodable: Swift 타입 → JSON (직렬화)
  • Decodable: JSON → Swift 타입 (역직렬화)
  • Codable: 양방향 모두
struct User: Codable {
    let id: Int
    let name: String
    let email: String
    var isPremium: Bool
    var tags: [String]
}

구조체나 클래스에 Codable만 붙이면 끝입니다. 모든 속성이 Codable을 지원하는 타입이면 컴파일러가 자동으로 변환 코드를 생성합니다.


JSON → Swift (Decoding)

import Foundation

let jsonString = """
{
  "id": 42,
  "name": "홍길동",
  "email": "hong@example.com",
  "isPremium": true,
  "tags": ["swift", "ios"]
}
"""

let jsonData = jsonString.data(using: .utf8)!

do {
    let decoder = JSONDecoder()
    let user = try decoder.decode(User.self, from: jsonData)
    print(user.name)    // 홍길동
    print(user.tags)    // ["swift", "ios"]
} catch {
    print("디코딩 실패: \(error)")
}

배열 디코딩

let jsonArray = """
[
  {"id": 1, "name": "홍길동", "email": "hong@example.com", "isPremium": true, "tags": []},
  {"id": 2, "name": "이순신", "email": "lee@example.com", "isPremium": false, "tags": ["swift"]}
]
"""

let data = jsonArray.data(using: .utf8)!
let users = try JSONDecoder().decode([User].self, from: data)
print(users.count)  // 2

Swift → JSON (Encoding)

let user = User(id: 1, name: "김철수", email: "kim@example.com",
                isPremium: false, tags: ["macos"])

do {
    let encoder = JSONEncoder()
    encoder.outputFormatting = [.prettyPrinted, .sortedKeys]  // 읽기 좋게 출력

    let data = try encoder.encode(user)
    let jsonString = String(data: data, encoding: .utf8)!
    print(jsonString)
} catch {
    print("인코딩 실패: \(error)")
}

출력 결과:

{
  "email" : "kim@example.com",
  "id" : 1,
  "isPremium" : false,
  "name" : "김철수",
  "tags" : [
    "macos"
  ]
}

키 이름 변환

서버 API는 보통 snake_case를 쓰고, Swift는 camelCase를 씁니다. 자동으로 변환하려면:

struct Post: Codable {
    let postId: Int         // JSON: "post_id"
    let createdAt: Date     // JSON: "created_at"
    let authorName: String  // JSON: "author_name"
}

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase  // snake_case → camelCase 자동 변환
decoder.dateDecodingStrategy = .iso8601               // "2024-01-15T10:30:00Z" 형식 자동 파싱

let post = try decoder.decode(Post.self, from: data)

CodingKeys로 수동 매핑

struct Product: Codable {
    let id: Int
    let productName: String  // JSON 키가 다를 때
    let price: Double

    enum CodingKeys: String, CodingKey {
        case id
        case productName = "product_title"  // JSON의 "product_title"을 productName으로
        case price = "cost"                  // JSON의 "cost"를 price로
    }
}

중첩 구조 처리

struct Order: Codable {
    let orderId: String
    let customer: Customer
    let items: [OrderItem]
    let total: Double

    struct Customer: Codable {
        let name: String
        let address: Address
    }

    struct Address: Codable {
        let city: String
        let zipCode: String
    }

    struct OrderItem: Codable {
        let productId: Int
        let quantity: Int
        let price: Double
    }
}

// JSON
let json = """
{
  "orderId": "ORD-001",
  "customer": {
    "name": "홍길동",
    "address": { "city": "서울", "zipCode": "04524" }
  },
  "items": [
    {"productId": 1, "quantity": 2, "price": 9900.0}
  ],
  "total": 19800.0
}
"""

let order = try JSONDecoder().decode(Order.self, from: json.data(using: .utf8)!)
print(order.customer.address.city)  // 서울

옵셔널 필드와 기본값

struct Profile: Codable {
    let username: String
    let bio: String?          // JSON에 없으면 nil
    let followerCount: Int    // JSON에 없으면 디코딩 실패!

    // 기본값이 필요하면 커스텀 init 사용
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        username = try container.decode(String.self, forKey: .username)
        bio = try container.decodeIfPresent(String.self, forKey: .bio)
        followerCount = try container.decodeIfPresent(Int.self, forKey: .followerCount) ?? 0
    }
}

decodeIfPresent는 키가 없을 때 nil을 반환합니다. 기본값을 주려면 ?? 0처럼 nil 병합 연산자를 쓰면 됩니다.


날짜 처리

struct Event: Codable {
    let title: String
    let date: Date
}

let decoder = JSONDecoder()

// ISO 8601 형식 ("2024-06-15T14:30:00Z")
decoder.dateDecodingStrategy = .iso8601

// Unix 타임스탬프 (1718459400)
decoder.dateDecodingStrategy = .secondsSince1970

// 커스텀 형식 ("2024-06-15")
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
decoder.dateDecodingStrategy = .formatted(formatter)

실전 — API 응답 처리

// 서버 응답 공통 래퍼
struct APIResponse<T: Decodable>: Decodable {
    let success: Bool
    let data: T?
    let error: String?
}

struct GitHubUser: Decodable {
    let login: String
    let name: String?
    let publicRepos: Int

    enum CodingKeys: String, CodingKey {
        case login, name
        case publicRepos = "public_repos"
    }
}

// async/await로 API 호출 + 디코딩
func fetchUser(username: String) async throws -> GitHubUser {
    let url = URL(string: "https://api.github.com/users/\(username)")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(GitHubUser.self, from: data)
}

// 사용
Task {
    let user = try await fetchUser(username: "apple")
    print("\(user.login): \(user.publicRepos)개 저장소")
}

다른 언어와 비교

언어 JSON 직렬화 특징
Swift Codable + JSONDecoder 컴파일 타임 타입 안전성
Python json.loads() / json.dumps() 딕셔너리로 바로 변환, 타입 없음
TypeScript JSON.parse() + 타입 캐스팅 런타임에 타입 보장 없음
Kotlin kotlinx.serialization / Gson @Serializable 어노테이션

Swift의 Codable은 타입 불일치를 컴파일 시점에 잡아주므로 런타임 크래시 위험이 크게 줄어듭니다.


핵심 요약

  • Codable = Encodable + Decodable. 구조체/클래스에 붙이면 JSON 변환 자동 지원
  • JSONDecoder: JSON Data → Swift 타입
  • JSONEncoder: Swift 타입 → JSON Data
  • keyDecodingStrategy = .convertFromSnakeCase: snake_case ↔ camelCase 자동 변환
  • CodingKeys: JSON 키 이름과 Swift 속성 이름이 다를 때 매핑
  • decodeIfPresent: 옵셔널 필드 처리 (없으면 nil)
  • 중첩 구조체도 모두 Codable이면 자동으로 처리됨

다음 편에서는 앱 설정을 기기에 저장하는 UserDefaults를 배웁니다.

🤖 Generated with Claude Code

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 항목은 *(으)로 표시합니다