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

스위프트 JSON 파싱과 Codable 프로토콜을
활용한 데이터 변환
📦 서버에서 날아오는 데이터, 이제 두렵지 않아! 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 프로토콜이야! 🎯
📜 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)이야:
// 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)
}
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"
// }
🔄 자동 변환: 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
.useDefaultKeys : 기본값, 변환 없음.convertFromSnakeCase : snake_case → camelCase 자동 변환.custom : 커스텀 변환 로직 적용
🪆 중첩 JSON 파싱 (Nested JSON)
실제 API 응답은 JSON 안에 JSON이 있는 중첩 구조가 많아. 이것도 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)
🎨 커스텀 인코딩/디코딩
자동 처리로는 한계가 있을 때, 직접 인코딩/디코딩 로직을 구현할 수 있어.
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을 반환해. 그래서 ?? 기본값으로 기본값을 설정할 수 있어! 👍
🌈 다양한 타입 지원
🎭 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 ?? "없음")
}
⚡ 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")]
🏎️ 성능 최적화 팁
JSONDecoder와 JSONEncoder는 생성 비용이 있어. 매번 새로 만들지 말고 싱글톤이나 공유 인스턴스를 사용해!
// ✅ 좋은 방법: 공유 인스턴스
extension JSONDecoder {
static let shared: JSONDecoder = {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
return decoder
}()
}
JSON을 읽기만 한다면
Codable 대신 Decodable만 채택해. 컴파일러가 불필요한 코드를 생성하지 않아도 돼!
Codable 모델은 대부분
struct를 사용해. 값 타입이라 스레드 안전하고, 복사 비용도 적어. 상속이 필요한 경우에만 class를 써!
수천 개의 아이템이 있는 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 다른 방법 비교
• Swift 표준 라이브러리 내장
• 타입 안전
• 컴파일 타임 검증
• 자동 코드 생성
• 외부 라이브러리 불필요
• 성능 우수
• 타입 캐스팅 필요
• 런타임 에러 위험
• 코드 장황함
• 유지보수 어려움
• 복잡한 구조 처리 힘듦
• 여전히 특수 케이스에 유용
• 동적 타입 접근 편리
• 외부 의존성 추가
• 타입 안전성 낮음
• 간단한 파싱엔 편리
• Codable 등장 후 사용 감소
• 최신 Swift 패턴
• 비동기 처리 완벽
• 반응형 프로그래밍 가능
• 현대적 앱 개발 표준
• 학습 곡선 있음
• 강력 추천! 💪
🎯 실전 체크리스트
🌟 마무리: 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
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.