QThread를 사용하는 이유
- UI 블로킹 방지: 메인 스레드(주로 UI 스레드)를 차단하는 작업을 QThread로 옮겨 실행하면, UI가 멈추지 않고 계속 반응하도록 처리할 수 있다.
- Qt의 이벤트 루프와 자연스럽게 연동: QThread를 사용하면 Qt의 이벤트 루프, 시그널/슬롯 시스템을 그대로 활용할 수 있으므로, 스레드 간 통신 구현이 용이하다.
PySide6에서 QThread를 사용하는 이유는 주로 UI(메인 스레드) 가 블로킹 되지 않도록하여 애플리케이션의 반응을 유지하고,
동시에 처리가 비교적 긴 작업(네트웍 통신, 대용량 파일 처리, 복잡한 계산, 크롤링 등)을 백그라운드에서 처리하기 위함이다.
QThread는 언제 사용함?
- 시간이 오래 걸리는 작업이 있는데, 해당 작업을 UI 스레드에서 수행하면 UI가 멈추는 현상이 발생할 때
- 네트워킹(HTTP 요청, 소켓 통신 등), 파일 I/O(대용량 파일 읽기/쓰기), 복잡한 연산 등을 수행해야 할 때
- 멀티스레딩을 통해 동시에 여러 작업을 병렬로 처리하여 퍼포먼스를 높이고 싶을 때
간단한 예제로 익숙해지기!
아래 예제는 QThread
를 상속받은 Worker
클래스를 사용하여,
1초 간격으로 숫자를 세는 작업을 백그라운드에서 실행하고, 그 진행 상황을 시그널을 통해 UI에 전달하는 코드이다.
import sys
import time
from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QTextEdit
from PySide6.QtCore import QThread, Signal, Slot
# QThread를 상속받은 Worker 클래스
class Worker(QThread):
# 스레드에서 발생하는 이벤트(메시지, 진행상황 등)를 전달할 시그널 정의
progress = Signal(str)
def run(self):
"""실제 스레드에서 수행할 작업"""
for i in range(1, 6):
time.sleep(1) # 1초 대기
# progress 시그널로 현재 카운트 상태를 보냄
self.progress.emit(f"Count: {i}")
class MyWidget(QWidget):
def __init__(self):
super().__init__()
self.text_edit = QTextEdit()
self.button = QPushButton("Start Thread")
layout = QVBoxLayout()
layout.addWidget(self.text_edit)
layout.addWidget(self.button)
self.setLayout(layout)
# Worker 스레드를 하나 생성
self.thread = Worker()
# Worker 스레드에서 전달하는 progress 시그널을 UI 업데이트 슬롯에 연결
self.thread.progress.connect(self.handle_progress)
# 버튼 클릭 시 스레드 시작
self.button.clicked.connect(self.start_thread)
@Slot(str)
def handle_progress(self, text):
"""스레드에서 넘어온 시그널에 따라 UI를 갱신하는 슬롯"""
self.text_edit.append(text)
def start_thread(self):
"""스레드를 시작하는 메서드"""
if not self.thread.isRunning():
self.text_edit.append("Thread started!")
self.thread.start()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MyWidget()
window.show()
sys.exit(app.exec())
코드 설명
Worker(QThread)
- QThread를 상속받은
Worker
클래스를 정의하였음. - progress라는
Signal
을 정의하여, 진행 상황(또는 메시지)을 메인 스레드(UI)로 전달할 수 있도록 하였음. run()
메서드 안에 실제 백그라운드 작업을 구현함.
스레드와 UI의 통신
Worker
스레드에서 반복 작업을 수행하고, 반복마다progress
시그널을emit
하고 있다.- 메인 스레드(UI)는
progress
시그널을 받아handle_progress()
슬롯을 호출하고, 그 결과를QTextEdit
에 표시하도록 하였다.
Slot(Type)
@Slot(<type>)
데코레이터에 <type>
이 명시되는 이유는,
이 슬롯 메서드가 특정 타입의 인수를 받을 것임을 Qt에 알려주기 위해서이다.
(위 예제에서는 str(문자열)
타입의 인수를 받을 것임을 명시하였음.)
Qt의 시그널/슬롯 메커니즘에서는 시그널과 슬롯 간 파라미터의 타입과 개수가 일치해야 올바르게 연결될 수 있다.
예제에서 progress = Signal(str)
로 선언된 시그널이 문자열을 보내기 때문에,
슬롯 역시 같은 형태(str
)의 인자를 받을 수 있도록 데코레이터에 @Slot(str)
를 지정해 준거다!
- 시그널:
progress = Signal(str)
- :문자열 타입(str) 파라미터를 가진 시그널.
- 슬롯:
@Slot(str)
- :문자열 타입(str) 파라미터를 받을 준비가 된 슬롯.
이렇게 타입을 명시하면,
Qt 내부에서 해당 메서드를 슬롯으로 인식하는 과정(시그널/슬롯 연결 및 호출 과정)이 정확해지고,
IDE 혹은 Qt 자체도 시그널/슬롯 타입 검증을 좀 더 명확히 할 수 있게 된다.
정리하자면,
슬롯에 전달될 인수의 타입을 Qt에 명시하는 것이며, 시그널에서 str 타입의 값을 보낼 때 슬롯이 정확히 그 타입을 받도록 보장하기 위한 장치라고 이해하면 된다.