🤖 이 글은 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이란?
Codable은 Encodable과 Decodable을 합친 타입 별칭입니다.
- 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