Realm CRUD를 배워보자 !
mobile DB 는 Realm을 따라올 자가 없다고 들었다. 지난 포스트 에서 언급했듯이 Realm은 무복사나 MVCC 같은 방식을 채택해 굉장히 효율적이라고 들었는데, 이번에 LocalDB 를 이용해야 하는 상황이 생겨서 Realm을 써보기로 했다. 처음 써보는 ORM은 얼마나 편리할지 기대가 됐다.
컨셉
강의를 들으면서 CRUD를 배웠고, 이 강의의 컨셉은 길거리에서 번호를 따고 그 사람에 대한 평점과 이메일 주소를 입력하여 로컬 디비에 저장하고 보여주는 방식이었다. 훌륭한 예제는 아니지만 컨셉이 있어서 이해에 도움은 됐다.
완성된 화면은 다음과 같을 것이며, 각각의 tableViewCell에 담긴 정보는 모두 Realm Local DB에 담겨 있는 것들이다.
환경
swift5(swift4 이상의 환경 권장.)
xcode 12.5
에서 테스트했다.
Realm 설치하기
Xcode에서 File -> Swift Packages -> Add Package Dependency 클릭!
원하는 프로젝트 클릭후 next, https://github.com/realm/realm-cocoa.git
붙여넣기.
다음 페이지에서 Up to Next Major
해놓고 next.
realm과 RealmSwift 모두 체크한 뒤 finish
PickupLines.swift
PickupLines.swift 만들기.
해당 파일 안에서 import RealmSwift
해줌!
line, score, email 받아 쓸 것이니까 다음 변수 선언해주기.
var line: String
var score: Integer?
var email: String?
이렇게!
init 메서드 선언
init(line: String, score: Int?, email: String?) {
self.line = line
self.score = score
self.email = email
}
PickupLine 을 Realm화하기.
Realm은 Object
라는 Data Model을 가지고 있다. (우리가 아는 그 object말고.)
이것을 구현해주자. 다음과 같이 클래스 뒤에 Object를 적어서: class PickupLine: Object {...}
그런데 문제점은 Object
를 구현(conform)하면 init
메소드로 initialize가 불가능하다.
Realm 자체가 init 하는 특별한 방법이 있기 때문에 그런 것인데,
이것을 해결하기 위해서는 init 메서드를 다음과 같이 고쳐주면 된다.
convenience init(line: String, score: Int?, email: String?) {
self.init()
self.line = line
self.score = score
self.email = email
}
dynamic한 변수로 고쳐주기
여기까지 따라해도 여전히 오류가 뜰 것인데, 그것은 Realm Object는 그 안에 있는 모든 변수가 dynamic
해야 하기 때문이다.
dynamic 하다는 것은 runtime에 변수를 사용할 수 있어야 한다는 것을 의미한다. 그리고 dynamic한 변수를 생성하기 위해서는 dynamic이라는 키워드를 변수 앞에 적어줘야 하며, 이 dynamic이라는 키워드는 objective-c 언어로 되어 있기 때문에 dynamic 앞에 @objc
키워드를 추가로 적어줘야 한다. 그리고 해당 변수가 런타임에 사용할 준비가 되어 있어야 하므로 기본값을 준 상태(예를 들어 String 변수에는 "" 이라는 값을 주는 것 같이.)여야 한다. 그런데 여기서 예외가 또 발생한다. 그 예외란 숫자(Int, UInt, Double, Float ...) 의 경우 Realm에서 RealmOptional<Generic>
과 같은 형태로 Optional 값을 제공한다. 이 경우 해당 변수는 dynamic 키워드를 필요로 하지 않는다. 또한 이렇게 선언한 RealmOptional<숫자자료형> 변수는 예를 들어 self.score.value = someInt
이런식으로 .value
로 접근하여 값을 넣어줘야 한다. 숫자 자료형과는 다르게 String Optional의 경우 ?
를 붙여 Optional로 선언하고 nil을 넣으면 된다.
아무튼 방금 언급한 내용에 따라 바꾼 다음 코드는 다음과 같다.
class PickupLine: Object {
@objc dynamic var line: String = ""
let score = RealmOptional<Int>()
@objc dynamic var email: String? = nil
convenience init(line: String, score: Int?, email: String?) {
self.init()
self.line = line
self.score.value = score
self.email = email
}
}
여기서 우리는 dynamic 변수마다 @objc 라는 키워드를 매번 쓰지 않는 방식으로 고쳐쓸 수 있다.
class
앞에 @objcMembers
를 적어줌으로써 변수 앞에 있는 @objc
는 없앨 수 있다.(dynamic은 없애면 안 됨.)
여기까지 따라하는 데에 조금 복잡하다는 생각이 들었겠지만 복잡한 건 여기서 끝이다. 이제 쭉쭉 따라오면 된다.
이제 Int 값인 score.value를 String 으로 받아쓰는 상황을 대비해 다음 함수를 간단하게 적어주고 다음 파일로 넘어가자.
func scoreString() -> String? {
guard let score = score.value else {return nil}
return String(score)
}
이제 PickupLines.swift는 다음과 같이 되어 있을 것이다.
//
// PickupLines.swift
// HoTechCourse
//
// Created by 최강훈 on 2021/05/03.
//
import Foundation
import RealmSwift
@objcMembers class PickupLine: Object {
dynamic var line: String = ""
let score = RealmOptional<Int>()
dynamic var email: String? = nil
convenience init(line: String, score: Int?, email: String?) {
self.init()
self.line = line
self.score.value = score
self.email = email
}
func scoreString() -> String? {
guard let score = score.value else {return nil}
return String(score)
}
}
CRUD 설명을 위한 세팅
지금 여기 부분을 쓰는 시점에서 너무 바빠서, 간략히 전체 코드를 올리고 CRUD
부분만 짚어서 설명하겠다.
파일은 이미지처럼 다섯 개 만들면 되고, 복사 붙여넣기로 모두 코드를 채워주자. @IBAction
이나 @IBOutlet
같은 것들은 여러분들이 스토리보드에 적절한 레이블이나 버튼을 만들어 연결하시길 바란다. tableViewCell Identifier도 적당히 줘야 한다. 어렵진 않을 것이다.
PickupLineTableViewCell.swift
//
// PickupLineTableViewCell.swift
// HoTechCourse
//
// Created by 최강훈 on 2021/05/04.
//
import UIKit
class PickupLineTableViewCell: UITableViewCell {
@IBOutlet weak var lineLabel: UILabel!
@IBOutlet weak var scoreLabel: UILabel!
@IBOutlet weak var emailLabel: UILabel!
func configure(with pickupLine: PickupLine) {
lineLabel?.text = pickupLine.line
scoreLabel.text = pickupLine.scoreString() ?? ""
emailLabel.text = pickupLine.email ?? ""
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
pickupLinesViewController.swift
//
// PickupLinesViewController.swift
// HoTechCourse
//
// Created by 최강훈 on 2021/05/04.
//
import UIKit
import RealmSwift
class PickupLinesViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
var pickupLines: Results<PickupLine>!
var notificationToken: NotificationToken?
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.delegate = self
self.tableView.dataSource = self
// CRUD의 'R'
// realm은 읽어낸 data 컬렉션을 memory에 올리지 않고 정보를 얻기 때문에
// 다른 DB 라이브러리보다 훨씬 효율적이다.
let realm = RealmService.shared.realm
self.pickupLines = realm.objects(PickupLine.self)
// 아래를 적어줌으로써 realm DB에 업데이트가 있을 때마다
// tableView의 데이터를 리로드한다.
notificationToken = pickupLines.observe { (changes) in
self.tableView.reloadData()
}
RealmService.shared.observeRealmErrors(in: self) { (error) in
print(error ?? "unrecognized error")
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
RealmService.shared.stopObservingErrors(in: self)
}
@IBAction func onAddTapped() {
AlertService.addAlert(in: self) { (line, score, email) in
let newPickupLine = PickupLine(line: line, score: score, email: email)
RealmService.shared.create(newPickupLine)
}
}
}
extension PickupLinesViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return pickupLines.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "pickupLineCell") as? PickupLineTableViewCell
else {return UITableViewCell()}
let pickupLine = pickupLines[indexPath.row]
cell.configure(with: pickupLine)
return cell
}
}
extension PickupLinesViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("selected")
let pickupLine = pickupLines[indexPath.row]
AlertService.updateAlert(in: self, pickupLine: pickupLine) {
(line, score, email) in
let dict: [String: Any?] = ["line": line,
"score": score,
"email": email]
RealmService.shared.update(pickupLine, with: dict)
}
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
guard editingStyle == .delete else {return}
let pickupLine = pickupLines[indexPath.row]
RealmService.shared.delete(pickupLine)
}
}
AlertService.swift
//
// AlertService.swift
// HoTechCourse
//
// Created by 최강훈 on 2021/05/04.
//
import Foundation
import UIKit
class AlertService {
private init() {}
static func addAlert(in vc: UIViewController,
completion: @escaping (String, Int?, String?) -> Void) {
let alert = UIAlertController(title: "Add Line", message: nil, preferredStyle: .alert)
alert.addTextField { (lineTextField) in
lineTextField.placeholder = "Enter Pickup Line"
}
alert.addTextField { (scoreTextField) in
scoreTextField.placeholder = "Enter Score"
}
alert.addTextField { (emailTextField) in
emailTextField.placeholder = "Enter Email"
}
let action = UIAlertAction(title: "Add", style: .default) { (_) in
guard let line = alert.textFields?.first?.text,
let scoreString = alert.textFields?[1].text,
let emailString = alert.textFields?.last?.text
else {return}
let score = scoreString == "" ? nil : Int(scoreString)
let email = emailString == "" ? nil : emailString
completion(line, score, email)
}
alert.addAction(action)
vc.present(alert, animated: true)
}
static func updateAlert(in vc: UIViewController,
pickupLine: PickupLine,
completion: @escaping (String, Int?, String?) -> Void) {
let alert = UIAlertController(title: "Update Line",
message: nil,
preferredStyle: .alert)
alert.addTextField { (lineTextField) in
lineTextField.placeholder = "Enter line"
lineTextField.text = pickupLine.line
}
alert.addTextField { (scoreTextField) in
scoreTextField.placeholder = "Enter score"
scoreTextField.text = pickupLine.scoreString()
}
alert.addTextField{ (emailTextField) in
emailTextField.placeholder = "Enter email"
emailTextField.text = pickupLine.email
}
let action = UIAlertAction(title: "Update",
style: .default) { (_) in
guard let line = alert.textFields?.first?.text,
let scoreString = alert.textFields?[1].text,
let emailString = alert.textFields?.last?.text
else {return}
let score = scoreString == "" ? nil : Int(scoreString)
let email = emailString == "" ? nil : emailString
completion(line, score, email)
}
alert.addAction(action)
vc.present(alert, animated: true)
}
}
RealmService.swift
//
// RealmService.swift
// HoTechCourse
//
// Created by 최강훈 on 2021/05/04.
//
import Foundation
import UIKit
import RealmSwift
class RealmService {
private init() {}
static let shared = RealmService()
// Realm() 으로 선언한 변수는 Document/ 밑에 있는 realm DB 에 대한 포인터이다.
// 기존에 생성한 realm DB가 없다면 자동으로 생성한다.
var realm = try! Realm()
// T는 Generic이다.
// T는 typename의 약어이며, 모든 Object를 받을 수 있음을 의미한다.
func create<T: Object>(_ object: T) {
do {
// realm에 object를 추가.
try realm.write {
realm.add(object)
}
} catch {
post(error)
}
}
// CRUD의 'U'
func update<T: Object>(_ object: T, with dictionary: [String: Any?]) {
do {
try realm.write {
for (key, value) in dictionary {
object.setValue(value, forKey: key)
}
}
} catch {
post(error)
}
}
// CRUD의 'D'
func delete<T: Object>(_ object: T) {
do {
try realm.write {
realm.delete(object)
}
} catch {
post(error)
}
}
func post(_ error: Error) {
NotificationCenter.default.post(
name: Notification.Name("RealmError"),
object: error)
}
func observeRealmErrors(in vc: UIViewController,
completion: @escaping (Error?) -> Void) {
NotificationCenter.default.addObserver(
forName: NSNotification.Name("RealmError"),
object: nil,
queue: nil) { (notification) in
completion(notification.object as? Error)
}
}
func stopObservingErrors(in vc: UIViewController) {
NotificationCenter.default.removeObserver(vc, name: NSNotification.Name("RealmError"), object: nil)
}
}
CRUD의 CUD
Realm에서 Read는 단 한 줄로 읽는 게 가능하다. 때문에 이것은 맨 마지막에 따로 설명하고, 먼저 CUD에 대해 설명하겠다.
위에서 코드를 써놓은 RealmService.swift
파일을 가지고 설명을 할 것이다.
Realm에서 CUD를 사용하기 위해서는 먼저 RealmService 객체가 필요하다. 또한 앱의 실행 단계에 가장 먼저 만들어져 있는 것이 안정적이므로 static 키워드를 사용하여 선언해주는 게 좋다.
따라서 static let shared = RealmService()
또한 Realm() 객체도 필요하다. 이 realm 변수는 간단히 try! Realm()
을 통해 realm 변수를 선언해주면 된다.
이 경우 선언한 변수는 Document/ 폴더 밑에 있는 realmDB에 대한 포인터이며,
기존에 생성한 realmDB가 없다면 자동으로 생성한다.
create & delete
create의 경우 do-try-catch
문 안에서 try realm.write { realm.add(YourRealmObject) }
와 같이 써주면 된다.
필자는 추가로 <T>
라는 키워드를 썼는데, T
는 swift의 근간이 되는 c++에서의 typename T
처럼 어떤 형태의 타입이든 다 받을 수 있다는 의미인데, swift에서도 마찬가지로 그렇게 쓰인다.
아무튼 Realm 이라는 ORM Local DB 서비스는 이토록 간단히 DB를 생성할 수 있게 서비스를 제공하고 있다.
여기서 delete는 create 코드와 비교했을 때 토씨하나 틀리지 않고 create만 delete로 바꿔주면 끝이다.
(위에서 설명한 create code)
// T는 Generic이다.
// T는 typename의 약어이며, 모든 Object를 받을 수 있음을 의미한다.
func create<T: Object>(_ object: T) {
do {
// realm에 object를 추가.
try realm.write {
realm.add(object)
}
} catch {
post(error)
}
}
(delete코드. 위와 다른 게 없음.)
// CRUD의 'D'
func delete<T: Object>(_ object: T) {
do {
try realm.write {
realm.delete(object)
}
} catch {
post(error)
}
}
update
update같은 경우에는 뭐 데이터를 받는 방식이 여러 방법이 있겠지만 가장 직관적인 건 key-value 쌍으로 받는 거기 때문에 다음과 같은 방식을 선택했다. 바뀐 것은 realm.write()
안에 있는 부분!
update
// CRUD의 'U'
func update<T: Object>(_ object: T, with dictionary: [String: Any?]) {
do {
try realm.write {
for (key, value) in dictionary {
object.setValue(value, forKey: key)
}
}
} catch {
post(error)
}
}
CRUD의 'R'
read는 정말 간단하다.
다음 코드의 첫 번째 줄처럼 realm 객체만 받아오면
단순히 realm객체.objects(객체.self)
한 줄로 read할 수 있다.
// CRUD의 'R'
// realm은 읽어낸 data 컬렉션을 memory에 올리지 않고 정보를 얻기 때문에
// 다른 DB 라이브러리보다 훨씬 효율적이다.
let realm = RealmService.shared.realm
self.pickupLines = realm.objects(PickupLine.self)
여기까지 Realm의 CRUD에 대해 알아보았다. 글을 쓰다가 오늘 안에 마무리해야지 마음먹었더니 시간이 너무 부족해 급하게 마무리한 느낌이 있다. 그래도 너무 바쁘니... 어서 끝내고 다음 글을 써야겠다 !! 테스트하는 화면을 공유하면서 글을 마치겠다.
예제 화면
create
createexample
update
delete
최근댓글