이 글은 누구를 위한 것인가
- 오프라인 우선 앱에서 어떤 DB를 써야 할지 모르는 팀
- 수만 건 데이터를 모바일에서 빠르게 검색해야 하는 개발자
- Room/Core Data에서 Realm으로 마이그레이션을 고려하는 팀
들어가며
"오프라인에서도 검색이 돼야 한다." 수만 건 상품 데이터를 로컬에 저장하고 빠르게 검색하려면 올바른 DB 선택이 중요하다. SQLite, Realm, WatermelonDB는 각각 다른 상황에 최적이다. SQLite가 적합한 경우와 아닌 경우는 공식 "When to use SQLite" 문서에 직접 정리돼 있으니 선택 전에 한 번 읽어볼 만하다.
이 글은 bluefoxdev.kr의 모바일 데이터 저장소 가이드 를 참고하여 작성했습니다.
1. DB 선택 기준
[로컬 DB 비교]
SQLite (Room/FMDB):
성숙도: 최고 (40년+)
쿼리: 완전한 SQL
성능: 대용량 집계 쿼리 강함
반응성: 트리거 필요 (자동 업데이트 없음)
크로스플랫폼: Room(Android)/FMDB(iOS) 별도
적합: 복잡한 쿼리, 레거시, 팀이 SQL 잘 알 때
Realm:
성숙도: 높음
쿼리: Realm Query Language
성능: 읽기 빠름, 쓰기 ACID
반응성: 객체 변경 자동 감지
크로스플랫폼: iOS, Android, React Native, Flutter
적합: 반응형 앱, 오프라인 동기화, Atlas Device Sync
WatermelonDB:
성숙도: 중간
기반: SQLite (C++)
반응성: Observable 쿼리 (RxJS)
성능: 수십만 레코드에 최적화
React Native: 네이티브 SQLite 직접 접근
적합: React Native 대용량 데이터
[선택 가이드]
단순 키-값: UserDefaults/SharedPreferences
구조화 데이터 < 1만건: Room/Core Data/SwiftData
반응형 + 오프라인 동기화: Realm
React Native 대용량: WatermelonDB
복잡한 SQL 집계: SQLite (Room/FMDB)
2. Realm 구현 (크로스플랫폼)
// iOS Swift - Realm
import RealmSwift
// 1. 모델 정의
class Product: Object {
@Persisted(primaryKey: true) var id: String = ""
@Persisted var name: String = ""
@Persisted var price: Double = 0
@Persisted var category: String = ""
@Persisted var isFavorite: Bool = false
@Persisted var updatedAt: Date = Date()
// 역참조
@Persisted(originProperty: "products") var orders: LinkingObjects<Order>
}
class Order: Object {
@Persisted(primaryKey: true) var id: String = ""
@Persisted var products: List<Product>
@Persisted var totalAmount: Double = 0
@Persisted var createdAt: Date = Date()
}
// 2. Realm 설정 및 마이그레이션
class RealmManager {
static let shared = RealmManager()
private var realm: Realm!
func configure() throws {
let config = Realm.Configuration(
schemaVersion: 3,
migrationBlock: { migration, oldSchemaVersion in
if oldSchemaVersion < 2 {
migration.enumerateObjects(ofType: "Product") { _, newObject in
newObject!["category"] = "기타"
}
}
if oldSchemaVersion < 3 {
// isFavorite 새로 추가 (기본값 false)
}
}
)
Realm.Configuration.defaultConfiguration = config
realm = try Realm()
}
// 3. 반응형 쿼리 (뷰 자동 업데이트)
func observeFavoriteProducts(onChange: @escaping ([Product]) -> Void) -> NotificationToken {
let results = realm.objects(Product.self)
.filter("isFavorite == true")
.sorted(byKeyPath: "name")
return results.observe { changes in
switch changes {
case .initial(let products):
onChange(Array(products))
case .update(let products, _, _, _):
onChange(Array(products))
case .error(let error):
print("Realm 에러: \(error)")
}
}
}
// 4. 쓰기 트랜잭션
func toggleFavorite(productId: String) throws {
guard let product = realm.object(ofType: Product.self, forPrimaryKey: productId) else { return }
try realm.write {
product.isFavorite = !product.isFavorite
}
}
// 5. 대량 데이터 삽입 (서버 동기화)
func syncProducts(_ products: [ProductDTO]) throws {
let realmProducts = products.map { dto -> Product in
let p = Product()
p.id = dto.id
p.name = dto.name
p.price = dto.price
p.category = dto.category
p.updatedAt = dto.updatedAt
return p
}
try realm.write {
realm.add(realmProducts, update: .modified)
}
}
// 6. 검색 (풀텍스트 유사)
func searchProducts(query: String) -> Results<Product> {
return realm.objects(Product.self)
.filter("name CONTAINS[cd] %@", query)
.sorted(byKeyPath: "name")
}
}
// SwiftUI에서 Realm 사용
struct ProductListView: View {
@ObservedResults(Product.self, sortDescriptor: SortDescriptor(keyPath: "name")) var products
var body: some View {
List(products) { product in
ProductRow(product: product)
}
.searchable(text: $searchText)
.onChange(of: searchText) { newValue in
$products.filter = NSPredicate(format: "name CONTAINS[cd] %@", newValue)
}
}
@State private var searchText = ""
}
struct ProductRow: View {
@ObservedRealmObject var product: Product
var body: some View {
HStack {
Text(product.name)
Spacer()
Button {
$product.isFavorite.wrappedValue.toggle() // 자동 Realm 업데이트
} label: {
Image(systemName: product.isFavorite ? "heart.fill" : "heart")
}
}
}
}
struct ProductDTO { let id: String; let name: String; let price: Double; let category: String; let updatedAt: Date }
마무리
DB 선택은 "얼마나 복잡한 쿼리가 필요한가"와 "반응형 UI가 필요한가"로 결정한다. 반응형 UI + 오프라인 동기화 → Realm, 복잡한 집계 SQL → Room/FMDB. Realm의 @ObservedResults와 @ObservedRealmObject는 DB 변경을 자동으로 UI에 반영해 코드를 크게 단순화한다.