์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ“ฑ Core Data์™€ SQLite๋กœ ์™„๋ฒฝํ•˜๊ฒŒ ์ •๋ณตํ•˜๋Š” iOS ๋ฐ์ดํ„ฐ ์˜์†ํ™” ๊ด€๋ฆฌ ๊ฐ€์ด๋“œ

๐Ÿ“ฑ Core Data์™€ SQLite๋กœ ์™„๋ฒฝํ•˜๊ฒŒ ์ •๋ณตํ•˜๋Š” iOS ๋ฐ์ดํ„ฐ ์˜์†ํ™” ๊ด€๋ฆฌ ๊ฐ€์ด๋“œ

์นœ๊ตฌ์ฒ˜๋Ÿผ ์‰ฝ๊ฒŒ ๋ฐฐ์šฐ๋Š” iOS ๋ฐ์ดํ„ฐ ์ €์žฅ์˜ ๋ชจ๋“  ๊ฒƒ ๐Ÿš€

๐ŸŽฏ ์™œ ๋ฐ์ดํ„ฐ ์˜์†ํ™”๊ฐ€ ์ค‘์š”ํ• ๊นŒ?

์•ˆ๋…•! ์˜ค๋Š˜์€ iOS ๊ฐœ๋ฐœ์—์„œ ์ •๋ง ์ค‘์š”ํ•œ ์ฃผ์ œ์ธ ๋ฐ์ดํ„ฐ ์˜์†ํ™”์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•ด๋ณผ๊ฒŒ. ๐Ÿ˜Š

์•ฑ์„ ๋งŒ๋“ค๋‹ค ๋ณด๋ฉด ํ•ญ์ƒ ๋งˆ์ฃผ์น˜๋Š” ๊ณ ๋ฏผ์ด ์žˆ์–ด. "์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋–ป๊ฒŒ ์ €์žฅํ•˜์ง€?", "์•ฑ์„ ๊ป๋‹ค ์ผœ๋„ ๋ฐ์ดํ„ฐ๊ฐ€ ๋‚จ์•„์žˆ๊ฒŒ ํ•˜๋ ค๋ฉด?"
์ด๋Ÿฐ ๊ณ ๋ฏผ๋“ค ๋ง์ด์•ผ. ๋ฐ”๋กœ ์ด๊ฒŒ ๋ฐ์ดํ„ฐ ์˜์†ํ™”(Data Persistence)์˜ ํ•ต์‹ฌ์ด์•ผ.

์ƒ๊ฐํ•ด๋ด. ๋ฉ”๋ชจ ์•ฑ์—์„œ ์—ด์‹ฌํžˆ ์ž‘์„ฑํ•œ ๋ฉ”๋ชจ๊ฐ€ ์•ฑ์„ ์ข…๋ฃŒํ•˜๋ฉด ์‚ฌ๋ผ์ง„๋‹ค๋ฉด? ๊ฒŒ์ž„์—์„œ ์—ด์‹ฌํžˆ ๋ชจ์€ ์•„์ดํ…œ์ด ๋‹ค์Œ์— ์ ‘์†ํ•˜๋ฉด ์—†์–ด์ง„๋‹ค๋ฉด?
์™„์ „ ์žฌ์•™์ด์ง€! ๐Ÿ˜ฑ ๊ทธ๋ž˜์„œ ์šฐ๋ฆฌ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์˜๊ตฌ์ ์œผ๋กœ ์ €์žฅํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์›Œ์•ผ ํ•ด.

๐Ÿ’ก ๋ฐ์ดํ„ฐ ์˜์†ํ™”๋ž€? ์•ฑ์ด ์ข…๋ฃŒ๋˜๊ฑฐ๋‚˜ ๊ธฐ๊ธฐ๊ฐ€ ์žฌ๋ถ€ํŒ…๋˜์–ด๋„ ๋ฐ์ดํ„ฐ๊ฐ€ ์‚ฌ๋ผ์ง€์ง€ ์•Š๊ณ  ๊ณ„์† ์œ ์ง€๋˜๋„๋ก ์ €์žฅํ•˜๋Š” ๊ธฐ์ˆ ์ด์•ผ.
๋ฉ”๋ชจ๋ฆฌ(RAM)์—๋งŒ ์žˆ๋Š” ๋ฐ์ดํ„ฐ๋Š” ์•ฑ์ด ์ข…๋ฃŒ๋˜๋ฉด ๋‚ ์•„๊ฐ€์ง€๋งŒ, ๋””์Šคํฌ์— ์ €์žฅํ•˜๋ฉด ์˜๊ตฌ์ ์œผ๋กœ ๋ณด๊ด€ํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿ’พ

๐Ÿ—‚๏ธ iOS์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” ๋‹ค์–‘ํ•œ ๋ฐฉ๋ฒ•๋“ค

iOS์—์„œ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•  ์ˆ˜ ์žˆ๋Š” ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ์žˆ์–ด. ๊ฐ๊ฐ์˜ ํŠน์ง•์„ ์‚ดํŽด๋ณผ๊นŒ? ๐Ÿ”

์ €์žฅ ๋ฐฉ๋ฒ• ํŠน์ง• ์ ํ•ฉํ•œ ์šฉ๋„
UserDefaults ๊ฐ„๋‹จํ•œ ํ‚ค-๊ฐ’ ์ €์žฅ์†Œ ์„ค์ •๊ฐ’, ๊ฐ„๋‹จํ•œ ๋ฐ์ดํ„ฐ
ํŒŒ์ผ ์‹œ์Šคํ…œ ์ง์ ‘ ํŒŒ์ผ๋กœ ์ €์žฅ ์ด๋ฏธ์ง€, ๋ฌธ์„œ, JSON
Core Data Apple์˜ ๊ฐ์ฒด ๊ทธ๋ž˜ํ”„ ๊ด€๋ฆฌ ํ”„๋ ˆ์ž„์›Œํฌ ๋ณต์žกํ•œ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ, ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ
SQLite ๊ฒฝ๋Ÿ‰ ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ตฌ์กฐํ™”๋œ ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ
Realm ๋ชจ๋ฐ”์ผ ํŠนํ™” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋น ๋ฅธ ์„ฑ๋Šฅ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ

์˜ค๋Š˜์€ ์ด ์ค‘์—์„œ ๊ฐ€์žฅ ๊ฐ•๋ ฅํ•˜๊ณ  ๋งŽ์ด ์‚ฌ์šฉ๋˜๋Š” Core Data์™€ SQLite์— ์ง‘์ค‘ํ•ด๋ณผ๊ฒŒ! ๐ŸŽฏ

Core Data ๊ฐ์ฒด ๊ทธ๋ž˜ํ”„ SQLite ๊ด€๊ณ„ํ˜• DB VS ๋†’์€ ์ถ”์ƒํ™” ์ง์ ‘ ์ œ์–ด Swift ์นœํ™”์  SQL ์ฟผ๋ฆฌ

๐ŸŒŸ Core Data: Apple์˜ ๊ฐ•๋ ฅํ•œ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ ํ”„๋ ˆ์ž„์›Œํฌ

Core Data๋Š” ๋‹จ์ˆœํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ์•„๋‹ˆ์•ผ. Apple์ด ๋งŒ๋“  ๊ฐ์ฒด ๊ทธ๋ž˜ํ”„ ๊ด€๋ฆฌ ํ”„๋ ˆ์ž„์›Œํฌ๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ์–ด.
์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด, Swift ๊ฐ์ฒด๋ฅผ ๊ทธ๋Œ€๋กœ ์ €์žฅํ•˜๊ณ  ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๋งˆ๋ฒ• ๊ฐ™์€ ๋„๊ตฌ์ง€! โœจ

๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด Core Data๋ฅผ ์–ด๋ ค์›Œํ•˜๋Š”๋ฐ, ์‚ฌ์‹ค ๊ฐœ๋…๋งŒ ์ œ๋Œ€๋กœ ์ดํ•ดํ•˜๋ฉด ์ •๋ง ํŽธ๋ฆฌํ•œ ์นœ๊ตฌ์•ผ.

๐Ÿ“š Core Data์˜ ํ•ต์‹ฌ ๊ฐœ๋…๋“ค

1๏ธโƒฃ Managed Object Model (.xcdatamodeld) ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ์ •์˜ํ•˜๋Š” ์ฒญ์‚ฌ์ง„์ด์•ผ. ์—‘์…€๋กœ ์น˜๋ฉด ํ…Œ์ด๋ธ” ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ๊ณผ ๋น„์Šทํ•ด.
Entity(์—”ํ‹ฐํ‹ฐ)๋ฅผ ๋งŒ๋“ค๊ณ , ๊ฐ Entity์— Attribute(์†์„ฑ)์™€ Relationship(๊ด€๊ณ„)์„ ์ •์˜ํ•˜์ง€.

์˜ˆ๋ฅผ ๋“ค์–ด 'Person' Entity๋ฅผ ๋งŒ๋“ค๊ณ , name(๋ฌธ์ž์—ด), age(์ •์ˆ˜), birthDate(๋‚ ์งœ) ๊ฐ™์€ ์†์„ฑ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฑฐ์•ผ.
2๏ธโƒฃ Managed Object Context ์‹ค์ œ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃจ๋Š” ์ž‘์—… ๊ณต๊ฐ„์ด์•ผ. ๋ฉ”๋ชจ์žฅ์„ ๋– ์˜ฌ๋ ค๋ด.
๋ฉ”๋ชจ์žฅ์—์„œ ๊ธ€์„ ์“ฐ๊ณ  ์ˆ˜์ •ํ•˜๋‹ค๊ฐ€, ์ €์žฅ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ์•ผ ํŒŒ์ผ๋กœ ์ €์žฅ๋˜์ž–์•„?
Context๋„ ๋งˆ์ฐฌ๊ฐ€์ง€์•ผ. ์—ฌ๊ธฐ์„œ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์ˆ˜์ •ํ•˜๋‹ค๊ฐ€, save()๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ์‹ค์ œ๋กœ ์ €์žฅ๋ผ! ๐Ÿ’พ
3๏ธโƒฃ Persistent Store Coordinator ์‹ค์ œ ์ €์žฅ์†Œ์™€ Context๋ฅผ ์—ฐ๊ฒฐํ•ด์ฃผ๋Š” ์ค‘๊ฐ„ ๊ด€๋ฆฌ์ž์•ผ.
SQLite ํŒŒ์ผ, XML, ๋ฐ”์ด๋„ˆ๋ฆฌ ๋“ฑ ๋‹ค์–‘ํ•œ ํ˜•ํƒœ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•  ์ˆ˜ ์žˆ๊ฒŒ ์กฐ์œจํ•ด์ค˜.
4๏ธโƒฃ Persistent Container iOS 10๋ถ€ํ„ฐ ๋“ฑ์žฅํ•œ ํŽธ๋ฆฌํ•œ ๋ž˜ํผ์•ผ. ์œ„์˜ ๋ณต์žกํ•œ ๊ตฌ์„ฑ ์š”์†Œ๋“ค์„ ํ•˜๋‚˜๋กœ ๋ฌถ์–ด์„œ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค˜.
์š”์ฆ˜์€ ๋Œ€๋ถ€๋ถ„ ์ด๊ฑธ ์‚ฌ์šฉํ•ด! ๐ŸŽ

๐Ÿ› ๏ธ Core Data ์‹œ์ž‘ํ•˜๊ธฐ

Step 1: ํ”„๋กœ์ ํŠธ์— Core Data ์ถ”๊ฐ€ํ•˜๊ธฐ

Xcode์—์„œ ์ƒˆ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค ๋•Œ "Use Core Data" ์ฒดํฌ๋ฐ•์Šค๋ฅผ ์„ ํƒํ•˜๋ฉด ์ž๋™์œผ๋กœ ์„ค์ •๋ผ.
์ด๋ฏธ ๋งŒ๋“  ํ”„๋กœ์ ํŠธ๋ผ๋ฉด? ๊ฑฑ์ • ๋งˆ! ์ง์ ‘ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด.

๋จผ์ € Data Model ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด๋ณด์ž. File โ†’ New โ†’ File โ†’ Data Model์„ ์„ ํƒํ•˜๋ฉด .xcdatamodeld ํŒŒ์ผ์ด ์ƒ์„ฑ๋ผ.
๋ณดํ†ต ํ”„๋กœ์ ํŠธ ์ด๋ฆ„๊ณผ ๊ฐ™์€ ์ด๋ฆ„์œผ๋กœ ๋งŒ๋“ค์–ด. ์˜ˆ๋ฅผ ๋“ค์–ด 'MyApp.xcdatamodeld' ์ด๋Ÿฐ ์‹์œผ๋กœ!

import CoreData

class CoreDataManager {
    static let shared = CoreDataManager()
    
    private init() {}
    
    // Persistent Container ์ƒ์„ฑ
    lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "MyApp")
        container.loadPersistentStores { description, error in
            if let error = error {
                fatalError("Core Data ๋กœ๋“œ ์‹คํŒจ: \(error)")
            }
        }
        return container
    }()
    
    // Context ์ ‘๊ทผ
    var context: NSManagedObjectContext {
        return persistentContainer.viewContext
    }
    
    // ์ €์žฅ ๋ฉ”์„œ๋“œ
    func saveContext() {
        let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
                print("โœ… ๋ฐ์ดํ„ฐ ์ €์žฅ ์„ฑ๊ณต!")
            } catch {
                print("โŒ ์ €์žฅ ์‹คํŒจ: \(error)")
            }
        }
    }
}

์œ„ ์ฝ”๋“œ๋Š” Core Data๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ๊ธฐ๋ณธ ๋งค๋‹ˆ์ € ํด๋ž˜์Šค์•ผ. ์‹ฑ๊ธ€ํ†ค ํŒจํ„ด์œผ๋กœ ๋งŒ๋“ค์–ด์„œ ์•ฑ ์ „์ฒด์—์„œ ํ•˜๋‚˜์˜ ์ธ์Šคํ„ด์Šค๋งŒ ์‚ฌ์šฉํ•˜๋„๋ก ํ–ˆ์–ด. ๐ŸŽฏ

Step 2: Entity ์ •์˜ํ•˜๊ธฐ

.xcdatamodeld ํŒŒ์ผ์„ ์—ด๊ณ  "Add Entity" ๋ฒ„ํŠผ์„ ํด๋ฆญํ•ด์„œ Entity๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž.

์˜ˆ๋ฅผ ๋“ค์–ด ๊ฐ„๋‹จํ•œ ๋ฉ”๋ชจ ์•ฑ์„ ๋งŒ๋“ ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด๋ณผ๊ฒŒ. 'Memo'๋ผ๋Š” Entity๋ฅผ ๋งŒ๋“ค๊ณ  ๋‹ค์Œ ์†์„ฑ๋“ค์„ ์ถ”๊ฐ€ํ•ด๋ณด์ž:

์†์„ฑ ์ด๋ฆ„ ํƒ€์ž… ์„ค๋ช…
id UUID ๊ณ ์œ  ์‹๋ณ„์ž
title String ๋ฉ”๋ชจ ์ œ๋ชฉ
content String ๋ฉ”๋ชจ ๋‚ด์šฉ
createdAt Date ์ƒ์„ฑ ๋‚ ์งœ
isImportant Boolean ์ค‘์š” ํ‘œ์‹œ
๐Ÿ’ก Pro Tip: Entity ์ด๋ฆ„์€ ๋‹จ์ˆ˜ํ˜•์œผ๋กœ ์ง“๋Š” ๊ฒŒ ๊ด€๋ก€์•ผ. 'Memos'๊ฐ€ ์•„๋‹ˆ๋ผ 'Memo'๋กœ!
๊ทธ๋ฆฌ๊ณ  Codegen์„ "Class Definition"์œผ๋กœ ์„ค์ •ํ•˜๋ฉด Xcode๊ฐ€ ์ž๋™์œผ๋กœ ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•ด์ค˜์„œ ํŽธ๋ฆฌํ•ด. ๐Ÿ˜Š
Step 3: ๋ฐ์ดํ„ฐ ์ƒ์„ฑํ•˜๊ธฐ (Create)

์ด์ œ ์‹ค์ œ๋กœ ๋ฉ”๋ชจ๋ฅผ ์ €์žฅํ•ด๋ณด์ž!
func createMemo(title: String, content: String) {
    let context = CoreDataManager.shared.context
    
    // ์ƒˆ๋กœ์šด Memo ๊ฐ์ฒด ์ƒ์„ฑ
    let memo = Memo(context: context)
    memo.id = UUID()
    memo.title = title
    memo.content = content
    memo.createdAt = Date()
    memo.isImportant = false
    
    // ์ €์žฅ
    CoreDataManager.shared.saveContext()
    print("๐Ÿ“ ๋ฉ”๋ชจ ์ƒ์„ฑ ์™„๋ฃŒ: \(title)")
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
createMemo(title: "์žฅ๋ณด๊ธฐ", content: "์šฐ์œ , ๊ณ„๋ž€, ๋นต ์‚ฌ๊ธฐ")

๋ณด๋‹ค์‹œํ”ผ ์ผ๋ฐ˜ Swift ๊ฐ์ฒด๋ฅผ ๋‹ค๋ฃจ๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด! ์ด๊ฒŒ Core Data์˜ ํฐ ์žฅ์ ์ด์•ผ. ๐ŸŒŸ

Step 4: ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ (Read)

์ €์žฅํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋ ค๋ฉด NSFetchRequest๋ฅผ ์‚ฌ์šฉํ•ด.
func fetchAllMemos() -> [Memo] {
    let context = CoreDataManager.shared.context
    let fetchRequest: NSFetchRequest<Memo> = Memo.fetchRequest()
    
    // ์ •๋ ฌ ๊ธฐ์ค€ ์ถ”๊ฐ€ (์ตœ์‹ ์ˆœ)
    let sortDescriptor = NSSortDescriptor(key: "createdAt", ascending: false)
    fetchRequest.sortDescriptors = [sortDescriptor]
    
    do {
        let memos = try context.fetch(fetchRequest)
        print("๐Ÿ“š ์ด \(memos.count)๊ฐœ์˜ ๋ฉ”๋ชจ๋ฅผ ๋ถˆ๋Ÿฌ์™”์–ด์š”")
        return memos
    } catch {
        print("โŒ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์‹คํŒจ: \(error)")
        return []
    }
}

// ํŠน์ • ์กฐ๊ฑด์œผ๋กœ ๊ฒ€์ƒ‰
func fetchImportantMemos() -> [Memo] {
    let context = CoreDataManager.shared.context
    let fetchRequest: NSFetchRequest<Memo> = Memo.fetchRequest()
    
    // ์กฐ๊ฑด ์ถ”๊ฐ€ (์ค‘์š” ํ‘œ์‹œ๋œ ๋ฉ”๋ชจ๋งŒ)
    fetchRequest.predicate = NSPredicate(format: "isImportant == %@", NSNumber(value: true))
    
    do {
        return try context.fetch(fetchRequest)
    } catch {
        print("โŒ ๊ฒ€์ƒ‰ ์‹คํŒจ: \(error)")
        return []
    }
}
๐Ÿ” NSPredicate ํ™œ์šฉํ•˜๊ธฐ NSPredicate๋Š” SQL์˜ WHERE ์ ˆ๊ณผ ๋น„์Šทํ•œ ์—ญํ• ์„ ํ•ด. ๋‹ค์–‘ํ•œ ์กฐ๊ฑด์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ํ•„ํ„ฐ๋งํ•  ์ˆ˜ ์žˆ์–ด!

์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ํŒจํ„ด๋“ค:
โ€ข format: "title CONTAINS[cd] %@", searchText - ์ œ๋ชฉ์— ํŠน์ • ํ…์ŠคํŠธ ํฌํ•จ (๋Œ€์†Œ๋ฌธ์ž ๋ฌด์‹œ)
โ€ข format: "createdAt >= %@", startDate - ํŠน์ • ๋‚ ์งœ ์ดํ›„
โ€ข format: "age > 20 AND age < 30" - ๋ณตํ•ฉ ์กฐ๊ฑด
โ€ข format: "title IN %@", ["๋ฉ”๋ชจ1", "๋ฉ”๋ชจ2"] - ์—ฌ๋Ÿฌ ๊ฐ’ ์ค‘ ํ•˜๋‚˜
Step 5: ๋ฐ์ดํ„ฐ ์ˆ˜์ •ํ•˜๊ธฐ (Update)

๊ธฐ์กด ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ •ํ•˜๋Š” ๊ฑด ์ •๋ง ๊ฐ„๋‹จํ•ด!
func updateMemo(memo: Memo, newTitle: String, newContent: String) {
    memo.title = newTitle
    memo.content = newContent
    
    // ์ €์žฅ๋งŒ ํ•˜๋ฉด ๋!
    CoreDataManager.shared.saveContext()
    print("โœ๏ธ ๋ฉ”๋ชจ ์ˆ˜์ • ์™„๋ฃŒ")
}

// ์ค‘์š” ํ‘œ์‹œ ํ† ๊ธ€
func toggleImportant(memo: Memo) {
    memo.isImportant.toggle()
    CoreDataManager.shared.saveContext()
}

๊ฐ์ฒด์˜ ์†์„ฑ์„ ๋ฐ”๊พธ๊ณ  save()๋งŒ ํ˜ธ์ถœํ•˜๋ฉด ๋ผ. ์ •๋ง ์ง๊ด€์ ์ด์ง€? ๐Ÿ˜„

Step 6: ๋ฐ์ดํ„ฐ ์‚ญ์ œํ•˜๊ธฐ (Delete)

์‚ญ์ œ๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๊ฐ„๋‹จํ•ด.
func deleteMemo(memo: Memo) {
    let context = CoreDataManager.shared.context
    context.delete(memo)
    CoreDataManager.shared.saveContext()
    print("๐Ÿ—‘๏ธ ๋ฉ”๋ชจ ์‚ญ์ œ ์™„๋ฃŒ")
}

// ๋ชจ๋“  ๋ฉ”๋ชจ ์‚ญ์ œ
func deleteAllMemos() {
    let context = CoreDataManager.shared.context
    let fetchRequest: NSFetchRequest<NSFetchRequestResult> = Memo.fetchRequest()
    let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
    
    do {
        try context.execute(deleteRequest)
        CoreDataManager.shared.saveContext()
        print("๐Ÿ—‘๏ธ ๋ชจ๋“  ๋ฉ”๋ชจ ์‚ญ์ œ ์™„๋ฃŒ")
    } catch {
        print("โŒ ์‚ญ์ œ ์‹คํŒจ: \(error)")
    }
}

๐Ÿ”— ๊ด€๊ณ„(Relationship) ์„ค์ •ํ•˜๊ธฐ

Core Data์˜ ์ง„์งœ ๊ฐ•๋ ฅํ•จ์€ Entity ๊ฐ„์˜ ๊ด€๊ณ„๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฑฐ์•ผ!
์˜ˆ๋ฅผ ๋“ค์–ด 'Category'์™€ 'Memo'์˜ ๊ด€๊ณ„๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊นŒ? ๐Ÿ“‚

์‹œ๋‚˜๋ฆฌ์˜ค: โ€ข ํ•˜๋‚˜์˜ Category๋Š” ์—ฌ๋Ÿฌ Memo๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์–ด (One-to-Many)
โ€ข ํ•˜๋‚˜์˜ Memo๋Š” ํ•˜๋‚˜์˜ Category์— ์†ํ•ด (Many-to-One)

.xcdatamodeld์—์„œ 'Category' Entity๋ฅผ ๋งŒ๋“ค๊ณ :
โ€ข name (String)
โ€ข color (String)
โ€ข memos (Relationship โ†’ Memo, Type: To Many)

๊ทธ๋ฆฌ๊ณ  'Memo' Entity์—:
โ€ข category (Relationship โ†’ Category, Type: To One)

func createCategory(name: String, color: String) -> Category {
    let context = CoreDataManager.shared.context
    let category = Category(context: context)
    category.name = name
    category.color = color
    CoreDataManager.shared.saveContext()
    return category
}

func createMemoWithCategory(title: String, content: String, category: Category) {
    let context = CoreDataManager.shared.context
    let memo = Memo(context: context)
    memo.id = UUID()
    memo.title = title
    memo.content = content
    memo.createdAt = Date()
    memo.category = category  // ๊ด€๊ณ„ ์„ค์ •!
    
    CoreDataManager.shared.saveContext()
}

// ํŠน์ • ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋ชจ๋“  ๋ฉ”๋ชจ ๊ฐ€์ ธ์˜ค๊ธฐ
func fetchMemos(in category: Category) -> [Memo] {
    // Category ๊ฐ์ฒด์˜ memos ์†์„ฑ์œผ๋กœ ๋ฐ”๋กœ ์ ‘๊ทผ ๊ฐ€๋Šฅ!
    return category.memos?.allObjects as? [Memo] ?? []
}

๊ด€๊ณ„๋ฅผ ์„ค์ •ํ•˜๋ฉด ์–‘๋ฐฉํ–ฅ์œผ๋กœ ์‰ฝ๊ฒŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์–ด. Category์—์„œ Memo๋กœ, Memo์—์„œ Category๋กœ! ๐Ÿ”„

โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ: Delete Rule ๊ด€๊ณ„๋ฅผ ์„ค์ •ํ•  ๋•Œ Delete Rule์„ ๊ผญ ํ™•์ธํ•ด์•ผ ํ•ด!
โ€ข Nullify: ๋ถ€๋ชจ๊ฐ€ ์‚ญ์ œ๋˜๋ฉด ์ž์‹์˜ ๊ด€๊ณ„๋งŒ null๋กœ (์ž์‹์€ ์œ ์ง€)
โ€ข Cascade: ๋ถ€๋ชจ๊ฐ€ ์‚ญ์ œ๋˜๋ฉด ์ž์‹๋„ ํ•จ๊ป˜ ์‚ญ์ œ
โ€ข Deny: ์ž์‹์ด ์žˆ์œผ๋ฉด ๋ถ€๋ชจ ์‚ญ์ œ ๋ถˆ๊ฐ€
โ€ข No Action: ์•„๋ฌด ์ฒ˜๋ฆฌ ์•ˆ ํ•จ (์œ„ํ—˜ํ•  ์ˆ˜ ์žˆ์–ด!)

๋ณดํ†ต Category-Memo ๊ด€๊ณ„์—์„œ๋Š” Cascade๋ฅผ ๋งŽ์ด ์‚ฌ์šฉํ•ด. ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ์ง€์šฐ๋ฉด ๊ทธ ์•ˆ์˜ ๋ฉ”๋ชจ๋„ ํ•จ๊ป˜ ์‚ญ์ œ๋˜๋„๋ก!

โšก Core Data ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ

1. Batch ์ž‘์—… ํ™œ์šฉํ•˜๊ธฐ ๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•  ๋•Œ๋Š” NSBatchInsertRequest, NSBatchUpdateRequest๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ›จ์”ฌ ๋นจ๋ผ!
func batchInsertMemos(count: Int) {
    let context = CoreDataManager.shared.context
    
    context.performAndWait {
        for i in 0..<count {
            let memo = Memo(context: context)
            memo.id = UUID()
            memo.title = "๋ฉ”๋ชจ \(i)"
            memo.content = "๋‚ด์šฉ \(i)"
            memo.createdAt = Date()
            
            // 100๊ฐœ๋งˆ๋‹ค ์ €์žฅ
            if i % 100 == 0 {
                try? context.save()
                context.reset() // ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ
            }
        }
        try? context.save()
    }
}
2. Fetch Request ์ตœ์ ํ™” ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ ๊ฐ€์ ธ์˜ค๊ธฐ!
func fetchOptimized() -> [Memo] {
    let fetchRequest: NSFetchRequest<Memo> = Memo.fetchRequest()
    
    // ๋ฐฐ์น˜ ํฌ๊ธฐ ์„ค์ • (ํ•œ ๋ฒˆ์— 20๊ฐœ์”ฉ)
    fetchRequest.fetchBatchSize = 20
    
    // ํ•„์š”ํ•œ ์†์„ฑ๋งŒ ๊ฐ€์ ธ์˜ค๊ธฐ
    fetchRequest.propertiesToFetch = ["title", "createdAt"]
    
    // ๊ฒฐ๊ณผ ๊ฐœ์ˆ˜ ์ œํ•œ
    fetchRequest.fetchLimit = 100
    
    return try! CoreDataManager.shared.context.fetch(fetchRequest)
}
3. Background Context ์‚ฌ์šฉํ•˜๊ธฐ ๋ฌด๊ฑฐ์šด ์ž‘์—…์€ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ!
func performBackgroundTask() {
    let container = CoreDataManager.shared.persistentContainer
    
    container.performBackgroundTask { context in
        // ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์‹คํ–‰๋˜๋Š” ์ž‘์—…
        for i in 0..<1000 {
            let memo = Memo(context: context)
            memo.title = "Background Memo \(i)"
            memo.createdAt = Date()
        }
        
        try? context.save()
        
        DispatchQueue.main.async {
            print("โœ… ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… ์™„๋ฃŒ!")
        }
    }
}
๐Ÿ’ก ์žฌ๋Šฅ๋„ท ํ™œ์šฉ ํŒ Core Data๊ฐ€ ์ฒ˜์Œ์ด๋ผ ์–ด๋ ต๊ฒŒ ๋А๊ปด์ง„๋‹ค๋ฉด, ์žฌ๋Šฅ๋„ท์—์„œ iOS ๊ฐœ๋ฐœ ์ „๋ฌธ๊ฐ€์—๊ฒŒ 1:1 ์ฝ”์นญ์„ ๋ฐ›์•„๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ!
์‹ค๋ฌด ๊ฒฝํ—˜์ด ํ’๋ถ€ํ•œ ๊ฐœ๋ฐœ์ž๋“ค์ด ํ”„๋กœ์ ํŠธ์— ๋งž๋Š” ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ์„ค๊ณ„๋ถ€ํ„ฐ ์ตœ์ ํ™”๊นŒ์ง€ ๋„์™€์ค„ ์ˆ˜ ์žˆ์–ด. ๐ŸŽ“

๐Ÿ—„๏ธ SQLite: ์ง์ ‘ ์ œ์–ดํ•˜๋Š” ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค

์ด์ œ SQLite์— ๋Œ€ํ•ด ์•Œ์•„๋ณผ ์ฐจ๋ก€์•ผ! SQLite๋Š” ์„ธ๊ณ„์—์„œ ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉ๋˜๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—”์ง„์ด์•ผ.
์Šค๋งˆํŠธํฐ, ๋ธŒ๋ผ์šฐ์ €, ์ž„๋ฒ ๋””๋“œ ์‹œ์Šคํ…œ ๋“ฑ ์ •๋ง ๋งŽ์€ ๊ณณ์—์„œ ์‚ฌ์šฉ๋˜๊ณ  ์žˆ์–ด. ๐Ÿ“ฑ๐Ÿ’ป

Core Data์™€ ๋‹ฌ๋ฆฌ SQLite๋Š” ์ง์ ‘ SQL ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•ด์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ค„.
๋” ๋ณต์žกํ•˜์ง€๋งŒ, ๊ทธ๋งŒํผ ์„ธ๋ฐ€ํ•œ ์ œ์–ด๊ฐ€ ๊ฐ€๋Šฅํ•˜๊ณ  ์„ฑ๋Šฅ ์ตœ์ ํ™”๋„ ์ž์œ ๋กญ๊ฒŒ ํ•  ์ˆ˜ ์žˆ์ง€!

๐ŸŽฏ SQLite์˜ ํŠน์ง•

โœ… ์žฅ์ 

โ€ข ๊ฐ€๋ณ๊ณ  ๋น ๋ฆ„ (Zero-configuration)
โ€ข ํ‘œ์ค€ SQL ์‚ฌ์šฉ
โ€ข ํฌ๋กœ์Šค ํ”Œ๋žซํผ ํ˜ธํ™˜์„ฑ
โ€ข ํŒŒ์ผ ํ•˜๋‚˜๋กœ ์ „์ฒด DB ๊ด€๋ฆฌ
โ€ข ํŠธ๋žœ์žญ์…˜ ์ง€์›
โ€ข ์„œ๋ฒ„ ๋ถˆํ•„์š” (Serverless)
โš ๏ธ ๋‹จ์ 

โ€ข SQL ์ฟผ๋ฆฌ๋ฅผ ์ง์ ‘ ์ž‘์„ฑํ•ด์•ผ ํ•จ
โ€ข ํƒ€์ž… ์•ˆ์ •์„ฑ์ด ์•ฝํ•จ
โ€ข ๊ฐ์ฒด ๋งคํ•‘์„ ์ง์ ‘ ๊ตฌํ˜„
โ€ข ๋™์‹œ ์“ฐ๊ธฐ ์ œํ•œ์ 
โ€ข ๋ณต์žกํ•œ ๊ด€๊ณ„ ์ฒ˜๋ฆฌ๊ฐ€ ๋ฒˆ๊ฑฐ๋กœ์›€

๐Ÿš€ iOS์—์„œ SQLite ์‚ฌ์šฉํ•˜๊ธฐ

iOS์—๋Š” SQLite๊ฐ€ ๊ธฐ๋ณธ์œผ๋กœ ๋‚ด์žฅ๋˜์–ด ์žˆ์–ด! ๋ณ„๋„ ์„ค์น˜ ์—†์ด ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด. ๐Ÿ˜Š
ํ•˜์ง€๋งŒ C API๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•˜๋Š” ๊ฑด ์ข€ ๋ณต์žกํ•˜๋‹ˆ๊นŒ, ๋ณดํ†ต ๋ž˜ํผ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด.

๋Œ€ํ‘œ์ ์ธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค:

๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํŠน์ง• ๋‚œ์ด๋„
FMDB Objective-C ๊ธฐ๋ฐ˜, ์•ˆ์ •์  ์ค‘๊ฐ„
SQLite.swift Swift ๋„ค์ดํ‹ฐ๋ธŒ, ํƒ€์ž… ์„ธ์ดํ”„ ์‰ฌ์›€
GRDB ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ, ๋†’์€ ์„ฑ๋Šฅ ์–ด๋ ค์›€

์˜ค๋Š˜์€ ๊ฐ€์žฅ Swift๋‹ต๊ณ  ์‚ฌ์šฉํ•˜๊ธฐ ์‰ฌ์šด SQLite.swift๋กœ ์˜ˆ์ œ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ! ๐ŸŽฏ

Step 1: SQLite.swift ์„ค์น˜ํ•˜๊ธฐ

Swift Package Manager๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ ์„ค์น˜ํ•  ์ˆ˜ ์žˆ์–ด.

Xcode์—์„œ File โ†’ Add Packages... โ†’ ๊ฒ€์ƒ‰์ฐฝ์— ์ž…๋ ฅ:
https://github.com/stephencelis/SQLite.swift

๋˜๋Š” CocoaPods๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด:

pod 'SQLite.swift', '~> 0.14.1'
Step 2: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งค๋‹ˆ์ € ๋งŒ๋“ค๊ธฐ

SQLite ์—ฐ๊ฒฐ์„ ๊ด€๋ฆฌํ•˜๋Š” ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž.
import SQLite

class SQLiteManager {
    static let shared = SQLiteManager()
    private var db: Connection?
    
    // ํ…Œ์ด๋ธ” ์ •์˜
    let memos = Table("memos")
    let id = Expression<Int64>("id")
    let title = Expression<String>("title")
    let content = Expression<String>("content")
    let createdAt = Expression<Date>("created_at")
    let isImportant = Expression<Bool>("is_important")
    
    private init() {
        do {
            let path = NSSearchPathForDirectoriesInDomains(
                .documentDirectory, .userDomainMask, true
            ).first!
            
            let dbPath = "\(path)/myapp.sqlite3"
            db = try Connection(dbPath)
            print("โœ… SQLite ์—ฐ๊ฒฐ ์„ฑ๊ณต: \(dbPath)")
            
            createTable()
        } catch {
            print("โŒ SQLite ์—ฐ๊ฒฐ ์‹คํŒจ: \(error)")
        }
    }
    
    // ํ…Œ์ด๋ธ” ์ƒ์„ฑ
    private func createTable() {
        do {
            try db?.run(memos.create(ifNotExists: true) { table in
                table.column(id, primaryKey: .autoincrement)
                table.column(title)
                table.column(content)
                table.column(createdAt)
                table.column(isImportant, defaultValue: false)
            })
            print("โœ… ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์™„๋ฃŒ")
        } catch {
            print("โŒ ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์‹คํŒจ: \(error)")
        }
    }
}

์œ„ ์ฝ”๋“œ์—์„œ ์ฃผ๋ชฉํ•  ์ ์€ Expression์„ ์‚ฌ์šฉํ•ด์„œ ํƒ€์ž… ์•ˆ์ •์„ฑ์„ ํ™•๋ณดํ–ˆ๋‹ค๋Š” ๊ฑฐ์•ผ!
์ผ๋ฐ˜ SQL ๋ฌธ์ž์—ด๋ณด๋‹ค ํ›จ์”ฌ ์•ˆ์ „ํ•˜๊ณ  Swift์Šค๋Ÿฌ์šด ๋ฐฉ์‹์ด์ง€. ๐Ÿ›ก๏ธ

Step 3: CRUD ์ž‘์—… ๊ตฌํ˜„ํ•˜๊ธฐ

์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ค„๋ณด์ž!

๐Ÿ“ Create - ๋ฐ์ดํ„ฐ ์‚ฝ์ž…

extension SQLiteManager {
    func insertMemo(title: String, content: String, isImportant: Bool = false) -> Int64? {
        do {
            let insert = memos.insert(
                self.title <- title,
                self.content <- content,
                self.createdAt <- Date(),
                self.isImportant <- isImportant
            )
            
            let rowId = try db?.run(insert)
            print("โœ… ๋ฉ”๋ชจ ์ถ”๊ฐ€ ์„ฑ๊ณต! ID: \(rowId ?? 0)")
            return rowId
        } catch {
            print("โŒ ๋ฉ”๋ชจ ์ถ”๊ฐ€ ์‹คํŒจ: \(error)")
            return nil
        }
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
let memoId = SQLiteManager.shared.insertMemo(
    title: "SQLite ๊ณต๋ถ€ํ•˜๊ธฐ",
    content: "์˜ค๋Š˜์€ SQLite๋ฅผ ๋ฐฐ์›Œ๋ณด์ž!",
    isImportant: true
)

๐Ÿ“– Read - ๋ฐ์ดํ„ฐ ์กฐํšŒ

// ๋ฉ”๋ชจ ๊ตฌ์กฐ์ฒด ์ •์˜
struct MemoModel {
    let id: Int64
    let title: String
    let content: String
    let createdAt: Date
    let isImportant: Bool
}

extension SQLiteManager {
    // ๋ชจ๋“  ๋ฉ”๋ชจ ์กฐํšŒ
    func fetchAllMemos() -> [MemoModel] {
        var result: [MemoModel] = []
        
        do {
            let query = memos.order(createdAt.desc)
            
            for memo in try db!.prepare(query) {
                let model = MemoModel(
                    id: memo[id],
                    title: memo[title],
                    content: memo[content],
                    createdAt: memo[createdAt],
                    isImportant: memo[isImportant]
                )
                result.append(model)
            }
            
            print("๐Ÿ“š ์ด \(result.count)๊ฐœ์˜ ๋ฉ”๋ชจ๋ฅผ ๋ถˆ๋Ÿฌ์™”์–ด์š”")
        } catch {
            print("โŒ ์กฐํšŒ ์‹คํŒจ: \(error)")
        }
        
        return result
    }
    
    // ํŠน์ • ๋ฉ”๋ชจ ์กฐํšŒ
    func fetchMemo(byId memoId: Int64) -> MemoModel? {
        do {
            let query = memos.filter(id == memoId)
            
            if let memo = try db?.pluck(query) {
                return MemoModel(
                    id: memo[id],
                    title: memo[title],
                    content: memo[content],
                    createdAt: memo[createdAt],
                    isImportant: memo[isImportant]
                )
            }
        } catch {
            print("โŒ ์กฐํšŒ ์‹คํŒจ: \(error)")
        }
        
        return nil
    }
    
    // ์ค‘์š” ๋ฉ”๋ชจ๋งŒ ์กฐํšŒ
    func fetchImportantMemos() -> [MemoModel] {
        var result: [MemoModel] = []
        
        do {
            let query = memos
                .filter(isImportant == true)
                .order(createdAt.desc)
            
            for memo in try db!.prepare(query) {
                let model = MemoModel(
                    id: memo[id],
                    title: memo[title],
                    content: memo[content],
                    createdAt: memo[createdAt],
                    isImportant: memo[isImportant]
                )
                result.append(model)
            }
        } catch {
            print("โŒ ์กฐํšŒ ์‹คํŒจ: \(error)")
        }
        
        return result
    }
    
    // ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ
    func searchMemos(keyword: String) -> [MemoModel] {
        var result: [MemoModel] = []
        
        do {
            let query = memos.filter(
                title.like("%\(keyword)%") || content.like("%\(keyword)%")
            )
            
            for memo in try db!.prepare(query) {
                let model = MemoModel(
                    id: memo[id],
                    title: memo[title],
                    content: memo[content],
                    createdAt: memo[createdAt],
                    isImportant: memo[isImportant]
                )
                result.append(model)
            }
            
            print("๐Ÿ” '\(keyword)' ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ: \(result.count)๊ฐœ")
        } catch {
            print("โŒ ๊ฒ€์ƒ‰ ์‹คํŒจ: \(error)")
        }
        
        return result
    }
}

SQLite.swift์˜ ์ฟผ๋ฆฌ ๋นŒ๋”๋Š” ์ •๋ง ์ง๊ด€์ ์ด์•ผ! filter, order, limit ๊ฐ™์€ ๋ฉ”์„œ๋“œ๋ฅผ ์ฒด์ด๋‹ํ•ด์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด. ๐Ÿ”—

โœ๏ธ Update - ๋ฐ์ดํ„ฐ ์ˆ˜์ •

extension SQLiteManager {
    func updateMemo(id memoId: Int64, title: String?, content: String?, isImportant: Bool?) -> Bool {
        do {
            let memo = memos.filter(id == memoId)
            var setters: [Setter] = []
            
            if let title = title {
                setters.append(self.title <- title)
            }
            if let content = content {
                setters.append(self.content <- content)
            }
            if let isImportant = isImportant {
                setters.append(self.isImportant <- isImportant)
            }
            
            let updated = try db?.run(memo.update(setters))
            
            if updated ?? 0 > 0 {
                print("โœ… ๋ฉ”๋ชจ ์ˆ˜์ • ์„ฑ๊ณต!")
                return true
            } else {
                print("โš ๏ธ ์ˆ˜์ •ํ•  ๋ฉ”๋ชจ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์–ด์š”")
                return false
            }
        } catch {
            print("โŒ ์ˆ˜์ • ์‹คํŒจ: \(error)")
            return false
        }
    }
    
    // ์ค‘์š” ํ‘œ์‹œ ํ† ๊ธ€
    func toggleImportant(id memoId: Int64) -> Bool {
        do {
            let memo = memos.filter(id == memoId)
            
            // ํ˜„์žฌ ๊ฐ’์„ ๊ฐ€์ ธ์™€์„œ ๋ฐ˜๋Œ€๋กœ ์„ค์ •
            if let current = try db?.pluck(memo) {
                let newValue = !current[isImportant]
                try db?.run(memo.update(isImportant <- newValue))
                print("โœ… ์ค‘์š” ํ‘œ์‹œ ํ† ๊ธ€ ์™„๋ฃŒ: \(newValue)")
                return true
            }
        } catch {
            print("โŒ ํ† ๊ธ€ ์‹คํŒจ: \(error)")
        }
        
        return false
    }
}

๐Ÿ—‘๏ธ Delete - ๋ฐ์ดํ„ฐ ์‚ญ์ œ

extension SQLiteManager {
    func deleteMemo(id memoId: Int64) -> Bool {
        do {
            let memo = memos.filter(id == memoId)
            let deleted = try db?.run(memo.delete())
            
            if deleted ?? 0 > 0 {
                print("โœ… ๋ฉ”๋ชจ ์‚ญ์ œ ์„ฑ๊ณต!")
                return true
            } else {
                print("โš ๏ธ ์‚ญ์ œํ•  ๋ฉ”๋ชจ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์–ด์š”")
                return false
            }
        } catch {
            print("โŒ ์‚ญ์ œ ์‹คํŒจ: \(error)")
            return false
        }
    }
    
    // ๋ชจ๋“  ๋ฉ”๋ชจ ์‚ญ์ œ
    func deleteAllMemos() -> Bool {
        do {
            try db?.run(memos.delete())
            print("โœ… ๋ชจ๋“  ๋ฉ”๋ชจ ์‚ญ์ œ ์™„๋ฃŒ!")
            return true
        } catch {
            print("โŒ ์ „์ฒด ์‚ญ์ œ ์‹คํŒจ: \(error)")
            return false
        }
    }
    
    // ์˜ค๋ž˜๋œ ๋ฉ”๋ชจ ์‚ญ์ œ (30์ผ ์ด์ƒ)
    func deleteOldMemos(days: Int = 30) -> Bool {
        do {
            let cutoffDate = Calendar.current.date(
                byAdding: .day,
                value: -days,
                to: Date()
            )!
            
            let oldMemos = memos.filter(createdAt < cutoffDate)
            let deleted = try db?.run(oldMemos.delete())
            
            print("โœ… \(deleted ?? 0)๊ฐœ์˜ ์˜ค๋ž˜๋œ ๋ฉ”๋ชจ ์‚ญ์ œ ์™„๋ฃŒ!")
            return true
        } catch {
            print("โŒ ์‚ญ์ œ ์‹คํŒจ: \(error)")
            return false
        }
    }
}

๐Ÿ”ง ๊ณ ๊ธ‰ SQLite ๊ธฐ๋Šฅ๋“ค

๐Ÿ“Š ์ง‘๊ณ„ ํ•จ์ˆ˜ ์‚ฌ์šฉํ•˜๊ธฐ

extension SQLiteManager {
    // ์ „์ฒด ๋ฉ”๋ชจ ๊ฐœ์ˆ˜
    func getMemoCount() -> Int {
        do {
            let count = try db?.scalar(memos.count) ?? 0
            return count
        } catch {
            print("โŒ ๊ฐœ์ˆ˜ ์กฐํšŒ ์‹คํŒจ: \(error)")
            return 0
        }
    }
    
    // ์ค‘์š” ๋ฉ”๋ชจ ๊ฐœ์ˆ˜
    func getImportantMemoCount() -> Int {
        do {
            let count = try db?.scalar(memos.filter(isImportant == true).count) ?? 0
            return count
        } catch {
            return 0
        }
    }
    
    // ํ†ต๊ณ„ ์ •๋ณด
    func getStatistics() -> (total: Int, important: Int, today: Int) {
        let total = getMemoCount()
        let important = getImportantMemoCount()
        
        let today = Calendar.current.startOfDay(for: Date())
        let todayMemos = memos.filter(createdAt >= today)
        let todayCount = (try? db?.scalar(todayMemos.count)) ?? 0
        
        return (total, important, todayCount)
    }
}

๐Ÿ”„ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ

์—ฌ๋Ÿฌ ์ž‘์—…์„ ํ•˜๋‚˜์˜ ๋‹จ์œ„๋กœ ๋ฌถ์–ด์„œ ์ฒ˜๋ฆฌํ•  ๋•Œ ํŠธ๋žœ์žญ์…˜์„ ์‚ฌ์šฉํ•ด. ์ค‘๊ฐ„์— ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์ „์ฒด๋ฅผ ๋กค๋ฐฑํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿ”’

extension SQLiteManager {
    func transferMemos(fromCategory: String, toCategory: String) -> Bool {
        do {
            try db?.transaction {
                // ์—ฌ๋Ÿฌ ์ž‘์—…์„ ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜์œผ๋กœ
                let memosToUpdate = memos.filter(title.like("%\(fromCategory)%"))
                
                for memo in try db!.prepare(memosToUpdate) {
                    let newTitle = memo[title].replacingOccurrences(
                        of: fromCategory,
                        with: toCategory
                    )
                    
                    let update = memos
                        .filter(id == memo[id])
                        .update(title <- newTitle)
                    
                    try db?.run(update)
                }
            }
            
            print("โœ… ์นดํ…Œ๊ณ ๋ฆฌ ์ด๋™ ์™„๋ฃŒ!")
            return true
        } catch {
            print("โŒ ํŠธ๋žœ์žญ์…˜ ์‹คํŒจ (๋กค๋ฐฑ๋จ): \(error)")
            return false
        }
    }
}

๐Ÿ” ์ธ๋ฑ์Šค ์ƒ์„ฑ์œผ๋กœ ์„ฑ๋Šฅ ํ–ฅ์ƒ

extension SQLiteManager {
    func createIndexes() {
        do {
            // title ์ปฌ๋Ÿผ์— ์ธ๋ฑ์Šค ์ƒ์„ฑ
            try db?.run(memos.createIndex(title, ifNotExists: true))
            
            // ๋ณตํ•ฉ ์ธ๋ฑ์Šค (์—ฌ๋Ÿฌ ์ปฌ๋Ÿผ)
            try db?.run(memos.createIndex(isImportant, createdAt, ifNotExists: true))
            
            print("โœ… ์ธ๋ฑ์Šค ์ƒ์„ฑ ์™„๋ฃŒ!")
        } catch {
            print("โŒ ์ธ๋ฑ์Šค ์ƒ์„ฑ ์‹คํŒจ: \(error)")
        }
    }
}
๐Ÿ’ก ์ธ๋ฑ์Šค ์‚ฌ์šฉ ํŒ โ€ข WHERE ์ ˆ์— ์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” ์ปฌ๋Ÿผ์— ์ธ๋ฑ์Šค๋ฅผ ๋งŒ๋“ค์–ด
โ€ข ORDER BY์— ์‚ฌ์šฉ๋˜๋Š” ์ปฌ๋Ÿผ๋„ ์ธ๋ฑ์Šค ๋Œ€์ƒ
โ€ข ํ•˜์ง€๋งŒ ๋„ˆ๋ฌด ๋งŽ์€ ์ธ๋ฑ์Šค๋Š” INSERT/UPDATE ์„ฑ๋Šฅ์„ ๋–จ์–ด๋œจ๋ ค
โ€ข ์‹ค์ œ ์ฟผ๋ฆฌ ํŒจํ„ด์„ ๋ถ„์„ํ•ด์„œ ํ•„์š”ํ•œ ๊ณณ์—๋งŒ ๋งŒ๋“ค์ž! ๐Ÿ“ˆ

๐Ÿ” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์•”ํ˜ธํ™”

๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•œ๋‹ค๋ฉด ์•”ํ˜ธํ™”๋Š” ํ•„์ˆ˜์•ผ! SQLCipher๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ „์ฒด๋ฅผ ์•”ํ˜ธํ™”ํ•  ์ˆ˜ ์žˆ์–ด.

// SQLCipher ์‚ฌ์šฉ ์˜ˆ์‹œ
import SQLite

class SecureSQLiteManager {
    private var db: Connection?
    
    init(password: String) {
        do {
            let path = // ... ๊ฒฝ๋กœ ์„ค์ •
            db = try Connection(path)
            
            // ์•”ํ˜ธํ™” ํ‚ค ์„ค์ •
            try db?.execute("PRAGMA key = '\(password)'")
            
            print("โœ… ์•”ํ˜ธํ™”๋œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์„ฑ๊ณต!")
        } catch {
            print("โŒ ์—ฐ๊ฒฐ ์‹คํŒจ: \(error)")
        }
    }
}
โš ๏ธ ๋ณด์•ˆ ์ฃผ์˜์‚ฌํ•ญ โ€ข ์•”ํ˜ธํ™” ํ‚ค๋Š” ์ ˆ๋Œ€ ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜์ง€ ๋ง ๊ฒƒ!
โ€ข Keychain์— ์•ˆ์ „ํ•˜๊ฒŒ ์ €์žฅํ•˜๊ฑฐ๋‚˜ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ž…๋ ฅ๋ฐ›์•„์•ผ ํ•ด
โ€ข ํ‚ค๋ฅผ ์žƒ์–ด๋ฒ„๋ฆฌ๋ฉด ๋ฐ์ดํ„ฐ๋ฅผ ๋ณต๊ตฌํ•  ์ˆ˜ ์—†์œผ๋‹ˆ ์ฃผ์˜! ๐Ÿ”‘

โš–๏ธ Core Data vs SQLite: ์–ด๋–ค ๊ฑธ ์„ ํƒํ•ด์•ผ ํ• ๊นŒ?

์ž, ์ด์ œ ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์„ ๋ชจ๋‘ ๋ฐฐ์› ์œผ๋‹ˆ ์‹ค์ „์—์„œ ์–ด๋–ค ๊ฑธ ์„ ํƒํ•ด์•ผ ํ• ์ง€ ๊ณ ๋ฏผ๋˜์ง€?
์ •๋‹ต์€... "์ƒํ™ฉ์— ๋”ฐ๋ผ ๋‹ค๋ฅด๋‹ค"์•ผ! ๐Ÿ˜… ๊ฐ๊ฐ์˜ ์žฅ๋‹จ์ ์„ ๋น„๊ตํ•ด๋ณด์ž.

์„ ํƒ ๊ฐ€์ด๋“œ Core Data ์ถ”์ฒœ SQLite ์ถ”์ฒœ โœ“ Swift ์ค‘์‹ฌ ๊ฐœ๋ฐœ โœ“ ๋ณต์žกํ•œ ๊ฐ์ฒด ๊ด€๊ณ„ โœ“ iCloud ๋™๊ธฐํ™” โœ“ ๋น ๋ฅธ ํ”„๋กœํ† ํƒ€์ดํ•‘ โœ“ Apple ์ƒํƒœ๊ณ„ ํ†ตํ•ฉ โœ“ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ž๋™ํ™” โœ“ ์ดˆ๋ณด์ž ์นœํ™”์  โœ“ ํฌ๋กœ์Šค ํ”Œ๋žซํผ โœ“ ๋ณต์žกํ•œ ์ฟผ๋ฆฌ ํ•„์š” โœ“ ์ตœ๋Œ€ ์„ฑ๋Šฅ ์ค‘์š” โœ“ ์„ธ๋ฐ€ํ•œ ์ œ์–ด ํ•„์š” โœ“ ๊ธฐ์กด DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ โœ“ SQL ์ „๋ฌธ ์ง€์‹ ๋ณด์œ  โœ“ ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ 80% 20% ์ผ๋ฐ˜์ ์ธ ์•ฑ ํŠน์ˆ˜ํ•œ ๊ฒฝ์šฐ

๐Ÿ“‹ ์ƒ์„ธ ๋น„๊ตํ‘œ

๋น„๊ต ํ•ญ๋ชฉ Core Data SQLite
ํ•™์Šต ๊ณก์„  ์ค‘๊ฐ„ (๊ฐœ๋… ์ดํ•ด ํ•„์š”) ๋†’์Œ (SQL ์ง€์‹ ํ•„์š”)
๊ฐœ๋ฐœ ์†๋„ ๋น ๋ฆ„ (์ž๋™ ์ƒ์„ฑ ๋งŽ์Œ) ๋А๋ฆผ (์ง์ ‘ ๊ตฌํ˜„)
ํƒ€์ž… ์•ˆ์ •์„ฑ ๋†’์Œ (Swift ๊ฐ์ฒด) ๋‚ฎ์Œ (์ˆ˜๋™ ๋ณ€ํ™˜)
์„ฑ๋Šฅ ์ข‹์Œ (์ตœ์ ํ™”๋จ) ๋งค์šฐ ์ข‹์Œ (์ง์ ‘ ์ œ์–ด)
๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ ์ž๋™ (Faulting) ์ˆ˜๋™ ๊ด€๋ฆฌ ํ•„์š”
๊ด€๊ณ„ ์ฒ˜๋ฆฌ ์‰ฌ์›€ (์ž๋™ ๊ด€๋ฆฌ) ์–ด๋ ค์›€ (JOIN ์ง์ ‘)
๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ฐ˜์ž๋™ (๋ชจ๋ธ ๋ฒ„์ „) ์ˆ˜๋™ (ALTER TABLE)
iCloud ๋™๊ธฐํ™” ์ง€์› (NSPersistentCloudKitContainer) ๋ฏธ์ง€์› (์ง์ ‘ ๊ตฌํ˜„)
๋””๋ฒ„๊น… ์–ด๋ ค์›€ (๋ธ”๋ž™๋ฐ•์Šค) ์‰ฌ์›€ (SQL ์ง์ ‘ ํ™•์ธ)
ํฌ๋กœ์Šค ํ”Œ๋žซํผ iOS/macOS๋งŒ ๋ชจ๋“  ํ”Œ๋žซํผ

๐ŸŽฏ ์‹ค์ „ ์„ ํƒ ๊ฐ€์ด๋“œ

Core Data๋ฅผ ์„ ํƒํ•˜์„ธ์š” if:
  • โœ… iOS/macOS ์•ฑ๋งŒ ๊ฐœ๋ฐœํ•˜๋Š” ๊ฒฝ์šฐ
  • โœ… ๊ฐ์ฒด ์ง€ํ–ฅ์  ์„ค๊ณ„๋ฅผ ์„ ํ˜ธํ•˜๋Š” ๊ฒฝ์šฐ
  • โœ… iCloud ๋™๊ธฐํ™”๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ
  • โœ… ๋น ๋ฅธ ๊ฐœ๋ฐœ์ด ์ค‘์š”ํ•œ ๊ฒฝ์šฐ
  • โœ… Apple์˜ ๋‹ค๋ฅธ ํ”„๋ ˆ์ž„์›Œํฌ(SwiftUI, Combine)์™€ ํ†ตํ•ฉํ•˜๋ ค๋Š” ๊ฒฝ์šฐ
  • โœ… ๋ณต์žกํ•œ ๊ฐ์ฒด ๊ทธ๋ž˜ํ”„๋ฅผ ๋‹ค๋ฃจ๋Š” ๊ฒฝ์šฐ
SQLite๋ฅผ ์„ ํƒํ•˜์„ธ์š” if:
  • โœ… ํฌ๋กœ์Šค ํ”Œ๋žซํผ ์•ฑ์„ ๊ฐœ๋ฐœํ•˜๋Š” ๊ฒฝ์šฐ
  • โœ… ๋ณต์žกํ•œ SQL ์ฟผ๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ
  • โœ… ์ตœ๋Œ€ ์„ฑ๋Šฅ์ด ์ค‘์š”ํ•œ ๊ฒฝ์šฐ
  • โœ… ๊ธฐ์กด SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ
  • โœ… ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ตฌ์กฐ๋ฅผ ์™„์ „ํžˆ ์ œ์–ดํ•˜๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ
  • โœ… SQL ์ „๋ฌธ ์ง€์‹์ด ์žˆ๋Š” ๊ฒฝ์šฐ
๐Ÿ’ก Pro Tip: ํ˜ผํ•ฉ ์‚ฌ์šฉ๋„ ๊ฐ€๋Šฅํ•ด! ์‹ค์ œ๋กœ ๋งŽ์€ ์•ฑ๋“ค์ด ๋‘ ๊ฐ€์ง€๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•ด.
โ€ข Core Data: ๋ฉ”์ธ ์•ฑ ๋ฐ์ดํ„ฐ (์‚ฌ์šฉ์ž ์„ค์ •, ์ฝ˜ํ…์ธ  ๋“ฑ)
โ€ข SQLite: ๋ถ„์„ ๋ฐ์ดํ„ฐ, ๋กœ๊ทธ, ์บ์‹œ ๋“ฑ

๊ฐ๊ฐ์˜ ๊ฐ•์ ์„ ์‚ด๋ ค์„œ ์‚ฌ์šฉํ•˜๋ฉด ์ตœ๊ณ ์˜ ๊ฒฐ๊ณผ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์–ด! ๐Ÿš€

๐Ÿ“Š ์‹ค์ œ ์‚ฌ์šฉ ์‚ฌ๋ก€

๐ŸŽต ์Œ์•… ์•ฑ

Core Data ์‚ฌ์šฉ
โ€ข ํ”Œ๋ ˆ์ด๋ฆฌ์ŠคํŠธ์™€ ๊ณก์˜ ๊ด€๊ณ„
โ€ข ์žฌ์ƒ ๊ธฐ๋ก ์ถ”์ 
โ€ข iCloud ๋™๊ธฐํ™”
โ€ข ๋ณต์žกํ•œ ํ•„ํ„ฐ๋ง
๐Ÿ“Š ๋ถ„์„ ์•ฑ

SQLite ์‚ฌ์šฉ
โ€ข ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ง‘๊ณ„
โ€ข ๋ณต์žกํ•œ ํ†ต๊ณ„ ์ฟผ๋ฆฌ
โ€ข ๋น ๋ฅธ ๊ฒ€์ƒ‰ ์„ฑ๋Šฅ
โ€ข CSV ๋‚ด๋ณด๋‚ด๊ธฐ
๐Ÿ“ ๋…ธํŠธ ์•ฑ

Core Data ์‚ฌ์šฉ
โ€ข ํด๋”-๋…ธํŠธ ๊ณ„์ธต ๊ตฌ์กฐ
โ€ข ํƒœ๊ทธ ์‹œ์Šคํ…œ
โ€ข ์ฒจ๋ถ€ ํŒŒ์ผ ๊ด€๋ฆฌ
โ€ข ๋ฒ„์ „ ๊ด€๋ฆฌ
๐Ÿ’ฐ ๊ฐ€๊ณ„๋ถ€ ์•ฑ

SQLite ์‚ฌ์šฉ
โ€ข ๋ณต์žกํ•œ ์ง‘๊ณ„ ์ฟผ๋ฆฌ
โ€ข ๋‚ ์งœ๋ณ„ ํ†ต๊ณ„
โ€ข ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๋ถ„์„
โ€ข ๋น ๋ฅธ ๊ฒ€์ƒ‰

๐Ÿ”„ ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜: ๋ฒ„์ „ ๊ด€๋ฆฌ์˜ ํ•ต์‹ฌ

์•ฑ์„ ์—…๋ฐ์ดํŠธํ•˜๋‹ค ๋ณด๋ฉด ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ๋ฐ”๊ฟ”์•ผ ํ•  ๋•Œ๊ฐ€ ์žˆ์–ด.
์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜, ๊ธฐ์กด ๊ตฌ์กฐ๋ฅผ ๊ฐœ์„ ํ•˜๊ฑฐ๋‚˜... ์ด๋Ÿด ๋•Œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด ํ•„์š”ํ•ด! ๐Ÿ”ง

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ž˜๋ชปํ•˜๋ฉด ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ๊ฐ€ ๋‚ ์•„๊ฐˆ ์ˆ˜ ์žˆ์œผ๋‹ˆ ์ •๋ง ์กฐ์‹ฌํ•ด์•ผ ํ•ด. ๐Ÿ˜ฐ

๐Ÿ“ฆ Core Data ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜

Step 1: ์ƒˆ ๋ชจ๋ธ ๋ฒ„์ „ ๋งŒ๋“ค๊ธฐ

.xcdatamodeld ํŒŒ์ผ์„ ์„ ํƒํ•˜๊ณ  Editor โ†’ Add Model Version...

์ƒˆ ๋ฒ„์ „์ด ๋งŒ๋“ค์–ด์ง€๋ฉด .xcdatamodeld ํŒŒ์ผ ์•ˆ์— ์—ฌ๋Ÿฌ ๋ฒ„์ „์ด ์ƒ๊ฒจ.
์˜ˆ: MyApp.xcdatamodeld โ†’ MyApp.xcdatamodel, MyApp 2.xcdatamodel

ํ˜„์žฌ ๋ฒ„์ „์„ ์„ค์ •ํ•˜๋ ค๋ฉด .xcdatamodeld ํŒŒ์ผ์„ ์„ ํƒํ•˜๊ณ  File Inspector์—์„œ "Current"๋ฅผ ๋ณ€๊ฒฝํ•ด.

Step 2: ๋ชจ๋ธ ๋ณ€๊ฒฝํ•˜๊ธฐ

์ƒˆ ๋ฒ„์ „์—์„œ ์›ํ•˜๋Š” ๋Œ€๋กœ Entity๋ฅผ ์ˆ˜์ •ํ•ด.

์˜ˆ๋ฅผ ๋“ค์–ด Memo Entity์— 'category' ์†์„ฑ์„ ์ถ”๊ฐ€ํ•œ๋‹ค๊ณ  ํ•ด๋ณด์ž:

// ๋ณ€๊ฒฝ ์ „ (๋ฒ„์ „ 1)
Entity: Memo
- id: UUID
- title: String
- content: String
- createdAt: Date

// ๋ณ€๊ฒฝ ํ›„ (๋ฒ„์ „ 2)
Entity: Memo
- id: UUID
- title: String
- content: String
- createdAt: Date
- category: String  // ์ƒˆ๋กœ ์ถ”๊ฐ€!
Step 3: ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ •์ฑ… ์„ค์ •

๊ฐ„๋‹จํ•œ ๋ณ€๊ฒฝ์€ ์ž๋™ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜(Lightweight Migration)์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด!
lazy var persistentContainer: NSPersistentContainer = {
    let container = NSPersistentContainer(name: "MyApp")
    
    // ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์˜ต์…˜ ์„ค์ •
    let description = container.persistentStoreDescriptions.first
    description?.shouldMigrateStoreAutomatically = true
    description?.shouldInferMappingModelAutomatically = true
    
    container.loadPersistentStores { description, error in
        if let error = error {
            fatalError("๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํŒจ: \(error)")
        }
        print("โœ… ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์„ฑ๊ณต!")
    }
    
    return container
}()
๐ŸŽฏ Lightweight Migration์ด ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ: โ€ข ์ƒˆ ์†์„ฑ ์ถ”๊ฐ€ (๊ธฐ๋ณธ๊ฐ’ ์„ค์ •)
โ€ข ์†์„ฑ ์‚ญ์ œ
โ€ข ํ•„์ˆ˜ ์†์„ฑ์„ ์˜ต์…”๋„๋กœ ๋ณ€๊ฒฝ
โ€ข ์˜ต์…”๋„ ์†์„ฑ์„ ํ•„์ˆ˜๋กœ ๋ณ€๊ฒฝ (๊ธฐ๋ณธ๊ฐ’ ์žˆ์„ ๋•Œ)
โ€ข ์ƒˆ Entity ์ถ”๊ฐ€
โ€ข Entity ์ด๋ฆ„ ๋ณ€๊ฒฝ (Renaming ID ์„ค์ •)
โ€ข Relationship ์ถ”๊ฐ€/์‚ญ์ œ

๐Ÿ”ง ๋ณต์žกํ•œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜: Mapping Model

๋ฐ์ดํ„ฐ ๋ณ€ํ™˜์ด ํ•„์š”ํ•œ ๋ณต์žกํ•œ ๊ฒฝ์šฐ์—๋Š” Mapping Model์„ ๋งŒ๋“ค์–ด์•ผ ํ•ด.

Step 1: Mapping Model ์ƒ์„ฑ

File โ†’ New โ†’ File โ†’ Mapping Model ์„ ํƒ
Step 2: Custom Migration Policy ์ž‘์„ฑ
import CoreData

class MemoMigrationPolicy: NSEntityMigrationPolicy {
    // ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ค‘ ํ˜ธ์ถœ๋˜๋Š” ๋ฉ”์„œ๋“œ
    override func createDestinationInstances(
        forSource sInstance: NSManagedObject,
        in mapping: NSEntityMapping,
        manager: NSMigrationManager
    ) throws {
        try super.createDestinationInstances(
            forSource: sInstance,
            in: mapping,
            manager: manager
        )
        
        // ์ปค์Šคํ…€ ๋กœ์ง
        guard let destinationMemo = manager.destinationInstances(
            forEntityMappingName: mapping.name,
            sourceInstances: [sInstance]
        ).first else { return }
        
        // ์˜ˆ: ๊ธฐ์กด ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€ํ™˜ํ•ด์„œ ์ƒˆ ์†์„ฑ์— ์„ค์ •
        if let oldTitle = sInstance.value(forKey: "title") as? String {
            // ์ œ๋ชฉ์—์„œ ์นดํ…Œ๊ณ ๋ฆฌ ์ถ”์ถœ
            let category = extractCategory(from: oldTitle)
            destinationMemo.setValue(category, forKey: "category")
        }
    }
    
    private func extractCategory(from title: String) -> String {
        // ์ œ๋ชฉ ์•ž๋ถ€๋ถ„์—์„œ ์นดํ…Œ๊ณ ๋ฆฌ ์ถ”์ถœํ•˜๋Š” ๋กœ์ง
        if title.hasPrefix("[์—…๋ฌด]") {
            return "์—…๋ฌด"
        } else if title.hasPrefix("[๊ฐœ์ธ]") {
            return "๊ฐœ์ธ"
        }
        return "๊ธฐํƒ€"
    }
}
โš ๏ธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ฃผ์˜์‚ฌํ•ญ โ€ข ํ•ญ์ƒ ๋ฐฑ์—…์„ ๋จผ์ €! ํ…Œ์ŠคํŠธ ๊ธฐ๊ธฐ์—์„œ ์ถฉ๋ถ„ํžˆ ๊ฒ€์ฆํ•˜๊ธฐ
โ€ข ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์€ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์–ด. ์‚ฌ์šฉ์ž์—๊ฒŒ ์ง„ํ–‰ ์ƒํ™ฉ ํ‘œ์‹œ
โ€ข ์‹คํŒจ ์‹œ ๋ณต๊ตฌ ๋ฐฉ๋ฒ•์„ ์ค€๋น„ํ•ด๋‘๊ธฐ
โ€ข ๋ฒ„์ „์„ ๊ฑด๋„ˆ๋›ฐ๋Š” ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜๋„ ๊ณ ๋ ค (1.0 โ†’ 3.0 ์ง์ ‘)
โ€ข ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ๋Š” ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์ฒ˜๋ฆฌ

๐Ÿ”„ SQLite ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜

SQLite๋Š” ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ง์ ‘ ๊ด€๋ฆฌํ•ด์•ผ ํ•ด. ํ•˜์ง€๋งŒ ๋” ์„ธ๋ฐ€ํ•œ ์ œ์–ด๊ฐ€ ๊ฐ€๋Šฅํ•˜์ง€! ๐Ÿ’ช

๋ฒ„์ „ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ ๊ตฌ์ถ•

class SQLiteMigrationManager {
    private let db: Connection
    private let currentVersion = 3  // ํ˜„์žฌ ์•ฑ์˜ DB ๋ฒ„์ „
    
    init(db: Connection) {
        self.db = db
    }
    
    func migrate() throws {
        let userVersion = try db.scalar("PRAGMA user_version") as! Int64
        
        print("๐Ÿ“ฆ ํ˜„์žฌ DB ๋ฒ„์ „: \(userVersion)")
        print("๐ŸŽฏ ๋ชฉํ‘œ ๋ฒ„์ „: \(currentVersion)")
        
        if userVersion < currentVersion {
            try performMigration(from: Int(userVersion), to: currentVersion)
        }
    }
    
    private func performMigration(from: Int, to: Int) throws {
        // ํŠธ๋žœ์žญ์…˜์œผ๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
        try db.transaction {
            for version in (from + 1)...to {
                print("๐Ÿ”„ ๋ฒ„์ „ \(version)์œผ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ค‘...")
                try migrateToVersion(version)
            }
            
            // ๋ฒ„์ „ ์—…๋ฐ์ดํŠธ
            try db.run("PRAGMA user_version = \(to)")
            print("โœ… ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ!")
        }
    }
    
    private func migrateToVersion(_ version: Int) throws {
        switch version {
        case 1:
            try migrateToV1()
        case 2:
            try migrateToV2()
        case 3:
            try migrateToV3()
        default:
            break
        }
    }
    
    // ๋ฒ„์ „ 1: ์ดˆ๊ธฐ ํ…Œ์ด๋ธ” ์ƒ์„ฑ
    private func migrateToV1() throws {
        try db.run("""
            CREATE TABLE IF NOT EXISTS memos (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                title TEXT NOT NULL,
                content TEXT NOT NULL,
                created_at DATETIME NOT NULL
            )
        """)
    }
    
    // ๋ฒ„์ „ 2: is_important ์ปฌ๋Ÿผ ์ถ”๊ฐ€
    private func migrateToV2() throws {
        try db.run("""
            ALTER TABLE memos 
            ADD COLUMN is_important INTEGER DEFAULT 0
        """)
    }
    
    // ๋ฒ„์ „ 3: category ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ๋ฐ ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜
    private func migrateToV3() throws {
        // 1. ์ƒˆ ์ปฌ๋Ÿผ ์ถ”๊ฐ€
        try db.run("""
            ALTER TABLE memos 
            ADD COLUMN category TEXT DEFAULT '๊ธฐํƒ€'
        """)
        
        // 2. ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜
        try db.run("""
            UPDATE memos 
            SET category = CASE 
                WHEN title LIKE '[์—…๋ฌด]%' THEN '์—…๋ฌด'
                WHEN title LIKE '[๊ฐœ์ธ]%' THEN '๊ฐœ์ธ'
                ELSE '๊ธฐํƒ€'
            END
        """)
        
        // 3. ์ธ๋ฑ์Šค ์ƒ์„ฑ
        try db.run("""
            CREATE INDEX IF NOT EXISTS idx_category 
            ON memos(category)
        """)
    }
}

์•ˆ์ „ํ•œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ „๋žต

class SafeMigrationManager {
    func migrateWithBackup() throws {
        let fileManager = FileManager.default
        let documentsPath = NSSearchPathForDirectoriesInDomains(
            .documentDirectory, .userDomainMask, true
        ).first!
        
        let dbPath = "\(documentsPath)/myapp.sqlite3"
        let backupPath = "\(documentsPath)/myapp_backup.sqlite3"
        
        // 1. ๋ฐฑ์—… ์ƒ์„ฑ
        print("๐Ÿ’พ ๋ฐฑ์—… ์ƒ์„ฑ ์ค‘...")
        if fileManager.fileExists(atPath: backupPath) {
            try fileManager.removeItem(atPath: backupPath)
        }
        try fileManager.copyItem(atPath: dbPath, toPath: backupPath)
        
        do {
            // 2. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹œ๋„
            let db = try Connection(dbPath)
            let migrationManager = SQLiteMigrationManager(db: db)
            try migrationManager.migrate()
            
            // 3. ์„ฑ๊ณต ์‹œ ๋ฐฑ์—… ์‚ญ์ œ
            print("โœ… ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์„ฑ๊ณต! ๋ฐฑ์—… ์‚ญ์ œ")
            try fileManager.removeItem(atPath: backupPath)
            
        } catch {
            // 4. ์‹คํŒจ ์‹œ ๋ฐฑ์—… ๋ณต๊ตฌ
            print("โŒ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํŒจ! ๋ฐฑ์—… ๋ณต๊ตฌ ์ค‘...")
            try fileManager.removeItem(atPath: dbPath)
            try fileManager.copyItem(atPath: backupPath, toPath: dbPath)
            
            throw error
        }
    }
}
๐Ÿ’ก ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค 1. ๋ฒ„์ „ ๋ฒˆํ˜ธ ๊ด€๋ฆฌ: PRAGMA user_version ํ™œ์šฉ
2. ์ˆœ์ฐจ์  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜: ๋ฒ„์ „์„ ๊ฑด๋„ˆ๋›ฐ์ง€ ๋ง๊ณ  ์ˆœ์„œ๋Œ€๋กœ
3. ํŠธ๋žœ์žญ์…˜ ์‚ฌ์šฉ: ์‹คํŒจ ์‹œ ๋กค๋ฐฑ ๊ฐ€๋Šฅํ•˜๋„๋ก
4. ๋ฐฑ์—… ํ•„์ˆ˜: ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ „ ํ•ญ์ƒ ๋ฐฑ์—…
5. ํ…Œ์ŠคํŠธ: ๋‹ค์–‘ํ•œ ๋ฒ„์ „์—์„œ ์ถฉ๋ถ„ํžˆ ํ…Œ์ŠคํŠธ
6. ์ง„ํ–‰ ์ƒํ™ฉ ํ‘œ์‹œ: ์‚ฌ์šฉ์ž์—๊ฒŒ ํ”ผ๋“œ๋ฐฑ ์ œ๊ณต
7. ๋กœ๊น…: ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ณผ์ •์„ ์ƒ์„ธํžˆ ๊ธฐ๋ก

๐ŸŽจ SwiftUI์™€ ๋ฐ์ดํ„ฐ ์˜์†ํ™” ํ†ตํ•ฉ

์š”์ฆ˜ iOS ๊ฐœ๋ฐœ์€ SwiftUI๊ฐ€ ๋Œ€์„ธ์ง€! SwiftUI์™€ ๋ฐ์ดํ„ฐ ์˜์†ํ™”๋ฅผ ์–ด๋–ป๊ฒŒ ํ†ตํ•ฉํ•˜๋Š”์ง€ ์•Œ์•„๋ณด์ž. ๐Ÿš€

๐ŸŒŸ Core Data + SwiftUI

SwiftUI๋Š” Core Data์™€ ์ •๋ง ์ž˜ ์–ด์šธ๋ ค! @FetchRequest๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ž๋™์œผ๋กœ UI๊ฐ€ ์—…๋ฐ์ดํŠธ๋ผ.

import SwiftUI
import CoreData

struct MemoListView: View {
    // Core Data ํ™˜๊ฒฝ ๋ณ€์ˆ˜
    @Environment(\.managedObjectContext) private var viewContext
    
    // ์ž๋™์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ๋ณ€๊ฒฝ์‚ฌํ•ญ ๊ฐ์ง€
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Memo.createdAt, ascending: false)],
        animation: .default
    )
    private var memos: FetchedResults<Memo>
    
    @State private var showingAddSheet = false
    
    var body: some View {
        NavigationView {
            List {
                ForEach(memos) { memo in
                    MemoRow(memo: memo)
                }
                .onDelete(perform: deleteMemos)
            }
            .navigationTitle("๋ฉ”๋ชจ ๐Ÿ“")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button(action: { showingAddSheet = true }) {
                        Label("์ถ”๊ฐ€", systemImage: "plus")
                    }
                }
            }
            .sheet(isPresented: $showingAddSheet) {
                AddMemoView()
                    .environment(\.managedObjectContext, viewContext)
            }
        }
    }
    
    private func deleteMemos(offsets: IndexSet) {
        withAnimation {
            offsets.map { memos[$0] }.forEach(viewContext.delete)
            
            do {
                try viewContext.save()
            } catch {
                print("โŒ ์‚ญ์ œ ์‹คํŒจ: \(error)")
            }
        }
    }
}

struct MemoRow: View {
    @ObservedObject var memo: Memo
    
    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            HStack {
                Text(memo.title ?? "์ œ๋ชฉ ์—†์Œ")
                    .font(.headline)
                
                Spacer()
                
                if memo.isImportant {
                    Image(systemName: "star.fill")
                        .foregroundColor(.yellow)
                }
            }
            
            Text(memo.content ?? "")
                .font(.subheadline)
                .foregroundColor(.secondary)
                .lineLimit(2)
            
            Text(memo.createdAt ?? Date(), style: .date)
                .font(.caption)
                .foregroundColor(.gray)
        }
        .padding(.vertical, 4)
    }
}

๋ฉ”๋ชจ ์ถ”๊ฐ€ ๋ทฐ

struct AddMemoView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @Environment(\.dismiss) private var dismiss
    
    @State private var title = ""
    @State private var content = ""
    @State private var isImportant = false
    
    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("๋ฉ”๋ชจ ์ •๋ณด")) {
                    TextField("์ œ๋ชฉ", text: $title)
                    
                    TextEditor(text: $content)
                        .frame(height: 200)
                    
                    Toggle("์ค‘์š” ํ‘œ์‹œ", isOn: $isImportant)
                }
            }
            .navigationTitle("์ƒˆ ๋ฉ”๋ชจ")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("์ทจ์†Œ") {
                        dismiss()
                    }
                }
                
                ToolbarItem(placement: .confirmationAction) {
                    Button("์ €์žฅ") {
                        saveMemo()
                    }
                    .disabled(title.isEmpty)
                }
            }
        }
    }
    
    private func saveMemo() {
        let newMemo = Memo(context: viewContext)
        newMemo.id = UUID()
        newMemo.title = title
        newMemo.content = content
        newMemo.createdAt = Date()
        newMemo.isImportant = isImportant
        
        do {
            try viewContext.save()
            dismiss()
        } catch {
            print("โŒ ์ €์žฅ ์‹คํŒจ: \(error)")
        }
    }
}

ํ•„ํ„ฐ๋ง๊ณผ ๊ฒ€์ƒ‰

struct FilteredMemoListView: View {
    @Environment(\.managedObjectContext) private var viewContext
    
    @State private var searchText = ""
    @State private var showImportantOnly = false
    
    var body: some View {
        VStack {
            // ๊ฒ€์ƒ‰ ๋ฐ”
            HStack {
                Image(systemName: "magnifyingglass")
                    .foregroundColor(.gray)
                
                TextField("๊ฒ€์ƒ‰...", text: $searchText)
                
                if !searchText.isEmpty {
                    Button(action: { searchText = "" }) {
                        Image(systemName: "xmark.circle.fill")
                            .foregroundColor(.gray)
                    }
                }
            }
            .padding(8)
            .background(Color(.systemGray6))
            .cornerRadius(10)
            .padding(.horizontal)
            
            // ํ•„ํ„ฐ ํ† ๊ธ€
            Toggle("์ค‘์š” ๋ฉ”๋ชจ๋งŒ", isOn: $showImportantOnly)
                .padding(.horizontal)
            
            // ๋™์  FetchRequest
            MemoList(
                searchText: searchText,
                showImportantOnly: showImportantOnly
            )
        }
        .navigationTitle("๋ฉ”๋ชจ ๊ฒ€์ƒ‰")
    }
}

struct MemoList: View {
    let searchText: String
    let showImportantOnly: Bool
    
    @FetchRequest private var memos: FetchedResults<Memo>
    
    init(searchText: String, showImportantOnly: Bool) {
        self.searchText = searchText
        self.showImportantOnly = showImportantOnly
        
        // ๋™์ ์œผ๋กœ predicate ์ƒ์„ฑ
        var predicates: [NSPredicate] = []
        
        if !searchText.isEmpty {
            let searchPredicate = NSPredicate(
                format: "title CONTAINS[cd] %@ OR content CONTAINS[cd] %@",
                searchText, searchText
            )
            predicates.append(searchPredicate)
        }
        
        if showImportantOnly {
            let importantPredicate = NSPredicate(format: "isImportant == %@", NSNumber(value: true))
            predicates.append(importantPredicate)
        }
        
        let compoundPredicate = predicates.isEmpty ? nil : NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
        
        _memos = FetchRequest<Memo>(
            sortDescriptors: [NSSortDescriptor(keyPath: \Memo.createdAt, ascending: false)],
            predicate: compoundPredicate,
            animation: .default
        )
    }
    
    var body: some View {
        List(memos) { memo in
            MemoRow(memo: memo)
        }
    }
}

๐Ÿ“ฑ SQLite + SwiftUI

SQLite๋ฅผ SwiftUI์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ObservableObject๋ฅผ ํ™œ์šฉํ•ด์•ผ ํ•ด.

import SwiftUI
import Combine

class MemoViewModel: ObservableObject {
    @Published var memos: [MemoModel] = []
    @Published var isLoading = false
    @Published var errorMessage: String?
    
    private let sqliteManager = SQLiteManager.shared
    
    func loadMemos() {
        isLoading = true
        
        DispatchQueue.global(qos: .userInitiated).async { [weak self] in
            let fetchedMemos = self?.sqliteManager.fetchAllMemos() ?? []
            
            DispatchQueue.main.async {
                self?.memos = fetchedMemos
                self?.isLoading = false
            }
        }
    }
    
    func addMemo(title: String, content: String, isImportant: Bool = false) {
        guard let id = sqliteManager.insertMemo(
            title: title,
            content: content,
            isImportant: isImportant
        ) else {
            errorMessage = "๋ฉ”๋ชจ ์ถ”๊ฐ€ ์‹คํŒจ"
            return
        }
        
        loadMemos()  // ์ƒˆ๋กœ๊ณ ์นจ
    }
    
    func deleteMemo(id: Int64) {
        if sqliteManager.deleteMemo(id: id) {
            memos.removeAll { $0.id == id }
        } else {
            errorMessage = "๋ฉ”๋ชจ ์‚ญ์ œ ์‹คํŒจ"
        }
    }
    
    func searchMemos(keyword: String) {
        isLoading = true
        
        DispatchQueue.global(qos: .userInitiated).async { [weak self] in
            let results = self?.sqliteManager.searchMemos(keyword: keyword) ?? []
            
            DispatchQueue.main.async {
                self?.memos = results
                self?.isLoading = false
            }
        }
    }
}

struct SQLiteMemoListView: View {
    @StateObject private var viewModel = MemoViewModel()
    @State private var showingAddSheet = false
    @State private var searchText = ""
    
    var body: some View {
        NavigationView {
            VStack {
                // ๊ฒ€์ƒ‰ ๋ฐ”
                SearchBar(text: $searchText, onSearch: {
                    if searchText.isEmpty {
                        viewModel.loadMemos()
                    } else {
                        viewModel.searchMemos(keyword: searchText)
                    }
                })
                
                if viewModel.isLoading {
                    ProgressView("๋กœ๋”ฉ ์ค‘...")
                } else {
                    List {
                        ForEach(viewModel.memos, id: \.id) { memo in
                            SQLiteMemoRow(memo: memo)
                        }
                        .onDelete { indexSet in
                            indexSet.forEach { index in
                                let memo = viewModel.memos[index]
                                viewModel.deleteMemo(id: memo.id)
                            }
                        }
                    }
                }
            }
            .navigationTitle("๋ฉ”๋ชจ ๐Ÿ“")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button(action: { showingAddSheet = true }) {
                        Label("์ถ”๊ฐ€", systemImage: "plus")
                    }
                }
            }
            .sheet(isPresented: $showingAddSheet) {
                AddSQLiteMemoView(viewModel: viewModel)
            }
            .onAppear {
                viewModel.loadMemos()
            }
            .alert("์˜ค๋ฅ˜", isPresented: .constant(viewModel.errorMessage != nil)) {
                Button("ํ™•์ธ") {
                    viewModel.errorMessage = nil
                }
            } message: {
                Text(viewModel.errorMessage ?? "")
            }
        }
    }
}

struct SQLiteMemoRow: View {
    let memo: MemoModel
    
    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            HStack {
                Text(memo.title)
                    .font(.headline)
                
                Spacer()
                
                if memo.isImportant {
                    Image(systemName: "star.fill")
                        .foregroundColor(.yellow)
                }
            }
            
            Text(memo.content)
                .font(.subheadline)
                .foregroundColor(.secondary)
                .lineLimit(2)
            
            Text(memo.createdAt, style: .date)
                .font(.caption)
                .foregroundColor(.gray)
        }
        .padding(.vertical, 4)
    }
}

struct SearchBar: View {
    @Binding var text: String
    let onSearch: () -> Void
    
    var body: some View {
        HStack {
            Image(systemName: "magnifyingglass")
                .foregroundColor(.gray)
            
            TextField("๊ฒ€์ƒ‰...", text: $text)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .onSubmit {
                    onSearch()
                }
            
            if !text.isEmpty {
                Button(action: {
                    text = ""
                    onSearch()
                }) {
                    Image(systemName: "xmark.circle.fill")
                        .foregroundColor(.gray)
                }
            }
        }
        .padding(.horizontal)
    }
}
๐Ÿ’ก SwiftUI ํ†ตํ•ฉ ํŒ Core Data:
โ€ข @FetchRequest๋กœ ์ž๋™ ์—…๋ฐ์ดํŠธ
โ€ข @Environment๋กœ context ์ „๋‹ฌ
โ€ข ๊ด€๊ณ„ ํƒ์ƒ‰์ด ์ž์—ฐ์Šค๋Ÿฌ์›€

SQLite:
โ€ข ObservableObject๋กœ ์ƒํƒœ ๊ด€๋ฆฌ
โ€ข ์ˆ˜๋™์œผ๋กœ UI ์—…๋ฐ์ดํŠธ ํ•„์š”
โ€ข ๋” ์„ธ๋ฐ€ํ•œ ์ œ์–ด ๊ฐ€๋Šฅ

ํ”„๋กœ์ ํŠธ ๊ทœ๋ชจ์™€ ์š”๊ตฌ์‚ฌํ•ญ์— ๋”ฐ๋ผ ์„ ํƒํ•˜๋ฉด ๋ผ! ๐ŸŽฏ

๐Ÿ”’ ๋ฐ์ดํ„ฐ ๋ณด์•ˆ๊ณผ ํ”„๋ผ์ด๋ฒ„์‹œ

์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃฐ ๋•Œ๋Š” ๋ณด์•ˆ์ด ์ •๋ง ์ค‘์š”ํ•ด! ํŠนํžˆ ๋ฏผ๊ฐํ•œ ์ •๋ณด๋ฅผ ์ €์žฅํ•œ๋‹ค๋ฉด ๋”๋”์šฑ. ๐Ÿ”
iOS์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๋ณดํ˜ธํ•˜๋Š” ๋ฐฉ๋ฒ•๋“ค์„ ์•Œ์•„๋ณด์ž.

๐Ÿ›ก๏ธ Data Protection API

iOS๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ํŒŒ์ผ ์‹œ์Šคํ…œ์„ ์•”ํ˜ธํ™”ํ•ด. ํ•˜์ง€๋งŒ Data Protection API๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋” ๊ฐ•๋ ฅํ•œ ๋ณดํ˜ธ๋ฅผ ํ•  ์ˆ˜ ์žˆ์–ด!

// Core Data์— Data Protection ์ ์šฉ
lazy var persistentContainer: NSPersistentContainer = {
    let container = NSPersistentContainer(name: "MyApp")
    
    let description = container.persistentStoreDescriptions.first
    
    // ํŒŒ์ผ ๋ณดํ˜ธ ๋ ˆ๋ฒจ ์„ค์ •
    description?.setOption(
        FileProtectionType.complete as NSObject,
        forKey: NSPersistentStoreFileProtectionKey
    )
    
    container.loadPersistentStores { description, error in
        if let error = error {
            fatalError("๋กœ๋“œ ์‹คํŒจ: \(error)")
        }
    }
    
    return container
}()
๐Ÿ“Š File Protection ๋ ˆ๋ฒจ: โ€ข .complete: ๊ธฐ๊ธฐ ์ž ๊ธˆ ์‹œ ์ ‘๊ทผ ๋ถˆ๊ฐ€ (๊ฐ€์žฅ ์•ˆ์ „)
โ€ข .completeUnlessOpen: ํŒŒ์ผ์ด ์—ด๋ ค์žˆ์œผ๋ฉด ์ž ๊ธˆ ํ›„์—๋„ ์ ‘๊ทผ ๊ฐ€๋Šฅ
โ€ข .completeUntilFirstUserAuthentication: ์žฌ๋ถ€ํŒ… ํ›„ ์ฒซ ์ž ๊ธˆ ํ•ด์ œ๊นŒ์ง€๋งŒ ๋ณดํ˜ธ
โ€ข .none: ๋ณดํ˜ธ ์—†์Œ (๊ธฐ๋ณธ ์•”ํ˜ธํ™”๋งŒ)

๐Ÿ”‘ Keychain์œผ๋กœ ๋ฏผ๊ฐ ์ •๋ณด ์ €์žฅ

๋น„๋ฐ€๋ฒˆํ˜ธ, ํ† ํฐ, ์•”ํ˜ธํ™” ํ‚ค ๊ฐ™์€ ๋ฏผ๊ฐํ•œ ์ •๋ณด๋Š” ์ ˆ๋Œ€ UserDefaults๋‚˜ ์ผ๋ฐ˜ ํŒŒ์ผ์— ์ €์žฅํ•˜๋ฉด ์•ˆ ๋ผ!
Keychain์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ด. ๐Ÿ”

import Security

class KeychainManager {
    static let shared = KeychainManager()
    
    private init() {}
    
    // ์ €์žฅ
    func save(key: String, value: String) -> Bool {
        guard let data = value.data(using: .utf8) else { return false }
        
        // ๊ธฐ์กด ํ•ญ๋ชฉ ์‚ญ์ œ
        delete(key: key)
        
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecValueData as String: data,
            kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
        ]
        
        let status = SecItemAdd(query as CFDictionary, nil)
        return status == errSecSuccess
    }
    
    // ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
    func load(key: String) -> String? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]
        
        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        
        guard status == errSecSuccess,
              let data = result as? Data,
              let string = String(data: data, encoding: .utf8) else {
            return nil
        }
        
        return string
    }
    
    // ์‚ญ์ œ
    func delete(key: String) -> Bool {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key
        ]
        
        let status = SecItemDelete(query as CFDictionary)
        return status == errSecSuccess || status == errSecItemNotFound
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
KeychainManager.shared.save(key: "apiToken", value: "secret_token_12345")
if let token = KeychainManager.shared.load(key: "apiToken") {
    print("ํ† ํฐ: \(token)")
}

๐Ÿ” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์•”ํ˜ธํ™”

SQLCipher๋กœ SQLite ์•”ํ˜ธํ™”

// Podfile์— ์ถ”๊ฐ€
// pod 'SQLCipher', '~> 4.5'

import SQLite

class EncryptedSQLiteManager {
    private var db: Connection?
    
    init(password: String) throws {
        let path = // ... ๊ฒฝ๋กœ ์„ค์ •
        db = try Connection(path)
        
        // ์•”ํ˜ธํ™” ํ‚ค ์„ค์ •
        try db?.execute("PRAGMA key = '\(password)'")
        
        // ์•”ํ˜ธํ™” ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์„ค์ • (์„ ํƒ์‚ฌํ•ญ)
        try db?.execute("PRAGMA cipher_page_size = 4096")
        try db?.execute("PRAGMA kdf_iter = 256000")
        
        // ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ
        try db?.execute("SELECT count(*) FROM sqlite_master")
        
        print("โœ… ์•”ํ˜ธํ™”๋œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์„ฑ๊ณต!")
    }
    
    // ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ
    func changePassword(newPassword: String) throws {
        try db?.execute("PRAGMA rekey = '\(newPassword)'")
        print("โœ… ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ์™„๋ฃŒ!")
    }
}
โš ๏ธ ์•”ํ˜ธํ™” ์ฃผ์˜์‚ฌํ•ญ โ€ข ์•”ํ˜ธํ™” ํ‚ค๋Š” Keychain์— ์ €์žฅํ•˜๊ฑฐ๋‚˜ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ž…๋ ฅ๋ฐ›๊ธฐ
โ€ข ํ‚ค๋ฅผ ์žƒ์–ด๋ฒ„๋ฆฌ๋ฉด ๋ฐ์ดํ„ฐ ๋ณต๊ตฌ ๋ถˆ๊ฐ€๋Šฅ!
โ€ข ์„ฑ๋Šฅ ์˜ค๋ฒ„ํ—ค๋“œ ๊ณ ๋ ค (์•ฝ 5-15% ๋А๋ ค์ง)
โ€ข ๋ฐฑ์—… ์‹œ ์•”ํ˜ธํ™” ์ƒํƒœ ์œ ์ง€ ํ™•์ธ
โ€ข ๋””๋ฒ„๊ทธ ๋นŒ๋“œ์—์„œ ํ‚ค ๋…ธ์ถœ ์ฃผ์˜

๐Ÿ” ๋ฐ์ดํ„ฐ ์œ ์ถœ ๋ฐฉ์ง€

// ์•ฑ ์Šค์œ„์ฒ˜์—์„œ ์Šคํฌ๋ฆฐ์ƒท ์ˆจ๊ธฐ๊ธฐ
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    private var blurView: UIVisualEffectView?
    
    func sceneWillResignActive(_ scene: UIScene) {
        // ์•ฑ์ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ๋กœ ๊ฐˆ ๋•Œ ๋ธ”๋Ÿฌ ํšจ๊ณผ
        addBlurEffect()
    }
    
    func sceneDidBecomeActive(_ scene: UIScene) {
        // ์•ฑ์ด ํฌ๊ทธ๋ผ์šด๋“œ๋กœ ์˜ฌ ๋•Œ ๋ธ”๋Ÿฌ ์ œ๊ฑฐ
        removeBlurEffect()
    }
    
    private func addBlurEffect() {
        guard blurView == nil else { return }
        
        let blurEffect = UIBlurEffect(style: .light)
        let blurView = UIVisualEffectView(effect: blurEffect)
        blurView.frame = window?.bounds ?? .zero
        blurView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        
        window?.addSubview(blurView)
        self.blurView = blurView
    }
    
    private func removeBlurEffect() {
        blurView?.removeFromSuperview()
        blurView = nil
    }
}

// ์Šคํฌ๋ฆฐ์ƒท ๊ฐ์ง€
NotificationCenter.default.addObserver(
    forName: UIApplication.userDidTakeScreenshotNotification,
    object: nil,
    queue: .main
) { _ in
    print("โš ๏ธ ์Šคํฌ๋ฆฐ์ƒท์ด ์ดฌ์˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค!")
    // ํ•„์š”ํ•œ ๊ฒฝ์šฐ ๊ฒฝ๊ณ  ํ‘œ์‹œ ๋˜๋Š” ๋กœ๊น…
}
๐Ÿ’ก ๋ณด์•ˆ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค 1. ์ตœ์†Œ ๊ถŒํ•œ ์›์น™: ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ ์ €์žฅ
2. ์•”ํ˜ธํ™”: ๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ๋Š” ํ•ญ์ƒ ์•”ํ˜ธํ™”
3. Keychain ํ™œ์šฉ: ์ธ์ฆ ์ •๋ณด๋Š” Keychain์—
4. ๋กœ๊ทธ ์ฃผ์˜: ํ”„๋กœ๋•์…˜์—์„œ ๋ฏผ๊ฐ ์ •๋ณด ๋กœ๊น… ๊ธˆ์ง€
5. ๋ฐฑ์—… ์ œ์™ธ: ํ•„์š”์‹œ iCloud ๋ฐฑ์—…์—์„œ ์ œ์™ธ
6. ์ •๊ธฐ ๊ฐ์‚ฌ: ๋ณด์•ˆ ์ทจ์•ฝ์  ์ •๊ธฐ ์ ๊ฒ€
7. ์—…๋ฐ์ดํŠธ: ๋ณด์•ˆ ํŒจ์น˜ ์‹ ์† ์ ์šฉ

โšก ์„ฑ๋Šฅ ์ตœ์ ํ™” ์‹ค์ „ ๊ฐ€์ด๋“œ

๋ฐ์ดํ„ฐ๊ฐ€ ๋งŽ์•„์ง€๋ฉด ์„ฑ๋Šฅ์ด ์ค‘์š”ํ•ด์ ธ. ์‚ฌ์šฉ์ž๊ฐ€ ๋‹ต๋‹ตํ•จ์„ ๋А๋ผ์ง€ ์•Š๋„๋ก ์ตœ์ ํ™”ํ•ด๋ณด์ž! ๐Ÿš€

๐Ÿ“Š Core Data ์„ฑ๋Šฅ ์ตœ์ ํ™”

1. Batch ํฌ๊ธฐ ์„ค์ •

let fetchRequest: NSFetchRequest<Memo> = Memo.fetchRequest()

// ํ•œ ๋ฒˆ์— 20๊ฐœ์”ฉ๋งŒ ๋ฉ”๋ชจ๋ฆฌ์— ๋กœ๋“œ
fetchRequest.fetchBatchSize = 20

// ๊ฒฐ๊ณผ ๊ฐœ์ˆ˜ ์ œํ•œ
fetchRequest.fetchLimit = 100

let memos = try context.fetch(fetchRequest)

2. Faulting ํ™œ์šฉ

Core Data๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ "faulting"์„ ์‚ฌ์šฉํ•ด. ๊ฐ์ฒด์˜ ์†์„ฑ์„ ์‹ค์ œ๋กœ ์ ‘๊ทผํ•  ๋•Œ๊นŒ์ง€ ๋กœ๋“œํ•˜์ง€ ์•Š์•„.
ํ•˜์ง€๋งŒ ๊ด€๊ณ„(relationship)๋ฅผ ํƒ์ƒ‰ํ•  ๋•Œ๋Š” ์ฃผ์˜ํ•ด์•ผ ํ•ด!

// โŒ ๋‚˜์œ ์˜ˆ: N+1 ๋ฌธ์ œ
let categories = try context.fetch(Category.fetchRequest())
for category in categories {
    // ๊ฐ ์นดํ…Œ๊ณ ๋ฆฌ๋งˆ๋‹ค ๋ณ„๋„ ์ฟผ๋ฆฌ ๋ฐœ์ƒ!
    print("\(category.name): \(category.memos?.count ?? 0)๊ฐœ")
}

// โœ… ์ข‹์€ ์˜ˆ: Prefetching ์‚ฌ์šฉ
let fetchRequest: NSFetchRequest<Category> = Category.fetchRequest()
fetchRequest.relationshipKeyPathsForPrefetching = ["memos"]

let categories = try context.fetch(fetchRequest)
for category in categories {
    // ์ด๋ฏธ ๋กœ๋“œ๋˜์–ด ์žˆ์–ด์„œ ์ถ”๊ฐ€ ์ฟผ๋ฆฌ ์—†์Œ!
    print("\(category.name): \(category.memos?.count ?? 0)๊ฐœ")
}

3. ํ•„์š”ํ•œ ์†์„ฑ๋งŒ ๊ฐ€์ ธ์˜ค๊ธฐ

let fetchRequest: NSFetchRequest<Memo> = Memo.fetchRequest()

// ์ œ๋ชฉ๊ณผ ๋‚ ์งœ๋งŒ ํ•„์š”ํ•œ ๊ฒฝ์šฐ
fetchRequest.propertiesToFetch = ["title", "createdAt"]
fetchRequest.resultType = .dictionaryResultType

let results = try context.fetch(fetchRequest) as! [[String: Any]]
for result in results {
    let title = result["title"] as? String
    let date = result["createdAt"] as? Date
    print("\(title ?? ""): \(date ?? Date())")
}

4. ๋ฐฑ๊ทธ๋ผ์šด๋“œ Context ํ™œ์šฉ

class DataImporter {
    let container: NSPersistentContainer
    
    init(container: NSPersistentContainer) {
        self.container = container
    }
    
    func importLargeDataset(data: [[String: Any]]) {
        // ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํ์—์„œ ์‹คํ–‰
        container.performBackgroundTask { context in
            // ๋ฐฐ์น˜ ์ž‘์—… ์ตœ์ ํ™”
            context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
            context.undoManager = nil  // Undo ๋น„ํ™œ์„ฑํ™”๋กœ ๋ฉ”๋ชจ๋ฆฌ ์ ˆ์•ฝ
            
            for (index, item) in data.enumerated() {
                let memo = Memo(context: context)
                memo.title = item["title"] as? String
                memo.content = item["content"] as? String
                memo.createdAt = Date()
                
                // 100๊ฐœ๋งˆ๋‹ค ์ €์žฅํ•˜๊ณ  ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ
                if index % 100 == 0 {
                    try? context.save()
                    context.reset()
                    
                    print("์ง„ํ–‰๋ฅ : \(index)/\(data.count)")
                }
            }
            
            // ๋งˆ์ง€๋ง‰ ์ €์žฅ
            try? context.save()
            
            DispatchQueue.main.async {
                print("โœ… ์ž„ํฌํŠธ ์™„๋ฃŒ!")
            }
        }
    }
}

โšก SQLite ์„ฑ๋Šฅ ์ตœ์ ํ™”

1. ์ธ๋ฑ์Šค ์ „๋žต

extension SQLiteManager {
    func optimizeDatabase() {
        do {
            // WHERE ์ ˆ์— ์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” ์ปฌ๋Ÿผ
            try db?.run("""
                CREATE INDEX IF NOT EXISTS idx_title 
                ON memos(title)
            """)
            
            // ๋ณตํ•ฉ ์ธ๋ฑ์Šค (์—ฌ๋Ÿฌ ์กฐ๊ฑด ๋™์‹œ ์‚ฌ์šฉ)
            try db?.run("""
                CREATE INDEX IF NOT EXISTS idx_category_date 
                ON memos(category, created_at DESC)
            """)
            
            // ๋ถ€๋ถ„ ์ธ๋ฑ์Šค (์กฐ๊ฑด๋ถ€)
            try db?.run("""
                CREATE INDEX IF NOT EXISTS idx_important 
                ON memos(created_at) 
                WHERE is_important = 1
            """)
            
            print("โœ… ์ธ๋ฑ์Šค ์ตœ์ ํ™” ์™„๋ฃŒ!")
        } catch {
            print("โŒ ์ตœ์ ํ™” ์‹คํŒจ: \(error)")
        }
    }
}

2. ํŠธ๋žœ์žญ์…˜ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ

func batchInsert(items: [[String: Any]]) {
    do {
        try db?.transaction {
            // ํŠธ๋žœ์žญ์…˜ ๋‚ด์—์„œ ์—ฌ๋Ÿฌ INSERT ์‹คํ–‰
            for item in items {
                let insert = memos.insert(
                    title <- item["title"] as! String,
                    content <- item["content"] as! String,
                    createdAt <- Date()
                )
                try db?.run(insert)
            }
        }
        
        print("โœ… \(items.count)๊ฐœ ํ•ญ๋ชฉ ์ผ๊ด„ ์‚ฝ์ž… ์™„๋ฃŒ!")
    } catch {
        print("โŒ ๋ฐฐ์น˜ ์‚ฝ์ž… ์‹คํŒจ: \(error)")
    }
}

// ์„ฑ๋Šฅ ๋น„๊ต
// ํŠธ๋žœ์žญ์…˜ ์—†์ด: 1000๊ฐœ ์‚ฝ์ž… โ†’ ์•ฝ 10์ดˆ
// ํŠธ๋žœ์žญ์…˜ ์‚ฌ์šฉ: 1000๊ฐœ ์‚ฝ์ž… โ†’ ์•ฝ 0.1์ดˆ (100๋ฐฐ ๋น ๋ฆ„!)

3. PRAGMA ์„ค์ • ์ตœ์ ํ™”

class OptimizedSQLiteManager {
    private var db: Connection?
    
    init() throws {
        let path = // ... ๊ฒฝ๋กœ ์„ค์ •
        db = try Connection(path)
        
        // ์„ฑ๋Šฅ ์ตœ์ ํ™” ์„ค์ •
        try optimizePragmas()
    }
    
    private func optimizePragmas() throws {
        // ์ €๋„ ๋ชจ๋“œ: WAL (Write-Ahead Logging)
        // ์ฝ๊ธฐ์™€ ์“ฐ๊ธฐ ๋™์‹œ ๊ฐ€๋Šฅ, ์„ฑ๋Šฅ ํ–ฅ์ƒ
        try db?.execute("PRAGMA journal_mode = WAL")
        
        // ๋™๊ธฐํ™” ๋ชจ๋“œ: NORMAL (์•ˆ์ „ํ•˜๋ฉด์„œ๋„ ๋น ๋ฆ„)
        try db?.execute("PRAGMA synchronous = NORMAL")
        
        // ์บ์‹œ ํฌ๊ธฐ ์ฆ๊ฐ€ (ํŽ˜์ด์ง€ ์ˆ˜, ์Œ์ˆ˜๋Š” KB)
        try db?.execute("PRAGMA cache_size = -10000")  // 10MB
        
        // ์ž„์‹œ ํŒŒ์ผ์„ ๋ฉ”๋ชจ๋ฆฌ์—
        try db?.execute("PRAGMA temp_store = MEMORY")
        
        // ๋ฉ”๋ชจ๋ฆฌ ๋งต ํฌ๊ธฐ (๋น ๋ฅธ ์ฝ๊ธฐ)
        try db?.execute("PRAGMA mmap_size = 30000000000")  // 30GB
        
        print("โœ… PRAGMA ์ตœ์ ํ™” ์™„๋ฃŒ!")
    }
}

4. ์ฟผ๋ฆฌ ์ตœ์ ํ™”

// โŒ ๋‚˜์œ ์˜ˆ: ์ „์ฒด ์Šค์บ”
let allMemos = try db?.prepare("""
    SELECT * FROM memos 
    WHERE title LIKE '%๊ฒ€์ƒ‰์–ด%'
""")

// โœ… ์ข‹์€ ์˜ˆ: ์ธ๋ฑ์Šค ํ™œ์šฉ
// Full-Text Search ํ…Œ์ด๋ธ” ์ƒ์„ฑ
try db?.run("""
    CREATE VIRTUAL TABLE IF NOT EXISTS memos_fts 
    USING fts5(title, content, content=memos, content_rowid=id)
""")

// FTS๋กœ ๋น ๋ฅธ ๊ฒ€์ƒ‰
let searchResults = try db?.prepare("""
    SELECT m.* FROM memos m
    JOIN memos_fts fts ON m.id = fts.rowid
    WHERE memos_fts MATCH '๊ฒ€์ƒ‰์–ด'
    ORDER BY rank
""")

// ์„ฑ๋Šฅ ์ฐจ์ด: 10,000๊ฐœ ๋ ˆ์ฝ”๋“œ ๊ธฐ์ค€
// LIKE ๊ฒ€์ƒ‰: ~500ms
// FTS ๊ฒ€์ƒ‰: ~10ms (50๋ฐฐ ๋น ๋ฆ„!)

5. ์—ฐ๊ฒฐ ํ’€๋ง

class ConnectionPool {
    private var connections: [Connection] = []
    private let maxConnections = 5
    private let queue = DispatchQueue(label: "connection.pool")
    
    func getConnection() -> Connection? {
        return queue.sync {
            if let connection = connections.popLast() {
                return connection
            }
            
            // ์ƒˆ ์—ฐ๊ฒฐ ์ƒ์„ฑ
            if let newConnection = try? Connection(/* path */) {
                return newConnection
            }
            
            return nil
        }
    }
    
    func returnConnection(_ connection: Connection) {
        queue.async {
            if self.connections.count < self.maxConnections {
                self.connections.append(connection)
            }
        }
    }
}

๐Ÿ“ˆ ์„ฑ๋Šฅ ์ธก์ •๊ณผ ํ”„๋กœํŒŒ์ผ๋ง

// ์‹คํ–‰ ์‹œ๊ฐ„ ์ธก์ • ์œ ํ‹ธ๋ฆฌํ‹ฐ
func measureTime(label: String, block: () -> Void) {
    let start = CFAbsoluteTimeGetCurrent()
    block()
    let end = CFAbsoluteTimeGetCurrent()
    let elapsed = (end - start) * 1000  // ๋ฐ€๋ฆฌ์ดˆ
    print("โฑ๏ธ \(label): \(String(format: "%.2f", elapsed))ms")
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
measureTime(label: "๋ฉ”๋ชจ 1000๊ฐœ ์กฐํšŒ") {
    let memos = SQLiteManager.shared.fetchAllMemos()
    print("์กฐํšŒ๋œ ๋ฉ”๋ชจ: \(memos.count)๊ฐœ")
}

// Core Data ์„ฑ๋Šฅ ๋””๋ฒ„๊น…
// Edit Scheme โ†’ Run โ†’ Arguments โ†’ Launch Arguments์— ์ถ”๊ฐ€:
// -com.apple.CoreData.SQLDebug 1  // SQL ์ฟผ๋ฆฌ ์ถœ๋ ฅ
// -com.apple.CoreData.Logging.stderr 1  // ์ƒ์„ธ ๋กœ๊ทธ
๐Ÿ’ก ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ฒดํฌ๋ฆฌ์ŠคํŠธ
  • โœ… ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ ๊ฐ€์ ธ์˜ค๊ธฐ (SELECT * ํ”ผํ•˜๊ธฐ)
  • โœ… ์ธ๋ฑ์Šค ์ ์ ˆํžˆ ์‚ฌ์šฉํ•˜๊ธฐ
  • โœ… ๋ฐฐ์น˜ ์ž‘์—…์€ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋ฌถ๊ธฐ
  • โœ… ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ์—์„œ ๋ฌด๊ฑฐ์šด ์ž‘์—… ์ฒ˜๋ฆฌ
  • โœ… ํŽ˜์ด์ง• ๊ตฌํ˜„ (๋ฌดํ•œ ์Šคํฌ๋กค)
  • โœ… ์บ์‹ฑ ์ „๋žต ์ˆ˜๋ฆฝ
  • โœ… ์ •๊ธฐ์ ์ธ VACUUM (SQLite)
  • โœ… ๋ถˆํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ
  • โœ… ํ”„๋กœํŒŒ์ผ๋ง ๋„๊ตฌ ํ™œ์šฉ (Instruments)
  • โœ… ์‹ค์ œ ๊ธฐ๊ธฐ์—์„œ ํ…Œ์ŠคํŠธ

๐ŸŽ“ ์‹ค์ „ ํ”„๋กœ์ ํŠธ: ์™„์ „ํ•œ ๋ฉ”๋ชจ ์•ฑ ๋งŒ๋“ค๊ธฐ

์ง€๊ธˆ๊นŒ์ง€ ๋ฐฐ์šด ๋‚ด์šฉ์„ ์ข…ํ•ฉํ•ด์„œ ์‹ค์ „ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž!
Core Data์™€ SQLite ๋‘ ๊ฐ€์ง€ ๋ฒ„์ „์œผ๋กœ ๊ตฌํ˜„ํ•ด๋ณผ๊ฒŒ. ๐Ÿš€

๐Ÿ“ฑ ์•ฑ ๊ธฐ๋Šฅ ๋ช…์„ธ

โœจ ์ฃผ์š” ๊ธฐ๋Šฅ โ€ข ๋ฉ”๋ชจ ์ž‘์„ฑ, ์ˆ˜์ •, ์‚ญ์ œ
โ€ข ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๋ถ„๋ฅ˜
โ€ข ์ค‘์š” ํ‘œ์‹œ (์ฆ๊ฒจ์ฐพ๊ธฐ)
โ€ข ์ „์ฒด ๊ฒ€์ƒ‰
โ€ข ์ •๋ ฌ (๋‚ ์งœ, ์ œ๋ชฉ, ์ค‘์š”๋„)
โ€ข ๋‹คํฌ ๋ชจ๋“œ ์ง€์›
โ€ข iCloud ๋™๊ธฐํ™” (Core Data ๋ฒ„์ „)
โ€ข ๋ฐ์ดํ„ฐ ๋‚ด๋ณด๋‚ด๊ธฐ/๊ฐ€์ ธ์˜ค๊ธฐ
โ€ข ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ

๐Ÿ—๏ธ ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„

// MVVM ํŒจํ„ด ์ ์šฉ
// Model: ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ
// View: SwiftUI ๋ทฐ
// ViewModel: ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง

// ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ
MyMemoApp/
โ”œโ”€โ”€ Models/
โ”‚   โ”œโ”€โ”€ Memo.swift
โ”‚   โ”œโ”€โ”€ Category.swift
โ”‚   โ””โ”€โ”€ MemoModel.xcdatamodeld
โ”œโ”€โ”€ ViewModels/
โ”‚   โ”œโ”€โ”€ MemoListViewModel.swift
โ”‚   โ”œโ”€โ”€ MemoDetailViewModel.swift
โ”‚   โ””โ”€โ”€ CategoryViewModel.swift
โ”œโ”€โ”€ Views/
โ”‚   โ”œโ”€โ”€ MemoListView.swift
โ”‚   โ”œโ”€โ”€ MemoDetailView.swift
โ”‚   โ”œโ”€โ”€ CategoryView.swift
โ”‚   โ””โ”€โ”€ SettingsView.swift
โ”œโ”€โ”€ Services/
โ”‚   โ”œโ”€โ”€ CoreDataManager.swift
โ”‚   โ”œโ”€โ”€ SQLiteManager.swift
โ”‚   โ””โ”€โ”€ SyncService.swift
โ””โ”€โ”€ Utilities/
    โ”œโ”€โ”€ Extensions.swift
    โ””โ”€โ”€ Constants.swift

๐Ÿ’พ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ์„ค๊ณ„

Core Data ๋ชจ๋ธ

// Entity: Memo
// Attributes:
// - id: UUID
// - title: String
// - content: String
// - createdAt: Date
// - updatedAt: Date
// - isImportant: Bool
// - color: String (hex)
// Relationships:
// - category: Category (To One)
// - tags: Tag (To Many)

// Entity: Category
// Attributes:
// - id: UUID
// - name: String
// - icon: String
// - color: String
// Relationships:
// - memos: Memo (To Many, Delete Rule: Cascade)

// Entity: Tag
// Attributes:
// - id: UUID
// - name: String
// Relationships:
// - memos: Memo (To Many)

SQLite ์Šคํ‚ค๋งˆ

CREATE TABLE categories (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL UNIQUE,
    icon TEXT,
    color TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE memos (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    content TEXT,
    category_id INTEGER,
    is_important INTEGER DEFAULT 0,
    color TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL
);

CREATE TABLE tags (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL UNIQUE
);

CREATE TABLE memo_tags (
    memo_id INTEGER,
    tag_id INTEGER,
    PRIMARY KEY (memo_id, tag_id),
    FOREIGN KEY (memo_id) REFERENCES memos(id) ON DELETE CASCADE,
    FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
);

-- ์ธ๋ฑ์Šค
CREATE INDEX idx_memos_category ON memos(category_id);
CREATE INDEX idx_memos_created ON memos(created_at DESC);
CREATE INDEX idx_memos_important ON memos(is_important, created_at DESC);

-- Full-Text Search
CREATE VIRTUAL TABLE memos_fts USING fts5(
    title, 
    content, 
    content=memos, 
    content_rowid=id
);

๐ŸŽจ ํ•ต์‹ฌ ๊ธฐ๋Šฅ ๊ตฌํ˜„

1. ๊ณ ๊ธ‰ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ

class SearchViewModel: ObservableObject {
    @Published var searchText = ""
    @Published var selectedCategory: Category?
    @Published var showImportantOnly = false
    @Published var sortOption: SortOption = .dateDescending
    @Published var results: [Memo] = []
    
    enum SortOption {
        case dateDescending
        case dateAscending
        case titleAscending
        case titleDescending
    }
    
    func performSearch() {
        let fetchRequest: NSFetchRequest<Memo> = Memo.fetchRequest()
        var predicates: [NSPredicate] = []
        
        // ๊ฒ€์ƒ‰์–ด ํ•„ํ„ฐ
        if !searchText.isEmpty {
            let searchPredicate = NSPredicate(
                format: "title CONTAINS[cd] %@ OR content CONTAINS[cd] %@",
                searchText, searchText
            )
            predicates.append(searchPredicate)
        }
        
        // ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ
        if let category = selectedCategory {
            predicates.append(NSPredicate(format: "category == %@", category))
        }
        
        // ์ค‘์š” ํ‘œ์‹œ ํ•„ํ„ฐ
        if showImportantOnly {
            predicates.append(NSPredicate(format: "isImportant == YES"))
        }
        
        // ๋ณตํ•ฉ ์กฐ๊ฑด
        if !predicates.isEmpty {
            fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
        }
        
        // ์ •๋ ฌ
        fetchRequest.sortDescriptors = getSortDescriptors()
        
        // ์‹คํ–‰
        do {
            results = try CoreDataManager.shared.context.fetch(fetchRequest)
        } catch {
            print("โŒ ๊ฒ€์ƒ‰ ์‹คํŒจ: \(error)")
        }
    }
    
    private func getSortDescriptors() -> [NSSortDescriptor] {
        switch sortOption {
        case .dateDescending:
            return [NSSortDescriptor(keyPath: \Memo.createdAt, ascending: false)]
        case .dateAscending:
            return [NSSortDescriptor(keyPath: \Memo.createdAt, ascending: true)]
        case .titleAscending:
            return [NSSortDescriptor(keyPath: \Memo.title, ascending: true)]
        case .titleDescending:
            return [NSSortDescriptor(keyPath: \Memo.title, ascending: false)]
        }
    }
}

2. ๋ฐ์ดํ„ฐ ๋‚ด๋ณด๋‚ด๊ธฐ/๊ฐ€์ ธ์˜ค๊ธฐ

class DataExportManager {
    // JSON์œผ๋กœ ๋‚ด๋ณด๋‚ด๊ธฐ
    func exportToJSON() -> URL? {
        let memos = CoreDataManager.shared.fetchAllMemos()
        
        let exportData = memos.map { memo in
            return [
                "id": memo.id?.uuidString ?? "",
                "title": memo.title ?? "",
                "content": memo.content ?? "",
                "createdAt": ISO8601DateFormatter().string(from: memo.createdAt ?? Date()),
                "isImportant": memo.isImportant,
                "category": memo.category?.name ?? ""
            ]
        }
        
        do {
            let jsonData = try JSONSerialization.data(withJSONObject: exportData, options: .prettyPrinted)
            
            let fileName = "memos_\(Date().timeIntervalSince1970).json"
            let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent(fileName)
            
            try jsonData.write(to: fileURL)
            print("โœ… ๋‚ด๋ณด๋‚ด๊ธฐ ์™„๋ฃŒ: \(fileURL)")
            
            return fileURL
        } catch {
            print("โŒ ๋‚ด๋ณด๋‚ด๊ธฐ ์‹คํŒจ: \(error)")
            return nil
        }
    }
    
    // JSON์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ
    func importFromJSON(url: URL) -> Bool {
        do {
            let jsonData = try Data(contentsOf: url)
            let importData = try JSONSerialization.jsonObject(with: jsonData) as! [[String: Any]]
            
            let context = CoreDataManager.shared.context
            let dateFormatter = ISO8601DateFormatter()
            
            for item in importData {
                let memo = Memo(context: context)
                memo.id = UUID(uuidString: item["id"] as? String ?? "") ?? UUID()
                memo.title = item["title"] as? String
                memo.content = item["content"] as? String
                memo.isImportant = item["isImportant"] as? Bool ?? false
                
                if let dateString = item["createdAt"] as? String {
                    memo.createdAt = dateFormatter.date(from: dateString)
                }
                
                // ์นดํ…Œ๊ณ ๋ฆฌ ์ฐพ๊ธฐ ๋˜๋Š” ์ƒ์„ฑ
                if let categoryName = item["category"] as? String, !categoryName.isEmpty {
                    let category = findOrCreateCategory(name: categoryName, context: context)
                    memo.category = category
                }
            }
            
            try context.save()
            print("โœ… ๊ฐ€์ ธ์˜ค๊ธฐ ์™„๋ฃŒ: \(importData.count)๊ฐœ")
            
            return true
        } catch {
            print("โŒ ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ: \(error)")
            return false
        }
    }
    
    private func findOrCreateCategory(name: String, context: NSManagedObjectContext) -> Category {
        let fetchRequest: NSFetchRequest<Category> = Category.fetchRequest()
        fetchRequest.predicate = NSPredicate(format: "name == %@", name)
        
        if let existing = try? context.fetch(fetchRequest).first {
            return existing
        }
        
        let newCategory = Category(context: context)
        newCategory.id = UUID()
        newCategory.name = name
        return newCategory
    }
}

3. ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ

class StatisticsViewModel: ObservableObject {
    @Published var totalMemos = 0
    @Published var importantMemos = 0
    @Published var todayMemos = 0
    @Published var weeklyMemos = 0
    @Published var categoryStats: [CategoryStat] = []
    @Published var dailyActivity: [DailyActivity] = []
    
    struct CategoryStat {
        let name: String
        let count: Int
        let percentage: Double
    }
    
    struct DailyActivity {
        let date: Date
        let count: Int
    }
    
    func loadStatistics() {
        let context = CoreDataManager.shared.context
        
        // ์ „์ฒด ๋ฉ”๋ชจ ์ˆ˜
        let totalRequest: NSFetchRequest<Memo> = Memo.fetchRequest()
        totalMemos = (try? context.count(for: totalRequest)) ?? 0
        
        // ์ค‘์š” ๋ฉ”๋ชจ ์ˆ˜
        let importantRequest: NSFetchRequest<Memo> = Memo.fetchRequest()
        importantRequest.predicate = NSPredicate(format: "isImportant == YES")
        importantMemos = (try? context.count(for: importantRequest)) ?? 0
        
        // ์˜ค๋Š˜ ์ž‘์„ฑํ•œ ๋ฉ”๋ชจ
        let today = Calendar.current.startOfDay(for: Date())
        let todayRequest: NSFetchRequest<Memo> = Memo.fetchRequest()
        todayRequest.predicate = NSPredicate(format: "createdAt >= %@", today as NSDate)
        todayMemos = (try? context.count(for: todayRequest)) ?? 0
        
        // ์ด๋ฒˆ ์ฃผ ๋ฉ”๋ชจ
        let weekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date())!
        let weekRequest: NSFetchRequest<Memo> = Memo.fetchRequest()
        weekRequest.predicate = NSPredicate(format: "createdAt >= %@", weekAgo as NSDate)
        weeklyMemos = (try? context.count(for: weekRequest)) ?? 0
        
        // ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ํ†ต๊ณ„
        loadCategoryStatistics()
        
        // ์ผ๋ณ„ ํ™œ๋™
        loadDailyActivity()
    }
    
    private func loadCategoryStatistics() {
        let context = CoreDataManager.shared.context
        let categoryRequest: NSFetchRequest<Category> = Category.fetchRequest()
        
        guard let categories = try? context.fetch(categoryRequest) else { return }
        
        categoryStats = categories.map { category in
            let count = category.memos?.count ?? 0
            let percentage = totalMemos > 0 ? Double(count) / Double(totalMemos) * 100 : 0
            
            return CategoryStat(
                name: category.name ?? "๋ฏธ๋ถ„๋ฅ˜",
                count: count,
                percentage: percentage
            )
        }.sorted { $0.count > $1.count }
    }
    
    private func loadDailyActivity() {
        let context = CoreDataManager.shared.context
        let calendar = Calendar.current
        
        // ์ตœ๊ทผ 30์ผ
        dailyActivity = (0..<30).compactMap { daysAgo in
            let date = calendar.date(byAdding: .day, value: -daysAgo, to: Date())!
            let startOfDay = calendar.startOfDay(for: date)
            let endOfDay = calendar.date(byAdding: .day, value: 1, to: startOfDay)!
            
            let request: NSFetchRequest<Memo> = Memo.fetchRequest()
            request.predicate = NSPredicate(
                format: "createdAt >= %@ AND createdAt < %@",
                startOfDay as NSDate,
                endOfDay as NSDate
            )
            
            let count = (try? context.count(for: request)) ?? 0
            
            return DailyActivity(date: startOfDay, count: count)
        }.reversed()
    }
}

// SwiftUI ๋ทฐ
struct StatisticsView: View {
    @StateObject private var viewModel = StatisticsViewModel()
    
    var body: some View {
        ScrollView {
            VStack(spacing: 20) {
                // ์š”์•ฝ ์นด๋“œ
                LazyVGrid(columns: [GridItem(), GridItem()], spacing: 15) {
                    StatCard(title: "์ „์ฒด ๋ฉ”๋ชจ", value: "\(viewModel.totalMemos)", icon: "doc.text")
                    StatCard(title: "์ค‘์š” ๋ฉ”๋ชจ", value: "\(viewModel.importantMemos)", icon: "star.fill")
                    StatCard(title: "์˜ค๋Š˜", value: "\(viewModel.todayMemos)", icon: "calendar")
                    StatCard(title: "์ด๋ฒˆ ์ฃผ", value: "\(viewModel.weeklyMemos)", icon: "chart.line.uptrend.xyaxis")
                }
                
                // ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ํ†ต๊ณ„
                VStack(alignment: .leading) {
                    Text("์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๋ถ„ํฌ")
                        .font(.headline)
                    
                    ForEach(viewModel.categoryStats, id: \.name) { stat in
                        HStack {
                            Text(stat.name)
                            Spacer()
                            Text("\(stat.count)๊ฐœ")
                            Text("(\(String(format: "%.1f", stat.percentage))%)")
                                .foregroundColor(.secondary)
                        }
                    }
                }
                .padding()
                .background(Color(.systemGray6))
                .cornerRadius(10)
                
                // ์ผ๋ณ„ ํ™œ๋™ ๊ทธ๋ž˜ํ”„ (๊ฐ„๋‹จํ•œ ๋ง‰๋Œ€ ๊ทธ๋ž˜ํ”„)
                VStack(alignment: .leading) {
                    Text("์ตœ๊ทผ 30์ผ ํ™œ๋™")
                        .font(.headline)
                    
                    // ์—ฌ๊ธฐ์— ์ฐจํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
                    // ์˜ˆ: Charts framework (iOS 16+)
                }
                .padding()
                .background(Color(.systemGray6))
                .cornerRadius(10)
            }
            .padding()
        }
        .navigationTitle("ํ†ต๊ณ„ ๐Ÿ“Š")
        .onAppear {
            viewModel.loadStatistics()
        }
    }
}

struct StatCard: View {
    let title: String
    let value: String
    let icon: String
    
    var body: some View {
        VStack {
            Image(systemName: icon)
                .font(.largeTitle)
                .foregroundColor(.blue)
            
            Text(value)
                .font(.title)
                .fontWeight(.bold)
            
            Text(title)
                .font(.caption)
                .foregroundColor(.secondary)
        }
        .frame(maxWidth: .infinity)
        .padding()
        .background(Color(.systemGray6))
        .cornerRadius(10)
    }
}
๐Ÿ’ก ์‹ค์ „ ๊ฐœ๋ฐœ ํŒ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์„ ๋ฐฐ์šฐ๋‹ค ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด, ์žฌ๋Šฅ๋„ท์—์„œ ๊ฒฝํ—˜ ๋งŽ์€ ๊ฐœ๋ฐœ์ž์—๊ฒŒ ๋ฉ˜ํ† ๋ง์„ ๋ฐ›์•„๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ!
์‹ค๋ฌด์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค์™€ ๋…ธํ•˜์šฐ๋ฅผ ์ง์ ‘ ๋ฐฐ์šธ ์ˆ˜ ์žˆ์–ด. ํŠนํžˆ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„๋‚˜ ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ฐ™์€ ๋ถ€๋ถ„์€ ๊ฒฝํ—˜์ด ์ •๋ง ์ค‘์š”ํ•˜๊ฑฐ๋“ ! ๐ŸŽฏ

4. ์˜คํ”„๋ผ์ธ ๋™๊ธฐํ™” (Core Data + CloudKit)

// iOS 13+์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
lazy var persistentContainer: NSPersistentCloudKitContainer = {
    let container = NSPersistentCloudKitContainer(name: "MyMemoApp")
    
    // CloudKit ๋™๊ธฐํ™” ์„ค์ •
    guard let description = container.persistentStoreDescriptions.first else {
        fatalError("์„ค์ • ์‹คํŒจ")
    }
    
    // ์ž๋™ ๋™๊ธฐํ™” ํ™œ์„ฑํ™”
    description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(
        containerIdentifier: "iCloud.com.yourcompany.MyMemoApp"
    )
    
    // ๋™๊ธฐํ™” ์•Œ๋ฆผ ๊ตฌ๋…
    NotificationCenter.default.addObserver(
        self,
        selector: #selector(handleCloudKitSync),
        name: NSPersistentCloudKitContainer.eventChangedNotification,
        object: nil
    )
    
    container.loadPersistentStores { description, error in
        if let error = error {
            fatalError("Core Data ๋กœ๋“œ ์‹คํŒจ: \(error)")
        }
        
        // ์ž๋™ ๋ณ‘ํ•ฉ ํ™œ์„ฑํ™”
        container.viewContext.automaticallyMergesChangesFromParent = true
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
    }
    
    return container
}()

@objc private func handleCloudKitSync(_ notification: Notification) {
    if let event = notification.userInfo?[NSPersistentCloudKitContainer.eventNotificationUserInfoKey] 
        as? NSPersistentCloudKitContainer.Event {
        
        switch event.type {
        case .setup:
            print("โ˜๏ธ CloudKit ์„ค์ • ์™„๋ฃŒ")
        case .import:
            print("โฌ‡๏ธ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ ์™„๋ฃŒ")
        case .export:
            print("โฌ†๏ธ ๋ฐ์ดํ„ฐ ๋‚ด๋ณด๋‚ด๊ธฐ ์™„๋ฃŒ")
        @unknown default:
            break
        }
        
        if let error = event.error {
            print("โŒ ๋™๊ธฐํ™” ์˜ค๋ฅ˜: \(error)")
        }
    }
}

๐Ÿ› ๋””๋ฒ„๊น…๊ณผ ๋ฌธ์ œ ํ•ด๊ฒฐ

๊ฐœ๋ฐœํ•˜๋‹ค ๋ณด๋ฉด ํ•ญ์ƒ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ์ง€! ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ์ด์Šˆ๋“ค๊ณผ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž. ๐Ÿ”ง

๐Ÿšจ Core Data ์ผ๋ฐ˜์ ์ธ ๋ฌธ์ œ๋“ค

๋ฌธ์ œ 1: "The model used to open the store is incompatible" ์›์ธ: ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์ด ๋ณ€๊ฒฝ๋˜์—ˆ๋Š”๋ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด ์•ˆ ๋จ

ํ•ด๊ฒฐ:
โ€ข ๊ฐœ๋ฐœ ์ค‘: ์•ฑ ์‚ญ์ œ ํ›„ ์žฌ์„ค์น˜
โ€ข ํ”„๋กœ๋•์…˜: ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ตฌํ˜„ ํ•„์ˆ˜
โ€ข Lightweight Migration ํ™œ์„ฑํ™” ํ™•์ธ
๋ฌธ์ œ 2: ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ์›์ธ: Context๋‚˜ ๊ฐ์ฒด๋ฅผ ๊ฐ•ํ•˜๊ฒŒ ์ฐธ์กฐ

ํ•ด๊ฒฐ:
// โŒ ๋‚˜์œ ์˜ˆ
class MemoViewController {
    var context: NSManagedObjectContext  // ๊ฐ•ํ•œ ์ฐธ์กฐ
    var memos: [Memo] = []  // ๋ฉ”๋ชจ๋ฆฌ ๊ณ„์† ์ฆ๊ฐ€
}

// โœ… ์ข‹์€ ์˜ˆ
class MemoViewController {
    weak var context: NSManagedObjectContext?  // ์•ฝํ•œ ์ฐธ์กฐ
    
    func loadMemos() {
        // ํ•„์š”ํ•  ๋•Œ๋งŒ ๊ฐ€์ ธ์˜ค๊ธฐ
        let fetchRequest: NSFetchRequest<Memo> = Memo.fetchRequest()
        fetchRequest.fetchBatchSize = 20
        
        // ์‚ฌ์šฉ ํ›„ ํ•ด์ œ
        defer {
            context?.reset()  // ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ
        }
    }
}
๋ฌธ์ œ 3: "Object has been deleted" ์›์ธ: ์ด๋ฏธ ์‚ญ์ œ๋œ ๊ฐ์ฒด์— ์ ‘๊ทผ

ํ•ด๊ฒฐ:
// ๊ฐ์ฒด๊ฐ€ ์œ ํšจํ•œ์ง€ ํ™•์ธ
if !memo.isFault && !memo.isDeleted {
    // ์•ˆ์ „ํ•˜๊ฒŒ ์‚ฌ์šฉ
    print(memo.title ?? "")
}

// ๋˜๋Š” try-catch๋กœ ๊ฐ์‹ธ๊ธฐ
do {
    let title = memo.title
} catch {
    print("๊ฐ์ฒด๊ฐ€ ์‚ญ์ œ๋˜์—ˆ๊ฑฐ๋‚˜ ์ ‘๊ทผ ๋ถˆ๊ฐ€")
}

๐Ÿ” SQLite ์ผ๋ฐ˜์ ์ธ ๋ฌธ์ œ๋“ค

๋ฌธ์ œ 1: "Database is locked" ์›์ธ: ๋™์‹œ์— ์—ฌ๋Ÿฌ ๊ณณ์—์„œ ์“ฐ๊ธฐ ์‹œ๋„

ํ•ด๊ฒฐ:
// WAL ๋ชจ๋“œ ํ™œ์„ฑํ™”
try db?.execute("PRAGMA journal_mode = WAL")

// ๋˜๋Š” ์žฌ์‹œ๋„ ๋กœ์ง
func executeWithRetry(maxAttempts: Int = 3, query: () throws -> Void) {
    var attempts = 0
    
    while attempts < maxAttempts {
        do {
            try query()
            return
        } catch {
            attempts += 1
            if attempts >= maxAttempts {
                print("โŒ ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜ ์ดˆ๊ณผ")
                throw error
            }
            Thread.sleep(forTimeInterval: 0.1)  // 100ms ๋Œ€๊ธฐ
        }
    }
}
๋ฌธ์ œ 2: ์„ฑ๋Šฅ ์ €ํ•˜ ์›์ธ: ์ธ๋ฑ์Šค ๋ถ€์กฑ, ๋น„ํšจ์œจ์ ์ธ ์ฟผ๋ฆฌ

ํ•ด๊ฒฐ:
// EXPLAIN QUERY PLAN์œผ๋กœ ๋ถ„์„
let query = "SELECT * FROM memos WHERE title LIKE '%๊ฒ€์ƒ‰%'"
let plan = try db?.prepare("EXPLAIN QUERY PLAN \(query)")

for row in plan! {
    print(row)
}

// ์ถœ๋ ฅ ์˜ˆ์‹œ:
// SCAN TABLE memos  โ† ์ „์ฒด ์Šค์บ” (๋А๋ฆผ)
// SEARCH TABLE memos USING INDEX idx_title  โ† ์ธ๋ฑ์Šค ์‚ฌ์šฉ (๋น ๋ฆ„)

// ์ธ๋ฑ์Šค ์ถ”๊ฐ€
try db?.run("CREATE INDEX IF NOT EXISTS idx_title ON memos(title)")
๋ฌธ์ œ 3: ๋ฐ์ดํ„ฐ ์†์ƒ ์›์ธ: ๋น„์ •์ƒ ์ข…๋ฃŒ, ๋””์Šคํฌ ์˜ค๋ฅ˜

ํ•ด๊ฒฐ:
// ๋ฌด๊ฒฐ์„ฑ ๊ฒ€์‚ฌ
func checkDatabaseIntegrity() -> Bool {
    do {
        let result = try db?.scalar("PRAGMA integrity_check") as? String
        
        if result == "ok" {
            print("โœ… ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ •์ƒ")
            return true
        } else {
            print("โŒ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์†์ƒ: \(result ?? "")")
            return false
        }
    } catch {
        print("โŒ ๊ฒ€์‚ฌ ์‹คํŒจ: \(error)")
        return false
    }
}

// ๋ณต๊ตฌ ์‹œ๋„
func repairDatabase() {
    // 1. ๋ฐฑ์—…์—์„œ ๋ณต๊ตฌ
    // 2. ๋˜๋Š” VACUUM์œผ๋กœ ์žฌ๊ตฌ์„ฑ
    try? db?.execute("VACUUM")
}

๐Ÿ› ๏ธ ๋””๋ฒ„๊น… ๋„๊ตฌ์™€ ํŒ

// 1. Core Data ๋””๋ฒ„๊น… ํ”Œ๋ž˜๊ทธ
// Edit Scheme โ†’ Run โ†’ Arguments โ†’ Launch Arguments
// -com.apple.CoreData.SQLDebug 3
// -com.apple.CoreData.ConcurrencyDebug 1

// 2. SQLite ์ฟผ๋ฆฌ ๋กœ๊น…
extension Connection {
    func enableLogging() {
        self.trace { sql in
            print("๐Ÿ” SQL: \(sql)")
        }
    }
}

// 3. ์„ฑ๋Šฅ ํ”„๋กœํŒŒ์ผ๋ง
func profileQuery(name: String, query: () throws -> Void) {
    let start = CFAbsoluteTimeGetCurrent()
    
    do {
        try query()
    } catch {
        print("โŒ ์ฟผ๋ฆฌ ์‹คํŒจ: \(error)")
    }
    
    let elapsed = (CFAbsoluteTimeGetCurrent() - start) * 1000
    print("โฑ๏ธ \(name): \(String(format: "%.2f", elapsed))ms")
}

// 4. ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ฒดํฌ
func printMemoryUsage() {
    var info = mach_task_basic_info()
    var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size)/4
    
    let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) {
        $0.withMemoryRebound(to: integer_t.self, capacity: 1) {
            task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
        }
    }
    
    if kerr == KERN_SUCCESS {
        let usedMB = Double(info.resident_size) / 1024.0 / 1024.0
        print("๐Ÿ’พ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰: \(String(format: "%.2f", usedMB))MB")
    }
}
๐Ÿ’ก ๋””๋ฒ„๊น… ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค 1. ๋กœ๊น… ์ฒด๊ณ„ํ™”: ์ผ๊ด€๋œ ๋กœ๊ทธ ํฌ๋งท ์‚ฌ์šฉ
2. ์—๋Ÿฌ ํ•ธ๋“ค๋ง: ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ž‘์—…์— try-catch
3. ๋‹จ์œ„ ํ…Œ์ŠคํŠธ: ๋ฐ์ดํ„ฐ ๋กœ์ง ํ…Œ์ŠคํŠธ ์ž‘์„ฑ
4. ํ”„๋กœํŒŒ์ผ๋ง: Instruments๋กœ ์ •๊ธฐ์  ๋ถ„์„
5. ํฌ๋ž˜์‹œ ๋ฆฌํฌํŠธ: Firebase Crashlytics ๋“ฑ ํ™œ์šฉ
6. ๋ฒ„์ „ ๊ด€๋ฆฌ: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ๋ฒ„์ „ ์ถ”์ 
7. ๋ฐฑ์—… ์ „๋žต: ์ค‘์š” ์ž‘์—… ์ „ ํ•ญ์ƒ ๋ฐฑ์—…

๐ŸŽฏ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€! ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์–ด. ๐Ÿ˜… Core Data์™€ SQLite๋ฅผ ํ™œ์šฉํ•œ iOS ๋ฐ์ดํ„ฐ ์˜์†ํ™”์— ๋Œ€ํ•ด ๊นŠ์ด ์žˆ๊ฒŒ ์•Œ์•„๋ดค์–ด.

ํ•ต์‹ฌ ๋‚ด์šฉ์„ ๋‹ค์‹œ ํ•œ๋ฒˆ ์ •๋ฆฌํ•ด๋ณผ๊นŒ? ๐Ÿ“

๐ŸŒŸ ํ•ต์‹ฌ ์š”์•ฝ 1. Core Data
โ€ข Apple์˜ ๊ฐ•๋ ฅํ•œ ๊ฐ์ฒด ๊ทธ๋ž˜ํ”„ ๊ด€๋ฆฌ ํ”„๋ ˆ์ž„์›Œํฌ
โ€ข Swift์™€ ์ž์—ฐ์Šค๋Ÿฌ์šด ํ†ตํ•ฉ
โ€ข ๋ณต์žกํ•œ ๊ด€๊ณ„ ์ฒ˜๋ฆฌ์— ์œ ๋ฆฌ
โ€ข iCloud ๋™๊ธฐํ™” ์ง€์›
โ€ข ๋Œ€๋ถ€๋ถ„์˜ iOS ์•ฑ์— ์ ํ•ฉ

2. SQLite
โ€ข ๊ฒฝ๋Ÿ‰ ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค
โ€ข ์ง์ ‘์ ์ธ SQL ์ œ์–ด
โ€ข ํฌ๋กœ์Šค ํ”Œ๋žซํผ ํ˜ธํ™˜์„ฑ
โ€ข ์ตœ๋Œ€ ์„ฑ๋Šฅ ์ถ”๊ตฌ ์‹œ ์œ ๋ฆฌ
โ€ข ๋ณต์žกํ•œ ์ฟผ๋ฆฌ์™€ ๋ถ„์„์— ์ ํ•ฉ

3. ์„ ํƒ ๊ธฐ์ค€
โ€ข ํ”„๋กœ์ ํŠธ ์š”๊ตฌ์‚ฌํ•ญ ๋ถ„์„
โ€ข ํŒ€์˜ ๊ธฐ์ˆ  ์Šคํƒ ๊ณ ๋ ค
โ€ข ์„ฑ๋Šฅ vs ๊ฐœ๋ฐœ ์†๋„ ํŠธ๋ ˆ์ด๋“œ์˜คํ”„
โ€ข ์žฅ๊ธฐ์  ์œ ์ง€๋ณด์ˆ˜ ๊ณ ๋ ค
๐Ÿ’ช ์‹ค์ „ ๊ฐœ๋ฐœ ์ฒดํฌ๋ฆฌ์ŠคํŠธ
  • โœ… ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ์„ค๊ณ„ (์ •๊ทœํ™”, ๊ด€๊ณ„ ์„ค์ •)
  • โœ… ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ „๋žต ์ˆ˜๋ฆฝ
  • โœ… ์„ฑ๋Šฅ ์ตœ์ ํ™” (์ธ๋ฑ์Šค, ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ)
  • โœ… ๋ณด์•ˆ ๊ณ ๋ ค (์•”ํ˜ธํ™”, Keychain)
  • โœ… ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๊ตฌํ˜„
  • โœ… ๋ฐฑ์—… ๋ฐ ๋ณต๊ตฌ ๋ฉ”์ปค๋‹ˆ์ฆ˜
  • โœ… ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ
  • โœ… ๋ฌธ์„œํ™”
  • โœ… ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๋กœ๊น…
  • โœ… ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ ์ˆ˜์ง‘

๋ฐ์ดํ„ฐ ์˜์†ํ™”๋Š” ์•ฑ ๊ฐœ๋ฐœ์˜ ํ•ต์‹ฌ์ด์•ผ. ์‚ฌ์šฉ์ž์˜ ์†Œ์ค‘ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๋ณด๊ด€ํ•˜๊ณ ,
๋น ๋ฅด๊ฒŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” ๊ฒƒ์ด ์šฐ๋ฆฌ์˜ ์ฑ…์ž„์ด์ง€! ๐Ÿ’พ

์ฒ˜์Œ์—๋Š” ์–ด๋ ต๊ฒŒ ๋А๊ปด์งˆ ์ˆ˜ ์žˆ์ง€๋งŒ, ํ•˜๋‚˜์”ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ์ตํžˆ๋‹ค ๋ณด๋ฉด
์–ด๋А์ƒˆ ๋Šฅ์ˆ™ํ•˜๊ฒŒ ๋‹ค๋ฃจ๊ณ  ์žˆ๋Š” ์ž์‹ ์„ ๋ฐœ๊ฒฌํ•˜๊ฒŒ ๋  ๊ฑฐ์•ผ. ๐Ÿš€

Core Data๋“  SQLite๋“ , ์ค‘์š”ํ•œ ๊ฑด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด์•ผ.
๋น ๋ฅด๊ณ  ์•ˆ์ •์ ์ด๋ฉฐ ์•ˆ์ „ํ•œ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ๋ฅผ ํ†ตํ•ด ์ตœ๊ณ ์˜ ์•ฑ์„ ๋งŒ๋“ค์–ด๋ณด์ž! โœจ

๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๊ฑฐ๋‚˜ ๋” ๊นŠ์ด ๋ฐฐ์šฐ๊ณ  ์‹ถ๋‹ค๋ฉด, ์‹ค๋ฌด ๊ฒฝํ—˜์ด ํ’๋ถ€ํ•œ ๊ฐœ๋ฐœ์ž๋“ค๊ณผ
๊ต๋ฅ˜ํ•˜๋Š” ๊ฒƒ๋„ ํฐ ๋„์›€์ด ๋ผ. ํ•จ๊ป˜ ์„ฑ์žฅํ•˜๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ๋˜์ž! ๐ŸŒฑ

๐ŸŽ“ ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ์ด ๊ธ€์„ ํ†ตํ•ด ๊ธฐ๋ณธ๊ธฐ๋ฅผ ๋‹ค์กŒ๋‹ค๋ฉด, ์ด์ œ ์‹ค์ „ ํ”„๋กœ์ ํŠธ์— ๋„์ „ํ•ด๋ณด์ž!
โ€ข ๊ฐ„๋‹จํ•œ ๋ฉ”๋ชจ ์•ฑ๋ถ€ํ„ฐ ์‹œ์ž‘
โ€ข ์ ์ฐจ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ (๊ฒ€์ƒ‰, ์นดํ…Œ๊ณ ๋ฆฌ, ๋™๊ธฐํ™” ๋“ฑ)
โ€ข ์˜คํ”ˆ์†Œ์Šค ํ”„๋กœ์ ํŠธ ์ฐธ์—ฌ
โ€ข ๊ฐœ์ธ ํฌํŠธํด๋ฆฌ์˜ค ๊ตฌ์ถ•

๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด ์ฃผ์ €ํ•˜์ง€ ๋ง๊ณ  ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์งˆ๋ฌธํ•˜๊ฑฐ๋‚˜,
์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ! ๐Ÿ’ช

ํ–‰๋ณตํ•œ ์ฝ”๋”ฉ ๋˜์„ธ์š”! ๐ŸŽ‰
Happy Coding! ๐Ÿ’ปโœจ

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.