정보

왜 UI가 Main Thread에서만 그려져야 하나?

밤새는 탐험가89 2024. 12. 13. 04:09

1. UIKit은 왜 메인 스레드에서만 작동해야 하나?

UIKit스레드 세이프(thread-safe)하지 않기 때문에 메인 스레드에서만 UI 작업을 처리해야 합니다. 이 설계는 다음과 같은 이유에서 비롯되었습니다:

1.1 UIKit의 스레드 세이프 문제

  • UIKit의 많은 구성 요소는 nonatomic(서로 연결된)으로 설계되었습니다.
    • 즉, 한 스레드에서 UI 상태를 변경하는 동안 다른 스레드에서 동시에 변경을 시도하면 충돌이 발생할 수 있습니다.
    • 예를 들어, 한 스레드가 UITableView에서 셀을 제거하는 동시에 다른 스레드가 같은 셀의 인덱스를 조작하려고 하면 크래시가 발생합니다.
  • 이를 스레드 세이프하게 만들려면 많은 동기화 작업(NSLock, Atomic 등)이 필요하지만, 이는 성능 저하를 초래합니다.
    • UIKit은 방대한 프레임워크로, 모든 속성을 스레드 세이프하게 처리하는 것은 비현실적입니다.

 

2. UI 작업을 메인 스레드에서 처리해야 하는 이유

2.1 RunLoop와 View Drawing Cycle

UIKit의 UI 업데이트는 메인 스레드의 RunLoop에 의해 관리됩니다.

  • 메인 스레드의 RunLoop는 사용자 입력 이벤트UI 업데이트를 처리하며, 앱의 UI 반응성을 보장합니다.
  • UI 업데이트는 RunLoop의 특정 시점에서 일괄적으로 처리되며, 이를 View Drawing Cycle이라고 합니다.
  • 모든 뷰 변경 사항은 현재 RunLoop의 끝에서 한 번에 적용됩니다. 이를 통해 변경 사항이 일관되게 반영되도록 보장합니다.

백그라운드 스레드에서 UI를 업데이트하면 RunLoop가 이를 제어할 수 없게 되어 UI 동작이 불안정해질 수 있습니다. 예를 들어:

  • 뷰가 회전할 때 레이아웃이 일관되지 않을 수 있습니다.
  • 삭제된 뷰를 사용자가 탭하면 어떤 스레드가 이벤트를 처리해야 할지 명확하지 않아 충돌이 발생합니다.

2.2 Core Animation Pipeline

UIKit은 자체적으로 렌더링 작업을 하지 않고, Core Animation 프레임워크를 통해 화면을 렌더링합니다. 렌더링은 아래와 같은 단계를 따릅니다:

  1. 커밋 트랜잭션: UIKit에서 변경된 뷰 정보를 수집하여 렌더링 서버에 전달.
  2. 렌더링 서버: 전달된 데이터를 분석하고 렌더링 지침을 생성.
  3. GPU 렌더링: OpenGL을 호출하여 화면을 렌더링.
  4. 디스플레이 출력: GPU가 준비한 데이터를 화면에 출력.

이 과정은 1/60초(초당 60프레임) 주기로 진행됩니다. 하지만 백그라운드 스레드에서 UI 업데이트가 이루어지면:

  • 여러 스레드에서 서로 다른 렌더링 정보를 커밋하게 됩니다.
  • Core Animation Pipeline에 과도한 트랜잭션이 발생하여 성능 저하프레임 드롭이 발생합니다.
  • 빈번한 컨텍스트 전환으로 GPU가 작업을 제시간에 완료하지 못할 수 있습니다.

 

2.3 UIKit 설계 의도

애플은 UIKit을 설계할 때 단일 직렬 대기열(메인 스레드)을 사용하여 모든 UI 작업을 처리하도록 의도적으로 제한했습니다.

  • 이를 통해 동시성 문제를 방지하고, 개발자가 UI 작업을 더 쉽게 관리할 수 있도록 했습니다.

 

3. 백그라운드 스레드에서 UI 업데이트를 하면 어떻게 될까?

백그라운드 스레드에서 UI 업데이트를 시도하면 다음과 같은 문제가 발생할 수 있습니다:

  1. 앱 충돌: UI 요소의 속성을 동시에 변경하거나 잘못된 상태에서 접근하면 크래시가 발생합니다.
  2. UI 불안정성: 화면 레이아웃이 일관되지 않거나 일부 변경 사항이 반영되지 않을 수 있습니다.
  3. 성능 저하: GPU와 렌더링 파이프라인의 과부하로 인해 UI 반응 속도가 느려지고 프레임 드롭이 발생합니다.

 

4. 결론: UI 업데이트는 항상 메인 스레드에서!

UIKit의 설계와 iOS의 렌더링 프로세스를 고려할 때, UI 업데이트는 항상 메인 스레드에서 처리해야 합니다.

애플이 이를 강제하지 않더라도 메인 스레드에서 UI 작업을 처리하는 것은 앱의 안정성성능을 보장하는 가장 효과적인 방법입니다.

만약 백그라운드 작업에서 UI를 업데이트해야 한다면, 반드시 메인 스레드로 디스패치해야 합니다:

DispatchQueue.main.async {
    self.imageView.image = UIImage(named: "example")
}