모바일 로컬 DB 선택: SQLite, Realm, WatermelonDB 비교

모바일 개발

SQLiteRealmWatermelonDB로컬 DB오프라인

이 글은 누구를 위한 것인가

  • 오프라인 우선 앱에서 어떤 DB를 써야 할지 모르는 팀
  • 수만 건 데이터를 모바일에서 빠르게 검색해야 하는 개발자
  • Room/Core Data에서 Realm으로 마이그레이션을 고려하는 팀

들어가며

"오프라인에서도 검색이 돼야 한다." 수만 건 상품 데이터를 로컬에 저장하고 빠르게 검색하려면 올바른 DB 선택이 중요하다. SQLite, Realm, WatermelonDB는 각각 다른 상황에 최적이다.

이 글은 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에 반영해 코드를 크게 단순화한다.