조하~
여러분 혹시 이런 말 들은 적 있으신가요? "UI작업은 메인스레드에서 이루어져야 한다!"는 말이요.
제가 미모(MIMO)앱을 개발하면서 겪었던 관련 이슈를 잠깐 이야기하고 가볼게요.
[UI작업] must be used from mainThread
미모앱을 만들면서 다음과 같은 오류를 겪게 되었습니다. 오류를 읽어보면 "UISwitch.setOn(_:animated:) 메소드가 반드시 메인스레드에서 동작되어야한다" 는 말인거 같습니다.
UISwitch.setOn(_:animated:)메소드는 UI작업 메소드이죠? 하지만 조금 이상하죠. 분명 따로 스레드 작업을 처리해주지 않으면 메인스레드에서 동작하는게 기본일텐데 왜 저런 오류가 나는걸까요?
알아보니 위의 genPendingNotificationRequests의 클로저 안에는 비동기적으로 처리가 되어서 다음과 같은 오류가 발생한 것이었습니다. 즉 UI 작업 메소드가 메인스레드가 아니라 백그라운드에서 동작되어 저런 오류가 나타난 것이었죠.
notificationCenter.getPendingNotificationRequests { requests in
if requests.isEmpty == true {
DispatchQueue.main.async {
self.mainView.alarmToggle.setOn(false, animated: false)
self.mainView.setTimeButton.setTitle("시간설정", for: .normal)
}
}
}
이렇게 메인스레드에서 동작하게 하면 오류가 해결됩니다.
"Xcode에서 저런 오류를 내니까 UI 작업은 메인스레드에서 이루어져야하는구나!"라고 이해하면 될까요?
네.. 당연히 아니겠죠?! Xcode에서 저런 오류를 내는 이유가 분명히 있을거에요.
오늘은 바로 그 이유 "왜 UI작업은 메인스레드에서 해야되나?"에 대해서 알아보고자 합니다. 그럼 바로 시작할게요! 레쓰기릿🔥
🔎 UI작업이 메인스레드에서 되어야하는 이유
1. UIKit이 Thread-safe 하지 않게 설계되어서
Thread-safe하다는 것의 의미는 "한 작업을 하나의 스레드에서 한 번 접근할 수 있도록 한다"는 의미입니다.
우리가 UI를 메인스레드에서 직렬로 처리하도록 한게 애초에 Thread-safe하지 않기 때문에 그런 것입니다. Thread-safe하지 않기 때문에 하나의 스레드에서 작업이 이루어질 수 있도록 우리가 DispatchQueue.main.async로 작업을 해주는 것입니다.
먄약 Thread-safe 했다면 어떻게 됐을까요? DispatchQueue.main.async 작업을 할 필요가 아예 없어집니다. 왜냐하면 알아서 한 스레드에서 잘 될테니까요.
✅ 왜 UIKit은 Thread-safe 하지 않게 설계되었나?
UIKit이 매우 방대하여 Thread-safe 하게 설계하는 것이 사실상 불가능하기 때문입니다.
Thread-safe한 프레임워크를 설계하는 것은 서로 독립적으로 변경하고 NSLock도 추가하고... 굉장히 복잡한 작업을 해야합니다.
그리고 만약 성공적으로 설계했다해도 느려짐과 같은 성능저하가 발생하게 됩니다. 그래서 애플에서 의도적으로 Thread-safe하지 않게 설계한 것입니다.
때문에 우리는 뷰작업이 Thread-safe하게 될 수 있도록 1차적으로 UI관련 작업은 하나의 스레드에서 한 번 처리 될 수 있도록 해주어야합니다.
2. 런루프 이슈
앱을 시작하면 위 그림과 같은 과정을 거쳐 메인런루프가 돌아가면서 이벤트(터치, 화면 돌리기) 등의 이벤트를 지속적으로 처리하게 됩니다.
iOS에선 이렇게 우리가 만든 이벤트를 하나하나 쪼개서 EventQueue에 쌓습니다.
그리고 쌓은 이벤트를 하나하나 메인런루프에 던져주고 메인런루프는 어떤 이벤트인지 인식하고 그에 맞는 처리를 하게됩니다.
카카오톡 전송 버튼 눌렀을 때를 예시로 들어보겠습니다.
- 카카오톡 문자 전송버튼 클릭
- 전송버튼 클릭 이벤트 Event Queue로 전달
- Event Queue에서 Main run loop로 전달
- Main run loop에서 전송 버튼이 눌린 것 파악하고 이 버튼이 눌리면 문자를 전송하는 메소드 실행하는 것을 인지하고 처리
- 카카오톡 문자 전송 완료
이와 같은 흐름으로 동작하게 됩니다. 그런데 여기서 문제가 발생합니다.
위의 그림에서 볼 수 있듯 각각의 스레드는 각자의 Run Loop를 가지게 됩니다.
만약 우리가 UI관련 작업을 여러 스레드에서 처리될 수 있도록 해주면 어떻게 될까요?
네. 이벤트가 서로 다른 Run Loop에서 처리되기 때문에 작업이 한 번에 부드럽게 이루어지지 않을 가능성이 있습니다.
예를들어, 화면을 가로모드로 전환한다고 하면 한 번에 부드럽게 가로로 전환되는 것이 아니라 UI가 따로따로 놀게 될 수 있다는 것이죠.
3. 렌더링 성능 이슈
마지막은 렌더링 성능 이슈 입니다.
iOS에서 뷰를 그리고 표시하고 애니메이션을 처리하는 작업은 Core Animation에서 이루어집니다.
Core Animation은 위 그림의 Core Animation Pipeline을 따라 렌더링을 합니다. 그리고 해당 Pipeline에선 1/60초 후 작업을 준비하고 렌더링 서버로 데이터를 보낸 다음 1/60초 후에 렌더링을 완료합니다.
여기서 1/60초인 이후는 아이폰 화면의 주사율이 60Hz라 그런건데 아마 아이폰 10부터 120Hz로 올랐으니 이 과정이 1/120초로 바뀌었을 것 같습니다.
여튼 이런 과정으로 화면이 업데이트 되는데 이 작업을 여러 스레드에서 처리해주면 각각의 뷰 변경 사항을 GPU로 보내고 GPU가 각각의 정보를 해석하게 되기 때문에 느려지거나 비효율적으로 자원을 더 쓰게 됩니다.
이러한 이유들 때문에 iOS 뿐만 아니라 다른 OS에서도 이와 유사하게 UI관련 작업은 메인스레드에서 진행되게 설계되었다고 합니다...
Xcode에서 괜히 오류를 냈던게 아니였던거죠!!!
이상 정리하고 마치겠습니다.
📌 정리
- UIKit이 Thread-safe하지 않기 때문에
- 스레드는 각각의 런루프를 가지기 때문에
- 렌더링 과정에서의 성능이슈
이와 같은 이유때문에 iOS에서 아니 다른 OS에서도 유사하게 UI관련 작업은 메인스레드에서 처리한다!!!
📄 참고
'iOS' 카테고리의 다른 글
[iOS] GCD 누구냐 넌 - 2. sync/async & main/global (0) | 2022.12.29 |
---|---|
[iOS] GCD 누구냐 넌 - 1. GCD의 등장배경 (0) | 2022.12.26 |
[iOS] - Realm 마이그레이션 정리 (0) | 2022.10.13 |
[iOS] - Push Notification(feat. Firebase) (0) | 2022.10.11 |
[iOS] Map Kit View 사용하기 - 1 (0) | 2022.08.11 |