SoDoIt ๊ฐœ๋ฐœ ๊ธฐ๋ก ๐Ÿ“(feat. SwiftUI + CoreData)

ํ•œ๋‹ด๊ณฐยท2026๋…„ 5์›” 13์ผ

์˜์˜ Swift_Study

๋ชฉ๋ก ๋ณด๊ธฐ
17/17
post-thumbnail

๋‚˜์˜ ๋‘ ๋ฒˆ์งธ ์•ฑ ์ถœ์‹œ๊ธฐ โ€” SwiftUI + CoreData๋กœ SoDoIt ๋งŒ๋“ค๊ธฐ!

1์ธ ๊ฐœ๋ฐœ๋กœ SoDoIt(์˜๋‘์ž‡)์ด๋ผ๋Š” ํˆฌ๋‘ ์•ฑ์„ App Store์— ์ถœ์‹œํ•˜๋ฉด์„œ ๋งˆ์ฃผ์นœ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… 4๊ฐ€์ง€๋ฅผ ์ •๋ฆฌํ•ด ๋ณด์•˜์Šต๋‹ˆ๋‹ค!


๋ชฉ์ฐจ

  1. SoDoIt ์†Œ๊ฐœ
  2. ๊ธฐ์ˆ  ์Šคํƒ๊ณผ ์•„ํ‚คํ…์ฒ˜
  3. ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… 1 โ€” SwiftUI์—์„œ CoreData ๊ฐ์ฒด๋ฅผ ์‚ญ์ œํ•˜๋ฉด ํฌ๋ž˜์‹œ๊ฐ€ ๋‚œ๋‹ค
  4. ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… 2 โ€” ์œ„์ ฏ๊ณผ ๋ฉ”์ธ ์•ฑ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ณต์œ ํ•˜๊ธฐ
  5. ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… 3 โ€” Color(hex:)์˜ ํ•จ์ •: Swift ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„
  6. ํšŒ๊ณ  โ€” v2์—์„œ ๋ฐ”๊พธ๊ณ  ์‹ถ์€ ๊ฒƒ๋“ค
  7. ๋งˆ์น˜๋ฉฐ

SoDoIt ์†Œ๊ฐœ

SoDoIt(์˜๋‘์ž‡) ์€ ์˜ค๋Š˜ ํ•  ์ผ์„ ๊น”๋”ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜๋Š” ํˆฌ๋‘ ์•ฑ์ž…๋‹ˆ๋‹ค.

  • ํ•  ์ผ CRUD: ์ถ”๊ฐ€ ยท ํŽธ์ง‘ ยท ์‚ญ์ œ ยท ์™„๋ฃŒ ํ† ๊ธ€
  • ์นดํ…Œ๊ณ ๋ฆฌ: 10๊ฐ€์ง€ ์ƒ‰์ƒ๊ณผ SF Symbol ์•„์ด์ฝ˜ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•
  • ํ•„ํ„ฐยท์ •๋ ฌ: ์Šค๋งˆํŠธ ํ•„ํ„ฐ(์ „์ฒด/์˜ค๋Š˜/์˜ˆ์ •/์™„๋ฃŒ), ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ, ๋‹ค์ค‘ ์ •๋ ฌ
  • ๋งˆ๊ฐ์ผ ์•Œ๋ฆผ: UNUserNotificationCenter ๋กœ์ปฌ ํ‘ธ์‹œ
  • ํ†ต๊ณ„: Swift Charts๋กœ ์™„๋ฃŒ์œจยท์šฐ์„ ์ˆœ์œ„ยท์ฃผ๊ฐ„ ์ถ”์ด ์‹œ๊ฐํ™”
  • ํ™ˆ ํ™”๋ฉด ์œ„์ ฏ: Small / Medium ๋‘ ํฌ๊ธฐ

App Store์— v1.0์œผ๋กœ ์ถœ์‹œํ–ˆ๊ณ , ์ „๋ถ€ 1์ธ ๊ฐœ๋ฐœ + Apple ๊ณต์‹ ํ”„๋ ˆ์ž„์›Œํฌ๋งŒ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค! (์™ธ๋ถ€ ์˜์กด์„ฑ 0)


๊ธฐ์ˆ  ์Šคํƒ๊ณผ ์•„ํ‚คํ…์ฒ˜

ํ•ญ๋ชฉ์„ ํƒ์ด์œ 
UI ํ”„๋ ˆ์ž„์›ŒํฌSwiftUI์„ ์–ธํ˜• ํŒจ๋Ÿฌ๋‹ค์ž„์— ์ต์ˆ™ํ•ด์ง€๊ณ  ์‹ถ์—ˆ์Œ
๋ฐ์ดํ„ฐ ์ €์žฅCoreData (NSPersistentContainer)์™ธ๋ถ€ ์˜์กด์„ฑ ์—†์ด ๋กœ์ปฌ ์˜์†ํ™”
๋ฐ˜์‘ํ˜• ๊ฐฑ์‹ NSFetchedResultsController + @ObservableSwiftUI์— ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์ „ํŒŒ
์•„ํ‚คํ…์ฒ˜MVVM + RepositoryView / ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง / ์˜์†ํ™” ์ฑ…์ž„ ๋ถ„๋ฆฌ
์œ„์ ฏWidgetKit + App Group๋ฉ”์ธ ์•ฑ๊ณผ ๋ฐ์ดํ„ฐ ๊ณต์œ 
ํฐํŠธPretendard (๋Ÿฐํƒ€์ž„ ๋“ฑ๋ก)ํ•œ๊ตญ์–ด ๊ฐ€๋…์„ฑ
View (SwiftUI)
  โ†“ @Observable
ViewModel
  โ†“ ์˜์กด์„ฑ ์ฃผ์ž…
Repository (CoreData CRUD ์บก์Аํ™”)
  โ†“
CoreDataManager (NSPersistentContainer)

SwiftUI๋ฅผ ์“ฐ๋‹ค ๋ณด๋ฉด "๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐ”๋€Œ๋ฉด UI๊ฐ€ ์ž๋™์œผ๋กœ ๋‹ค์‹œ ๊ทธ๋ ค์ง„๋‹ค"๊ฐ€ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋А๊ปด์ง€๋Š”๋ฐ, CoreData NSManagedObject๋ฅผ ๊ฐ™์ด ์‚ฌ์šฉํ•˜๋ฉด ํ—ท๊ฐˆ๋ฆฌ๋Š” ๋ถ€๋ถ„์ด ๋งŽ์•„์ง€๋”๋ผ๊ตฌ์š”. ๊ทธ๋ž˜์„œ ์ด ๋ถ€๋ถ„์— ๋Œ€ํ•ด ์ž‘์„ฑํ•ด ๋ณด์•˜์Šต๋‹ˆ๋‹ค.


ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… 1 โ€” SwiftUI์—์„œ CoreData ๊ฐ์ฒด๋ฅผ ์‚ญ์ œํ•˜๋ฉด ํฌ๋ž˜์‹œ๊ฐ€ ๋‚œ๋‹ค.

SwiftUI + CoreData๋ฅผ ์ฒ˜์Œ ๋งŒ์ง€๋ฉด ๊ฑฐ์˜ ๋ฌด์กฐ๊ฑด ๊ฒช๋Š” ํ•จ์ •์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๋Š” ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค. ์ด ๋ถ€๋ถ„์€ ํ•œ ๋ฒˆ ์ œ๋Œ€๋กœ ์ดํ•ดํ•ด๋‘๋ฉด ๋น„์Šทํ•œ ๋ฅ˜์˜ ๋ฌธ์ œ๋Š” ๋‹ค ํ’€ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฌธ์ œ 1

์นดํ…Œ๊ณ ๋ฆฌ ๋ชฉ๋ก์—์„œ ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ์Šค์™€์ดํ”„๋กœ ์‚ญ์ œํ•˜๋ฉด ์•ฑ์ด ๊ทธ๋Œ€๋กœ ๊บผ์กŒ์Šต๋‹ˆ๋‹ค. (ใ… ใ… )

Thread 1: EXC_BREAKPOINT (code=1, subcode=0x19744d754)

Xcode ์ฝ˜์†”์—๋Š” ๋ณ„๋‹ค๋ฅธ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋„ ์—†์ด ๊ทธ๋ƒฅ ํŠธ๋žฉ์ด ๋–จ์–ด์ง‘๋‹ˆ๋‹ค. ์ฒ˜์Œ์—” "๋‚ด๊ฐ€ ๋ญ˜ ์ž˜๋ชป ์ž…๋ ฅํ–ˆ๋‚˜?" ์‹ถ์—ˆ๋Š”๋ฐ ๊ฐ™์€ ์ฝ”๋“œ๊ฐ€ ๋‹ค๋ฅธ ๊ธฐ๊ธฐ์—์„  ๋ฉ€์ฉกํžˆ ๋™์ž‘ํ–ˆ์–ด์š”. ์žฌํ˜„์€ 100%์˜€๋Š”๋ฐ ๋””๋ฒ„๊ฑฐ ๋ฉˆ์ถ”๋Š” ์œ„์น˜๊ฐ€ ๋งค๋ฒˆ ๋‹ฌ๋ž์Šต๋‹ˆ๋‹ค.

์ฝ”๋“œ โ€” ์ฒ˜์Œ์˜ ๋‹จ์ˆœํ•œ ๊ตฌ์กฐ

struct CategoryListView: View {
    @State private var viewModel = CategoryListViewModel()

    var body: some View {
        List {
            ForEach(viewModel.categories) { category in
                CategoryRowView(category: category)
                    .swipeActions {
                        Button("์‚ญ์ œ", role: .destructive) {
                            viewModel.delete(category)
                        }
                    }
            }
        }
    }
}

struct CategoryRowView: View {
    @ObservedObject var category: Category  // NSManagedObject

    var body: some View {
        Label(category.name, systemImage: category.iconName)
    }
}

CoreData ๋ชจ๋ธ์€ ํ‰๋ฒ”ํ•œ NSManagedObject ์„œ๋ธŒํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค:

@objc(Category)
public class Category: NSManagedObject {}

extension Category {
    @NSManaged public var id: UUID           // โ† non-optional!
    @NSManaged public var name: String       // โ† non-optional!
    @NSManaged public var iconName: String   // โ† non-optional!
    @NSManaged public var colorHex: String   // โ† non-optional!
}

์›์ธ ์ฐพ๊ธฐ

EXC_BREAKPOINT subcode๊ฐ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ฃผ์†Œ์ฒ˜๋Ÿผ ๋ณด์—ฌ์„œ ์ฒ˜์Œ์—” ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ฌธ์ œ์ธ ์ค„ ์•Œ์•˜๋Š”๋ฐ, ์™ธ๋ถ€ ์˜์กด์„ฑ์ด ์—†๋Š” ํ”„๋กœ์ ํŠธ๋ผ ๊ทธ๋Ÿด ๋ฆฌ๊ฐ€ ์—†์—ˆ์–ด์š”.

๊ฐ€์„ค 1: @ObservedObject var category: Category๊ฐ€ KVO๋กœ ๊ฐ์ฒด ๋ณ€๊ฒฝ์„ ๊ฐ์ง€ํ•˜๋Š”๋ฐ, ๊ฐ์ฒด๊ฐ€ ์‚ญ์ œ๋˜๋Š” ์ˆœ๊ฐ„ KVO ๋ฐœํ™” โ†’ SwiftUI ๋ฆฌ๋ Œ๋” โ†’ ์‚ญ์ œ๋œ ๊ฐ์ฒด์˜ ํ”„๋กœํผํ‹ฐ ์ ‘๊ทผ โ†’ ํฌ๋ž˜์‹œ?

์ด ๊ฐ€์„ค์„ ํ™•์ธํ•˜๋ ค๋ฉด KVO์™€ KVC๊ฐ€ ๋ฌด์—‡์ธ์ง€๋ถ€ํ„ฐ ์งš์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

KVO์™€ KVC โ€” ์™œ NSManagedObject์—์„œ ์ค‘์š”ํ•œ๊ฐ€

  • KVC (Key-Value Coding): ๊ฐ์ฒด์˜ ํ”„๋กœํผํ‹ฐ๋ฅผ ๋ฌธ์ž์—ด ํ‚ค๋กœ ๋™์  ์ ‘๊ทผํ•˜๋Š” ๋ฉ”์ปค๋‹ˆ์ฆ˜. [obj valueForKey:@"name"] ๊ฐ™์€ Obj-C ์ฝ”๋“œ๊ฐ€ ๊ทธ๊ฒƒ.
  • KVO (Key-Value Observing): ๊ทธ ํ‚ค ๊ฒฝ๋กœ์˜ ๊ฐ’์ด ๋ฐ”๋€Œ๋ฉด ์•Œ๋ ค์ฃผ๋Š” ์˜ต์ €๋ฒ„ ๋ฉ”์ปค๋‹ˆ์ฆ˜.

CoreData์˜ @NSManaged ํ”„๋กœํผํ‹ฐ๋Š” ์ปดํŒŒ์ผ ํƒ€์ž„์ด ์•„๋‹ˆ๋ผ ๋Ÿฐํƒ€์ž„์— KVC๋ฅผ ํ†ตํ•ด ๊ฐ’์„ ์ฝ๊ณ  ์”๋‹ˆ๋‹ค. ์ฆ‰:

let name = category.name  // ์ปดํŒŒ์ผ๋Ÿฌ๊ฐ€ ๋ณด๊ธฐ์—” ์ผ๋ฐ˜ ํ”„๋กœํผํ‹ฐ ์ ‘๊ทผ
// โ†“ ์‹ค์ œ๋กœ๋Š”
[category valueForKey:@"name"]  // KVC ํ˜ธ์ถœ โ†’ CoreData๊ฐ€ store์—์„œ ๊ฐ’ ์กฐํšŒ

๊ทธ๋ฆฌ๊ณ  SwiftUI์˜ @ObservedObject๋Š” ObservableObject์˜ objectWillChange ํผ๋ธ”๋ฆฌ์…”๋ฅผ ๊ตฌ๋…ํ•ฉ๋‹ˆ๋‹ค. NSManagedObject๋Š” iOS 14+์—์„œ ObservableObject๋ฅผ ์ž๋™ ์ฑ„ํƒํ•˜๊ณ , KVO ์•Œ๋ฆผ์„ ๋ฐ›์œผ๋ฉด objectWillChange.send()๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์นดํ…Œ๊ณ ๋ฆฌ๊ฐ€ ์‚ญ์ œ๋˜๋ฉด ์ผ์–ด๋‚˜๋Š” ์ผ:

  1. viewModel.delete(category) โ†’ ๋‚ด๋ถ€์—์„œ context.delete(category) + save()
  2. CoreData๊ฐ€ category๋ฅผ deleted ์ƒํƒœ๋กœ ๋งˆํ‚น (isDeleted = true)
  3. ๊ด€๊ณ„๋‚˜ ์†์„ฑ์ด ๋ณ€๊ฒฝ๋˜๋ฉฐ KVO ์•Œ๋ฆผ ๋ฐœํ™”
  4. @ObservedObject๊ฐ€ ์•Œ๋ฆผ ์ˆ˜์‹  โ†’ SwiftUI์—๊ฒŒ "์ด ๋ทฐ ๋‹ค์‹œ ๊ทธ๋ ค"
  5. viewModel.categories ๋ฐฐ์—ด์€ ์•„์ง ๊ฐฑ์‹  ์ „(NSFetchedResultsController.controllerDidChangeContent๊ฐ€ ์•„์ง ํ˜ธ์ถœ ์•ˆ ๋จ)
  6. SwiftUI๊ฐ€ deleted Category๋กœ body๋ฅผ ํ•œ ๋ฒˆ ๋” ํ‰๊ฐ€
  7. category.name ์ ‘๊ทผ โ†’ KVC ํ˜ธ์ถœ โ†’ deleted ๊ฐ์ฒด๋ผ nil ๋ฐ˜ํ™˜
  8. Swift๋Š” name์„ String(non-optional)์œผ๋กœ ์„ ์–ธํ–ˆ์œผ๋ฏ€๋กœ nil์„ ๋ฐ›์„ ์ˆ˜ ์—†์Œ
  9. ๋Ÿฐํƒ€์ž„์ด "non-optional์— nil ๊ฐ•์ œ ์บ์ŠคํŒ… ์‹œ๋„"๋ฅผ ๊ฐ์ง€ โ†’ ํŠธ๋žฉ โ†’ EXC_BREAKPOINT

์—ฌ๊ธฐ์„œ ํ•ต์‹ฌ์€ @NSManaged var name: String์ด non-optional์ด๋ผ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋งŒ์•ฝ String?์ด์—ˆ๋‹ค๋ฉด nil์„ ๋ฐ›์•„๋„ ํฌ๋ž˜์‹œ ์•ˆ ๋‚ฌ์„ ๊ฑฐ์˜ˆ์š”. CoreData ์ฝ”๋“œ์  ์ด ๋ชจ๋ธ์˜ "Optional" ์ฒดํฌ๋ฐ•์Šค๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์˜ต์…”๋„ ์—ฌ๋ถ€๋ฅผ ๊ฒฐ์ •ํ•˜๋Š”๋ฐ, ๊ธฐ๋ณธ๊ฐ’์ด non-optional๋กœ ๋˜๊ธฐ ์‰ฝ์Šต๋‹ˆ๋‹ค.

๊ฐ€์„ค 2 โ€” ForEach์˜ identity

์œ„ ๊ฐ€์„ค๋กœ ๋ทฐ ๋ณธ๋ฌธ์— ๊ฐ€๋“œ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค:

struct CategoryRowView: View {
    @ObservedObject var category: Category

    var body: some View {
        if category.isDeleted || category.managedObjectContext == nil {
            EmptyView()
        } else {
            Label(category.name, systemImage: category.iconName)
        }
    }
}

isDeleted๋Š” NSManagedObject์˜ ์ž์ฒด ํ”„๋กœํผํ‹ฐ๋ผ deleted ์ƒํƒœ์—์„œ๋„ ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ฐ€๋“œ๋ฉด ๋์ผ ์ค„ ์•Œ์•˜๋Š”๋ฐโ€ฆ

์—ฌ์ „ํžˆ ํฌ๋ž˜์‹œ.

๊นŠ์ด ๋ณด๋‹ˆ ๋˜ ๋‹ค๋ฅธ ๊ฒฝ๋กœ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ForEach๊ฐ€ ๋ฐฐ์—ด diffing์„ ํ•  ๋•Œ ๊ฐ ์›์†Œ์˜ Identifiable.id๋ฅผ ํ‰๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. Category๋Š” ์ด๋ ‡๊ฒŒ Identifiable์— conform๋˜์–ด ์žˆ์—ˆ์–ด์š”:

extension Category: Identifiable {
    // id๋Š” @NSManaged var id: UUID ๋ฅผ ์ž๋™ ์‚ฌ์šฉ
}

๋ฐฐ์—ด์ด ๊ฐฑ์‹ ๋˜๋Š” ์ˆœ๊ฐ„(viewModel.categories = newCategories) ForEach๋Š” ์˜› ๋ฐฐ์—ด๊ณผ ์ƒˆ ๋ฐฐ์—ด์˜ id๋ฅผ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ์˜› ๋ฐฐ์—ด์—๋Š” deleted Category๊ฐ€ ๋“ค์–ด์žˆ๊ณ , ๊ทธ ๊ฐ์ฒด์˜ id(@NSManaged UUID)๋ฅผ KVC๋กœ ์ฝ์œผ๋ฉด nil โ†’ ํŠธ๋žฉ.

์ด ํฌ๋ž˜์‹œ๋Š” row์˜ body๊ฐ€ ํ˜ธ์ถœ๋˜๊ธฐ ์ „, ForEach ๋‚ด๋ถ€ diffing ๋‹จ๊ณ„์—์„œ ์ผ์–ด๋‚ฉ๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ body ๊ฐ€๋“œ๊ฐ€ ๋‹ฟ์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ์ฑ… โ€” ๋‘ ์žฅ์น˜๋ฅผ ๋™์‹œ์—

  1. ForEach์˜ identity๋ฅผ objectID๋กœ ์šฐํšŒ: NSManagedObjectID๋Š” ๊ฐ์ฒด์˜ ๋ผ์ดํ”„์‚ฌ์ดํด๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ ํ•ญ์ƒ ์œ ํšจํ•ฉ๋‹ˆ๋‹ค (๋ฉ”๋ชจ๋ฆฌ์—์„œ ์‚ฌ๋ผ์งˆ ๋•Œ๊นŒ์ง€).
  2. body ์ง„์ž… ๊ฐ€๋“œ ์œ ์ง€: KVO race๋กœ ์ธํ•œ row ๋ฆฌ๋ Œ๋” ์‹œ ์•ˆ์ „๋ง ์—ญํ• .
struct CategoryListView: View {
    @State private var viewModel = CategoryListViewModel()

    var body: some View {
        List {
            // โœ… id: \.objectID ๋กœ ๋ณ€๊ฒฝ
            ForEach(viewModel.categories, id: \.objectID) { category in
                CategoryRowView(category: category)
                    .swipeActions {
                        Button("์‚ญ์ œ", role: .destructive) {
                            viewModel.delete(category)
                        }
                    }
            }
        }
    }
}

struct CategoryRowView: View {
    @ObservedObject var category: Category

    var body: some View {
        // โœ… body ์ง„์ž…์—์„œ deleted ์ƒํƒœ ์ฐจ๋‹จ
        if category.isDeleted || category.managedObjectContext == nil {
            EmptyView()
        } else {
            Label(category.name, systemImage: category.iconName)
        }
    }
}

์ด ๋‘ ์žฅ์น˜๋ฅผ ์ ์šฉํ•œ ๋’ค๋กœ๋Š” ํฌ๋ž˜์‹œ๊ฐ€ ๋‹จ ํ•œ ๋ฒˆ๋„ ๋‚˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค! ๐ŸŽŠ

๋ฐฐ์šด ์ 

  • NSManagedObject๋Š” ์ผ๋ฐ˜ Swift ๊ฐ์ฒด๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค. @NSManaged ํ”„๋กœํผํ‹ฐ๋Š” KVC๋ฅผ ํ†ตํ•ด ๋™์ ์œผ๋กœ ์ฝ๊ณ  ์“ฐ๋ฏ€๋กœ, deleted ์ƒํƒœ์—์„œ ์ ‘๊ทผํ•˜๋ฉด ์œ„ํ—˜ํ•ฉ๋‹ˆ๋‹ค.
  • SwiftUI + CoreData๋Š” ๋ผ์ดํ”„์‚ฌ์ดํด ๊ฒฝ๊ณ„๊ฐ€ ๋ฏธ๋ฌ˜ํ•ฉ๋‹ˆ๋‹ค. @ObservedObject๋Š” KVO๋ฅผ ๊ตฌ๋…ํ•˜๊ธฐ ๋•Œ๋ฌธ์— deleted ๊ฐ์ฒด๊ฐ€ ์ž ๊น ๋ทฐ์— ๋‹ค์‹œ ๋“ค์–ด์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ForEach์˜ identity๋กœ objectID๋ฅผ ์“ฐ์„ธ์š”. Identifiable.id(@NSManaged UUID)๋Š” ๊ฐ์ฒด ๋ผ์ดํ”„์‚ฌ์ดํด์— ๋ฌถ์ด์ง€๋งŒ, objectID๋Š” ํ•ญ์ƒ ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.
  • ๋ชจ๋ธ์˜ String/UUID ํ”„๋กœํผํ‹ฐ๋ฅผ Optional๋กœ ๋‘๋Š” ๊ฒƒ๋„ ํ•œ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค. ๋‹ค๋งŒ ์ด๋ฏธ ์งœ์ธ ์ฝ”๋“œ๋ฅผ ์ „๋ถ€ ์˜ต์…”๋„๋กœ ๋ฐ”๊พธ๋Š” ๊ฑด ํฐ ์ž‘์—…์ด๋ผ ์ด๋ฒˆ PR์—์„œ๋Š” ๊ฐ€๋“œ ์ „๋žต์œผ๋กœ ๋งˆ๋ฌด๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ’ก ๋” ๊ทผ๋ณธ์ ์ธ ํ•ด๋ฒ•: ๋ชจ๋ธ์˜ name/colorHex ๋“ฑ์„ Optional๋กœ ์„ ์–ธํ•˜๊ณ  wrappedName ๊ฐ™์€ ์•ˆ์ „ ์–ธ๋ž˜ํ•‘ ๊ณ„์‚ฐ ํ”„๋กœํผํ‹ฐ๋กœ ๋…ธ์ถœํ•˜๋Š” ํŒจํ„ด. CLAUDE.md ๊ฐ€์ด๋“œ์ฒ˜๋Ÿผ ์ปจ๋ฒค์…˜์„ ๋ฏธ๋ฆฌ ์ •ํ•ด๋‘๋ฉด ๊ฐ™์€ ํ•จ์ •์„ ๋ฐ˜๋ณตํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… 2 โ€” ์œ„์ ฏ๊ณผ ๋ฉ”์ธ ์•ฑ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ณต์œ ํ•˜๊ธฐ

ํ™ˆ ํ™”๋ฉด ์œ„์ ฏ์„ ์ถ”๊ฐ€ํ•˜๋ ค๊ณ  ๋ณด๋‹ˆ ์œ„์ ฏ์—์„œ ๋ฉ”์ธ ์•ฑ์˜ CoreData๋ฅผ ์ง์ ‘ ๋ชป ์ฝ์—ˆ์Šต๋‹ˆ๋‹ค.

์ฒ˜์Œ ์‹œ๋„ โ€” ๊ทธ๋ƒฅ ๋ฉ”์ธ ์•ฑ๊ณผ ๋˜‘๊ฐ™์ด

// SoDoItWidget ํƒ€๊นƒ์˜ Provider ์•ˆ์—์„œ
let context = CoreDataManager.shared.viewContext
let request = TodoItem.fetchRequest()
let todos = try context.fetch(request)
// โŒ ์ปดํŒŒ์ผ์€ ๋˜์ง€๋งŒ ์‹คํ–‰ํ•˜๋ฉด ๋นˆ ๊ฒฐ๊ณผ
//    ํ˜น์€ ๋‹ค๋ฅธ sqlite ํŒŒ์ผ์„ ๋ณด๊ณ  ์žˆ์Œ

์œ„์ ฏ์—์„œ fetchedObjects๊ฐ€ ํ•ญ์ƒ 0๊ฐœ๋กœ ๋‚˜์™”์Šต๋‹ˆ๋‹ค. ๋ฉ”์ธ ์•ฑ์—๋Š” ๋ถ„๋ช… ํ•  ์ผ์ด ์žˆ๋Š”๋ฐ๋„์š”...

์›์ธ

์œ„์ ฏ Extension์€ ๋ฉ”์ธ ์•ฑ๊ณผ ๋ถ„๋ฆฌ๋œ ๋ณ„๊ฐœ์˜ ํ”„๋กœ์„ธ์Šค์ž…๋‹ˆ๋‹ค. ๊ฐ™์€ ์ฝ”๋“œ๋ฅผ ๋นŒ๋“œํ•ด์„œ ๊ฐ™์€ ํด๋ž˜์Šค๋ฅผ ํ˜ธ์ถœํ•ด๋„, ์‹ค์ œ ๋™์ž‘ํ•˜๋Š” ์ปจํ…์ŠคํŠธ๋Š” ๋‹ค๋ฆ…๋‹ˆ๋‹ค:

  • ๋ฉ”์ธ ์•ฑ: App.app/Library/Application Support/SoDoIt.sqlite ๊ฐ™์€ ๊ฒฝ๋กœ์— ๋ฐ์ดํ„ฐ ์ €์žฅ
  • ์œ„์ ฏ Extension: ์ž๊ธฐ๋งŒ์˜ sandbox์— ๋ณ„๋„์˜ sqlite๋ฅผ ๋งŒ๋“ฆ
  • ๋‘˜์€ ์„œ๋กœ์˜ ํŒŒ์ผ ์‹œ์Šคํ…œ์— ์ ‘๊ทผ ๋ถˆ๊ฐ€

CoreDataManager.shared๋Š” ๊ฐ ํ”„๋กœ์„ธ์Šค์—์„œ ๋”ฐ๋กœ ์ธ์Šคํ„ด์Šค๊ฐ€ ์ƒ์„ฑ๋˜๋ฉฐ, ์„œ๋กœ ๋‹ค๋ฅธ sqlite ํŒŒ์ผ์„ ๋ด…๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ์ฑ… โ€” App Group + ๊ณต์œ  UserDefaults

Apple์ด ์ œ๊ณตํ•˜๋Š” ์ •์‹ ํ•ด๋ฒ•์€ App Group์ž…๋‹ˆ๋‹ค. ๋‘ ํƒ€๊นƒ์„ ๊ฐ™์€ App Group์— ๋ฌถ์œผ๋ฉด ๊ทธ ๊ทธ๋ฃน์šฉ ๊ณต์œ  ์ปจํ…Œ์ด๋„ˆ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

1๋‹จ๊ณ„: Xcode์—์„œ App Group ํ™œ์„ฑํ™”

๋ฉ”์ธ ์•ฑ + ์œ„์ ฏ Extension ๋‘ ํƒ€๊นƒ ๋ชจ๋‘์— group.sso.SoDoIt ๊ฐ™์€ ๊ทธ๋ฃน ID ์ถ”๊ฐ€. Entitlements ํŒŒ์ผ์— ์ž๋™์œผ๋กœ ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค.

2๋‹จ๊ณ„: ์œ„์ ฏ์šฉ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ์ •์˜ (Codable)

CoreData NSManagedObject๋Š” ์ง๋ ฌํ™”ยท์—ญ์ง๋ ฌํ™”์— ์ ํ•ฉํ•˜์ง€ ์•Š์œผ๋‹ˆ, ์œ„์ ฏ ์ „์šฉ ๊ฐ€๋ฒผ์šด struct๋ฅผ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

struct WidgetTodoItem: Identifiable, Codable {
    let id: UUID
    let title: String
    let priority: Int16
    let dueDate: Date?
    let categoryName: String?
    let categoryColorHex: String?
}

3๋‹จ๊ณ„: ๋ฉ”์ธ ์•ฑ์—์„œ ์“ฐ๊ธฐ

enum AppGroup {
    static let identifier = "group.sso.SoDoIt"
    static let sharedDefaults = UserDefaults(suiteName: identifier)
    static let todosKey = "widget.todos"
}

final class WidgetDataManager {
    static let shared = WidgetDataManager()

    func updateWidgetData() {
        let context = CoreDataManager.shared.viewContext
        let request = TodoItem.fetchRequest()
        // ์œ„์ ฏ์— ๋ณด์—ฌ์ค„ ์ ๋‹น๋Ÿ‰๋งŒ (์˜ค๋Š˜ / ๋‹ค๊ฐ€์˜ค๋Š” ๋งˆ๊ฐ์ผ ์œ„์ฃผ)
        request.fetchLimit = 5

        do {
            let todos = try context.fetch(request)
            let snapshot = todos.map { todo in
                WidgetTodoItem(
                    id: todo.id,
                    title: todo.title,
                    priority: todo.priority,
                    dueDate: todo.dueDate,
                    categoryName: todo.category?.name,
                    categoryColorHex: todo.category?.colorHex
                )
            }
            let data = try JSONEncoder().encode(snapshot)
            AppGroup.sharedDefaults?.set(data, forKey: AppGroup.todosKey)

            // ์œ„์ ฏ์—๊ฒŒ "์ง€๊ธˆ ์ƒˆ ๋ฐ์ดํ„ฐ ์žˆ์–ด, ์ฆ‰์‹œ ๊ฐฑ์‹ ํ•ด" ์‹ ํ˜ธ
            WidgetCenter.shared.reloadTimelines(ofKind: "TodoListWidget")
        } catch {
            print("์œ„์ ฏ ๋ฐ์ดํ„ฐ ๊ฐฑ์‹  ์‹คํŒจ: \(error)")
        }
    }
}

CRUD ์งํ›„๋งˆ๋‹ค WidgetDataManager.shared.updateWidgetData()๋ฅผ ํ˜ธ์ถœํ–ˆ์Šต๋‹ˆ๋‹ค.

4๋‹จ๊ณ„: ์œ„์ ฏ์—์„œ ์ฝ๊ธฐ

struct TodoWidgetProvider: TimelineProvider {
    func getSnapshot(in context: Context,
                     completion: @escaping (TodoWidgetEntry) -> Void) {
        let todos = loadTodos()
        completion(TodoWidgetEntry(date: Date(), todos: todos))
    }

    func getTimeline(in context: Context,
                     completion: @escaping (Timeline<TodoWidgetEntry>) -> Void) {
        let todos = loadTodos()
        let entry = TodoWidgetEntry(date: Date(), todos: todos)
        // 30๋ถ„๋งˆ๋‹ค ์ž์ฒด ๊ฐฑ์‹  (์•ฑ์ด ๊บผ์ ธ ์žˆ์–ด๋„)
        let next = Date().addingTimeInterval(60 * 30)
        completion(Timeline(entries: [entry], policy: .after(next)))
    }

    private func loadTodos() -> [WidgetTodoItem] {
        guard let data = AppGroup.sharedDefaults?.data(forKey: AppGroup.todosKey)
        else { return [] }
        return (try? JSONDecoder().decode([WidgetTodoItem].self, from: data)) ?? []
    }
}

๊ฒฐ๊ณผ์™€ ๋ฐฐ์šด ์ 

  • ๋ณ€๊ฒฝ ์ฆ‰์‹œ ์œ„์ ฏ ๋ฐ˜์˜: WidgetCenter.reloadTimelines๊ฐ€ ํŠธ๋ฆฌ๊ฑฐ๋˜๋ฉด ์œ„์ ฏ์ด ๊ฑฐ์˜ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ฐฑ์‹ ๋ฉ๋‹ˆ๋‹ค.
  • ์•ฑ ๋ฏธ์‹คํ–‰ ์ƒํƒœ์—์„œ๋„ 30๋ถ„๋งˆ๋‹ค ์ž์ฒด ๊ฐฑ์‹ : TimelineProvider์˜ policy๋ฅผ .after(next)๋กœ ๋‘๋ฉด ์‹œ์Šคํ…œ์ด ์•Œ์•„์„œ ๊นจ์›Œ์ค๋‹ˆ๋‹ค.
  • ๊ฒฐํ•ฉ๋„ ๋ถ„๋ฆฌ: ์œ„์ ฏ์ด ๋ฉ”์ธ ์•ฑ์˜ CoreData ์Šคํ‚ค๋งˆ์— ์ง์ ‘ ์˜์กดํ•˜์ง€ ์•Š๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ํ–ฅํ›„ ๋ชจ๋ธ์ด ๋ฐ”๋€Œ์–ด๋„ WidgetTodoItem ๋ณ€ํ™˜ ๋‹จ๊ณ„์—์„œ ํก์ˆ˜ ๊ฐ€๋Šฅ.
  • ์œ„์ ฏ์€ ๋ณ„๊ฐœ์˜ ํ”„๋กœ์„ธ์Šค๋ผ๋Š” ์‚ฌ์‹ค์„ ๋Š˜ ๊ธฐ์–ตํ•˜์„ธ์š”. ๋ฉ”์ธ ์•ฑ์˜ singleton, ์ธ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ, ์˜์กด์„ฑ ์ฃผ์ž… ์ปจํ…Œ์ด๋„ˆ โ€” ์–ด๋А ๊ฒƒ๋„ ์œ„์ ฏ ํ”„๋กœ์„ธ์Šค์—๋Š” ์•ˆ ๋ณด์ž…๋‹ˆ๋‹ค.

๐Ÿ’ก ๋” ๋ฌด๊ฑฐ์šด ๋ฐ์ดํ„ฐ๋ฅผ ๊ณต์œ ํ•œ๋‹ค๋ฉด NSPersistentContainer์˜ store URL์„ App Group ์ปจํ…Œ์ด๋„ˆ ๊ฒฝ๋กœ๋กœ ์„ค์ •ํ•ด CoreData ์ž์ฒด๋ฅผ ๊ณต์œ ํ•˜๋Š” ๋ฐฉ์‹๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋งŒ ๋™์‹œ ์“ฐ๊ธฐ ๋™๊ธฐํ™”๊ฐ€ ๊นŒ๋‹ค๋กœ์›Œ์„œ, ์œ„์ ฏ์šฉ ๊ฐ€๋ฒผ์šด snapshot ํŒจํ„ด์ด ๋” ์•ˆ์ „ํ•œ ์„ ํƒ์ด๋ผ๊ณ  ๋ด…๋‹ˆ๋‹ค.


ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… 3 โ€” Color(hex:)์˜ ํ•จ์ •: Swift ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„

์นดํ…Œ๊ณ ๋ฆฌ์— ์ƒ‰์ƒ์„ ์ž…ํžˆ๋ ค๊ณ  hex string โ†’ SwiftUI Color ๋ณ€ํ™˜ ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์—ˆ๋Š”๋ฐ, ์นดํ…Œ๊ณ ๋ฆฌ๋“ค์ด ์ฃ„๋‹ค ๋น„์Šทํ•œ ํ†ค์œผ๋กœ ๋ณด์˜€์Šต๋‹ˆ๋‹ค. ๋นจ๊ฐ•์€ ๊ฒ€์€๋น› ๋„๋Š” ํšŒ๋…น์ƒ‰, ํŒŒ๋ž‘์€ ํ•‘ํฌ๋น› ๋งˆ์  ํƒ€๋กœ ๋ Œ๋”๋ง๋˜๋Š” ์‹์ด์—ˆ์–ด์š”.

์ฒ˜์Œ ์ฝ”๋“œ

extension Color {
    init?(hex: String) {
        var hexString = hex.trimmingCharacters(in: .whitespacesAndNewlines)
        if hexString.hasPrefix("#") { hexString.removeFirst() }

        guard hexString.count == 6,
              let hexNumber = UInt64(hexString, radix: 16) else {
            return nil
        }

        let r = Double(hexNumber & 0xFF0000 >> 16) / 255.0  // โš ๏ธ
        let g = Double((hexNumber & 0x00FF00) >> 8) / 255.0
        let b = Double(hexNumber & 0x0000FF) / 255.0

        self.init(red: r, green: g, blue: b)
    }
}

์–ธ๋œป ๋ณด๋ฉด ๋ฉ€์ฉกํ•ฉ๋‹ˆ๋‹ค. ๋นจ๊ฐ• ์ฑ„๋„์€ 0xFF0000์œผ๋กœ ๋งˆ์Šคํ‚นํ•˜๊ณ  16๋น„ํŠธ ์˜ค๋ฅธ์ชฝ ์‹œํ”„ํŠธ, ๋…น์ƒ‰์€ 8๋น„ํŠธ ์‹œํ”„ํŠธ, ํŒŒ๋ž‘์€ ๊ทธ๋Œ€๋กœ. ๊ต๊ณผ์„œ์ ์ด์ฃ .

์›์ธ โ€” Swift ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„

๋ฌธ์ œ๋Š” ๋นจ๊ฐ• ๋ผ์ธ์˜ ๊ด„ํ˜ธ์ž…๋‹ˆ๋‹ค. Swift์—์„œ:

  • >> (BitwiseShiftPrecedence): ์šฐ์„ ์ˆœ์œ„ ๋†’์Œ
  • & (MultiplicationPrecedence): ์šฐ์„ ์ˆœ์œ„ ๋‚ฎ์Œ

๊ทธ๋ž˜์„œ hexNumber & 0xFF0000 >> 16์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ‰๊ฐ€๋ฉ๋‹ˆ๋‹ค:

hexNumber & (0xFF0000 >> 16)
= hexNumber & 0xFF   // โ† ํŒŒ๋ž‘ ๋ฐ”์ดํŠธ๋ฅผ ์ถ”์ถœ!

์ฆ‰ ๋นจ๊ฐ• ์ฑ„๋„์ด ํŒŒ๋ž‘ ๋ฐ”์ดํŠธ์˜ ๊ฐ’์„ ๊ฐ–๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ์ ์œผ๋กœ ๋ชจ๋“  ์ƒ‰์ƒ์ด R == B์ธ ๋ณ€์ข…์œผ๋กœ ๋ Œ๋”๋ง๋˜์–ด ์ƒ‰์ƒํ™˜์˜ ๋‹ค์–‘์„ฑ์ด ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด ์‹œ์Šคํ…œ ๋นจ๊ฐ• #FF3B30์„ ํ†ต๊ณผ์‹œํ‚ค๋ฉด:

  • r = 0x30 / 255 = 0.188 (ํŒŒ๋ž‘ ๋ฐ”์ดํŠธ!)
  • g = 0x3B / 255 = 0.231
  • b = 0x30 / 255 = 0.188

โ†’ ์šฐ์ค‘์ถฉํ•œ ๋‹คํฌ ๊ทธ๋ ˆ์ด๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ๋นจ๊ฐ•์ด ์•„๋‹ˆ์ฃ .

๋…น์ƒ‰ยทํŒŒ๋ž‘ ๋ผ์ธ์ด ๋ฉ€์ฉกํ•œ ์ด์œ ๋Š” ๋‘˜ ๋‹ค ๋ช…์‹œ์  ๊ด„ํ˜ธ (hexNumber & 0x00FF00) >> 8 ํ˜•ํƒœ๋กœ ๋˜์–ด ์žˆ์–ด์„œ์ž…๋‹ˆ๋‹ค. ๋นจ๊ฐ• ๋ผ์ธ๋งŒ ๊ด„ํ˜ธ๊ฐ€ ๋น ์ง„ ๊ฒŒ ํ•จ์ •์ด์—ˆ์–ด์š”.

์ˆ˜์ •

๊ด„ํ˜ธ ํ•˜๋‚˜ ์ถ”๊ฐ€ํ•˜๋ฉด ๋๋‚ฉ๋‹ˆ๋‹ค. (ํ—ˆ๋ฌดํ•˜์ง€๋งŒ ์‹œ์›ํ–ˆ์–ด์š”....!!ใ…Žใ…Ž)

extension Color {
    init?(hex: String) {
        var hexString = hex.trimmingCharacters(in: .whitespacesAndNewlines)
        if hexString.hasPrefix("#") { hexString.removeFirst() }

        guard hexString.count == 6,
              let hexNumber = UInt64(hexString, radix: 16) else {
            return nil
        }

        let r = Double((hexNumber & 0xFF0000) >> 16) / 255.0  // โœ…
        let g = Double((hexNumber & 0x00FF00) >> 8) / 255.0
        let b = Double(hexNumber & 0x0000FF) / 255.0

        self.init(red: r, green: g, blue: b)
    }
}

๋ฐฐ์šด ์ 

  • ๋น„ํŠธ ์—ฐ์‚ฐ์€ ํ•ญ์ƒ ๊ด„ํ˜ธ๋ฅผ ๋„‰๋„‰ํžˆ ์‚ฌ์šฉํ•˜๊ธฐ. ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„๋ฅผ ์™ธ์šฐ๋Š” ๊ฒƒ๋ณด๋‹ค ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.
  • ๋ฒ„๊ทธ๋Š” "10๊ฐœ ์ƒ‰์ƒ์ด ๋‹ค ๋น„์Šทํ•˜๋‹ค"๋Š” ์‹œ๊ฐ์  ๋‹จ์„œ๋กœ ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž ์ž…์žฅ์˜ ๋‹จ์„œ โ†’ ์ฝ”๋“œ์˜ ๊ฐ€์„ค โ†’ ํ•œ ์ค„ ์ˆ˜์ •. ์ด ๊ณผ์ •์„ ๊ฑฐ์น˜๋ฉด ์‹ค๋ ฅ์ด ๋Šก๋‹ˆ๋‹ค.
  • ์œ ๋‹› ํ…Œ์ŠคํŠธ๋กœ ์ƒ‰์ƒ ๋ณ€ํ™˜์„ ๊ฒ€์ฆํ•˜๊ธฐ. 5๋ถ„์ด๋ฉด ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๊ณ , ๋‹ค์‹œ ๊ฐ™์€ ์˜ค๋ฅ˜์— ์•ˆ ๋น ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
func test_Color_hexParsing() {
    let red = Color(hex: "#FF3B30")
    XCTAssertNotNil(red)
    // UIColor๋กœ ๋ณ€ํ™˜ํ•ด์„œ r, g, b ์ฑ„๋„์„ ์ง์ ‘ ๊ฒ€์ฆ
    // ...
}

ํšŒ๊ณ  โ€” v2์—์„œ ๋ฐ”๊พธ๊ณ  ์‹ถ์€ ๊ฒƒ๋“ค

1. NSManagedObject์˜ String/UUID ํ”„๋กœํผํ‹ฐ๋ฅผ Optional๋กœ

์ด๋ฒˆ PR์—์„  ๊ฐ€๋“œ ๋‘ ์žฅ์น˜๋กœ ๋ง‰์•˜์ง€๋งŒ, ๊ทผ๋ณธ์ ์œผ๋กœ name: String? + wrappedName: String ํŒจํ„ด์ด ๋” ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค. CLAUDE.md ๊ฐ€์ด๋“œ์—๋„ ๋ช…์‹œ๋˜์–ด ์žˆ๋Š”๋ฐ ์ •์ž‘ ๋ชจ๋ธ์— ์ ์šฉ์„ ์•ˆ ํ–ˆ์–ด์š”. v2 ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹œ์ ์— ๊ฐ™์ด ์ฒ˜๋ฆฌํ•  ์˜ˆ์ •.

2. CoreData ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜

App Store์— ์ถœ์‹œํ•œ ํ›„ ๋ชจ๋ธ์„ ๋ฐ”๊พธ๋ฉด ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ๊ฐ€ ๋‚ ์•„๊ฐˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ผ์ดํŠธ์›จ์ดํŠธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด ์ž๋™์œผ๋กœ ๋™์ž‘ํ•˜๊ธด ํ•˜์ง€๋งŒ, ์ถœ์‹œ ์ „์— ์˜๋„์ ์œผ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ํ•œ๋ฒˆ ๋Œ๋ ค๋ณด๋Š” ๊ฒ€์ฆ ๋‹จ๊ณ„๋ฅผ ๋‘๋Š” ๊ฒŒ ์ข‹๊ฒ ๋‹ค๋Š” ์ƒ๊ฐ.

3. ์œ„์ ฏ ๋ฐ์ดํ„ฐ๋ฅผ CoreData ๊ณต์œ ๋กœ ์ „ํ™˜?

์ง€๊ธˆ์€ JSON ์ง๋ ฌํ™” ๋ฐฉ์‹์ธ๋ฐ, ๋ฐ์ดํ„ฐ๊ฐ€ ์ปค์ง€๋ฉด ๋น„ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค. NSPersistentContainer์˜ store URL์„ App Group ์ปจํ…Œ์ด๋„ˆ๋กœ ์„ค์ •ํ•ด CoreData ์ž์ฒด๋ฅผ ๊ณต์œ ํ•˜๋Š” ๋ฐฉ์‹๋„ ๊ฒ€ํ† ํ•ด๋ณผ ๋งŒํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋งŒ ๋™์‹œ ์“ฐ๊ธฐ ๋™๊ธฐํ™”๊ฐ€ ๊นŒ๋‹ค๋กœ์›Œ์„œ ์‹ ์ค‘ํžˆ.

4. Snapshot ํ…Œ์ŠคํŠธ

UI ํšŒ๊ท€๋ฅผ ์žก์œผ๋ ค๋ฉด ์Šค๋ƒ…์ƒท ํ…Œ์ŠคํŠธ๊ฐ€ ํ•„์ˆ˜์ธ๋ฐ ์ด๋ฒˆ ์ถœ์‹œ์—” ๋ชป ๋„ฃ์—ˆ์Šต๋‹ˆ๋‹ค. point-free์˜ swift-snapshot-testing์„ ๋‹ค์Œ ๋ฒ„์ „์— ์ถ”๊ฐ€ํ•  ๊ณ„ํš.


๋งˆ์น˜๋ฉฐ

๊ฐ€์žฅ ํฌ๊ฒŒ ๋ฐฐ์šด ๊ฑด "์‹ ํ˜ธ๋ฅผ ์˜์‹ฌํ•˜๋Š” ๋ฒ•"์ด์—ˆ์Šต๋‹ˆ๋‹ค.

์ฒ˜์Œ์—๋Š” EXC_BREAKPOINT๋ฅผ ๋ณด๋ฉด "์–ด๋”˜๊ฐ€ ์ฝ”๋“œ๊ฐ€ ์ž˜๋ชป๋๊ฒ ์ง€" ์ •๋„๋กœ ๋ง‰์—ฐํ•˜๊ฒŒ ๋ดค์–ด์š”. ๊ทธ๋Ÿฐ๋ฐ ๊ทธ ์‹ ํ˜ธ ๋’ค์—” KVOยทKVCยทNSManagedObject ๋ผ์ดํ”„์‚ฌ์ดํดยทSwiftUI ํŠธ๋žœ์žญ์…˜ ๊ฐ™์€ ์—ฌ๋Ÿฌ ๋ ˆ์ด์–ด์˜ ์ƒํ˜ธ์ž‘์šฉ์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์‹ ์ž…์€ ๊ทธ ๋ ˆ์ด์–ด ๋ชจ๋‘๋ฅผ ๊นŠ์ด ์•Œ ์ˆ˜ ์—†์ง€๋งŒ, "์ด ํ•จ์ •์€ X์™€ Y์˜ ๊ฒฝ๊ณ„์—์„œ ์ผ์–ด๋‚ฌ๊ฒ ๊ตฌ๋‚˜" ๋ผ๋Š” ๊ฐ๊ฐ๋งŒ ๊ธธ๋Ÿฌ๋„ ๊ฐ™์€ ๋ฅ˜์˜ ๋ฌธ์ œ๋Š” ๋น ๋ฅด๊ฒŒ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋”๋ผ๊ณ ์š”.

ํˆฌ๋‘ ์•ฑ์€ ํ”ํ•˜์ง€๋งŒ, ํ˜ผ์ž์„œ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ๋งŒ๋“ค๊ณ  App Store๊นŒ์ง€ ์˜ฌ๋ฆฐ ๊ฒฝํ—˜์€ ํ”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์‹ ์ž… ๊ฐœ๋ฐœ์ž๋ผ๋ฉด ํ•œ ๋ฒˆ์ฏค ์ž๊ธฐ ์ด๋ฆ„์ด ๋ฐ•ํžŒ ์•ฑ์„ ์ถœ์‹œํ•ด๋ณด๊ธธ ๊ฐ•๋ ฅ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค. ํŠœํ† ๋ฆฌ์–ผ๋งŒ์œผ๋กœ๋Š” ์ ˆ๋Œ€ ๋ฐฐ์šธ ์ˆ˜ ์—†๋Š” ๊ฒŒ ๋„ˆ๋ฌด ๋งŽ์•„์š”.

๋ ˆํฌ ๋ฐ ์ถœ์‹œ ์ •๋ณด

  • App Store: ๊ฒ€์ƒ‰ "์˜๋‘์ž‡"
  • GitHub: haansohee/SoDoIt
  • ๊ฐœ์ธ์ •๋ณด ์ฒ˜๋ฆฌ๋ฐฉ์นจ / ์ง€์› ํŽ˜์ด์ง€: haansohee.github.io/SoDoIt
profile
iโ™กS Developer

0๊ฐœ์˜ ๋Œ“๊ธ€