Ver3.0 ๐ฑ 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: Apple์ ๊ฐ๋ ฅํ ๋ฐ์ดํฐ ๊ด๋ฆฌ ํ๋ ์์ํฌ
Core Data๋ ๋จ์ํ ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ์๋์ผ. Apple์ด ๋ง๋ ๊ฐ์ฒด ๊ทธ๋ํ ๊ด๋ฆฌ ํ๋ ์์ํฌ๋ผ๊ณ ํ ์ ์์ด.
์ฝ๊ฒ ๋งํ๋ฉด, Swift ๊ฐ์ฒด๋ฅผ ๊ทธ๋๋ก ์ ์ฅํ๊ณ ๋ถ๋ฌ์ฌ ์ ์๊ฒ ํด์ฃผ๋ ๋ง๋ฒ ๊ฐ์ ๋๊ตฌ์ง! โจ
๋ง์ ๊ฐ๋ฐ์๋ค์ด Core Data๋ฅผ ์ด๋ ค์ํ๋๋ฐ, ์ฌ์ค ๊ฐ๋
๋ง ์ ๋๋ก ์ดํดํ๋ฉด ์ ๋ง ํธ๋ฆฌํ ์น๊ตฌ์ผ.
๐ Core Data์ ํต์ฌ ๊ฐ๋ ๋ค
Entity(์ํฐํฐ)๋ฅผ ๋ง๋ค๊ณ , ๊ฐ Entity์ Attribute(์์ฑ)์ Relationship(๊ด๊ณ)์ ์ ์ํ์ง.
์๋ฅผ ๋ค์ด 'Person' Entity๋ฅผ ๋ง๋ค๊ณ , name(๋ฌธ์์ด), age(์ ์), birthDate(๋ ์ง) ๊ฐ์ ์์ฑ์ ์ถ๊ฐํ๋ ๊ฑฐ์ผ.
๋ฉ๋ชจ์ฅ์์ ๊ธ์ ์ฐ๊ณ ์์ ํ๋ค๊ฐ, ์ ์ฅ ๋ฒํผ์ ๋๋ฌ์ผ ํ์ผ๋ก ์ ์ฅ๋์์?
Context๋ ๋ง์ฐฌ๊ฐ์ง์ผ. ์ฌ๊ธฐ์ ๊ฐ์ฒด๋ฅผ ์์ฑํ๊ณ ์์ ํ๋ค๊ฐ, save()๋ฅผ ํธ์ถํด์ผ ์ค์ ๋ก ์ ์ฅ๋ผ! ๐พ
SQLite ํ์ผ, XML, ๋ฐ์ด๋๋ฆฌ ๋ฑ ๋ค์ํ ํํ๋ก ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ ์ ์๊ฒ ์กฐ์จํด์ค.
์์ฆ์ ๋๋ถ๋ถ ์ด๊ฑธ ์ฌ์ฉํด! ๐
๐ ๏ธ 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๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํ ๊ธฐ๋ณธ ๋งค๋์ ํด๋์ค์ผ. ์ฑ๊ธํค ํจํด์ผ๋ก ๋ง๋ค์ด์ ์ฑ ์ ์ฒด์์ ํ๋์ ์ธ์คํด์ค๋ง ์ฌ์ฉํ๋๋ก ํ์ด. ๐ฏ
.xcdatamodeld ํ์ผ์ ์ด๊ณ "Add Entity" ๋ฒํผ์ ํด๋ฆญํด์ Entity๋ฅผ ๋ง๋ค์ด๋ณด์.
์๋ฅผ ๋ค์ด ๊ฐ๋จํ ๋ฉ๋ชจ ์ฑ์ ๋ง๋ ๋ค๊ณ ๊ฐ์ ํด๋ณผ๊ฒ. 'Memo'๋ผ๋ Entity๋ฅผ ๋ง๋ค๊ณ ๋ค์ ์์ฑ๋ค์ ์ถ๊ฐํด๋ณด์:
| ์์ฑ ์ด๋ฆ | ํ์ | ์ค๋ช |
|---|---|---|
| id | UUID | ๊ณ ์ ์๋ณ์ |
| title | String | ๋ฉ๋ชจ ์ ๋ชฉ |
| content | String | ๋ฉ๋ชจ ๋ด์ฉ |
| createdAt | Date | ์์ฑ ๋ ์ง |
| isImportant | Boolean | ์ค์ ํ์ |
๊ทธ๋ฆฌ๊ณ Codegen์ "Class Definition"์ผ๋ก ์ค์ ํ๋ฉด Xcode๊ฐ ์๋์ผ๋ก ํด๋์ค๋ฅผ ์์ฑํด์ค์ ํธ๋ฆฌํด. ๐
์ด์ ์ค์ ๋ก ๋ฉ๋ชจ๋ฅผ ์ ์ฅํด๋ณด์!
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์ ํฐ ์ฅ์ ์ด์ผ. ๐
์ ์ฅํ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋ ค๋ฉด 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 []
}
}
์์ฃผ ์ฌ์ฉํ๋ ํจํด๋ค:
โข format: "title CONTAINS[cd] %@", searchText - ์ ๋ชฉ์ ํน์ ํ ์คํธ ํฌํจ (๋์๋ฌธ์ ๋ฌด์)
โข format: "createdAt >= %@", startDate - ํน์ ๋ ์ง ์ดํ
โข format: "age > 20 AND age < 30" - ๋ณตํฉ ์กฐ๊ฑด
โข format: "title IN %@", ["๋ฉ๋ชจ1", "๋ฉ๋ชจ2"] - ์ฌ๋ฌ ๊ฐ ์ค ํ๋
๊ธฐ์กด ๋ฐ์ดํฐ๋ฅผ ์์ ํ๋ ๊ฑด ์ ๋ง ๊ฐ๋จํด!
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()๋ง ํธ์ถํ๋ฉด ๋ผ. ์ ๋ง ์ง๊ด์ ์ด์ง? ๐
์ญ์ ๋ ๋ง์ฐฌ๊ฐ์ง๋ก ๊ฐ๋จํด.
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'์ ๊ด๊ณ๋ฅผ ๋ง๋ค์ด๋ณผ๊น? ๐
โข ํ๋์ 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๋ก! ๐
โข Nullify: ๋ถ๋ชจ๊ฐ ์ญ์ ๋๋ฉด ์์์ ๊ด๊ณ๋ง null๋ก (์์์ ์ ์ง)
โข Cascade: ๋ถ๋ชจ๊ฐ ์ญ์ ๋๋ฉด ์์๋ ํจ๊ป ์ญ์
โข Deny: ์์์ด ์์ผ๋ฉด ๋ถ๋ชจ ์ญ์ ๋ถ๊ฐ
โข No Action: ์๋ฌด ์ฒ๋ฆฌ ์ ํจ (์ํํ ์ ์์ด!)
๋ณดํต Category-Memo ๊ด๊ณ์์๋ Cascade๋ฅผ ๋ง์ด ์ฌ์ฉํด. ์นดํ ๊ณ ๋ฆฌ๋ฅผ ์ง์ฐ๋ฉด ๊ทธ ์์ ๋ฉ๋ชจ๋ ํจ๊ป ์ญ์ ๋๋๋ก!
โก Core Data ์ฑ๋ฅ ์ต์ ํ ํ
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()
}
}
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)
}
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("โ
๋ฐฑ๊ทธ๋ผ์ด๋ ์์
์๋ฃ!")
}
}
}
์ค๋ฌด ๊ฒฝํ์ด ํ๋ถํ ๊ฐ๋ฐ์๋ค์ด ํ๋ก์ ํธ์ ๋ง๋ ๋ฐ์ดํฐ ๋ชจ๋ธ ์ค๊ณ๋ถํฐ ์ต์ ํ๊น์ง ๋์์ค ์ ์์ด. ๐
๐๏ธ 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๋ก ์์ ๋ฅผ ๋ง๋ค์ด๋ณผ๊ฒ! ๐ฏ
Swift Package Manager๋ก ๊ฐ๋จํ๊ฒ ์ค์นํ ์ ์์ด.
Xcode์์ File โ Add Packages... โ ๊ฒ์์ฐฝ์ ์
๋ ฅ:
https://github.com/stephencelis/SQLite.swift
๋๋ CocoaPods๋ฅผ ์ฌ์ฉํ๋ค๋ฉด:
pod 'SQLite.swift', '~> 0.14.1'
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์ค๋ฌ์ด ๋ฐฉ์์ด์ง. ๐ก๏ธ
์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ค๋ค๋ณด์!
๐ 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)")
}
}
}
โข 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 |
|---|---|---|
| ํ์ต ๊ณก์ | ์ค๊ฐ (๊ฐ๋ ์ดํด ํ์) | ๋์ (SQL ์ง์ ํ์) |
| ๊ฐ๋ฐ ์๋ | ๋น ๋ฆ (์๋ ์์ฑ ๋ง์) | ๋๋ฆผ (์ง์ ๊ตฌํ) |
| ํ์ ์์ ์ฑ | ๋์ (Swift ๊ฐ์ฒด) | ๋ฎ์ (์๋ ๋ณํ) |
| ์ฑ๋ฅ | ์ข์ (์ต์ ํ๋จ) | ๋งค์ฐ ์ข์ (์ง์ ์ ์ด) |
| ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ | ์๋ (Faulting) | ์๋ ๊ด๋ฆฌ ํ์ |
| ๊ด๊ณ ์ฒ๋ฆฌ | ์ฌ์ (์๋ ๊ด๋ฆฌ) | ์ด๋ ค์ (JOIN ์ง์ ) |
| ๋ง์ด๊ทธ๋ ์ด์ | ๋ฐ์๋ (๋ชจ๋ธ ๋ฒ์ ) | ์๋ (ALTER TABLE) |
| iCloud ๋๊ธฐํ | ์ง์ (NSPersistentCloudKitContainer) | ๋ฏธ์ง์ (์ง์ ๊ตฌํ) |
| ๋๋ฒ๊น | ์ด๋ ค์ (๋ธ๋๋ฐ์ค) | ์ฌ์ (SQL ์ง์ ํ์ธ) |
| ํฌ๋ก์ค ํ๋ซํผ | iOS/macOS๋ง | ๋ชจ๋ ํ๋ซํผ |
๐ฏ ์ค์ ์ ํ ๊ฐ์ด๋
- โ iOS/macOS ์ฑ๋ง ๊ฐ๋ฐํ๋ ๊ฒฝ์ฐ
- โ ๊ฐ์ฒด ์งํฅ์ ์ค๊ณ๋ฅผ ์ ํธํ๋ ๊ฒฝ์ฐ
- โ iCloud ๋๊ธฐํ๊ฐ ํ์ํ ๊ฒฝ์ฐ
- โ ๋น ๋ฅธ ๊ฐ๋ฐ์ด ์ค์ํ ๊ฒฝ์ฐ
- โ Apple์ ๋ค๋ฅธ ํ๋ ์์ํฌ(SwiftUI, Combine)์ ํตํฉํ๋ ค๋ ๊ฒฝ์ฐ
- โ ๋ณต์กํ ๊ฐ์ฒด ๊ทธ๋ํ๋ฅผ ๋ค๋ฃจ๋ ๊ฒฝ์ฐ
- โ ํฌ๋ก์ค ํ๋ซํผ ์ฑ์ ๊ฐ๋ฐํ๋ ๊ฒฝ์ฐ
- โ ๋ณต์กํ SQL ์ฟผ๋ฆฌ๊ฐ ํ์ํ ๊ฒฝ์ฐ
- โ ์ต๋ ์ฑ๋ฅ์ด ์ค์ํ ๊ฒฝ์ฐ
- โ ๊ธฐ์กด SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฌ์ฉํด์ผ ํ๋ ๊ฒฝ์ฐ
- โ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ตฌ์กฐ๋ฅผ ์์ ํ ์ ์ดํ๊ณ ์ถ์ ๊ฒฝ์ฐ
- โ SQL ์ ๋ฌธ ์ง์์ด ์๋ ๊ฒฝ์ฐ
โข Core Data: ๋ฉ์ธ ์ฑ ๋ฐ์ดํฐ (์ฌ์ฉ์ ์ค์ , ์ฝํ ์ธ ๋ฑ)
โข SQLite: ๋ถ์ ๋ฐ์ดํฐ, ๋ก๊ทธ, ์บ์ ๋ฑ
๊ฐ๊ฐ์ ๊ฐ์ ์ ์ด๋ ค์ ์ฌ์ฉํ๋ฉด ์ต๊ณ ์ ๊ฒฐ๊ณผ๋ฅผ ์ป์ ์ ์์ด! ๐
๐ ์ค์ ์ฌ์ฉ ์ฌ๋ก
Core Data ์ฌ์ฉ
โข ํ๋ ์ด๋ฆฌ์คํธ์ ๊ณก์ ๊ด๊ณ
โข ์ฌ์ ๊ธฐ๋ก ์ถ์
โข iCloud ๋๊ธฐํ
โข ๋ณต์กํ ํํฐ๋ง
SQLite ์ฌ์ฉ
โข ๋๋ ๋ฐ์ดํฐ ์ง๊ณ
โข ๋ณต์กํ ํต๊ณ ์ฟผ๋ฆฌ
โข ๋น ๋ฅธ ๊ฒ์ ์ฑ๋ฅ
โข CSV ๋ด๋ณด๋ด๊ธฐ
Core Data ์ฌ์ฉ
โข ํด๋-๋ ธํธ ๊ณ์ธต ๊ตฌ์กฐ
โข ํ๊ทธ ์์คํ
โข ์ฒจ๋ถ ํ์ผ ๊ด๋ฆฌ
โข ๋ฒ์ ๊ด๋ฆฌ
SQLite ์ฌ์ฉ
โข ๋ณต์กํ ์ง๊ณ ์ฟผ๋ฆฌ
โข ๋ ์ง๋ณ ํต๊ณ
โข ์นดํ ๊ณ ๋ฆฌ๋ณ ๋ถ์
โข ๋น ๋ฅธ ๊ฒ์
๐ ๋ฐ์ดํฐ ๋ง์ด๊ทธ๋ ์ด์ : ๋ฒ์ ๊ด๋ฆฌ์ ํต์ฌ
์ฑ์ ์
๋ฐ์ดํธํ๋ค ๋ณด๋ฉด ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ๋ฐ๊ฟ์ผ ํ ๋๊ฐ ์์ด.
์๋ก์ด ๊ธฐ๋ฅ์ ์ถ๊ฐํ๊ฑฐ๋, ๊ธฐ์กด ๊ตฌ์กฐ๋ฅผ ๊ฐ์ ํ๊ฑฐ๋... ์ด๋ด ๋ ๋ง์ด๊ทธ๋ ์ด์
์ด ํ์ํด! ๐ง
๋ง์ด๊ทธ๋ ์ด์
์ ์๋ชปํ๋ฉด ์ฌ์ฉ์ ๋ฐ์ดํฐ๊ฐ ๋ ์๊ฐ ์ ์์ผ๋ ์ ๋ง ์กฐ์ฌํด์ผ ํด. ๐ฐ
๐ฆ Core Data ๋ง์ด๊ทธ๋ ์ด์
.xcdatamodeld ํ์ผ์ ์ ํํ๊ณ Editor โ Add Model Version...
์ ๋ฒ์ ์ด ๋ง๋ค์ด์ง๋ฉด .xcdatamodeld ํ์ผ ์์ ์ฌ๋ฌ ๋ฒ์ ์ด ์๊ฒจ.
์: MyApp.xcdatamodeld โ MyApp.xcdatamodel, MyApp 2.xcdatamodel
ํ์ฌ ๋ฒ์ ์ ์ค์ ํ๋ ค๋ฉด .xcdatamodeld ํ์ผ์ ์ ํํ๊ณ File Inspector์์ "Current"๋ฅผ ๋ณ๊ฒฝํด.
์ ๋ฒ์ ์์ ์ํ๋ ๋๋ก 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 // ์๋ก ์ถ๊ฐ!
๊ฐ๋จํ ๋ณ๊ฒฝ์ ์๋ ๋ง์ด๊ทธ๋ ์ด์ (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
}()
โข ์์ฑ ์ญ์
โข ํ์ ์์ฑ์ ์ต์ ๋๋ก ๋ณ๊ฒฝ
โข ์ต์ ๋ ์์ฑ์ ํ์๋ก ๋ณ๊ฒฝ (๊ธฐ๋ณธ๊ฐ ์์ ๋)
โข ์ Entity ์ถ๊ฐ
โข Entity ์ด๋ฆ ๋ณ๊ฒฝ (Renaming ID ์ค์ )
โข Relationship ์ถ๊ฐ/์ญ์
๐ง ๋ณต์กํ ๋ง์ด๊ทธ๋ ์ด์ : Mapping Model
๋ฐ์ดํฐ ๋ณํ์ด ํ์ํ ๋ณต์กํ ๊ฒฝ์ฐ์๋ Mapping Model์ ๋ง๋ค์ด์ผ ํด.
File โ New โ File โ Mapping Model ์ ํ
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
}
}
}
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)
}
}
โข @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
}()
โข .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("โ
๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ ์๋ฃ!")
}
}
โข ํค๋ฅผ ์์ด๋ฒ๋ฆฌ๋ฉด ๋ฐ์ดํฐ ๋ณต๊ตฌ ๋ถ๊ฐ๋ฅ!
โข ์ฑ๋ฅ ์ค๋ฒํค๋ ๊ณ ๋ ค (์ฝ 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("โ ๏ธ ์คํฌ๋ฆฐ์ท์ด ์ดฌ์๋์์ต๋๋ค!")
// ํ์ํ ๊ฒฝ์ฐ ๊ฒฝ๊ณ ํ์ ๋๋ ๋ก๊น
}
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 ์ผ๋ฐ์ ์ธ ๋ฌธ์ ๋ค
ํด๊ฒฐ:
โข ๊ฐ๋ฐ ์ค: ์ฑ ์ญ์ ํ ์ฌ์ค์น
โข ํ๋ก๋์ : ๋ง์ด๊ทธ๋ ์ด์ ๊ตฌํ ํ์
โข Lightweight Migration ํ์ฑํ ํ์ธ
ํด๊ฒฐ:
// โ ๋์ ์
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() // ๋ฉ๋ชจ๋ฆฌ ์ ๋ฆฌ
}
}
}
ํด๊ฒฐ:
// ๊ฐ์ฒด๊ฐ ์ ํจํ์ง ํ์ธ
if !memo.isFault && !memo.isDeleted {
// ์์ ํ๊ฒ ์ฌ์ฉ
print(memo.title ?? "")
}
// ๋๋ try-catch๋ก ๊ฐ์ธ๊ธฐ
do {
let title = memo.title
} catch {
print("๊ฐ์ฒด๊ฐ ์ญ์ ๋์๊ฑฐ๋ ์ ๊ทผ ๋ถ๊ฐ")
}
๐ SQLite ์ผ๋ฐ์ ์ธ ๋ฌธ์ ๋ค
ํด๊ฒฐ:
// 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 ๋๊ธฐ
}
}
}
ํด๊ฒฐ:
// 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)")
ํด๊ฒฐ:
// ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฌ
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")
}
}
2. ์๋ฌ ํธ๋ค๋ง: ๋ชจ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ ์ try-catch
3. ๋จ์ ํ ์คํธ: ๋ฐ์ดํฐ ๋ก์ง ํ ์คํธ ์์ฑ
4. ํ๋กํ์ผ๋ง: Instruments๋ก ์ ๊ธฐ์ ๋ถ์
5. ํฌ๋์ ๋ฆฌํฌํธ: Firebase Crashlytics ๋ฑ ํ์ฉ
6. ๋ฒ์ ๊ด๋ฆฌ: ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง ๋ฒ์ ์ถ์
7. ๋ฐฑ์ ์ ๋ต: ์ค์ ์์ ์ ํญ์ ๋ฐฑ์
๐ฏ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์! ์ ๋ง ๊ธด ์ฌ์ ์ด์์ด. ๐
Core Data์ SQLite๋ฅผ ํ์ฉํ iOS ๋ฐ์ดํฐ ์์ํ์ ๋ํด ๊น์ด ์๊ฒ ์์๋ดค์ด.
ํต์ฌ ๋ด์ฉ์ ๋ค์ ํ๋ฒ ์ ๋ฆฌํด๋ณผ๊น? ๐
โข Apple์ ๊ฐ๋ ฅํ ๊ฐ์ฒด ๊ทธ๋ํ ๊ด๋ฆฌ ํ๋ ์์ํฌ
โข Swift์ ์์ฐ์ค๋ฌ์ด ํตํฉ
โข ๋ณต์กํ ๊ด๊ณ ์ฒ๋ฆฌ์ ์ ๋ฆฌ
โข iCloud ๋๊ธฐํ ์ง์
โข ๋๋ถ๋ถ์ iOS ์ฑ์ ์ ํฉ
2. SQLite
โข ๊ฒฝ๋ ๊ด๊ณํ ๋ฐ์ดํฐ๋ฒ ์ด์ค
โข ์ง์ ์ ์ธ SQL ์ ์ด
โข ํฌ๋ก์ค ํ๋ซํผ ํธํ์ฑ
โข ์ต๋ ์ฑ๋ฅ ์ถ๊ตฌ ์ ์ ๋ฆฌ
โข ๋ณต์กํ ์ฟผ๋ฆฌ์ ๋ถ์์ ์ ํฉ
3. ์ ํ ๊ธฐ์ค
โข ํ๋ก์ ํธ ์๊ตฌ์ฌํญ ๋ถ์
โข ํ์ ๊ธฐ์ ์คํ ๊ณ ๋ ค
โข ์ฑ๋ฅ vs ๊ฐ๋ฐ ์๋ ํธ๋ ์ด๋์คํ
โข ์ฅ๊ธฐ์ ์ ์ง๋ณด์ ๊ณ ๋ ค
- โ ๋ฐ์ดํฐ ๋ชจ๋ธ ์ค๊ณ (์ ๊ทํ, ๊ด๊ณ ์ค์ )
- โ ๋ง์ด๊ทธ๋ ์ด์ ์ ๋ต ์๋ฆฝ
- โ ์ฑ๋ฅ ์ต์ ํ (์ธ๋ฑ์ค, ๋ฐฐ์น ์ฒ๋ฆฌ)
- โ ๋ณด์ ๊ณ ๋ ค (์ํธํ, Keychain)
- โ ์๋ฌ ํธ๋ค๋ง ๊ตฌํ
- โ ๋ฐฑ์ ๋ฐ ๋ณต๊ตฌ ๋ฉ์ปค๋์ฆ
- โ ํ ์คํธ ์ฝ๋ ์์ฑ
- โ ๋ฌธ์ํ
- โ ๋ชจ๋ํฐ๋ง ๋ฐ ๋ก๊น
- โ ์ฌ์ฉ์ ํผ๋๋ฐฑ ์์ง
๋ฐ์ดํฐ ์์ํ๋ ์ฑ ๊ฐ๋ฐ์ ํต์ฌ์ด์ผ. ์ฌ์ฉ์์ ์์คํ ๋ฐ์ดํฐ๋ฅผ ์์ ํ๊ฒ ๋ณด๊ดํ๊ณ ,
๋น ๋ฅด๊ฒ ์ ๊ทผํ ์ ์๋๋ก ํ๋ ๊ฒ์ด ์ฐ๋ฆฌ์ ์ฑ
์์ด์ง! ๐พ
์ฒ์์๋ ์ด๋ ต๊ฒ ๋๊ปด์ง ์ ์์ง๋ง, ํ๋์ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ์ตํ๋ค ๋ณด๋ฉด
์ด๋์ ๋ฅ์ํ๊ฒ ๋ค๋ฃจ๊ณ ์๋ ์์ ์ ๋ฐ๊ฒฌํ๊ฒ ๋ ๊ฑฐ์ผ. ๐
Core Data๋ SQLite๋ , ์ค์ํ ๊ฑด ์ฌ์ฉ์ ๊ฒฝํ์ด์ผ.
๋น ๋ฅด๊ณ ์์ ์ ์ด๋ฉฐ ์์ ํ ๋ฐ์ดํฐ ๊ด๋ฆฌ๋ฅผ ํตํด ์ต๊ณ ์ ์ฑ์ ๋ง๋ค์ด๋ณด์! โจ
๊ถ๊ธํ ์ ์ด ์๊ฑฐ๋ ๋ ๊น์ด ๋ฐฐ์ฐ๊ณ ์ถ๋ค๋ฉด, ์ค๋ฌด ๊ฒฝํ์ด ํ๋ถํ ๊ฐ๋ฐ์๋ค๊ณผ
๊ต๋ฅํ๋ ๊ฒ๋ ํฐ ๋์์ด ๋ผ. ํจ๊ป ์ฑ์ฅํ๋ ๊ฐ๋ฐ์๊ฐ ๋์! ๐ฑ
โข ๊ฐ๋จํ ๋ฉ๋ชจ ์ฑ๋ถํฐ ์์
โข ์ ์ฐจ ๊ธฐ๋ฅ ์ถ๊ฐ (๊ฒ์, ์นดํ ๊ณ ๋ฆฌ, ๋๊ธฐํ ๋ฑ)
โข ์คํ์์ค ํ๋ก์ ํธ ์ฐธ์ฌ
โข ๊ฐ์ธ ํฌํธํด๋ฆฌ์ค ๊ตฌ์ถ
๋งํ๋ ๋ถ๋ถ์ด ์๋ค๋ฉด ์ฃผ์ ํ์ง ๋ง๊ณ ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํ๊ฑฐ๋,
์ ๋ฌธ๊ฐ์ ๋์์ ๋ฐ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ! ๐ช
ํ๋ณตํ ์ฝ๋ฉ ๋์ธ์! ๐
Happy Coding! ๐ปโจ
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.