Ver2.5 내 앱의 디지털 두뇌 만들기: iOS Core Data 완전 정복 가이드 🚀

내 앱의 디지털 두뇌 만들기: iOS Core Data 완전 정복 가이드 🚀
데이터, 더 이상 두려워하지 마세요! 당신의 앱에 똑똑한 기억 저장소를 만들어주는 Core Data, 친구처럼 쉽고 재미있게 알려드릴게요.
🤔 Core Data, 너 도대체 정체가 뭐야?
안녕! 우리 iOS 개발자들의 영원한 숙제이자 든든한 지원군, '데이터 관리'에 대해 이야기해 보려고 해.
앱을 만들다 보면 꼭 필요한 게 있지? 바로 사용자의 소중한 데이터를 어딘가에 저장하고, 필요할 때 다시 꺼내 쓰는 능력이야.
메모 앱이라면 메모 내용을, SNS 앱이라면 친구 목록이나 게시글을, 게임이라면 최고 점수를 저장해야 하잖아.
이때 우리 앞에 나타나는 거대한 산이 바로 Core Data야. 이름부터 뭔가 '코어'하고 '데이터'스러워서 괜히 어렵게 느껴지지? 😅
많은 입문자들이 "Core Data는 그냥 데이터베이스 아니야?"라고 생각하는데, 여기서부터 첫 번째 오해가 시작돼.
🚨 잠깐! 가장 큰 오해 바로잡기
Core Data는 데이터베이스가 아니야! 정확히 말하면, 애플이 제공하는 객체 그래프 관리 및 영속성 프레임워크(Object Graph and Persistence Framework)야. 말이 좀 어렵지? 쉽게 풀어서 설명해 줄게.
- 객체 그래프 관리: 앱에서 사용하는 데이터 조각들(객체)과 그들 사이의 관계(그래프)를 메모리 상에서 체계적으로 관리해 주는 역할.
- 영속성(Persistence): 메모리에서 관리하던 그 데이터들을 앱을 꺼도 사라지지 않게 파일 형태로 '영구히' 저장해 주는 역할.
즉, Core Data는 데이터베이스 그 자체가 아니라, 데이터를 객체처럼 편하게 다루고, 그걸 SQLite 같은 실제 데이터베이스나 다른 파일 형식으로 저장하는 과정을 '대신' 해주는 똑똑한 매니저라고 생각하면 돼.
"아니, 그냥 바로 데이터베이스 쓰면 되지 왜 이렇게 복잡한 걸 써?" 라고 생각할 수도 있어.
하지만 우리가 SQL 쿼리문을 직접 짜고, 데이터베이스 연결을 관리하고, 가져온 데이터를 다시 앱에서 쓸 수 있는 객체 형태로 변환하는... 이 모든 귀찮고 반복적인 작업을 Core Data가 대신해 준다고 생각해 봐. 엄청난 축복이지! ✨
우리는 그냥 Swift 코드에 익숙한 '객체'를 만들고, 수정하고, 삭제하기만 하면 돼. 나머지는 Core Data가 알아서 다 처리해 주니까.
Core Data를 움직이는 어벤져스: The Core Data Stack
Core Data가 마법처럼 데이터를 관리하는 데에는 숨은 영웅들이 있어. 바로 'Core Data Stack'이라고 불리는 친구들이야.
이 스택의 구조를 이해하면 Core Data의 절반은 이해한 거나 다름없어. 한번 각 멤버들을 만나볼까?
1. NSManagedObjectModel (데이터 설계도 🗺️):
우리 앱이 어떤 종류의 데이터를 다룰지 정의하는 설계도야. Xcode에서 `.xcdatamodeld` 라는 확장자를 가진 파일을 시각적으로 편집해서 만들지. 예를 들어 'Memo'라는 데이터 덩어리(Entity)는 'title'(문자열), 'content'(문자열), 'creationDate'(날짜) 같은 속성(Attribute)을 가진다고 정의하는 곳이야. 집을 짓기 전에 설계도부터 그리는 것과 똑같아.
2. NSPersistentStoreCoordinator (프로젝트 총괄 매니저 🧑💼): 이름이 길고 어렵지만 역할은 명확해. 바로 '중개자' 역할! 설계도(Model)와 실제 데이터 창고(Store) 사이를 연결해 주는 총괄 매니저야. Context로부터 "데이터 저장해줘!" 또는 "데이터 좀 찾아줘!" 같은 요청을 받으면, 설계도를 보고 어떤 데이터를 어디에(어떤 파일에) 저장하고 가져와야 할지 지시하는 거지.
3. NSPersistentStore (실제 데이터 창고 💾): 데이터가 최종적으로 저장되는 물리적인 파일이야. 우리는 대부분 기본값인 SQLite 데이터베이스를 사용하게 될 거야. 하지만 필요에 따라 XML, 바이너리, 또는 메모리 내 저장소(In-Memory Store)를 선택할 수도 있어. 중요한 건, 우리는 이 창고를 직접 건드릴 필요가 거의 없다는 거야. 총괄 매니저(Coordinator)가 다 알아서 해주니까!
4. NSManagedObjectContext (나의 작업 공간 📝): 바로 우리가 가장 많이 상호작용하게 될 핵심 멤버야! 데이터를 생성하고, 읽고, 수정하고, 삭제하는 모든 작업이 일어나는 임시 작업 공간이라고 생각하면 돼. 마치 우리가 글을 쓸 때 메모장에 먼저 초안을 작성하고, 수정하고, 다듬은 뒤에 '저장' 버튼을 눌러야 실제 파일로 저장되는 것과 같아. Context에서 아무리 많은 데이터를 바꾸더라도, `save()` 메소드를 호출하기 전까지는 실제 파일(Persistent Store)에는 아무런 변화가 없어. 이건 정말 중요한 개념이니 꼭 기억해 둬!
💡 최신 트렌드: NSPersistentContainer (만능 해결사)
과거에는 위에 설명한 4개의 멤버를 우리가 직접 코드로 만들고 연결해 주는 복잡한 설정 과정이 필요했어. 하지만 iOS 10부터 애플이 NSPersistentContainer라는 멋진 친구를 소개해 줬지. 이 친구는 Core Data Stack 전체를 하나의 객체로 감싸서, 아주 간단한 코드로 스택 전체를 초기화하고 사용할 수 있게 해줘. 요즘은 거의 모든 프로젝트에서 `NSPersistentContainer`를 사용하니까, 우리도 이 친구와 친해져야 해! 😉
🛠️ 첫걸음 떼기: Core Data 프로젝트 설정하기
백문이 불여일견! 이론은 이쯤하고 직접 Core Data를 사용하는 프로젝트를 만들어보자.
Xcode와 함께라면 정말 간단하게 시작할 수 있어.
1. Xcode 프로젝트 생성 (feat. Core Data 체크박스)
Xcode를 열고 새로운 프로젝트를 생성해 봐. (File > New > Project...)
iOS 탭에서 'App' 템플릿을 선택하고 다음으로 넘어가자.
프로젝트 옵션을 설정하는 화면에서 아주 중요한 체크박스가 하나 있어.
바로 'Use Core Data' 라는 체크박스야. 이걸 꼭 체크해 줘!
이 체크박스 하나만으로 Xcode는 우리를 위해 Core Data Stack 설정 코드를 `AppDelegate.swift` (또는 SwiftUI 프로젝트의 경우 `Persistence.swift`) 파일에 자동으로 생성해 줘. 아까 말했던 `NSPersistentContainer`를 설정하는 바로 그 코드야. 세상 참 편해졌지?
2. 데이터 설계도 그리기: .xcdatamodeld 파일 탐험
프로젝트를 생성하고 나면, 프로젝트 네비게이터에 `[프로젝트이름].xcdatamodeld` 라는 파일이 보일 거야. 이게 바로 아까 설명했던 데이터 설계도, `NSManagedObjectModel`을 시각적으로 편집하는 파일이야.
이 파일을 클릭하면 마치 데이터베이스 설계 툴 같은 화면이 나타나. 여기서 우리는 앱에서 사용할 데이터의 구조를 정의할 수 있어.
1) 엔티티(Entity) 추가하기
화면 하단의 'Add Entity' 버튼을 눌러서 새로운 엔티티를 추가해 보자. 엔티티는 우리가 다룰 데이터의 기본 단위야. '메모' 앱을 만든다면 'Memo'라는 엔티티가 필요하겠지? 엔티티 이름을 'Memo'로 바꿔주자.
2) 속성(Attribute) 추가하기
'Memo' 엔티티를 선택한 상태에서, 오른쪽의 'Attributes' 섹션에 있는 '+' 버튼을 눌러 속성을 추가할 수 있어. 속성은 엔티티가 가지는 구체적인 데이터 항목들이야.
- title: 메모의 제목. Type은 'String'으로 설정.
- content: 메모의 내용. Type은 'String'으로 설정.
- creationDate: 생성 날짜. Type은 'Date'로 설정.
- isFavorite: 즐겨찾기 여부. Type은 'Boolean'으로 설정.
각 속성마다 'Optional' 체크박스가 있는데, 이걸 해제하면 해당 속성은 반드시 값을 가져야 한다는 의미(non-optional)가 돼.
3) 관계(Relationship) 설정하기
Core Data의 강력함은 엔티티 간의 관계를 설정할 수 있다는 점에서 빛을 발해. 예를 들어, 메모를 '폴더'별로 관리하고 싶다고 가정해 보자.
'Add Entity'를 눌러 'Folder'라는 새 엔티티를 만들고, 'name'이라는 String 타입 속성을 추가해 줘.
이제 'Folder'와 'Memo'를 연결해 줄 차례야.
- Folder 엔티티 선택: 'Relationships' 섹션에서 '+' 버튼을 눌러 'memos'라는 이름의 관계를 추가.
- Destination 설정: 'memos' 관계의 목적지를 'Memo' 엔티티로 설정.
- Type 설정: 하나의 폴더는 여러 개의 메모를 가질 수 있으니, Type을 'To Many'로 설정.
이것만 하면 끝이 아니야! 좋은 관계는 쌍방향이어야 하지. 😜
데이터 무결성을 위해 역관계(Inverse Relationship)를 설정해 주는 것이 매우 중요해.
- Memo 엔티티 선택: 'Relationships' 섹션에서 '+' 버튼을 눌러 'folder'라는 이름의 관계를 추가.
- Destination 설정: 'folder' 관계의 목적지를 'Folder' 엔티티로 설정.
- Type 설정: 하나의 메모는 하나의 폴더에만 속하므로, Type은 'To One'으로 설정.
- Inverse 설정: 오른쪽 Data Model Inspector 패널에서 'folder' 관계의 Inverse를 'Folder' 엔티티의 'memos' 관계로 설정해 줘. (반대로 'Folder'의 'memos' 관계 Inverse도 'Memo'의 'folder'로 자동 설정될 거야)
💡 관계의 삭제 규칙(Delete Rule)도 중요해!
관계를 설정할 때 'Delete Rule'이라는 옵션이 있어. 이건 관계를 맺은 한쪽 데이터가 삭제될 때 다른 쪽 데이터를 어떻게 처리할지 정하는 규칙이야.
- Nullify (기본값): 폴더가 삭제되면, 그 안에 있던 메모들의 'folder' 관계를 nil(null)로 만들어. 메모 자체는 삭제되지 않고 소속 없는 메모가 되는 거지.
- Cascade: 폴더가 삭제되면, 그 안에 있던 모든 메모들도 함께 삭제돼. 매우 강력하고 편리하지만, 의도치 않게 데이터가 날아갈 수 있으니 신중하게 사용해야 해.
- Deny: 폴더 안에 메모가 하나라도 남아있으면, 폴더 삭제 자체가 실패해. "자식들부터 처리하고 오세요!" 라고 막는 셈이지.
상황에 맞는 삭제 규칙을 선택하는 건 데이터의 안정성을 위해 아주 중요해!
💾 CRUD 완전 정복: 데이터 가지고 놀기
자, 이제 설계도도 완성됐고 프로젝트 설정도 끝났어. 드디어 진짜 데이터를 만져볼 시간이야!
모든 데이터 처리의 기본은 CRUD라고 불리는 네 가지 작업으로 이루어져. 바로 Create(생성), Read(읽기), Update(수정), Delete(삭제)야. Core Data에서는 이 작업들을 어떻게 수행하는지 하나씩 알아보자.
먼저, 작업을 수행할 Context(작업 공간)에 접근해야 해. `NSPersistentContainer`를 사용하면 아주 간단하게 가져올 수 있어.
import CoreData
// AppDelegate.swift 또는 Persistence.swift 등에서 container를 가져오거나,
// SwiftUI의 경우 @Environment(\.managedObjectContext) 를 사용.
// 여기서는 기본적인 접근법을 보여줄게.
let container = (UIApplication.shared.delegate as! AppDelegate).persistentContainer
let context = container.viewContext
Create (생성): 새로운 데이터 만들기
새로운 메모를 하나 만들어보자. 마치 클래스의 인스턴스를 만드는 것과 비슷해.
func createMemo(title: String, content: String) {
// 1. 작업 공간(Context)을 알려주며 새로운 Memo 객체를 생성해.
let newMemo = Memo(context: context)
// 2. 객체의 속성에 값을 할당해.
newMemo.title = title
newMemo.content = content
newMemo.creationDate = Date() // 현재 시간
newMemo.isFavorite = false
// 3. ⭐️ 가장 중요! Context의 변경사항을 저장해서 실제 파일에 반영해.
saveContext()
}
func saveContext() {
do {
try context.save()
print("메모 저장 성공!")
} catch {
print("메모 저장 실패: \(error)")
}
}
// 사용 예시
createMemo(title: "오늘 할 일", content: "Core Data 공부하기 🤓")
정말 간단하지? `Memo(context: context)`로 새로운 객체를 만들고, 속성 값을 채워준 다음, `context.save()`를 호출하면 끝이야. `save()`를 호출하기 전까지는 모든 변경사항이 메모리에만 있다는 점, 다시 한번 강조할게!
Read (읽기/가져오기): 저장된 데이터 불러오기
데이터를 저장했으니 이제 불러와야겠지? 데이터를 불러오는 작업을 'Fetch'라고 해. 이때 `NSFetchRequest`라는 친구가 필요해. "이런이런 조건에 맞는 데이터를 찾아줘!" 라고 요청하는 요청서라고 생각하면 돼.
func fetchMemos() -> [Memo] {
// 1. 어떤 엔티티를 가져올지 정하는 요청서(Fetch Request)를 만들어.
let request: NSFetchRequest<Memo> = Memo.fetchRequest()
// 2. (옵션) 정렬 방식이나 필터링 조건을 추가할 수 있어.
// 예: 최신 날짜 순으로 정렬하기
let sortByDate = NSSortDescriptor(key: "creationDate", ascending: false)
request.sortDescriptors = [sortByDate]
// 3. Context에게 요청서를 전달해서 데이터를 가져와.
do {
let memos = try context.fetch(request)
print("총 \(memos.count)개의 메모를 가져왔습니다.")
return memos
} catch {
print("메모 가져오기 실패: \(error)")
return []
}
}
// 사용 예시
let myMemos = fetchMemos()
for memo in myMemos {
print("제목: \(memo.title ?? "제목 없음")")
}
`NSFetchRequest`는 정말 강력한 도구야. 단순히 모든 데이터를 가져오는 것뿐만 아니라, 복잡한 조건으로 원하는 데이터만 쏙쏙 골라낼 수 있거든. 이때 사용되는 것이 바로 `NSPredicate`와 `NSSortDescriptor`야.
🔍 탐정의 돋보기, NSPredicate!
`NSPredicate`는 데이터를 필터링하는 조건을 정의하는 클래스야. 마치 탐정이 범인을 찾기 위해 조건을 하나씩 좁혀나가는 것과 같아.
// "isFavorite" 속성이 true인 메모만 찾기
let predicate = NSPredicate(format: "isFavorite == %@", NSNumber(value: true))
request.predicate = predicate
// 제목에 "공부"라는 단어가 포함된 메모만 찾기
let predicate = NSPredicate(format: "title CONTAINS[c] %@", "공부")
// [c]는 대소문자를 구분하지 않겠다는 옵션이야.
request.predicate = predicate
// 여러 조건을 조합할 수도 있어.
// 즐겨찾기 되어 있고, 제목에 "오늘"이 포함된 메모
let predicate = NSPredicate(format: "isFavorite == YES AND title CONTAINS %@", "오늘")
request.predicate = predicate
`NSPredicate`를 잘 다루면 Core Data의 활용도가 몇 배는 높아지니, 꼭 여러 가지 예제를 통해 익숙해지는 걸 추천해. 마치 데이터베이스의 `WHERE` 절과 같은 역할을 한다고 생각하면 쉬워.
Update (수정): 기존 데이터 변경하기
데이터 수정은 생성과 읽기의 조합이야. 먼저 수정하고 싶은 데이터를 Fetch로 가져온 다음, 그 객체의 속성을 바꾸고, 마지막에 저장하면 돼.
func updateMemo(memoToUpdate: Memo, newTitle: String, newContent: String) {
// 1. 수정할 객체는 이미 Fetch를 통해 가져왔다고 가정.
// 2. 객체의 속성을 새로운 값으로 변경해.
memoToUpdate.title = newTitle
memoToUpdate.content = newContent
// 3. 변경사항을 저장!
saveContext()
print("메모 업데이트 성공!")
}
// 사용 예시
// 먼저 수정할 메모를 찾아야겠지?
let memos = fetchMemos()
if let firstMemo = memos.first {
updateMemo(memoToUpdate: firstMemo, newTitle: "수정된 제목!", newContent: "내용도 수정했어요. ✍️")
}
어때? 정말 직관적이지? 그냥 객체의 프로퍼티를 바꾸는 것과 똑같아. Core Data가 객체 지향적으로 데이터를 다룰 수 있게 해준다는 장점이 바로 이런 데서 드러나는 거야.
Delete (삭제): 데이터 지우기
삭제도 수정과 비슷해. 삭제할 데이터를 먼저 찾아서 가져온 다음, Context에게 "이 객체 지워줘!" 라고 알려주기만 하면 돼.
func deleteMemo(memoToDelete: Memo) {
// 1. 삭제할 객체는 이미 Fetch를 통해 가져왔다고 가정.
// 2. Context에게 이 객체를 삭제하라고 명령해.
context.delete(memoToDelete)
// 3. 변경사항(삭제)을 저장!
saveContext()
print("메모 삭제 성공!")
}
// 사용 예시
let memos = fetchMemos()
if let lastMemo = memos.last {
deleteMemo(memoToDelete: lastMemo)
}
`context.delete()` 메소드를 호출하면 해당 객체는 '삭제 예정' 상태가 되고, `context.save()`가 호출되는 순간 실제 저장소에서 데이터가 사라지게 돼.
지금까지 CRUD 작업을 살펴봤어. 이 네 가지 기본기만 탄탄히 다져두면, 웬만한 앱의 데이터 관리는 문제없이 해낼 수 있을 거야. 이런 전문적인 기술을 배우고 익혀서 **재능넷** 같은 플랫폼에서 자신의 능력을 발휘하거나, 다른 전문가의 도움을 받는 것도 좋은 성장 방법이 될 수 있겠지? 😉
⚡️ 레벨업! Core Data 심화 과정
기본기를 마스터했다면, 이제 Core Data를 한 단계 더 깊게 파고들 시간이야.
실무에서 마주칠 수 있는 문제들을 해결하고, 더 효율적이고 안정적인 앱을 만들기 위한 고급 기술들을 알아보자.
Concurrency (동시성): 멀티스레딩 환경에서 살아남기
Core Data를 사용하면서 개발자들이 가장 많이 겪는 어려움이자, 가장 많은 앱 크래시의 원인이 바로 '동시성' 문제야.
요즘 앱들은 부드러운 UI를 위해 복잡한 작업(네트워크 통신, 대용량 데이터 처리 등)을 백그라운드 스레드에서 처리하는 게 기본이지. 하지만 Core Data의 `NSManagedObjectContext`는 기본적으로 스레드에 안전하지 않아(not thread-safe).
🚨 절대 규칙: Context와 ManagedObject는 생성된 스레드에서만 사용하라!
메인 스레드에서 만든 `NSManagedObjectContext`나 거기서 가져온 `NSManagedObject`(예: `Memo` 객체)를 백그라운드 스레드에서 직접 접근하거나 수정하려고 하면, 앱은 높은 확률로 크래시되거나 데이터가 깨지는 무서운 일을 겪게 될 거야. 이건 Core Data의 절대적인 규칙이니 반드시 지켜야 해.
그럼 어떻게 해야 할까? `NSPersistentContainer`가 해답을 제시해 줘.
// NSPersistentContainer는 두 종류의 Context를 기본적으로 제공해.
// 1. container.viewContext: 메인 스레드(UI 스레드) 전용 Context
// 2. container.performBackgroundTask: 백그라운드 스레드에서 안전하게 작업을 수행할 수 있는 임시 Context를 만들어주는 메소드
func processLargeDataInBackground() {
container.performBackgroundTask { backgroundContext in
// 이 클로저 안은 이제 안전한 백그라운드 스레드야!
// 여기서 사용하는 backgroundContext는 이 스레드 전용 Context.
// 예: 대량의 JSON 데이터를 받아서 Core Data에 저장하는 작업
for i in 1...10000 {
let newMemo = Memo(context: backgroundContext)
newMemo.title = "백그라운드 메모 \(i)"
newMemo.content = "이 메모는 백그라운드에서 생성됨"
newMemo.creationDate = Date()
}
// 백그라운드 Context에서 변경사항을 저장.
do {
try backgroundContext.save()
print("백그라운드 작업 및 저장 완료!")
} catch {
print("백그라운드 저장 실패: \(error)")
}
}
}
`performBackgroundTask`를 사용하면 Core Data가 알아서 스레드에 안전한 별도의 Context를 만들어서 제공해 줘. 우리는 그 안에서 마음껏 데이터를 처리하고 저장하면 돼. 저장이 완료되면, Core Data가 똑똑하게 메인 Context와 데이터를 동기화해주기 때문에 UI 업데이트도 문제없어.
만약 다른 스레드에 있는 특정 객체를 알려줘야 한다면, 객체 자체를 전달하는 대신 `NSManagedObjectID`를 사용해야 해. `NSManagedObjectID`는 스레드에 안전하며, 어떤 Context에서든 `context.object(with: objectID)` 메소드를 통해 해당 객체를 안전하게 다시 불러올 수 있는 '신분증' 같은 역할을 해.
Data Migration (데이터 마이그레이션): 앱 업데이트 시 모델 변경 대처하기
앱을 출시하고 운영하다 보면, 데이터 모델을 변경해야 할 때가 반드시 찾아와.
'Memo' 엔티티에 'tags'라는 새로운 속성을 추가하거나, 기존 'title' 속성의 이름을 'subject'로 바꾸는 것처럼 말이야.
이때 아무 생각 없이 `.xcdatamodeld` 파일만 수정하고 앱을 업데이트해서 배포하면 어떻게 될까?
기존 버전의 앱을 사용하던 유저들은 앱을 업데이트하는 순간 크래시를 경험하게 될 거야. 왜냐하면 이미 저장된 데이터(이전 버전의 설계도)와 새로운 코드(새로운 버전의 설계도)가 맞지 않기 때문이지.
이 문제를 해결하는 과정이 바로 데이터 마이그레이션(Data Migration)이야. Core Data는 대부분의 간단한 변경사항에 대해 아주 우아한 해결책을 제공해.
1) 경량 마이그레이션 (Lightweight Migration)
이름처럼 가볍고 자동화된 마이그레이션 방법이야. 다음과 같은 간단한 변경사항들은 Core Data가 알아서 처리해 줘.
- 엔티티 또는 속성 추가/삭제
- Optional이 아니었던 속성을 Optional로 변경
- 엔티티 또는 속성의 이름 변경 (Renaming Identifier 설정 필요)
- 새로운 관계 추가
경량 마이그레이션을 활성화하는 방법은 놀랍도록 간단해. `NSPersistentContainer`를 초기화할 때, `NSPersistentStoreDescription`의 두 가지 옵션을 `true`로 설정해주기만 하면 돼.
// AppDelegate.swift 또는 Persistence.swift 파일에서
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "YourAppName")
// 아래 두 줄이 경량 마이그레이션을 위한 핵심 코드!
let description = container.persistentStoreDescriptions.first
description?.shouldInferMappingModelAutomatically = true
description?.shouldMigrateStoreAutomatically = true
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
// ... 에러 처리
})
return container
}()
이 두 줄의 코드를 추가하는 것만으로, 대부분의 모델 변경에 따른 앱 크래시를 막을 수 있어. 정말 강력하지?
2) 중량 마이그레이션 (Heavyweight Migration)
속성의 데이터 타입을 바꾸거나, 엔티티를 분리/병합하는 등 복잡한 변경이 필요할 때는 경량 마이그레이션만으로는 부족해. 이때는 개발자가 직접 데이터 변환 규칙을 정의하는 '매핑 모델(Mapping Model)'을 만들고 코드를 작성해야 하는데, 이걸 중량 마이그레이션이라고 해. 과정이 꽤 복잡하기 때문에, 가능하면 데이터 모델을 신중하게 설계해서 중량 마이그레이션을 피하는 것이 좋아.
Core Data + SwiftUI: 환상의 짝꿍 🤝
SwiftUI 시대가 오면서 Core Data 사용법도 훨씬 더 간결하고 우아해졌어.
SwiftUI는 Core Data와 아주 잘 통합되도록 설계되었거든. 몇 가지 핵심적인 프로퍼티 래퍼(Property Wrapper)만 알면 돼.
1. @Environment(\.managedObjectContext): 뷰 계층 어디서든 `NSPersistentContainer`의 `viewContext`에 접근할 수 있게 해주는 환경 변수야.
2. @FetchRequest: 이게 바로 SwiftUI + Core Data의 꽃이야! 뷰가 필요로 하는 데이터를 선언적으로 가져올 수 있게 해줘. 정렬 방식이나 필터링(Predicate)도 바로 적용할 수 있고, 데이터가 변경되면 뷰를 자동으로 업데이트해주는 마법까지 부려.
간단한 메모 목록을 SwiftUI로 만드는 예제를 한번 볼까?
import SwiftUI
import CoreData
struct MemoListView: View {
// 1. 환경 변수를 통해 Context에 접근
@Environment(\.managedObjectContext) private var viewContext
// 2. @FetchRequest로 데이터를 가져와.
// creationDate를 기준으로 내림차순 정렬된 모든 Memo를 가져온다.
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Memo.creationDate, ascending: false)],
animation: .default)
private var memos: FetchedResults<Memo>
var body: some View {
NavigationView {
List {
ForEach(memos) { memo in
VStack(alignment: .leading) {
Text(memo.title ?? "제목 없음").font(.headline)
Text(memo.content ?? "").font(.subheadline).lineLimit(1)
}
}
.onDelete(perform: deleteMemos) // 스와이프해서 삭제 기능
}
.navigationTitle("내 메모장")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: addMemo) {
Label("Add Memo", systemImage: "plus")
}
}
}
}
}
private func addMemo() {
withAnimation {
let newMemo = Memo(context: viewContext)
newMemo.title = "새로운 메모"
newMemo.content = "내용을 입력하세요."
newMemo.creationDate = Date()
do {
try viewContext.save()
} catch {
// 에러 처리
}
}
}
private func deleteMemos(offsets: IndexSet) {
withAnimation {
offsets.map { memos[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
// 에러 처리
}
}
}
}
어때? `fetchMemos()` 같은 함수를 직접 호출할 필요도 없이, `@FetchRequest` 하나로 데이터 로딩과 뷰 업데이트가 모두 해결됐어. CRUD 작업도 훨씬 간결해졌지. 이처럼 SwiftUI와 Core Data는 함께 사용할 때 엄청난 시너지를 내.
🧐 Core Data, 이럴 땐 조심해! (Pitfalls & Alternatives)
Core Data는 강력한 도구지만, 만병통치약은 아니야. 잘못 사용하면 오히려 앱의 성능을 저하시키거나 개발을 더 복잡하게 만들 수도 있어. 몇 가지 흔한 실수와 대안에 대해 알아보자.
흔히 저지르는 실수들
- Core Data를 SQL 데이터베이스처럼 생각하기: 자꾸 SQL 쿼리 관점에서 접근하려고 하면 Core Data의 객체 그래프라는 본질을 놓치게 돼. JOIN 대신 관계(Relationship)를 통해 객체에 접근하는 방식을 익혀야 해.
- 스레드 규칙 무시하기: 위에서 강조했듯이, 동시성 규칙을 어기는 것은 크래시로 가는 지름길이야.
- 대용량 바이너리 데이터(BLOB)를 직접 저장하기: 이미지, 비디오, 오디오 파일 같은 큰 데이터를 Core Data에 직접 저장하면 성능이 급격히 저하될 수 있어. 이런 데이터는 파일 시스템에 저장하고, Core Data에는 그 파일의 경로(Path)나 ID만 문자열로 저장하는 것이 훨씬 효율적이야.
- 과도한 Fetch 요청: 루프 안에서 Fetch 요청을 반복적으로 호출하는 등의 코드는 성능에 치명적일 수 있어. 필요한 데이터는 한 번의 Fetch로 최대한 가져오고, `NSPredicate`를 활용해 효율적으로 필터링하는 습관을 들이자.
Core Data가 정답이 아닐 때: 대안들
모든 상황에 Core Data가 필요한 건 아니야. 때로는 더 가볍고 간단한 대안이 더 좋을 수 있어.
- UserDefaults: 앱의 간단한 설정 값(다크 모드 여부, 알림 설정 등)이나 사용자의 기본 정보를 저장하는 데는 `UserDefaults`가 가장 적합해. 아주 적은 양의 Key-Value 데이터를 저장하기 위한 용도야.
- Keychain: 비밀번호, 인증 토큰 등 보안이 중요한 정보를 저장해야 한다면 반드시 `Keychain`을 사용해야 해. iOS가 제공하는 가장 안전한 저장 공간이야.
- 파일 시스템 (FileManager): 이미지, 문서, 로그 파일 등 원본 파일 형태를 그대로 유지하며 저장하고 싶을 때는 앱의 Documents 디렉토리 등에 직접 저장하는 것이 좋아.
- Realm, GRDB.swift (SQLite 래퍼): 만약 멀티플랫폼(iOS, Android)을 지원해야 하거나, 순수한 SQL의 강력함을 더 선호한다면 Realm이나 GRDB 같은 서드파티 라이브러리를 고려해 볼 수 있어. 각각의 장단점이 뚜렷하니 프로젝트의 성격에 맞게 선택해야 해.
💡 어떤 저장 방식을 선택할까?
결국 정답은 없어. "내가 다루려는 데이터는 어떤 특징을 가지고 있는가?"를 먼저 고민하는 것이 중요해.
- 데이터가 복잡한 관계를 가지고 있고, 객체 지향적으로 다루고 싶다면? 👉 Core Data
- 단순한 설정 값이나 플래그(flag) 정보라면? 👉 UserDefaults
- 절대 노출되면 안 되는 민감한 정보라면? 👉 Keychain
- SQL에 더 익숙하고, 크로스플랫폼이 필요하다면? 👉 Realm, GRDB 등
마치며: Core Data와 함께 성장하기 🌟
와, 정말 긴 여정이었어! Core Data의 기본 개념부터 스택의 구조, CRUD, 그리고 동시성과 마이그레이션 같은 고급 주제까지 함께 살펴봤네. 👏
처음에는 `NSManagedThis`, `NSPersistentThat` 같은 낯선 이름들 때문에 어렵게 느껴질 수 있지만, 오늘 우리가 함께 탐험한 개념들을 차근차근 곱씹어보면 Core Data가 사실은 개발자를 편하게 해주기 위해 얼마나 많은 고민을 담고 있는 프레임워크인지 알게 될 거야.
Core Data는 단순히 데이터를 저장하는 기술이 아니야. 앱의 데이터를 어떻게 구조화하고, 어떻게 상호작용하게 할 것인지에 대한 깊은 고민을 담고 있는 '설계 철학'에 가까워.
이 철학을 이해하고 나면, 당신의 앱은 훨씬 더 안정적이고, 확장 가능하며, 효율적으로 동작하게 될 거야.
두려워하지 말고, 작은 토이 프로젝트부터 시작해서 오늘 배운 CRUD를 하나씩 적용해 봐. 에러를 만나고, 해결하는 과정에서 당신의 Core Data 실력은 쑥쑥 성장할 테니까. 이 지식 콘텐츠가 당신의 앱에 똑똑한 '디지털 두뇌'를 만들어주는 여정에 든든한 첫걸음이 되었기를 바라.
앞으로도 개발의 길을 걸으며 마주치는 수많은 도전들, 즐겁게 헤쳐나가길 응원할게! 화이팅! 💪
관련 키워드
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.