이 포스팅부터 내가 회사를 다니면서 알게 되거나 느낀 것들을 끄적여 보려고 한다.
서버에서 데이터를 받아오고 이를 화면에 뿌려주는 과정에는 네트워크 상태에 따라 약간의 딜레이가 생긴다. 이때 "사용자가 보려는 화면은 서버에서 가져오고 있는 중이니 잠시 기다려라"라는 의미로 Activity Indicator를 띄워줘야 한다.
지금 컴퓨터가 멈춘 것 같지만 아쉽게도 이미지이다. 화면에 Indicator를 띄워주어야 사용자가 원하는 정보를 불러오고 있는 중이라는 것을 명시하는 역할을 한다.
처음 코드를 봤을 때 커스텀한 Activity Indicator 클래스를 싱글턴으로 사용하고 있었다. 그래서 정말 많은 View Controller에서 이 클래스의 static 메서드를 호출하여 화면 위에 Indicator를 표시하고 있었다.
물론 화면 하나하나에서 이런 방식으로 사용하는 것이 특별히 문제되지는 않는다. 하지만 문제는 다른 곳에서 발생했다.
사용자가 웹을 통해 링크를 클릭했을 때 깔려있는 앱으로 이동하면서 특정 기능이 실행되는 것을 apple 진영에서는 다이나믹 링크라고 한다.
우리 앱에서도 다이나믹 링크를 사용하는데 문제는 다이나믹 링크를 통해 앱의 특정 View Controller로 진입하면서 View Controller를 여러 개 지나게 된다.
쉽게 말하면 이 기능에 진입하려면 A -> B -> C 순서로 진입하게 되는데 사용자가 앱에서 이 기능에 도달하는 것은 사용자에 따라 몇 초의 시간이 소요된다. 하지만 다이나믹 링크로는 수십 ms의 시간에 도달하게 된다는 것이다.
그렇게 되면 문제가 뭐냐면 A에 진입하면서 Indicator가 띄워지고, B로 진입하면서 Indicator가 띄워지고, C로 진입하면서 Indicator가 띄워지는데 각 View Controller에서 띄워준 Indicator를 내려주는 dismiss()
라는 함수가 나중에 불리게 되면서 이는 동기화 문제를 유발하게 된다.
간단히 설명하자면 화면에 Indicator가 정상적으로 보이려면 show - dismiss - show - dismiss 순으로 차례대로 일어나야 하는데 다이나믹 링크로 이동하면 show - show - show - dismiss - dismiss ... 이런 식으로 이루어지게 된다.
그래서 C 화면에 도달했는데 A 화면에서 호출한 dismiss()
메서드 때문에 C에서의 서버 호출이 끝나지도 않았는데 Indicator가 사라지는 현상이 있었다. 이를 해결하고자 어떤 식으로 동기화할지 고민하게 되었다.
결론부터 말하면 DispatchGroup을 사용하였다.
func show () {
dispatchGroup.enter()
if activityIndicator == nil {
activityIndicator = ActivityIndicator()
}
activityIndicator.show()
}
func dismiss() {
dispatchGroup.leave()
dispatchGroup.notify(queue: .main) {
activityIndicator.dismiss()
activityIndicator = nil
}
}
하지만 이렇게 할 경우 문제가 있었다. DispatchGroup의 leave()
메서드는 enter()
메서드보다 먼저 불리거나 더 많이 불리면 crash를 유발하게 된다. 그래서 내가 이를 인지하고 사용하면 괜찮겠지만 회사의 코드는 여러 사람이 봤을 때 이해하기 쉽도록 짜여야하기 때문에 다소 수정이 필요했다.
func show () {
if dispatchGroup == nil {
dispatchGroup = DispatchGroup()
}
if activityIndicator == nil {
activityIndicator = ActivityIndicator()
}
dispatchGroup?.enter()
activityIndicator.show()
}
func dismiss() {
dispatchGroup?.leave()
dispatchGroup?.notify(queue: .main) {
activityIndicator.dismiss()
activityIndicator = nil
dispatchGroup = nil
}
}
activity Indicator가 dismiss()
될 때 DispatchGroup을 nil로 변경하여 더이상 leave()
메서드가 호출되지 않도록 하는 것이다. 이렇게 되면 enter()
보다 leave()
가 더 많이 호출되어도 crash가 발생하지 않는다.
마지막으로 고려해야 할 것은 show()
와 dismiss()
메서드의 실행 순서에 관한 동기화 문제였다. 만약 dismiss() 메서드가 여러 화면에서 동시에 불리게 된다면 위의 dismiss()
함수 내부의 dispatchGroup?.leave()
가 동시에 불려 crash를 유발하는 가능성도 있었다.
let dispatchQueue = DispatchQueue.global(qos: .default)![](https://velog.velcdn.com/images/qwer15417/post/a7ef4556-e41d-4680-89fa-63c85236100e/image.png)
func show () {
dispatchQueue.sync {
if dispatchGroup == nil {
dispatchGroup = DispatchGroup()
}
if activityIndicator == nil {
activityIndicator = ActivityIndicator()
}
dispatchGroup?.enter()
activityIndicator.show()
}
}
func dismiss() {
dispatchQueue.sync {
dispatchGroup?.leave()
dispatchGroup?.notify(queue: dispatchQueue) {
activityIndicator.dismiss()
activityIndicator = nil
dispatchGroup = nil
}
}
}
이 코드에서 눈여겨봐야 할 것은 dispatchQueue의 sync를 이용하여 실행 순서를 제어해준 것이다.
또한 dispatchGroup?.notify의 queue를 dispatchQueue로 지정해주어 notify의 클로저 코드 또한 실행 순서에 맞게 동작해야 하도록 하였다.
아 그리고 정말 마지막으로 화면과 관련된 코드는 main thread에서 실행되어야 하므로 다음 코드를 추가하면서 포스팅을 마친다.
let dispatchQueue = DispatchQueue.global(qos: .default)
func show () {
dispatchQueue.sync {
if dispatchGroup == nil {
dispatchGroup = DispatchGroup()
}
if activityIndicator == nil {
activityIndicator = ActivityIndicator()
}
dispatchGroup?.enter()
DispatchQueue.main.async {
activityIndicator.show()
}
}
}
func dismiss() {
dispatchQueue.sync {
dispatchGroup?.leave()
dispatchGroup?.notify(queue: dispatchQueue) {
DispatchQueue.main.async {
activityIndicator.dismiss()
activityIndicator = nil
dispatchGroup = nil
}
}
}
}
참고로 기억에 의존한, 정확히 돌아가지는 않는 코드일 수 있으니 의도만 봐주시면 좋을 것 같다:)