챌린지 2-1 · 6 Topics

💾 데이터를 앱에 저장하기

앱을 껐다 켜도 데이터가 남아야 한다

Codable · JSON UserDefaults @Model ModelContainer · ModelContext @Query CRUD — Create · Read · Update · Delete
시작 전에 던져볼 질문
01
iOS 앱이 데이터를 저장하는 방법에는 어떤 것들이 있는가?
검색 → iOS data persistence options UserDefaults CoreData SwiftData
02
Swift의 Codable은 무엇이며 왜 필요한가?
검색 → Swift Codable Encodable Decodable JSON tutorial
03
SwiftData와 CoreData의 차이는 무엇인가?
검색 → SwiftData vs CoreData iOS 17 differences
Topic 01

Codable — Swift ↔ JSON

데이터를 JSON으로 변환하고 다시 복원하는 방법
Swift
// Codable = Encodable + Decodable
struct User: Codable {
    let id: Int
    let name: String
    let email: String
}

// Swift → JSON (Encoding)
let user = User(id: 1, name: "이현호", email: "test@test.com")
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
if let data = try? encoder.encode(user),
   let json = String(data: data, encoding: .utf8) {
    print(json)
    // { "id": 1, "name": "이현호", "email": "test@test.com" }
}

// JSON → Swift (Decoding)
let jsonString = """
{ "id": 1, "name": "이현호", "email": "test@test.com" }
"""
let decoder = JSONDecoder()
if let data = jsonString.data(using: .utf8),
   let decoded = try? decoder.decode(User.self, from: data) {
    print(decoded.name) // 이현호
}
생각해보기
  • 서버의 JSON 키가 user_name인데 Swift 속성을 userName으로 쓰려면 어떻게 하는가? (CodingKeys)
  • try?try!의 차이는 무엇인가? 실제 앱에서 어느 쪽이 더 안전한가?
  • 중첩 JSON 구조(객체 안에 객체)는 어떻게 Codable로 표현하는가?
Codable 프로토콜
Encodable + Decodable의 타입 별칭. struct의 모든 프로퍼티가 Codable이면 자동으로 구현이 생성된다
JSONEncoder / JSONDecoder
Swift 표준 라이브러리 제공. keyDecodingStrategy로 snake_case ↔ camelCase 자동 변환 가능
CodingKeys enum
JSON 키와 Swift 프로퍼티 이름이 다를 때 매핑. case userName = "user_name"
Topic 02

UserDefaults — 간단한 설정 저장

작은 데이터를 앱이 꺼져도 유지하는 가장 빠른 방법
Swift
// 기본 타입 저장/불러오기
UserDefaults.standard.set(true, forKey: "isDarkMode")
UserDefaults.standard.set(42, forKey: "launchCount")
UserDefaults.standard.set("한국어", forKey: "language")

let isDark = UserDefaults.standard.bool(forKey: "isDarkMode")
let count = UserDefaults.standard.integer(forKey: "launchCount")

// Codable 구조체 저장 (JSON으로 변환 후 Data로 저장)
struct AppSettings: Codable {
    var theme: String
    var fontSize: Int
}

let settings = AppSettings(theme: "dark", fontSize: 16)
if let data = try? JSONEncoder().encode(settings) {
    UserDefaults.standard.set(data, forKey: "appSettings")
}

// 불러오기
if let data = UserDefaults.standard.data(forKey: "appSettings"),
   let saved = try? JSONDecoder().decode(
       AppSettings.self, from: data) {
    print(saved.theme) // dark
}

// @AppStorage — SwiftUI 전용 UserDefaults
struct ContentView: View {
    @AppStorage("isDarkMode") var isDarkMode = false

    var body: some View {
        Toggle("다크 모드", isOn: $isDarkMode)
    }
}
생각해보기
  • UserDefaults는 어떤 데이터에 적합하고 어떤 데이터에 부적합한가? (사용자 토큰, 장바구니 목록, 결제 정보는?)
  • @AppStorage@State의 가장 큰 차이는 무엇인가?
  • UserDefaults에 저장된 값을 앱 삭제 없이 초기화하려면 어떻게 하는가?
언제 UserDefaults를 쓰는가
앱 설정, 사용자 선택값, 마지막 실행 날짜 등 소량의 단순 데이터. 대용량 데이터나 민감 정보에는 부적합
@AppStorage
SwiftUI에서 UserDefaults를 @State처럼 바인딩으로 사용. 값이 바뀌면 자동으로 View가 다시 그려진다
Topic 03

@Model — SwiftData 데이터 모델

iOS 17+에서 Core Data를 대체하는 현대적인 데이터 저장소
Swift
import SwiftData

// @Model 매크로로 영구 저장 모델 선언
@Model
class TodoItem {
    var title: String
    var isCompleted: Bool
    var createdAt: Date

    init(title: String) {
        self.title = title
        self.isCompleted = false
        self.createdAt = Date()
    }
}

// 앱 진입점에서 ModelContainer 연결
@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: TodoItem.self)
    }
}
생각해보기
  • SwiftData의 @Model은 왜 struct가 아닌 class여야 하는가?
  • .modelContainer(for:)를 최상위 App에 달면 하위 모든 View에서 사용할 수 있는 이유는 무엇인가?
  • 여러 Model 타입을 함께 사용하려면 .modelContainer를 어떻게 설정하는가?
@Model 매크로
class에 붙이면 SwiftData가 영구 저장, 변경 추적, 관계 관리를 자동 처리. iOS 17+ 필요
ModelContainer
데이터베이스 파일 관리. .modelContainer(for: [A.self, B.self])로 여러 모델 등록 가능
ModelContext
CRUD 작업의 진입점. @Environment(\.modelContext)로 View에서 접근. insert / delete / save
Topic 04

@Query — 데이터 조회

SwiftData에서 데이터를 읽고 필터링하고 정렬하는 방법
Swift
struct TodoListView: View {
    // @Query: DB에서 자동으로 데이터를 가져오고
    // 변경 시 View를 자동으로 업데이트
    @Query(sort: \TodoItem.createdAt, order: .reverse)
    var todos: [TodoItem]

    // 필터 조건 추가
    @Query(filter: #Predicate { !$0.isCompleted },
           sort: \TodoItem.createdAt)
    var activeTodos: [TodoItem]

    var body: some View {
        List(todos) { todo in
            Text(todo.title)
        }
    }
}

// ModelContext로 CRUD
struct AddTodoView: View {
    @Environment(\.modelContext) private var context
    @State private var title = ""

    var body: some View {
        HStack {
            TextField("할 일", text: $title)
            Button("추가") {
                let item = TodoItem(title: title)
                context.insert(item)  // Create
                title = ""
            }
        }
    }
}

// Delete
func delete(_ item: TodoItem) {
    context.delete(item)
}
생각해보기
  • @Query로 가져온 배열은 @State와 어떻게 다른가? 직접 수정할 수 있는가?
  • context.insert()try context.save()를 해야 하는가, 안 해도 되는가?
  • #Predicate에서 여러 조건을 AND / OR 로 결합하려면?
@Query 프로퍼티 래퍼
DB를 관찰하는 라이브 쿼리. 데이터 변경 시 View 자동 갱신. sort, filter, order 파라미터 지원
#Predicate 매크로
타입 안전한 필터 조건. #Predicate<TodoItem> { $0.isCompleted == false }
자동 저장
SwiftData는 변경 사항을 자동으로 저장. 명시적 save()는 선택적이지만 중요한 시점에는 호출 권장
Topic 05

관계 (Relationship) — 연결된 데이터

모델 간의 일대다, 다대다 관계를 표현하는 방법
Swift
@Model
class Category {
    var name: String
    // 일대다: 하나의 카테고리에 여러 Todo
    @Relationship(deleteRule: .cascade)
    var todos: [TodoItem] = []

    init(name: String) { self.name = name }
}

@Model
class TodoItem {
    var title: String
    var isCompleted: Bool
    // 역방향 관계
    var category: Category?

    init(title: String) {
        self.title = title
        self.isCompleted = false
    }
}

// 관계 설정
let category = Category(name: "업무")
let todo = TodoItem(title: "회의 준비")
todo.category = category
context.insert(category)

// deleteRule: .cascade → category 삭제 시 하위 todos도 삭제
생각해보기
  • deleteRule: .cascade.nullify의 차이는? 어느 상황에서 각각 적합한가?
  • 양방향 관계를 설정할 때 양쪽 모델에 모두 @Relationship을 달아야 하는가?
  • Category를 먼저 저장해야 하는가, TodoItem을 먼저 저장해야 하는가?
@Relationship
deleteRule로 연쇄 삭제 제어. .cascade / .nullify / .deny / .noAction
역방향 관계
양쪽 모델에 서로를 참조하면 SwiftData가 자동으로 동기화. 한쪽만 설정해도 동작
Topic 06

Preview와 테스트용 컨테이너

SwiftUI Preview에서 SwiftData를 사용하는 방법
Swift
// Preview용 인메모리 컨테이너
#Preview {
    let config = ModelConfiguration(isStoredInMemoryOnly: true)
    let container = try! ModelContainer(
        for: TodoItem.self,
        configurations: config
    )

    // 샘플 데이터 삽입
    let context = container.mainContext
    context.insert(TodoItem(title: "SwiftData 공부"))
    context.insert(TodoItem(title: "앱 배포하기"))

    return TodoListView()
        .modelContainer(container)
}

// 실제 앱 — 영구 저장
@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: TodoItem.self)
    }
}
생각해보기
  • isStoredInMemoryOnly: true로 만든 컨테이너는 앱을 재실행하면 데이터가 사라진다. 어떤 상황에서 유용한가?
  • @Model 클래스에 새 프로퍼티를 추가하면 기존 앱 사용자의 데이터는 어떻게 되는가? (Schema Migration)
  • Unit Test에서 SwiftData를 사용하려면 어떻게 설정하는가?
인메모리 컨테이너
Preview와 테스트에서 디스크에 저장하지 않는 임시 DB. isStoredInMemoryOnly: true
Schema Migration
모델 구조 변경 시 기존 데이터 유지를 위한 마이그레이션. VersionedSchema와 SchemaMigrationPlan 활용
Codable
{
  "id": 1,
  "name": "이현호",
  "email": "test@.."
}
Swift
JSON
UserDefaults
APP STORAGE
다크 모드
알림
@Model
SWIFTDATA ▸ TODO
💾 영구 저장됨
SwiftData 공부
앱 배포하기
+ 새 항목 추가
@Query
DB → VIEW 자동 동기화
SwiftData 공부
앱 배포하기
코드 리뷰
Relationship
📁 업무
• 회의 준비
• 보고서 작성
📁 개인
• 운동하기
Preview 컨테이너
#Preview {
 inMemory
 + sampleData
}
디스크에 저장 ✗
Preview 전용
Homework

챌린지 2-1을 마쳤다면

필수
SwiftData로 일기 앱 만들기
@Model로 DiaryEntry(title, content, date) 정의. @Query로 목록 표시. ModelContext로 추가/삭제. 앱 재실행 시 데이터 유지 확인
필수
@AppStorage로 설정 화면 만들기
다크 모드 토글, 폰트 크기 슬라이더를 @AppStorage로 연결. 앱을 껐다 켜도 설정이 유지되는지 확인
도전
카테고리 분류 기능 추가
Category 모델을 만들고 DiaryEntry와 일대다 관계 설정. 카테고리별 필터 기능 구현 (#Predicate 활용)
완료 체크리스트
Codable로 Swift 구조체를 JSON으로 변환/역변환할 수 있다
@AppStorage로 설정값을 영구 저장할 수 있다
@Model 클래스를 선언하고 .modelContainer()를 연결할 수 있다
@Query로 데이터를 불러오고 ModelContext로 추가/삭제할 수 있다
← Stage 2 목록 Stage 2 인덱스
코멘트
후원하기
콘텐츠가 도움이 됐다면
커피 한 잔의 후원을 부탁드려요 :)
카카오페이 QR
이현호
카카오페이
카카오톡 QR
카카오톡 오픈채팅 💬
학습 질문 · 코드 리뷰 · 링크로 참여