콘텐츠 대표 이미지 - 스위프트 JSON 파싱과 Codable 프로토콜을활용한 데이터 변환
🍎 Swift 완전정복

스위프트 JSON 파싱과 Codable 프로토콜을
활용한 데이터 변환

📦 서버에서 날아오는 데이터, 이제 두렵지 않아! Codable로 완벽 정복하기

🖥️ 서버 JSON 데이터 { "name": "Swift" } 파싱 중... ⚙️ Codable Encodable + Decodable Swift 4.0+ 변환 완료! 📱 Swift 구조체 struct User { var name: String } JSON ↔ Swift 타입, Codable이 다 해결해줘! 🎉

🤔 JSON이 뭔데? 왜 파싱이 필요해?

앱 개발하다 보면 서버랑 데이터를 주고받는 건 거의 필수잖아. 그때 서버가 보내주는 데이터 형식이 대부분 JSON(JavaScript Object Notation)이야.

JSON은 사람이 읽기도 쉽고, 기계가 처리하기도 쉬운 텍스트 기반 데이터 포맷이야. 예를 들면 이런 생김새야:

{
  "name": "김스위프트",
  "age": 28,
  "isActive": true,
  "skills": ["Swift", "iOS", "Xcode"],
  "address": {
    "city": "서울",
    "zip": "04524"
  }
}

근데 Swift는 이 텍스트를 그냥 쓸 수가 없어. Swift는 타입 안전(Type-Safe) 언어라서, 데이터를 Swift가 이해하는 타입(String, Int, Bool, 구조체 등)으로 변환해줘야 해.

이 변환 과정을 파싱(Parsing)이라고 불러. 그리고 Swift에서 이걸 가장 우아하게 처리하는 방법이 바로 Codable 프로토콜이야! 🎯

💡 파싱(Parsing)이란? 원시 데이터(텍스트, 바이트 등)를 프로그램이 사용할 수 있는 구조화된 데이터로 변환하는 과정이야. JSON 파싱은 JSON 문자열 → Swift 타입으로의 변환을 의미해!

📜 Codable 이전의 세상 (옛날 방식)

Codable이 등장하기 전(Swift 4.0 이전)에는 JSON 파싱이 정말 고통스러웠어. 어떻게 했냐고? 이런 식이었어:

// 😱 옛날 방식 (Swift 3.x 이하)
if let data = jsonString.data(using: .utf8),
   let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
   let name = json["name"] as? String,
   let age = json["age"] as? Int {
    // 이제야 쓸 수 있어... 😭
    print(name, age)
}

보이지? 매번 as? String, as? Int 이런 식으로 타입 캐스팅을 일일이 해줘야 했어.
필드가 10개, 20개면? 코드가 어마어마하게 길어지고, 실수할 확률도 폭발적으로 올라가지. 😤

그래서 Swift 4.0에서 Codable이 등장했고, 개발자들이 환호했어! 🎉


✨ Codable 프로토콜의 정체

Codable typealias Codable = Encodable & Decodable Encodable Swift → JSON Decodable JSON → Swift Codable = Encodable + Decodable 의 합체! 🦸 인코딩 전담 디코딩 전담

Codable은 사실 두 개의 프로토콜을 합친 타입 별칭(typealias)이야:

// Swift 표준 라이브러리 내부 정의
public typealias Codable = Decodable & Encodable

Encodable : Swift 타입 → JSON (데이터 내보내기)
Decodable : JSON → Swift 타입 (데이터 받아오기)
Codable : 둘 다 가능! (양방향 변환)

만약 JSON을 읽기만 한다면 Decodable만, 보내기만 한다면 Encodable만 채택해도 돼. 하지만 보통은 편하게 Codable을 쓰지! 😄


🚀 기본 사용법: 이렇게 쉬워?!

1️⃣ 구조체에 Codable 채택하기

가장 기본적인 사용법이야. 구조체나 클래스를 정의할 때 Codable을 채택하면 끝!

// ✅ 이렇게 간단해!
struct User: Codable {
    var name: String
    var age: Int
    var isActive: Bool
}

2️⃣ JSON → Swift 타입 (디코딩)

서버에서 JSON 데이터를 받아서 Swift 타입으로 변환하는 과정이야. JSONDecoder를 사용해:

// JSON 문자열 (실제로는 서버에서 Data 형태로 옴)
let jsonString = """
{
    "name": "김스위프트",
    "age": 28,
    "isActive": true
}
"""

// String → Data 변환
let jsonData = jsonString.data(using: .utf8)!

// 🎯 디코딩! JSONDecoder 사용
let decoder = JSONDecoder()
do {
    let user = try decoder.decode(User.self, from: jsonData)
    print(user.name)     // "김스위프트"
    print(user.age)      // 28
    print(user.isActive) // true
} catch {
    print("디코딩 실패:", error)
}
💡 do-catch 필수! decode(_:from:)는 throws 함수야. 즉, 실패할 수 있어. JSON 형식이 잘못됐거나, 타입이 안 맞으면 에러를 던지니까 반드시 do-catch로 감싸줘야 해!

3️⃣ Swift 타입 → JSON (인코딩)

반대로 Swift 타입을 JSON으로 변환할 때는 JSONEncoder를 사용해:

let user = User(name: "박코딩", age: 25, isActive: false)

// 🎯 인코딩! JSONEncoder 사용
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted // 예쁘게 출력

do {
    let jsonData = try encoder.encode(user)
    let jsonString = String(data: jsonData, encoding: .utf8)!
    print(jsonString)
    // {
    //   "name" : "박코딩",
    //   "age" : 25,
    //   "isActive" : false
    // }
} catch {
    print("인코딩 실패:", error)
}

어때? 옛날 방식이랑 비교하면 엄청 깔끔하지? 🤩
Swift가 내부적으로 프로퍼티 이름을 JSON 키와 자동으로 매핑해줘서 우리가 일일이 처리할 필요가 없어!


🗝️ CodingKeys: 키 이름이 다를 때

현실에서는 서버가 보내주는 JSON 키 이름이 Swift 네이밍 컨벤션(camelCase)과 다른 경우가 많아.
예를 들어 서버는 user_name이라고 보내는데, Swift에서는 userName으로 쓰고 싶잖아?

이럴 때 CodingKeys를 사용해!

struct User: Codable {
    var userName: String    // Swift 스타일 (camelCase)
    var userAge: Int
    var profileURL: String

    // 🗝️ CodingKeys로 JSON 키 ↔ Swift 프로퍼티 매핑
    enum CodingKeys: String, CodingKey {
        case userName = "user_name"    // JSON: "user_name" → Swift: userName
        case userAge = "user_age"      // JSON: "user_age" → Swift: userAge
        case profileURL = "profile_url"
    }
}

// JSON 예시
// {
//   "user_name": "이스위프트",
//   "user_age": 30,
//   "profile_url": "https://example.com/photo.jpg"
// }
⚠️ 주의! CodingKeys를 정의하면, 모든 프로퍼티를 CodingKeys에 포함시켜야 해. 하나라도 빠지면 그 프로퍼티는 인코딩/디코딩에서 제외돼!

🔄 자동 변환: keyDecodingStrategy

만약 서버가 항상 snake_case를 쓴다면, 매번 CodingKeys 쓰기 귀찮잖아. 이럴 때 keyDecodingStrategy를 쓰면 자동으로 변환해줘!

struct User: Codable {
    var userName: String   // JSON의 "user_name"과 자동 매핑!
    var userAge: Int
}

let decoder = JSONDecoder()
// snake_case → camelCase 자동 변환
decoder.keyDecodingStrategy = .convertFromSnakeCase

// 인코더도 마찬가지
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
💡 keyDecodingStrategy 종류 .useDefaultKeys : 기본값, 변환 없음
.convertFromSnakeCase : snake_case → camelCase 자동 변환
.custom : 커스텀 변환 로직 적용

🪆 중첩 JSON 파싱 (Nested JSON)

실제 API 응답은 JSON 안에 JSON이 있는 중첩 구조가 많아. 이것도 Codable로 쉽게 처리할 수 있어!

중첩 JSON 구조 시각화 User (최상위) "name": "김스위프트" "age": 28 "address" (중첩 객체) "city": "서울" "zip": "04524" "country": "KR" "skills": ["Swift", "iOS", "Xcode"] (배열) 각 중첩 구조마다 별도의 Codable 구조체를 정의해줘! 🎯
// 중첩 구조체도 각각 Codable 채택!
struct Address: Codable {
    var city: String
    var zip: String
    var country: String
}

struct User: Codable {
    var name: String
    var age: Int
    var address: Address    // 중첩 구조체
    var skills: [String]    // 배열도 OK!
}

// JSON
// {
//   "name": "김스위프트",
//   "age": 28,
//   "address": {
//     "city": "서울",
//     "zip": "04524",
//     "country": "KR"
//   },
//   "skills": ["Swift", "iOS", "Xcode"]
// }

let decoder = JSONDecoder()
let user = try decoder.decode(User.self, from: jsonData)
print(user.address.city)  // "서울"
print(user.skills[0])     // "Swift"

중첩 구조체의 각 타입이 Codable을 채택하면, 상위 구조체도 자동으로 중첩 파싱을 처리해줘. 진짜 편하지? 😍


❓ 옵셔널 프로퍼티 처리

서버 응답에서 특정 필드가 없을 수도 있잖아. 이럴 때는 프로퍼티를 옵셔널(Optional)로 선언하면 돼!

struct User: Codable {
    var name: String          // 필수 (없으면 에러!)
    var age: Int              // 필수
    var bio: String?          // 옵셔널 (없어도 nil로 처리)
    var profileImage: String? // 옵셔널
}

// JSON에 "bio" 키가 없어도 OK!
// {
//   "name": "최코딩",
//   "age": 22
// }
// → user.bio == nil
💡 기본값 설정하기 옵셔널 대신 기본값을 주고 싶다면, 커스텀 디코딩을 사용하거나 @propertyWrapper를 활용할 수 있어. 아래에서 더 자세히 다룰게!

🔢 배열 파싱

JSON 배열도 당연히 처리 가능해. 최상위가 배열인 경우도 OK!

// JSON이 배열로 시작하는 경우
// [
//   {"name": "김스위프트", "age": 28},
//   {"name": "박코딩", "age": 25}
// ]

let users = try decoder.decode([User].self, from: jsonData)
// [User] 타입으로 디코딩!
print(users.count)      // 2
print(users[0].name)    // "김스위프트"

📅 날짜(Date) 처리

날짜 데이터는 JSON에서 다양한 형식으로 올 수 있어. ISO 8601 형식, Unix 타임스탬프, 커스텀 포맷 등등.
JSONDecoder의 dateDecodingStrategy로 처리할 수 있어!

struct Post: Codable {
    var title: String
    var createdAt: Date
}

let decoder = JSONDecoder()

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

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

// 3. 밀리초 타임스탬프 (1705312200000)
decoder.dateDecodingStrategy = .millisecondsSince1970

// 4. 커스텀 포맷 ("2024-01-15")
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
decoder.dateDecodingStrategy = .formatted(formatter)

let post = try decoder.decode(Post.self, from: jsonData)
⚠️ dateDecodingStrategy 기본값은 .deferredToDate야! 이건 Apple의 내부 포맷을 사용하는데, 일반적인 JSON API와는 맞지 않아. 반드시 서버 형식에 맞는 전략을 설정해줘!

🎨 커스텀 인코딩/디코딩

자동 처리로는 한계가 있을 때, 직접 인코딩/디코딩 로직을 구현할 수 있어.
init(from decoder: Decoder)와 encode(to encoder: Encoder)를 직접 구현하면 돼!

🔧 커스텀 디코딩 예시

struct User: Codable {
    var name: String
    var age: Int
    var isAdult: Bool  // JSON에는 없는 필드! 계산해서 만들 거야

    // 커스텀 디코딩
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        name = try container.decode(String.self, forKey: .name)
        age = try container.decode(Int.self, forKey: .age)
        isAdult = age >= 18  // age를 기반으로 계산!
    }

    // 커스텀 인코딩 (isAdult는 인코딩에서 제외)
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        try container.encode(age, forKey: .age)
        // isAdult는 encode 안 함
    }

    enum CodingKeys: String, CodingKey {
        case name, age
    }
}

🛡️ 기본값 처리 (decodeIfPresent)

struct Config: Codable {
    var theme: String
    var fontSize: Int
    var showNotifications: Bool

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        theme = try container.decodeIfPresent(String.self, forKey: .theme) ?? "light"
        fontSize = try container.decodeIfPresent(Int.self, forKey: .fontSize) ?? 16
        showNotifications = try container.decodeIfPresent(Bool.self, forKey: .showNotifications) ?? true
    }

    enum CodingKeys: String, CodingKey {
        case theme, fontSize, showNotifications
    }
}

decodeIfPresent는 키가 없거나 값이 null이면 nil을 반환해. 그래서 ?? 기본값으로 기본값을 설정할 수 있어! 👍


🌈 다양한 타입 지원

Codable이 지원하는 타입들 🎁 String 문자열 Int / Double 숫자 Bool 불리언 Date 날짜 URL URL 주소 Data 바이너리 Array<T> 배열 (T: Codable) Dictionary<K,V> 딕셔너리 Optional<T> 옵셔널 Enum (RawValue) 열거형 위 타입들은 모두 기본적으로 Codable을 준수해! 🎉 커스텀 타입도 Codable 채택하면 중첩 사용 가능

🎭 Enum과 Codable

열거형도 Codable을 채택할 수 있어! RawValue가 있는 열거형은 특히 쉬워:

// String RawValue를 가진 Enum
enum UserRole: String, Codable {
    case admin = "ADMIN"
    case user = "USER"
    case guest = "GUEST"
}

struct User: Codable {
    var name: String
    var role: UserRole  // Enum 타입 사용!
}

// JSON: {"name": "김관리자", "role": "ADMIN"}
// → user.role == .admin ✅
// Int RawValue도 가능
enum Priority: Int, Codable {
    case low = 1
    case medium = 2
    case high = 3
}

struct Task: Codable {
    var title: String
    var priority: Priority
}

// JSON: {"title": "버그 수정", "priority": 3}
// → task.priority == .high ✅

🔥 실전 패턴: API 응답 처리

실제 앱에서 API 응답은 보통 이런 구조야. 성공/실패 여부와 데이터를 함께 담아서 보내줘:

// 일반적인 API 응답 구조
// {
//   "success": true,
//   "message": "성공",
//   "data": {
//     "users": [...],
//     "total": 100,
//     "page": 1
//   }
// }

// 제네릭을 활용한 범용 응답 모델!
struct APIResponse<T: Codable>: Codable {
    var success: Bool
    var message: String
    var data: T?
}

struct UserListData: Codable {
    var users: [User]
    var total: Int
    var page: Int
}

// 사용
let response = try decoder.decode(
    APIResponse<UserListData>.self,
    from: jsonData
)

if response.success, let data = response.data {
    print("총 유저 수:", data.total)
    print("첫 번째 유저:", data.users.first?.name ?? "없음")
}
💡 제네릭(Generic) + Codable = 최강 조합! 제네릭을 활용하면 하나의 응답 구조체로 다양한 API 응답을 처리할 수 있어. 코드 재사용성이 엄청 올라가!

⚡ URLSession + Codable 실전 조합

실제 앱에서는 URLSession으로 네트워크 요청을 하고, 받은 데이터를 Codable로 파싱해. 이게 가장 흔한 패턴이야!

// 실전 네트워크 요청 + Codable 파싱
struct GitHubUser: Codable {
    var login: String
    var name: String?
    var publicRepos: Int
    var followers: Int

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

func fetchGitHubUser(username: String) async throws -> GitHubUser {
    let url = URL(string: "https://api.github.com/users/\(username)")!

    let (data, response) = try await URLSession.shared.data(from: url)

    guard let httpResponse = response as? HTTPURLResponse,
          httpResponse.statusCode == 200 else {
        throw URLError(.badServerResponse)
    }

    let decoder = JSONDecoder()
    return try decoder.decode(GitHubUser.self, from: data)
}

// 사용 (async context에서)
Task {
    do {
        let user = try await fetchGitHubUser(username: "apple")
        print("이름:", user.name ?? "없음")
        print("팔로워:", user.followers)
        print("공개 레포:", user.publicRepos)
    } catch {
        print("에러:", error)
    }
}

Swift 5.5+의 async/await와 Codable을 함께 쓰면 비동기 네트워크 코드가 정말 깔끔해져! 🚀

이런 기술들을 익히고 싶다면 재능넷에서 Swift 개발 전문가의 강의나 멘토링을 찾아보는 것도 좋은 방법이야. 다양한 실력자들이 자신의 재능을 공유하고 있거든! 😊


🐛 에러 처리와 디버깅

Codable 사용 중 에러가 발생하면 어떻게 디버깅할까? DecodingError를 잘 활용하면 돼!

do {
    let user = try decoder.decode(User.self, from: jsonData)
} catch DecodingError.keyNotFound(let key, let context) {
    // 키가 JSON에 없을 때
    print("키를 찾을 수 없음:", key.stringValue)
    print("경로:", context.codingPath)
} catch DecodingError.typeMismatch(let type, let context) {
    // 타입이 맞지 않을 때
    print("타입 불일치:", type)
    print("경로:", context.codingPath)
    print("설명:", context.debugDescription)
} catch DecodingError.valueNotFound(let type, let context) {
    // 값이 null인데 옵셔널이 아닐 때
    print("값 없음:", type)
    print("경로:", context.codingPath)
} catch DecodingError.dataCorrupted(let context) {
    // JSON 형식 자체가 잘못됐을 때
    print("데이터 손상:", context.debugDescription)
} catch {
    print("기타 에러:", error)
}
💡 디버깅 꿀팁!
context.codingPath를 출력하면 어떤 키에서 에러가 났는지 정확히 알 수 있어. 중첩 구조에서 특히 유용해! 예:
[CodingKeys(stringValue: "address"), CodingKeys(stringValue: "zip")]

🏎️ 성능 최적화 팁

1
JSONDecoder/JSONEncoder 재사용하기
JSONDecoder와 JSONEncoder는 생성 비용이 있어. 매번 새로 만들지 말고 싱글톤이나 공유 인스턴스를 사용해!
// ✅ 좋은 방법: 공유 인스턴스
extension JSONDecoder {
    static let shared: JSONDecoder = {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        decoder.dateDecodingStrategy = .iso8601
        return decoder
    }()
}
2
필요한 프로토콜만 채택하기
JSON을 읽기만 한다면 Codable 대신 Decodable만 채택해. 컴파일러가 불필요한 코드를 생성하지 않아도 돼!
3
struct vs class 선택
Codable 모델은 대부분 struct를 사용해. 값 타입이라 스레드 안전하고, 복사 비용도 적어. 상속이 필요한 경우에만 class를 써!
4
대용량 데이터는 스트리밍 파싱 고려
수천 개의 아이템이 있는 JSON이라면 한 번에 파싱하는 것보다 페이지네이션을 활용하거나, 필요한 경우 JSONSerialization과 병행 사용을 고려해봐.

🔮 고급 기법: 다형성 처리

JSON에서 타입이 동적으로 결정되는 경우(다형성)는 어떻게 처리할까? 예를 들어 type 필드에 따라 다른 구조체로 파싱해야 할 때!

// JSON 예시
// {"type": "text", "content": "안녕하세요"}
// {"type": "image", "url": "https://...", "width": 800}

// 프로토콜 정의
protocol Message: Codable {
    var type: String { get }
}

struct TextMessage: Message {
    var type: String
    var content: String
}

struct ImageMessage: Message {
    var type: String
    var url: String
    var width: Int
}

// 커스텀 디코딩으로 타입 분기
struct AnyMessage: Decodable {
    var message: any Message

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let type = try container.decode(String.self, forKey: .type)

        switch type {
        case "text":
            message = try TextMessage(from: decoder)
        case "image":
            message = try ImageMessage(from: decoder)
        default:
            throw DecodingError.dataCorruptedError(
                forKey: .type,
                in: container,
                debugDescription: "알 수 없는 타입: \(type)"
            )
        }
    }

    enum CodingKeys: String, CodingKey {
        case type
    }
}

📊 Codable vs 다른 방법 비교

✅ Codable (권장)

• Swift 표준 라이브러리 내장
• 타입 안전
• 컴파일 타임 검증
• 자동 코드 생성
• 외부 라이브러리 불필요
• 성능 우수
⚠️ JSONSerialization (구식)

• 타입 캐스팅 필요
• 런타임 에러 위험
• 코드 장황함
• 유지보수 어려움
• 복잡한 구조 처리 힘듦
• 여전히 특수 케이스에 유용
🔧 SwiftyJSON (서드파티)

• 동적 타입 접근 편리
• 외부 의존성 추가
• 타입 안전성 낮음
• 간단한 파싱엔 편리
• Codable 등장 후 사용 감소
🚀 Codable + Combine/async

• 최신 Swift 패턴
• 비동기 처리 완벽
• 반응형 프로그래밍 가능
• 현대적 앱 개발 표준
• 학습 곡선 있음
• 강력 추천! 💪

🎯 실전 체크리스트

Codable 실전 체크리스트 ✅ ✅ 구조체/클래스에 Codable 프로토콜 채택했나? ✅ JSON 키와 Swift 프로퍼티 이름이 다르면 CodingKeys 정의했나? ✅ 없을 수 있는 필드는 옵셔널(?)로 선언했나? ✅ 날짜 필드가 있다면 dateDecodingStrategy 설정했나? ✅ do-catch로 에러 처리 했나? (throws 함수니까!) ✅ 중첩 구조체도 각각 Codable 채택했나? 모두 체크했다면 완벽한 Codable 마스터! 🏆

🌟 마무리: Codable 완전 정복 요약

📌 핵심 정리 Codable = Encodable + Decodable 의 합체 프로토콜

🔹 디코딩 (JSON → Swift): JSONDecoder().decode(타입.self, from: data)
🔹 인코딩 (Swift → JSON): JSONEncoder().encode(인스턴스)
🔹 키 매핑: CodingKeys 열거형 또는 keyDecodingStrategy
🔹 날짜 처리: dateDecodingStrategy 설정
🔹 옵셔널 필드: 프로퍼티를 ?로 선언
🔹 커스텀 로직: init(from:), encode(to:) 직접 구현
🔹 에러 처리: DecodingError 케이스별 처리

Codable은 Swift 생태계에서 JSON 처리의 표준이 된 지 오래야. 처음엔 낯설 수 있지만, 한 번 익숙해지면 다른 방법으로 돌아가기 싫어질 거야! 😄

특히 URLSession + async/await + Codable 조합은 현대 iOS 앱 개발의 핵심 패턴이니까, 꼭 마스터해두길 추천해!

더 깊이 배우고 싶다면 재능넷에서 Swift 전문가들의 실전 강의를 찾아봐. 이론보다 실전 코드로 배우는 게 훨씬 빠르거든! 💪

댓글 작성

이 글에 대한 여러분의 생각을 들려주세요

댓글 0
댓글을 불러오는 중입니다.

아직 댓글이 없습니다.