[iOS] 연구해본 Database Event-Driven Reloading View

유인호·2024년 2월 22일
1

iOS

목록 보기
31/64

0. 서론

SwiftData같은 DB 프레임 워크는 데이터가 변경되면 뷰도 알아서 변경되는데, Realm은 그렇지 않다. 데이터가 변경 되어도 tableView.reloadData()같은 녀석들은 알아서 호출을 해주어야 하는데, viewWillApper()같은 메서드에 넣어주기에는 DB가 변경되지 않아도 계속 호출이 되기 때문에 비효율적이다.

런닝머신을 뛰면서 어떻게 해야 효율적일지 생각해보았음.
내가 생각하는 요구조건은,
1. 여러 뷰 컨트롤러에 TableView, CollectionView가 있는데, 이 녀석들을 전부 reload를 해주고 싶다.
2. 클로저에 담아놓은 데이터들이 사라지면 안되니까 싱글톤으로 구성하면 괜찮을까?

1. 첫번째 안

class DBObserver {
	static let shared = DBObserver()
	private init() { }

	private var closures: [(()->Void)] = []

	func occurEvent() {
		for item in closures {
			item()
		}
	}

	func bind(_ newClosure: @escaping ()->Void) {
		self.closures.append(newClosure)
	}
}

DB가 변경될때 할 일들을 클로저에 담아놓고, occurEvent()메서드를 호출하면 클로저들을 전부 실행시키는 방식이다. 상상으로 코딩해보았을때, viewDidLoad에 보통 클로저들을 정의해놓을 탠데, 그러면 똑같이 중복되는 클로저 구문이 쌓일 것 같다는 생각이 들었음.

2. 두번째 안

class DBObserver {
	static let shared = DBObserver()
	private init() { }

	private var closures: [(AnyClass, (()->Void))] = []

	func occurEvent() {
		for item in closures {
			item.1()
		}
	}

	func bind(withObject object: AnyClass, _ newClosure: @escaping ()->Void) {
		for item in closures where item.0 == object { return }
		self.closures.append((object, newClosure))
	}
}

저렇게 짜놓고, 데이터를 변경하는 곳과 필요한곳에 이런식으로 추가해보았다.

// 이벤트 발생
DBObserver.shared.occurEvent()

// 이벤트 설정
DBObserver.shared.bind(withObject: DetailViewController.self) {
	print(#file)
	self.detailView.tableView.reloadData()
}

예상대로 print는 잘 찍히는데, tableView가 reload되지 않는 상황이 발생, 아무래도 sync의 문제인것 같다.

3. 세번째 안 (+ GCD)

func occurEvent() {
	for item in closures {
		DispatchQueue.main.async {
			item.1()
		}
	}
}

GCD를 추가해보았다. 예상대로 매우 잘 되는것을 확인할 수 있었다.

여기서 우려스러웠던점은, 여러 뷰에서 clousre들을 정의했을때 하나 이상의 뷰에서 메모리에서 해제 되었을때 런타임 에러 이슈가 발생할까 우려했는데, 다행히 그런 이슈는 발생하지 않았다.

4. 끝...! 인줄 알았으나..

끝낼려고 정리하고 있는데 문득 혹여나 위의 케이스에서 메모리에서 해제되지 않아 런타임 이슈가 발생하지 않은게 아닐까 싶어 deinit()을 통해 메모리가 해제 되었는지 알아보았음.

.... 충격적이게도 ViewController가 메모리에서 해제되지 않았다. 정확한 실험결과가 아니였다는것..
Reasons for deinit is not getting called iOS Swift를 통해 clousre에 weak self를 붙여줘야 한다는 것을 발견, 온갖 클로저에 [weak self]를 붙이니 deinit이 실행되는걸 볼 수 있었다.

DetailView deinit

하지만... 문제는 따로 있었으니, 예상대로 closures안에 클로저는 그대로 있었고, 중복을 막아둔 탓인지 들어와서 클로저를 만들고, deinit후 다시 뷰에 들어왔을때 실행이 되지 않는 모습을 볼 수 있었다.

// VC
DBObserver.shared.bind(withObject: DetailViewController.self) { [weak self] in
	print("DetailViewController")
	self?.detailView.tableView.reloadData()
}

// Observer
func bind(withObject object: AnyClass, _ newClosure: @escaping ()->Void) {
	for item in closures where item.0 == object { return }
	self.closures.append((object, newClosure))
}

그래서 item이 중복을 방지하는 코드를 삭제하고 돌려보자면,, 굉장히 잘 됨을 알 수 있었으나

func bind(withObject object: AnyClass, _ newClosure: @escaping ()->Void) {
	self.closures.append((object, newClosure))
}

HomeViewController
DetailViewController
DetailViewController
DetailViewController

DetailViewController의 동작은 어처피 한번만 하지만, closures에 여러 함수가 들어가서 여러번 실행되는걸 볼 수 있었다.

그래서 최종적으로

func bind(withObject object: AnyClass, _ newClosure: @escaping ()->Void) {
	for item in closures where item.0 == object { closures = closures.filter { $0.0 != object } }
	print("add closure", object)
	self.closures.append((object, newClosure))
}

이런식으로 처리를 해서, 중복도 피하고 DetailViewController에 의미없는 클로저가 여러번 호출이 되지 않도록 처리할 수 있었다.


5. 결론

  1. DB가 변경될때만 tableView를 reload할 수 있는 방안을 한번 만들어보았음.
  2. 이게 명확하게 좋은건지는,, 더 써봐야 알 수 있을 것 같다. 주의할 점이 ViewController가 명확하게 deinit이 되지 않으면 문제가 생길수도 있다는점 이겠다.
  3. 그냥 viewWillAppear에 reloadData하는게 정신건강에 좋을 수 있다.

다음편 : [iOS] 연구해본 Database Event-Driven Reloading View

profile
🍎Apple Developer Academy @ POSTECH 2nd, 🌱SeSAC iOS 4th

0개의 댓글