μΉ΄ν…Œκ³ λ¦¬ μ—†μŒ

PyQt6 μ‹€μ‹œκ°„ 데이터 μ‹œκ°ν™” ν™”λ©΄ 멈좀 ν•΄κ²°: QThreadPool 기반 비동기 νŒŒμ΄ν”„λΌμΈ μ΅œμ ν™” νŠΈλŸ¬λΈ”μŠˆνŒ…

κ²Œμž„κ΅μˆ˜ 2026. 9. 16. 13:01
λ°˜μ‘ν˜•

🌐 English Abstract

This technical article addresses main UI thread bottleneck and screen freezing issues encountered when building high-frequency real-time measurement data visualization systems using PyQt6. It explores architectural redesign using QThreadPool and QRunnable to decouple heavy I/O and data processing from the GUI event loop. Readers will gain actionable insights into implementing thread-safe signals, memory-efficient ring buffers, pyqtgraph optimization, and resolving real-world race conditions in Python multi-threading environments.

μ΄ˆλ‹Ή 수만 개 μ΄μƒμ˜ λŒ€μš©λŸ‰ μ‹ ν˜Έ 데이터λ₯Ό μ‹€μ‹œκ°„μœΌλ‘œ μˆ˜μ§‘ν•˜κ³  화면에 μ‹œκ°ν™”(Visualization)ν•˜λŠ” κ·Έλž˜ν”½ μ‚¬μš©μž μΈν„°νŽ˜μ΄μŠ€(GUI) μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ κ°œλ°œν•  λ•Œ κ°€μž₯ ν”νžˆ μ§λ©΄ν•˜λŠ” λ¬Έμ œλŠ” λ°”λ‘œ '메인 UI μŠ€λ ˆλ“œ 멈좀(UI Thread Freezing/Lagging)' ν˜„μƒμž…λ‹ˆλ‹€. μˆ˜μ‹ λ˜λŠ” λŒ€μš©λŸ‰ 데이터 νŒŒμ‹±(Data Parsing), 필터링, μ—°μ‚° μž‘μ—…μ΄ UI μ²˜λ¦¬μ™€ λ™μΌν•œ μŠ€λ ˆλ“œμ—μ„œ μ‹€ν–‰λ˜λ©΄ Qt의 이벀트 루프(Event Loop)κ°€ μ°¨λ‹¨λ˜μ–΄ λ²„νŠΌ 클릭 뢈λŠ₯, ν”„λ ˆμž„ λ“œλž, 응닡 μ—†μŒ 였λ₯˜κ°€ λ°œμƒν•©λ‹ˆλ‹€.

λ³Έ κΈ€μ—μ„œλŠ” **PyQt6** 기반 데이터 μ‹œκ°ν™” μ‹œμŠ€ν…œ 개발 쀑 λ°œμƒν•œ 병λͺ© ν˜„μƒμ„ μ§„λ‹¨ν•˜κ³ , QThreadPoolκ³Ό QRunnable을 ν™œμš©ν•˜μ—¬ μˆ˜μ§‘-μ—°μ‚°-λ Œλ”λ§ νŒŒμ΄ν”„λΌμΈμ„ λΉ„λ™κΈ°λ‘œ μ™„μ „ λΆ„λ¦¬ν•œ κ³ μ„±λŠ₯ νŠΈλŸ¬λΈ”μŠˆνŒ… 과정을 깊이 있게 λ‹€λ£Ήλ‹ˆλ‹€.



1. 문제 진단: 메인 UI μŠ€λ ˆλ“œ 병λͺ©κ³Ό 이벀트 루프 차단

PyQt6 μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ 핡심은 QApplication.exec()에 μ˜ν•΄ κ΄€λ¦¬λ˜λŠ” 메인 이벀트 루프(Main Event Loop)μž…λ‹ˆλ‹€. 이 이벀트 λ£¨ν”„λŠ” 마우슀 클릭, ν‚€λ³΄λ“œ μž…λ ₯, μœˆλ„μš° λ¦¬μ‚¬μ΄μ¦ˆλΏλ§Œ μ•„λ‹ˆλΌ ν™”λ©΄μ˜ κ·Έλž˜ν”½ μš”μ†Œ 재그리기(Repaint/Redraw) 이벀트λ₯Ό 일괄 μ²˜λ¦¬ν•©λ‹ˆλ‹€.

1.1. κΈ°μ‘΄ 단일 μŠ€λ ˆλ“œ ꡬ쑰의 문제점

초기 μ„€κ³„μ—μ„œλŠ” 직렬 포트(Serial Port) λ˜λŠ” TCP/IP μ†ŒμΌ“μœΌλ‘œλΆ€ν„° 데이터λ₯Ό μˆ˜μ‹ (Data Ingestion)받은 직후, λ™μΌν•œ μŠ€λ ˆλ“œ λ‚΄μ—μ„œ λ‹€μŒκ³Ό 같은 μž‘μ—…μ„ 연달아 μˆ˜ν–‰ν–ˆμŠ΅λ‹ˆλ‹€:

  • 데이터 μˆ˜μ‹  및 λ””μ½”λ”©: λ°”μ΄νŠΈ μŠ€νŠΈλ¦Όμ„ 파이썬 λ”•μ…”λ„ˆλ¦¬ λ˜λŠ” λ„˜νŒŒμ΄(NumPy) λ°°μ—΄λ‘œ μ „ν™˜
  • μ‹ ν˜Έ 처리(Signal Processing): 이동 평균 ν•„ν„°(Moving Average Filter) 및 FFT(Fast Fourier Transform) λ³€ν™˜
  • 차트 μ—…λ°μ΄νŠΈ: 차트 라이브러리(Graph Canvas)에 μƒˆλ‘œμš΄ 포인트λ₯Ό κ°€κ³΅ν•˜μ—¬ 그리기

데이터 μƒ˜ν”Œλ§ λ ˆμ΄νŠΈκ°€ 100Hz μˆ˜μ€€μΌ λ•ŒλŠ” λ¬Έμ œκ°€ λ“œλŸ¬λ‚˜μ§€ μ•Šμ•˜μœΌλ‚˜, 계츑 μž₯λΉ„μ˜ μŠ€νŽ™μ΄ ν™•μž₯λ˜μ–΄ 10kHz ~ 100kHz μ΄μƒμ˜ μ΄ˆκ³ μ† 멀티채널 연산이 ν•„μš”ν•΄μ§€μž 파이썬의 GIL(Global Interpreter Lock)κ³Ό CPU μ—°μ‚° λΆ€ν•˜κ°€ κ²°ν•©ν•˜λ©΄μ„œ 이벀트 루프가 수백 λ°€λ¦¬μ΄ˆ(ms) 이상 μ°¨λ‹¨λ˜μ—ˆμŠ΅λ‹ˆλ‹€. 결과적으둜 UIλŠ” '응닡 μ—†μŒ(Not Responding)' μƒνƒœμ— λΉ μ‘ŒμŠ΅λ‹ˆλ‹€.

1.2. 병λͺ© 지점 ν”„λ‘œνŒŒμΌλ§ (Profiling)

파이썬의 cProfile 및 PyQt6.QtCore.QElapsedTimerλ₯Ό μ΄μš©ν•΄ 각 단계별 μ†Œμš” μ‹œκ°„μ„ κ³„μΈ‘ν•œ κ²°κ³ΌλŠ” λ‹€μŒκ³Ό κ°™μ•˜μŠ΅λ‹ˆλ‹€.

νŒŒμ΄ν”„λΌμΈ 단계 μ΄ˆλ‹Ή 처리 횟수 μŠ€λ ˆλ“œμ μœ μ‹œκ°„(100k μƒ˜ν”Œ κΈ°μ€€) μ£Όμš” 병λͺ© 원인
데이터 μˆ˜μ‹ (Network/Serial I/O) 1,000 packets/sec 12ms I/O λŒ€κΈ° 및 λŒ€μš©λŸ‰ νŒ¨ν‚· λŒ€κΈ°
데이터 νŒŒμ‹± 및 DSP μ—°μ‚° 1,000 packets/sec 45ms NumPy λ°°μ—΄ μž¬ν• λ‹Ή 및 ν•„ν„° μ—°μ‚° CPU λΆ€ν•˜
GUI Chart λ Œλ”λ§ 60 fps λͺ©ν‘œ 85ms λ‹¨μˆœ Point 바이 Point μ—°μ‚° 및 메인 μŠ€λ ˆλ“œ 차단

계츑 κ²°κ³Ό 데이터 μ—°μ‚°κ³Ό UI μ—…λ°μ΄νŠΈκ°€ 메인 μŠ€λ ˆλ“œλ₯Ό 90% 이상 μ μœ ν•˜κ³  μžˆμ—ˆμœΌλ©°, 비동기 νŒŒμ΄ν”„λΌμΈ(Asynchronous Pipeline)으둜의 μ „ν™˜μ΄ ν•„μˆ˜μ μ΄μ—ˆμŠ΅λ‹ˆλ‹€.

2. μ•„ν‚€ν…μ²˜ μž¬μ„€κ³„: QThreadPool 및 Ring Buffer λ„μž…

μŠ€λ ˆλ“œλ₯Ό 뢄리할 λ•Œ λ¬΄λΆ„λ³„ν•œ QThread 생성은 μŠ€λ ˆλ“œ 생성/νŒŒκ΄΄μ— λ”°λ₯Έ μ˜€λ²„ν—€λ“œμ™€ λΉ„νš¨μœ¨μ μΈ λ©”λͺ¨λ¦¬ 관리λ₯Ό μ΄ˆλž˜ν•©λ‹ˆλ‹€. μ‹œμŠ€ν…œ μžμ›μ„ 졜적으둜 μž¬μ‚¬μš©ν•˜κΈ° μœ„ν•΄ **QThreadPool** 기반의 μŠ€λ ˆλ“œ 풀링 νŒ¨ν„΄μ„ μ±„νƒν–ˆμŠ΅λ‹ˆλ‹€.

2.1. μ™œ QThread λŒ€μ‹  QThreadPool인가?

  • μžμ› μž¬μ‚¬μš©: 반볡적으둜 λ°œμƒν•˜λŠ” 데이터 처리 μž‘μ—…(Task)에 λŒ€ν•΄ 이미 μƒμ„±λœ μ›Œμ»€ μŠ€λ ˆλ“œ(Worker Thread)λ₯Ό μž¬μ‚¬μš©ν•˜μ—¬ 생성 λΉ„μš©μ„ μ ˆκ°ν•©λ‹ˆλ‹€.
  • λ™μ‹œμ„± μ œμ–΄: setMaxThreadCount()λ₯Ό 톡해 μ‹œμŠ€ν…œμ˜ 물리 μ½”μ–΄ μˆ˜μ— 맞좰 κ³Όλ„ν•œ λ¬Έλ§₯ κ΅ν™˜(Context Switching)을 λ°©μ§€ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • μž‘μ—… 뢄리: λ‹¨λ°œμ„± λ˜λŠ” 주기적 μ—°μ‚° μž‘μ—…μ€ QRunnable둜 κ΅¬ν˜„ν•˜μ—¬ μŠ€λ ˆλ“œ 관리 μ±…μž„μ„ ν”„λ ˆμž„μ›Œν¬μ— μœ„μž„ν•©λ‹ˆλ‹€.

2.2. μƒμ‚°μž-μ†ŒλΉ„μž νŒ¨ν„΄κ³Ό Ring Buffer κ΅¬ν˜„

데이터 μˆ˜μ‹  속도(μƒμ‚°μž)와 ν™”λ©΄ λ Œλ”λ§ 속도(μ†ŒλΉ„μž)의 뢈일치λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄, 쀑간 맀개체둜 λ©”λͺ¨λ¦¬ 효율적인 μ›ν˜• 버퍼(Ring Buffer/Circular Buffer)λ₯Ό λ„μž…ν–ˆμŠ΅λ‹ˆλ‹€. Python의 collections.deque(maxlen=N) λ˜λŠ” κ³ μ„±λŠ₯ NumPy C-extension bufferλ₯Ό ν™œμš©ν•˜μ—¬ λ©”λͺ¨λ¦¬ ν• λ‹Ή 및 ν•΄μ œ μ˜€λ²„ν—€λ“œλ₯Ό μ΅œμ†Œν™”ν–ˆμŠ΅λ‹ˆλ‹€.



3. 싀무 μ΅œμ ν™” μ½”λ“œ κ΅¬ν˜„ 및 νŠΈλŸ¬λΈ”μŠˆνŒ…

λ‹€μŒμ€ `QThreadPool`κ³Ό custom `QObject` signal을 κ²°ν•©ν•˜μ—¬ UI λ©ˆμΆ€μ„ μ™„λ²½νžˆ ν•΄κ²°ν•œ ν”„λ‘œλ•μ…˜ 레벨의 비동기 νŒŒμ΄ν”„λΌμΈ κ΅¬μ‘°μž…λ‹ˆλ‹€.

3.1. Thread-safeν•œ QRunnable 및 Signal 섀계

QRunnable은 자체적으둜 Signal을 κ°€μ§ˆ 수 μ—†μœΌλ―€λ‘œ, λ³„λ„μ˜ QObject 래퍼 클래슀λ₯Ό 톡해 메인 μŠ€λ ˆλ“œλ‘œ μ•ˆμ „ν•˜κ²Œ 데이터λ₯Ό 전솑(Thread-Safe Communication)ν•΄μ•Ό ν•©λ‹ˆλ‹€.

from PyQt6.QtCore import QRunnable, QObject, pyqtSignal, pyqtSlot, QThreadPool
import numpy as np
import time

# 1. μ‹œκ·Έλ„ μ „λ‹¬μš© 래퍼 클래슀 μ •μ˜
class WorkerSignals(QObject):
    data_ready = pyqtSignal(np.ndarray, np.ndarray)  # time, value 전달
    error = pyqtSignal(str)

# 2. μ—°μ‚° μ „μš© Worker 클래슀 (QRunnable 상속)
class DataProcessingTask(QRunnable):
    def __init__(self, raw_bytes):
        super().__init__()
        self.raw_bytes = raw_bytes
        self.signals = WorkerSignals()

    @pyqtSlot()
    def run(self):
        try:
            # [비동기 μ—°μ‚°] λŒ€μš©λŸ‰ λ°”μ΄λ„ˆλ¦¬ νŒŒμ‹± 및 DSP μ—°μ‚° μˆ˜ν–‰
            parsed_data = np.frombuffer(self.raw_bytes, dtype=np.float32)
            
            # 이동 평균 필터링 (Heavy CPU Bound Task)
            window_size = 10
            filtered_data = np.convolve(parsed_data, np.ones(window_size)/window_size, mode='valid')
            time_axis = np.linspace(0, len(filtered_data)*0.0001, len(filtered_data))

            # 메인 μŠ€λ ˆλ“œλ‘œ κ²°κ³Ό μ•ˆμ „ν•˜κ²Œ 전달 (Queued Connection μžλ™ 적용)
            self.signals.data_ready.emit(time_axis, filtered_data)
        except Exception as e:
            self.signals.error.emit(str(e))

3.2. 메인 UI 및 PyQtGraph μ‹œκ°ν™” μ΅œμ ν™”

PyQt6 κΈ°λ³Έ QPainter 계열 λ Œλ”λ§μ€ μ‹€μ‹œκ°„ λŒ€μš©λŸ‰ 데이터 μ‹œκ°ν™”μ— μ ν•©ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. C++ μ—°μ‚° 기반의 pyqtgraphλ₯Ό μ‚¬μš©ν•˜κ³  λ‹€μš΄μƒ˜ν”Œλ§(Downsampling) μ˜΅μ…˜μ„ ν™œμ„±ν™”ν•΄μ•Ό ν•©λ‹ˆλ‹€.

from PyQt6.QtWidgets import QMainWindow, QVBoxLayout, QWidget
import pyqtgraph as pg

class RealtimePlotWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.thread_pool = QThreadPool.globalInstance()
        
        # UI 및 차트 μ΄ˆκΈ°ν™”
        self.central_widget = QWidget()
        self.layout = QVBoxLayout(self.central_widget)
        self.setCentralWidget(self.central_widget)

        # pyqtgraph μ„±λŠ₯ μ„€μ • μ΅œμ ν™”
        pg.setConfigOptions(useOpenGL=True, antialias=False)  # OpenGL 가속 ν™œμ„±ν™”
        self.plot_widget = pg.PlotWidget()
        self.layout.addWidget(self.plot_widget)
        
        self.curve = self.plot_widget.plot(pen='g')
        self.plot_widget.setDownsampling(ds=True, auto=True, mode='peak')  # 피크 ν”½μ…€ λ‹€μš΄μƒ˜ν”Œλ§
        self.plot_widget.setClipToView(True)  # 화면에 λ³΄μ΄λŠ” μ˜μ—­λ§Œ λ Œλ”λ§

    def on_raw_data_received(self, raw_bytes):
        # λ„€νŠΈμ›Œν¬/μ†ŒμΌ“ μˆ˜μ‹ λΆ€μ—μ„œ λΌμš°νŒ…λ˜λŠ” 이벀트
        task = DataProcessingTask(raw_bytes)
        task.signals.data_ready.connect(self.update_chart)
        self.thread_pool.start(task)

    @pyqtSlot(np.ndarray, np.ndarray)
    def update_chart(self, x_data, y_data):
        # 이 μŠ¬λ‘―μ€ 메인 GUI μŠ€λ ˆλ“œμ—μ„œ μ•ˆμ „ν•˜κ²Œ 싀행됨
        self.curve.setData(x_data, y_data)

3.3. ν•„μˆ˜ νŠΈλŸ¬λΈ”μŠˆνŒ… μ—£μ§€ μΌ€μ΄μŠ€ (Edge Cases)

  1. λ©”λͺ¨λ¦¬ λˆ„μˆ˜(Memory Leak) λ°©μ§€: QRunnable κ°μ²΄λŠ” μ‹€ν–‰ μ™„λ£Œ ν›„ μžλ™μœΌλ‘œ λ©”λͺ¨λ¦¬μ—μ„œ ν•΄μ œ(Auto-deletion)λ˜μ§€λ§Œ, WorkerSignals와 같은 QObject 연결이 ν•΄μ œλ˜μ§€ μ•ŠμœΌλ©΄ μ“°λ ˆκΈ° μˆ˜μ§‘κΈ°(Garbage Collector)κ°€ 이λ₯Ό μˆ˜κ±°ν•˜μ§€ λͺ»ν•  수 μžˆμŠ΅λ‹ˆλ‹€. μ‹œκ·Έλ„ μˆ˜μ‹  ν›„ 처리 μ™„λ£Œ μ‹œμ μ— 적절히 disconnect() 처리λ₯Ό 보μž₯ν•΄μ•Ό ν•©λ‹ˆλ‹€.
  2. Qt.ConnectionType 이슈: 메인 μŠ€λ ˆλ“œμ™€ μ›Œμ»€ μŠ€λ ˆλ“œ κ°„μ˜ μ‹œκ·Έλ„ 전달은 λ°˜λ“œμ‹œ Qt.ConnectionType.QueuedConnection λ°©μ‹μœΌλ‘œ μ „λ‹¬λ˜μ–΄μ•Ό ꡐ착 μƒνƒœ(Deadlock) 및 UI μŠ€λ ˆλ“œ κ°•μ œ 쀑단을 λ°©μ§€ν•  수 μžˆμŠ΅λ‹ˆλ‹€. PyQt6 μ‹œκ·Έλ„ μ‹œμŠ€ν…œμ€ μŠ€λ ˆλ“œ 경계λ₯Ό κ°μ§€ν•˜μ—¬ 이λ₯Ό μžλ™ μ²˜λ¦¬ν•˜μ§€λ§Œ, λ³΅μž‘ν•œ μΈμŠ€ν„΄μŠ€ μ°Έμ‘° μ‹œ λͺ…μ‹œμ  지정이 μ•ˆμ „ν•©λ‹ˆλ‹€.
  3. GUI κ°±μ‹  μ£Όκ³Όλ‹€(Over-rendering): μ›Œμ»€ μŠ€λ ˆλ“œμ—μ„œ μ΄ˆλ‹Ή 1,000번 μ‹œκ·Έλ„μ„ λ°œμƒμ‹œμΌœλ„ λͺ¨λ‹ˆν„°μ˜ μ£Όμ‚¬μœ¨(60Hz~144Hz) μ΄μƒμœΌλ‘œ 화면을 λ‹€μ‹œ κ·Έλ¦¬λŠ” 것은 λ¬΄μ˜λ―Έν•œ CPU/GPU μžμ› λ‚­λΉ„μž…λ‹ˆλ‹€. 메인 UIμ—μ„œλŠ” 타이머(QTimer) 기반으둜 버퍼 λ°μ΄ν„°μ˜ μ΅œμ‹  μƒνƒœλ§Œ 60 FPS 주기둜 간헐적 κ°±μ‹ (Throttling)ν•˜λŠ” ν”„λ ˆμž„ μ œν•œ 기법을 μ μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€.


4. μ΅œμ ν™” κ²°κ³Ό 및 μ„±λŠ₯ 비ꡐ

QThreadPool 비동기 νŒŒμ΄ν”„λΌμΈκ³Ό λ‹€μš΄μƒ˜ν”Œλ§ λ Œλ”λ§μ„ μ μš©ν•˜κΈ° μ „κ³Ό ν›„μ˜ μ‹œμŠ€ν…œ μ„±λŠ₯을 μ •λŸ‰μ μœΌλ‘œ μΈ‘μ •ν•œ κ²°κ³ΌλŠ” λ‹€μŒκ³Ό 같이 극적인 κ°œμ„ μ„ λ³΄μ—¬μ€λ‹ˆλ‹€.

μΈ‘μ • μ§€ν‘œ (Metrics) μ΅œμ ν™” μ „ (Single Thread) μ΅œμ ν™” ν›„ (QThreadPool + GPU Acceleration)
μ΅œλŒ€ 데이터 μˆ˜μ‹  μ²˜λ¦¬λŸ‰ 10 kHz (ν™”λ©΄ 멈좀 λ°œμƒ) 500 kHz 이상 (μ•ˆμ •μ  λ™μž‘)
UI Frame Rate (FPS) 12 ~ 18 FPS (λ“œλž 심함) κ³ μ • 60 FPS (Drop-free)
메인 μŠ€λ ˆλ“œ CPU 점유율 88% ~ 99% (병λͺ© λ°œμƒ) 12% ~ 15% (μΎŒμ ν•¨ μœ μ§€)
μ‚¬μš©μž μž…λ ₯을 μœ„ν•œ λ°˜μ‘μ„± μž…λ ₯ μ§€μ—° 2~5초 및 먹톡 μ‹€μ‹œκ°„ λ”œλ ˆμ΄ μ—†μŒ (< 10ms)

5. κ²°λ‘  및 μš”μ•½

PyQt6 ν™˜κ²½μ—μ„œ λŒ€μš©λŸ‰ μ‹€μ‹œκ°„ 계츑 데이터λ₯Ό λ‹€λ£° λ•Œ ν™”λ©΄ 멈좀 ν˜„μƒμ„ ν•΄κ²°ν•˜λ €λ©΄ λ‹€μŒ μš”μ†Œλ₯Ό λ°˜λ“œμ‹œ κ³ λ €ν•œ μ•„ν‚€ν…μ²˜λ₯Ό 섀계해야 ν•©λ‹ˆλ‹€.

  • μ—­ν•  뢄리: 메인 μŠ€λ ˆλ“œλŠ” 였직 μ‚¬μš©μž 이벀트 및 ν™”λ©΄ λ Œλ”λ§ μ „μš©μœΌλ‘œλ§Œ μ‚¬μš©ν•©λ‹ˆλ‹€.
  • QThreadPool ν™œμš©: 데이터 μˆ˜μ‹ , νŒŒμ‹±, ν•„ν„° 연산은 `QRunnable` 기반 μŠ€λ ˆλ“œ ν’€λ‘œ λ„˜κ²¨ λ™μ‹œμ„±μ„ κ·ΉλŒ€ν™”ν•©λ‹ˆλ‹€.
  • PyQtGraph μ˜΅μ…˜ μ΅œμ ν™”: OpenGL 가속, Downsampling, ClipToView 섀정을 μ΄μš©ν•΄ 차트 그리기 μ—°μ‚°λŸ‰μ„ μ§€λŠ₯적으둜 μ€„μž…λ‹ˆλ‹€.
  • λ Œλ”λ§ μŠ€λ‘―ν‹€λ§: UI 갱신은 `QTimer` 기반의 60 FPS μ œμ•½μ„ 두어 λͺ¨λ‹ˆν„° λ Œλ”λ§ ν•œκ³„λ₯Ό λ„˜μ–΄μ„œλŠ” λΉ„νš¨μœ¨μ  μž‘μ—…μ„ μ°¨λ‹¨ν•©λ‹ˆλ‹€.

μœ„ μ΅œμ ν™” νŒ¨ν„΄μ„ μ μš©ν•˜λ©΄ κ³ μ„±λŠ₯ μ‚°μ—…μš© μž₯λΉ„ 계츑 μ œμ–΄ ν”„λ‘œκ·Έλž¨ μ‹œμŠ€ν…œ μˆ˜μ€€μ˜ λ§€λ„λŸ½κ³  κ²¬κ³ ν•œ PyQt6 GUI μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ μ„±κ³΅μ μœΌλ‘œ ꡬ좕할 수 μžˆμŠ΅λ‹ˆλ‹€.

λ°˜μ‘ν˜•