2025. 3. 16. 14:05ㆍApple/Apple Frameworks
서버 없이 로컬 데이터 관리가 필요한 이유는 무엇일까? 개인프로젝트로 앱을 개발할 때, 부담없이 데이터를 관리할 수 있는 데이터베이스 시스템에 관심을 가지게 된다. 전통적인 방식의 RDBMS는 아니지만, 서버리스 DB 이자 임베디드 데이터베이스가 바로 그런 데이터베이스다.
이해하기 쉽고, 사용자 입장에서도 서버에 사용자의 개인적인 데이터가 저장되지 않으니 찜찜할 이유도 없다. 게다가, 서버와의 통신이 불안정한 걸 걱정할 필요도 없고, 오프라인 환경에서도 잘 동작한다. 서버 운용비용에 대한 부담도 없다. 어떤 로컬 데이터베이스를 선택하고 어떻게 사용해야 할까? 2024년에 내가 배포했던 소리의 기록(링크) 앱도 바로 이런 로컬 DB 시스템을 사용한 좋은 예시다.
응? 임베디드 데이터베이스(Embedded Database)? 그게 뭔데?
애플리케이션 내부에 내장되어 함께 동작하는 데이터베이스다. 앱이 실행되자마자 DB 도 같이 실행되어 속도가 빠르고 리소스도 적다. 또한, 설치하는 방식, 유지보수도 쉽다. 모바일 앱 내부에서 데이터를 로컬로 저장하고 관리하는 방식을 채택한다. 그런 방식을 객체 지향 데이터베이스(Object-Oriented Database: OODB) 라고 한다. 개인앱에서 썼던 라이브러리 Realm 도 이러한 특성을 지녔다. 그러다 우연히 알게 된 중요한 소식 중 Realm 이 더이상 SDK 업데이트를 하지 않는다는 얘길 들었다. 25년 9월 쯤은 아예 제거된다고 한다.
(Realm 작성 글이 궁금하신 분들은 여기 링크를 클릭해주세요.)

NOTE - Atlas Device SDK 지원 중단 문서
👯 🔥 발등에 불이 떨어졌군.. (이참에 앱의 기능과 UIUX 도 개선해야겠다.) 서드 파티 라이브러리는 업데이트 혹은 변화, 사라짐이 있을 수 있기에 Apple 에서 지원하는 기본 라이브러리를 써야겠다. SwiftData 나 CoreData 로 바꿔야할 계기가 된 것이다.
NOTE
CoreData 의 역사는 iOS 3(2009년) / macOS 는 10.4 Tiger os 일 때 처음 생겼다고 한다. `NS-` 는 NeXTStep 의 약자로 macOS 의 전 단계인 NeXTSTEP 체제를 개발하면서 사용한 네이밍 방식이다. 원래 macOS(cocoa) 에서 쓰던 네이밍 스타일이었는데, iOS 에서도 이를 유지하면서 코어 데이터 관련 객체들도 NS 접두어가 붙는 경우가 대부분이다.
자, 그럼 SwiftData 를 쓸까? CoreData 를 쓸까?
내 앱의 프레임워크는 UIKit 90% + SwiftUI 10% 에 MVVM 모델을 추구하고 있다. 2024년 4월 당시 개인 앱을 iOS 16 으로 Deployment target(배포 타겟)으로 설정했고, 현재는 SwiftUI 의 비중을 UIKit 보다 늘릴 계획은 없다. Model 은 UI 에서 노출되는 데이터 값임으로 Presentation(View단) - Scene 에 같이 넣었다. 이런 조건 하에서는 일단 CoreData 가 더 적합하다고 생각했다. SwiftData 는 SwiftUI 와 자연스럽게 연동되며, iOS 17 이상부터 적용 가능하다.
따라서, 현재 SwiftData 를 당장 차용할 수 없었다. 차후 SwiftUI 앱 기반으로 프로젝트를 만들게 된다면 SwiftData 를 써야겠다. CoreData는 UIKit과 SwiftUI 에서 모두사용 가능하므로 더 범용적인 선택이 될 것이다.
CoreData 를 적용하기 전에, Data Model 의 기본개념에 대해 알아보자
CoreData의 데이터 모델은 앱이 저장할 데이터 구조를 정의하는 핵심 요소다. 예를 들어, 'User' 엔터티는 이름, 나이, 이메일을 포함할 수 있다.
1. 엔터티(Entity)
- Entity는 관리 객체(Managed Object)의 데이터 모델을 정의하는 요소다.
- 관계형 데이터베이스(RDBMS)에서 테이블(Table)을 정의하는 Schema와 유사하다.
- 각 Entity는 여러 개의 속성(Attributes)을 포함하며, 특정 데이터 구조를 나타낸다.
2. 속성(Attribute)
- 속성(Attribute)은 엔터티(Entity) 내에서 관리 객체가 저장할 데이터의 개별 요소를 의미한다.
- 예를 들어, User라는 엔터티가 있다면 name, age, email 등의 속성이 포함될 수 있다.
- Core Data는 다양한 데이터 타입(문자열, 숫자, 날짜 등)을 속성으로 지원한다.
CoreData 의 강점
- 네트워크 연결이 설정되어 있지 않는 오프라인 사용도 가능하다. 물론 인터넷 환경이 필요한 써드 파티 라이브러리가 있다면 다른 부분에서는 와이파이나 셀룰러가 필요할 것이다. (라이브러리 firebase, open api를 파싱(parsing)하는 상황..등에서는 네트워크 연결이 되어 있어야함.)
→ 그렇지만 여기서는 데이터의 저장과 관리 관점으로만 보는 것이다. 여하튼 오프라인에서도 데이터 관리를 할 수 있다는 건 큰 강점이다. - 애플리케이션의 영구 데이터를 저장
→ 앱을 종료해도 데이터는 유지된다. - 실행 취소, 재실행 기능 제공: 개별, 그룹 단위로 변경 사항을 되돌릴 수 있다. Redo, Undo 기능도 지원이 된다는 얘기!
- CloudKit 을 애플에서 함께 사용할 수 있도록 제공하기 때문에 데이터 동기화가 가능하다.
→ 같은 iCloud 계정의 여러 기기에서 동일한 데이터를 사용 가능하다. 단, iCloud 월 이용료 1,100원만 낸다면 50GB 로 업그레이드도 가능하다. - CoreData 는 앱의 업데이트나 개발에 따라 데이터 모델을 버전화하고 사용자 데이터를 마이그레이션하는 메커니즘이 있다.
→ 마이그레이션? 사전적 의미의 이주? 엥 이건 또 뭔말이래..
예를 들어, 데이터 모델 버전 업데이트 시 기존 데이터와 새로운 모델 간의 불일치가 발생할 수 있다. 이 문제를 해결하는 과정이 데이터 마이그레이션이다.아니, 그래서 어떻게 설정하는 건데?
CoreData 는 자동 마이그레이션과 수동 마이그레이션을 지원해 데이터모델이 변경되면 사용자의 기존 데이터를 손실 없이 새로운 모델로 변환을 해준다고 한다.
단순한 변경인 속성 추가, 삭제 같은 것은 CoreData 가 알아서 해준다는 말씀!
✔️ 예제 케이스 1
- 속성 추가 (email 추가)
- 속성 삭제 (age 제거)
- 속성 이름 변경 (fullName → name)
let container = NSPersistentContainer(name: "MyAppModel")
let description = container.persistentStoreDescriptions.first
description?.setOption(true as NSNumber, forKey: NSMigratePersistentStoresAutomaticallyOption)
description?.setOption(true as NSNumber, forKey: NSInferMappingModelAutomaticallyOption)
이렇게 설정을 해두면 알아서 데이터 마이그레이션을 수행해준다. 그치만, 데이터 구조 변경이 크거나 기존 데이터를 변환해야할 경우에는 직접 마이그레이션 로직을 작성하긴 해야한다.
✔️ 예제 케이스 2 : 수동으로 마이그레이션 해야할 때
- fullName을 firstName과 lastName으로 분리
- birthDate를 age로 변환
순서
- 새로운 데이터 모델 버전을 추가 (.xcdatamodeld 파일에서 추가)
- 매핑 모델 (.xcmappingmodel)을 생성해 데이터 변환 규칙을 설정
- NSMigrationManager를 활용해 직접 마이그레이션 수행
let sourceURL = URL(fileURLWithPath: "OldModel.sqlite")
let destinationURL = URL(fileURLWithPath: "NewModel.sqlite")
let mappingModel = NSMappingModel(from: nil,
forSourceModel: oldModel,
destinationModel: newModel)
let migrationManager = NSMigrationManager(sourceModel: oldModel,
destinationModel: newModel)
do {
try migrationManager.migrateStore(from: sourceURL,
sourceType: NSSQLiteStoreType,
options: nil,
with: mappingModel,
toDestinationURL: destinationURL,
destinationType: NSSQLiteStoreType,
destinationOptions: nil)
print("마이그레이션 성공!")
} catch {
print("마이그레이션 실패: \(error)")
}
CoreData 버전관리에 대한 디테일을 생각도 못했다. Apple 이 그렇게 호락호락하고 허술하게 CoreData 를 제공하진 않을테니까..ㅎㅎ
그럼 버전관리는 왜 중요해?
- 앱 업데이트 시 기존 데이터가 유지됨
- 사용자가 기존 데이터를 잃지 않고 새로운 기능을 사용할 수 있음
- 데이터 구조 변경을 유연하게 처리할 수 있음
Core Data의 마이그레이션 기능을 잘 활용하면 앱의 데이터 모델이 변경되더라도 기존 사용자 데이터가 유지될 수 있도록 지원할 수 있음!
여기서 유의하거나 짚고 넘어가야 할 부분이 있음!
앱을 삭제한다면, 그래도 데이터가 유지가 되는건가?! 당근 아니다..(말밥도 아니다..)
앱을 삭제하면 당연히 CoreData 의 데이터는 사라진다. 그치만 기존 서버-클라이언트 형식의 관계형 데이터베이스 시스템은 그렇지 않다. 서버에 따로 저장을 해두니까. 앱을 지워도 사용자의 계정을 가입시켜 그 사용자 정보값 안에 데이터 정보들이 저장되어 있다면 다시 앱을 설치해도 불러들일 수 있다. 그래서 엄연히 CoreData는 앱이 업데이트 될 때 사용자의 정보가 유지되고 데이터 구조가 변경되어도 유지가 된다는 거지, 앱을 삭제해도 사용자의 데이터가 남아 있는 거랑은 별개다.
다시 한번 정리하자면 CoreData 는 앱 내부 로컬 저장소에 데이터를 저장하는 녀석이다. 앱이 삭제되면 해당 앱의 모든 데이터도 함께 삭제되는 것이다. 이 부분은 iOS 의 샌드박싱 보안 정책과 연관이 있다. 그렇기 때문에 앱을 삭제하고 재설치하면 초기상태(빈 백지)에서 다시 시작하는 것이다! 유지하고 싶다고? 그럼 위에서 언급한 CloudKit + CoreData 를 사용하는 것이다. ㅎㅎ (단, icloud 동기화가 꺼져 있으면 데이터 복원은 안됨으로 이 부분 기억할 것)
후..돌고 돌아 CoreData 에 대한 강점과 특징을 살펴봤다. 그럼 여기서 CoreData 가 어떻게 쓰이는지 길지 않게 한 코드 예시로 이해하고 넘어갈 수 있을 것 같다. (아직도 더 남았다고..?!?)
CoreData 참 매력이 있는 걸? 구조나 요소들은 어떤 게 있을까?
* 핵심만 골라 배우는 SwiftUI 기반의 iOS 프로그래밍 책(개정증보판)과 Apple 의 공식문서를 참고했습니다.
NSManagedObjectModel
앱의 유형, 속성 및 관계를 설명하는 앱의 모델 파일을 정의해준다.
NSManagedObjectContext
앱 유형의 인스턴스에 대한 데이터 변경 사항을 관리해준다.
NSPersistentContainer
스토어에서 앱 유형의 인스턴스를 저장하고 가져온다.
NSPersistentStoreCoordinator
모델, 컨텍스트 및 스토어 코디네이터를 한번에 설정할 수 있다. 즉, 데이터 저장소와 연결해주는 역할을 한다.
대표적인 Core Data의 저장소 타입
저장소 타입 | 속도 | 메모리 내 객체 그래프 | 특징 |
XML (atomic) | 느림 | 전체 로드 | XML 형식으로 저장됨 (iOS에서는 지원 안 함) |
Binary (atomic) | 빠름 | 전체 로드 | 바이너리 파일로 저장됨 |
SQLite | 빠름 | 부분 로드 | 가장 일반적으로 사용됨 (백엔드: SQLite) |
In-Memory | 빠름 | 전체 로드 | 디스크에 저장되지 않고 메모리에만 존재 |
CoreData(코어데이터)를 활용해 데이터를 로컬 디바이스에 저장해보자!
작업환경: SwiftUI, CoreData
CoreData - Data model 생성하기


new file template > iOS > Core Data > Data model 을 선택해 데이터모델 파일명을 지정해주면 오른쪽과 같이 엔터티와 속성을 생성해줄 수 있다.
이게 바로 Entity Description 을 생성해 주는 가장 첫번째 관문이다.
데이터를 관리하는 핵심 요소 - 객체 만들기
앱에 저장하고 데이터를 관리하는 핵심 요소를 통틀어 운영하는 객체가 바로 NSPersistentContainer 다. new file 을 선택해 파일 생성해서 PersistenceController 라는 구조체를 만들어 준다.
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init() {
container = NSPersistentContainer(name: "Products")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
전역적으로 사용하기 위해 설정된 PersistentController 의 container 변수 안 NSPersistentContainer 를 만들었다. 코어데이터의 저장소를 한번에 관리할 수 있게 된다. CoreData 저장소를 불러오기 위해, loadPersisentStores 메소드를 호출한다.
참고로 PersistentController 는 앱 실행시 한번만 생성되면 된다.
SwiftUI 앱과 CoreData 연결하기
import SwiftUI
@main
struct CoreDataDemoApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(
\.managedObjectContext,
persistenceController.container.viewContext)
}
}
}
앱의 진입점인 @main CoreDataDemoApp 이고, 이 부분에서 코어데이터를 초기화하고 주입시켜줘야 한다.
open var viewContext: NSManagedObjectContext { get }
viewContext는 NSManagedObjectContext 의 인스턴스로 코어데이터에서 데이터를 불러오거나 저장하는 작업을 관리하는 공간(컨텍스트)다. 메인스레드에서 실행되도록 설정된 기본 컨텍스트다. UI 업데이트를 위한 데이터를 가져오거나 저장시 사용한다.
위에서도 언급했듯 데이터 변경을 추적하는 작업 공간으로 데이터를 가지고 오거나 추가 삭제시 코어데이터는 즉시 데이터베이스에 적용되지는 않는다. 그래서 viewContext 가 변경사항을 추적하고 있다가 save() 를 호출하면 데이터베이스(SQLite) 에 저장되는 것이다. 비유하자면 viewContext 는 🗒️ 임시 메모장이라고 생각하면 이해가 쉬울 것 같다.
제품 관리 화면 만들기
- 제품 추가, 삭제, 검색하기 기능
- 제품을 추가하고 추가한 제품의 데이터를 저장
import SwiftUI
import CoreData
struct ContentView: View {
@State var name: String = ""
@State var quantity: String = ""
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
entity: Product.entity(),
sortDescriptors: [
NSSortDescriptor(
key: "name",
ascending: true
)
]
)
private var products: FetchedResults<Product>
var body: some View {
NavigationView {
VStack {
TextField("Product name", text: $name)
TextField("Product quantity", text: $quantity)
HStack {
Spacer()
Button("Add") {
addProduct()
}
Spacer()
NavigationLink(destination:ResultView(name: name,
viewContext: viewContext)) {
Text("Search")
}
Spacer()
Button("Clear") {
name = ""
quantity = ""
}
Spacer()
}
.padding()
.frame(maxWidth: .infinity)
List {
ForEach(products) { product in
HStack {
Text(product.name ?? "제품명 찾을 수 없음")
Spacer()
Text(product.quantity ?? "제품 수량 찾을 수 없음")
}
}
.onDelete(perform: deleteProducts)
}
.navigationTitle("Product Database")
}
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
private func addProduct() {
withAnimation {
let newProduct = Product(context: viewContext)
newProduct.name = name
newProduct.quantity = quantity
saveContext()
}
}
private func deleteProducts(offsets: IndexSet) {
withAnimation {
offsets.map { products[$0] }.forEach(viewContext.delete)
saveContext()
}
}
private func saveContext() {
do {
try viewContext.save()
} catch {
let error = error as NSError
fatalError("Unresolved error \(error))")
}
}
}
struct ResultView: View {
var name: String
var viewContext: NSManagedObjectContext
@State var matches: [Product]?
var body: some View {
return VStack {
List {
ForEach(matches ?? []) { match in
HStack {
Text(match.name ?? "결과를 찾을 수 없습니다.")
Spacer()
Text(match.quantity ?? "결과를 찾을 수 없습니다.")
}
}
}
.navigationTitle("Results")
}
.task {
let fetchRequest: NSFetchRequest<Product> = Product.fetchRequest()
fetchRequest.entity = Product.entity()
fetchRequest.predicate = NSPredicate(format: "name CONTAINS[cd] %@", name)
matches = try? viewContext.fetch(fetchRequest)
}
}
}
- @FetchRequest 는 CoreData 에 저장된 데이터를 가지고 올 수 있고, 정렬 순도 오름,내림차순으로 리스트에 표시할 수 있다.
- NSManagedObject를 생성하고 saveContext()를 호출하여 데이터 저장
- NSPredicate를 활용한 데이터 검색
- SwiftUI에서 Core Data를 자연스럽게 연동하는 방법


마무리하며
결국, CoreData와 SwiftData와 같은 로컬 데이터베이스는 서버 의존도를 줄이고, 사용자 데이터를 안전하게 관리하며, 앱 성능 최적화에 중요한 역할을 한다. 특히 CoreData의 데이터 마이그레이션 및 자동 업데이트 기능은 앱 개발과 유지보수에서 큰 도움이 될 것이다. 다만, 앱을 삭제하면 데이터도 함께 사라지므로 중요 데이터를 보존하고 싶을 땐, CloudKit을 도입해 사용자에게 데이터를 백업할 수 있는 선택권도 줘야할 것이다.
다른 방식으로 서버와 로컬 데이터를 적절히 혼합하여 사용하면 앱의 성능과 효율성 모두 개선할 수 있을 것이다. 이를 통해 안정적이고 사용자 경험을 향상시킬 수 있는 앱을 만들어 나갈 계획이다.
'Apple > Apple Frameworks' 카테고리의 다른 글
CloudKit 도입해서 iCloud 활용하기 (0) | 2025.03.23 |
---|---|
[ UIView ] SnapKit 사용하기 ( 공부하면서 연습 중..) (0) | 2022.03.25 |
[ uiux ] storyboard 와 autolayout 을 코드로..? (0) | 2022.03.25 |