Programming/PySide(PyQT), QML

QML은 왜 반드시 메인 스레드에서 실행되어야 할까?

khj1999 2025. 5. 5. 11:36

회사에 입사하고 나서, PySide2 + QML을 통해 주어진 과제를 진행 도중 통신을 해야 할 일이 있었는데
평소대로 프로그램을 진행 했더니 내가 의도한대로 프로그램이 실행되지 않았다.
그래서 문제를 찾아보니 스레드를 통해 프로그램을 진행하면 문제를 해결 할 수 있다는 정보를 찾았고, 이것을 적용해 문제를 해결 했었다. 하지만 왜 스레드를 사용해야 하는지 궁금해서 정보를 조사해보고 여기에 정리 한다.

QML 프로그램은 왜 항상 메인 스레드에서 실행되어야 할까?

Qt를 활용해 QML UI를 개발하다 보면 흔히 접하게 되는 문장이 있다:

"QML UI는 반드시 메인 스레드(GUI 스레드)에서 실행되어야 한다."

하지만 왜 그래야 할까? 멀티스레드 환경에서 UI 요소를 접근하면 정말 위험한 걸까?
이번 글에서는 QML의 UI가 메인 스레드에서만 안전한 이유와 그 배경을 살펴본다.


1. QML UI 요소는 내부 상태(state)를 가진다

QML에서 사용하는 대부분의 UI 요소(Rectangle, Text, Image, Button 등)는 단순한 그리기 객체가 아니라 다음과 같은 복잡한 내부 상태를 가진 객체입니다:

  • 위치 (x, y), 크기 (width, height)
  • 색상 (color), 글꼴(font) 등 시각적 속성
  • 부모-자식 구조를 이루는 객체 계층 트리
  • 렌더링 파이프라인과 연결된 리소스 (OpenGL, Vulkan 등)
  • 바인딩 및 속성 감시 메커니즘 (Property Binding)

이 모든 정보는 Qt의 렌더링 엔진 및 이벤트 루프와 강하게 연결되어 있다. 즉, 단순 변수처럼 대충 다뤘다간 전체 UI 상태가 꼬일 수 있다.


2. QML 렌더링과 UI 갱신은 메인 이벤트 루프에서 이루어진다

Qt는 내부적으로 메인 루프 (QGuiApplication 또는 QApplication)를 통해 UI 요소를 주기적으로 그리는 작업(Rendering Loop)을 수행한다. 예를 들어, 다음과 같은 일이 반복된다:

  1. 사용자 이벤트 처리 (클릭, 터치, 키보드 등)
  2. 변경된 속성 값 확인 및 바인딩 트리거
  3. UI 요소의 새 레이아웃 계산
  4. 렌더링 파이프라인을 통해 화면 출력

이 일련의 과정은 모두 메인 스레드에서 일관된 흐름으로 동작해야 한다.

만약 다른 스레드에서 동시에 UI 요소를 수정한다면?

  • UI 상태를 읽는 중에 값이 변경되면 렌더링 결과가 비정상적일 수 있다.
  • 심한 경우 세그먼트 폴트(segmentation fault)로 프로그램이 종료될 수 있다.
  • 렌더링 리소스가 손상되어 UI가 더 이상 그려지지 않을 수도 있다.

3. Qt의 UI 객체는 스레드 안전하지 않다

스레드 안전(Thread-Safe)하다는 것은 여러 스레드에서 동시에 접근해도 문제가 발생하지 않도록 설계되었음을 의미한다.

그러나 Qt의 UI 객체는 기본적으로 스레드 안전하지 않습니다. 즉, 다음과 같은 코드에서 문제 발생 가능성이 매우 높다:

# 메인 스레드에서 실행 중
self.rect.color = "red"

# 동시에 백그라운드 스레드에서
self.rect.color = "blue"

이러한 동시 접근에 대해 Qt는 내부적으로 락(lock)을 제공하지 않으며, 책임은 개발자에게 있다.

Qt 공식 문서: "All QML and GUI-related code must run in the main thread."


4. 그럼 백그라운드 작업은 어떻게 할까?

실제로 UI 앱에서는 네트워크 통신, 파일 입출력, 장비 통신 등 시간이 오래 걸리는 작업을 하게 됩니다. 이런 작업을 메인 스레드에서 처리하면 UI가 멈추거나 버벅이게 됩다.

따라서 다음과 같은 방법으로 백그라운드 스레드를 따로 두고, UI와는 안전하게 통신하는 방식이 권장된다:

방법 1: QThread + Signal/Slot

# 백그라운드에서 데이터를 가져오고, 메인 스레드로 결과 전달
self.worker.dataReady.connect(self.updateUI)

방법 2: QtConcurrent (비동기 실행)

QtConcurrent.run(self.longTask)

방법 3: Python의 asyncio + QEventLoop (PySide2 + asyncio 연동)


요약: QML UI는 왜 메인 스레드에서만 안전할까?

항목 설명
내부 상태 QML 요소는 렌더링, 이벤트, 바인딩과 연결된 상태를 가짐
렌더링 메인 루프에서 렌더링되므로 상태 충돌 시 문제 발생
동기화 미제공 Qt는 UI 객체에 대해 락을 제공하지 않음
공식 권장 모든 UI 처리는 메인 스레드에서만 하도록 권장됨

마무리

QML UI 개발에서는 메인 스레드의 안전성을 보장하는 것이 매우 중요하다. 백그라운드 연산이 필요하다면 반드시 스레드를 분리하고, UI 업데이트는 시그널 등을 통해 간접적으로 수행하는 구조를 유지해야 한다.

Qt는 강력한 UI 프레임워크이지만, 그만큼 주의해야 할 구조도 많다. 스레드와 UI가 어떻게 협업할 수 있는지 이해하고 활용하면 더욱 견고하고 유연한 어플리케이션을 만들 수 있다.