์นดํ…Œ๊ณ ๋ฆฌ ์—†์Œ

PySide6 ๊ธฐ๋ฐ˜ QGraphicsView ๋Œ€์šฉ๋Ÿ‰ ์‹ค์‹œ๊ฐ„ ์‹œ๊ฐํ™” UI ์Šค๋ ˆ๋“œ ๋ณ‘๋ชฉ ๋ฐ ํ”„๋ ˆ์ž„ ๋“œ๋กญ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

๊ฒŒ์ž„๊ต์ˆ˜ 2026. 8. 31. 07:01
๋ฐ˜์‘ํ˜•

๐ŸŒ English Abstract

High-frequency data stream visualization in PySide6 using QGraphicsView often triggers severe UI thread saturation and frame drops. This article explores the architectural root causes, including event loop congestion, C++ wrapper overhead, and BSP tree indexing performance degradation. We provide a comprehensive troubleshooting strategy featuring multithreaded ring-buffering, signal throttling, custom QGraphicsItem batch rendering, and QOpenGLWidget GPU acceleration.

์˜๋ฃŒ๊ธฐ๊ธฐ ์„ผ์„œ ๋ฐ์ดํ„ฐ, ๊ธˆ์œต ๋ฐ์ดํ„ฐ ํƒ€์ž„์‹œ๋ฆฌ์ฆˆ, ๋กœ๋ณดํ‹ฑ์Šค ํ…”๋ ˆ๋ฉ”ํŠธ๋ฆฌ, ๋˜๋Š” ์‚ฐ์—…์šฉ IoT ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ์„ ๊ฐœ๋ฐœํ•  ๋•Œ ํŒŒ์ด์ฌ(Python)์˜ ์ƒํƒœ๊ณ„์™€ PySide6(Qt for Python)์˜ ๊ฐ•๋ ฅํ•œ GUI ํ”„๋ ˆ์ž„์›Œํฌ๋Š” ๋งค์šฐ ํ›Œ๋ฅญํ•œ ์„ ํƒ์ง€์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ดˆ๋‹น ์ˆ˜์ฒœ ๋‚ด์ง€ ์ˆ˜๋งŒ ๊ฑด์— ๋‹ฌํ•˜๋Š” ๊ณ ์ฃผํŒŒ ๋ฐ์ดํ„ฐ(High-Frequency Data)๋ฅผ ์ˆ˜์‹ ํ•˜๋ฉด์„œ QGraphicsView ๋ฐ QGraphicsScene ํ™˜๊ฒฝ์—์„œ ์‹ค์‹œ๊ฐ„ ๋ Œ๋”๋ง์„ ์ˆ˜ํ–‰ํ•˜๋ฉด ๋ฐ˜๋“œ์‹œ ์ง๋ฉดํ•˜๊ฒŒ ๋˜๋Š” ๋‚œ๊ด€์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐ”๋กœ UI ์Šค๋ ˆ๋“œ ๋ณ‘๋ชฉ(UI Thread Bottleneck)๊ณผ ์ด๋กœ ์ธํ•œ ํ”„๋ ˆ์ž„ ๋“œ๋กญ(Frame Drop) ํ˜„์ƒ์ž…๋‹ˆ๋‹ค.

๋ฐ์ดํ„ฐ ์ˆ˜์‹  ์†๋„๊ฐ€ ํ™”๋ฉด ๊ฐฑ์‹  ์ฃผ๊ธฐ๋ฅผ ์ดˆ๊ณผํ•˜๊ฑฐ๋‚˜, ๋งค ๋ฐ์ดํ„ฐ ํฌ์ธํŠธ๋งˆ๋‹ค ์‹œ๊ทธ๋„-์Šฌ๋กฏ(Signal-Slot) ์—ฐ๊ฒฐ์„ ํ†ตํ•ด UI ์š”์†Œ๋ฅผ ์ง์ ‘ ๋ณ€๊ฒฝํ•˜๋ ค ํ•  ๊ฒฝ์šฐ ์ด๋ฒคํŠธ ๋ฃจํ”„๊ฐ€ ๊ณ ๊ฐˆ๋˜์–ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฐ˜์‘์„ฑ์ด ์‹ฌ๊ฐํ•˜๊ฒŒ ์ €ํ•˜๋ฉ๋‹ˆ๋‹ค. ๋ณธ ํฌ์ŠคํŒ…์—์„œ๋Š” ์ด๋Ÿฌํ•œ ํ˜„์ƒ์˜ ๊ทผ๋ณธ ์›์ธ์„ Qt ๊ตฌ์กฐ์  ๊ด€์ ์—์„œ ๊นŠ์ด ์žˆ๊ฒŒ ๋ถ„์„ํ•˜๊ณ , ์‹ค๋ฌด ํ˜„์žฅ์—์„œ ์ฆ‰์‹œ ์ ์šฉ ๊ฐ€๋Šฅํ•œ ์ตœ์ ํ™” ๊ธฐ์ˆ  ๋ฐ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ๋ฐฉ๋ฒ•๋ก ์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.



1. QGraphicsView UI ์Šค๋ ˆ๋“œ ๋ณ‘๋ชฉ์˜ ๊ทผ๋ณธ ์›์ธ ๋ถ„์„ (Root Cause Analysis)

PySide6 ๊ธฐ๋ฐ˜ ์‹œ๊ฐํ™” ์‹œ์Šคํ…œ์—์„œ ํ”„๋ ˆ์ž„ ๋ ˆ์ดํŠธ(Frame Rate)๊ฐ€ ๋–จ์–ด์ง€๊ณ  ํ™”๋ฉด์ด ๋ฉˆ์ถ”๋Š”(Freezing) ํ˜„์ƒ์€ ๋‹จ์ˆœํ•œ ํŒŒ์ด์ฌ์˜ ์—ฐ์‚ฐ ์†๋„ ๋ฌธ์ œ๋ผ๊ธฐ๋ณด๋‹ค๋Š” **Qt์˜ ์ด๋ฒคํŠธ ๋ฃจํ”„(Event Loop) ๊ตฌ์กฐ์™€ ๋ Œ๋”๋ง ํŒŒ์ดํ”„๋ผ์ธ์˜ ์ƒํ˜ธ์ž‘์šฉ**์—์„œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

๊ฐ€. ์ด๋ฒคํŠธ ๋ฃจํ”„ ํฌํ™” (Event Loop Saturation)

Qt์˜ UI ์Šค๋ ˆ๋“œ(Main Thread)๋Š” ๋‹จ์ผ ์ด๋ฒคํŠธ ๋ฃจํ”„(QCoreApplication::exec())๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž ์ž…๋ ฅ, ํƒ€์ด๋จธ, ๋„คํŠธ์›Œํฌ I/O ๋ฐ ํ™”๋ฉด ์žฌ๊ทธ๋ฆฌ๊ธฐ(Paint Event) ์ด๋ฒคํŠธ๋ฅผ ์ˆœ์ฐจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๊ณ ์ฃผํŒŒ ์†Œ์ผ“ ํ†ต์‹ ์ด๋‚˜ ์‹œ๋ฆฌ์–ผ ํฌํŠธ๋กœ๋ถ€ํ„ฐ ์ˆ˜์‹ ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์ˆ˜ ๋ฐ€๋ฆฌ์ดˆ(ms) ๊ฐ„๊ฒฉ์œผ๋กœ UI ์Šค๋ ˆ๋“œ์— Signal์„ ๋ฐœ์ƒ์‹œํ‚ค๋ฉด, ์ด๋ฒคํŠธ ํ(Event Queue)์— ์ด๋ฒคํŠธ๊ฐ€ ๋ˆ„์ ๋˜์–ด ๋ Œ๋”๋ง ๋ช…๋ น(Paint Event)์ด ์ œ๋•Œ ์‹คํ–‰๋˜์ง€ ๋ชปํ•˜๊ณ  ๋ฐ€๋ฆฌ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๋‚˜. QGraphicsItem ๊ฐ์ฒด ์˜ค๋ฒ„ํ—ค๋“œ ๋ฐ Spatial Indexing ๋ถ€๋‹ด

QGraphicsScene์€ ๊ธฐ๋ณธ์ ์œผ๋กœ **BSP Tree (Binary Space Partitioning Tree)** ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์‚ฌ์šฉํ•ด ๊ทธ๋ž˜ํ”ฝ ์š”์†Œ๋“ค์˜ ์ถฉ๋Œ ๋ฐ ์˜์—ญ ์กฐํšŒ๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์ˆ˜๋งŒ ๊ฐœ ์ด์ƒ์˜ QGraphicsItem(์˜ˆ: QGraphicsEllipseItem, QGraphicsLineItem)์„ ๋งค๋ฒˆ ๋™์ ์œผ๋กœ ์ƒ์„ฑ/์ด๋™/์‚ญ์ œํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ณ‘๋ชฉ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค:

  • C++ / Python ๋ฐ”์ธ๋”ฉ ์˜ค๋ฒ„ํ—ค๋“œ: PySide6๋Š” PyBind11/Shiboken์„ ๊ฑฐ์ณ C++ ๊ฐ์ฒด์™€ ํ†ต์‹ ํ•˜๋ฏ€๋กœ, ๋นˆ๋ฒˆํ•œ ๊ฐ์ฒด ์ƒ์„ฑ/์ˆ˜์ •์€ ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ ๋ฐ ๊ฐ€๋น„์ง€ ์ปฌ๋ ‰ํ„ฐ(Garbage Collector) ๋ถ€๋‹ด์„ ๊ฐ€์ค‘์‹œํ‚ต๋‹ˆ๋‹ค.
  • ๊ณต๊ฐ„ ๋ถ„ํ•  ์ธ๋ฑ์‹ฑ ์žฌ๊ณ„์‚ฐ: ์•„์ดํ…œ์˜ ์œ„์น˜๊ฐ€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์žฅ๋ฉด(Scene) ๋‚ด์˜ BSP ํŠธ๋ฆฌ๊ฐ€ ๊ฐฑ์‹ ๋˜์–ด CPU ์ž์›์„ ๋Œ€๋Ÿ‰ ์†Œ๋ชจํ•ฉ๋‹ˆ๋‹ค.

๋‹ค. ์žฆ์€ ๋ทฐํฌํŠธ ์žฌ๊ทธ๋ฆฌ๊ธฐ (Excessive Viewport Redraws)

๊ธฐ๋ณธ ์„ค์ •์˜ QGraphicsView๋Š” ์•„์ดํ…œ ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค(Bounding Rect)์— ๋ณ€๊ฒฝ์ด ์ƒ๊ธธ ๋•Œ๋งˆ๋‹ค ๋ทฐํฌํŠธ์˜ ๋ถ€๋ถ„ ๋˜๋Š” ์ „์ฒด ์˜์—ญ์— Paint Event๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๊ฐ€ ์ˆ˜์‹ ๋  ๋•Œ๋งˆ๋‹ค update()๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ํ™”๋ฉด ๋ฆฌํ”„๋ ˆ์‹œ์œจ(์˜ˆ: 60Hz)๋ณด๋‹ค ํ›จ์”ฌ ๋นˆ๋ฒˆํ•œ ๋ผ์Šคํ„ฐ๋ผ์ด์ œ์ด์…˜(Rasterization)์ด ์†Œํ”„ํŠธ์›จ์–ด ๋ ˆ๋ฒจ์—์„œ ์ผ์–ด๋‚ฉ๋‹ˆ๋‹ค.

2. ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ๋‹จ๊ณ„๋ณ„ ์ง„๋‹จ ๋ฐ ์ƒํƒœ ์ธก์ • (Diagnostics)

์ตœ์ ํ™”๋ฅผ ์ง„ํ–‰ํ•˜๊ธฐ ์ „ ๋ณ‘๋ชฉ์˜ ๊ธฐ์ ์„ ๋ช…ํ™•ํžˆ ์ง„๋‹จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํŒŒ์ด์ฌ ๋ฐ Qt ๋‚ด๋ถ€ ๋ชจ๋‹ˆํ„ฐ๋ง ๊ธฐ๋ฒ•์„ ์ด์šฉํ•ด ๋ฌธ์ œ๋ฅผ ๋ถ„๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

  1. ์Šค๋ ˆ๋“œ ๋ถ„๋ฆฌ๋„ ํ™•์ธ: ๋ฐ์ดํ„ฐ ์ˆ˜์‹  ๋ฐ ํŒŒ์‹ฑ ๋กœ์ง์ด UI ์Šค๋ ˆ๋“œ(Main Thread) ๋‚ด์—์„œ ์ง์ ‘ ์‹คํ–‰๋˜๋Š”์ง€ ์ ๊ฒ€ํ•ฉ๋‹ˆ๋‹ค. ์ˆ˜์‹ ๋ถ€ ์—ฐ์‚ฐ์ด 1ms ์ด์ƒ ๊ฑธ๋ฆฐ๋‹ค๋ฉด ์ง€์ฒด ์—†์ด ๋ณ„๋„ ์Šค๋ ˆ๋“œ๋กœ ๊ฒฉ๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  2. ํ”„๋ ˆ์ž„ ๋“œ๋กญ ๋ ˆ์ดํŠธ ์ธก์ •์„ ์œ„ํ•œ ๋ Œ๋”๋ง ํ›…(Hook): Custom QGraphicsView ํด๋ž˜์Šค์˜ paintEvent ๋‚ด์— ํƒ€์ž„์Šคํƒฌํ”„๋ฅผ ๊ธฐ๋กํ•˜์—ฌ ์—ฐ์‡„ ํ˜ธ์ถœ ๊ฐ„๊ฒฉ์„ ์ธก์ •ํ•ฉ๋‹ˆ๋‹ค. Target FPS(์˜ˆ: 60FPS = 16.6ms)๋ฅผ ์ง€์†์ ์œผ๋กœ ์ดˆ๊ณผํ•œ๋‹ค๋ฉด ์ด๋ฒคํŠธ ํ ๋ณ‘๋ชฉ์ž…๋‹ˆ๋‹ค.
  3. cProfile ๋ฐ PySide6 Profiling: cProfile ๋˜๋Š” yappi ๋ชจ๋“ˆ์„ ํ†ตํ•ด QCoreApplication.processEvents()๋‚˜ ์‹œ๊ทธ๋„ ์ „๋‹ฌ ๊ณผ์ •์—์„œ ์‹œ๊ฐ„์ด ์†Œ๋ชจ๋˜๋Š”์ง€ ์ถ”์ ํ•ฉ๋‹ˆ๋‹ค.


3. ๊ณ ์ฃผํŒŒ ์‹œ๊ฐํ™” ์„ฑ๋Šฅ ์ตœ์ ํ™”๋ฅผ ์œ„ํ•œ 5๊ฐ€์ง€ ํ•ต์‹ฌ ๊ธฐ๋ฒ•

๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด architecture ์ฐจ์›์—์„œ์˜ ๊ฐœ์„ ๊ณผ Qt ๋‚ด์žฅ ์ตœ์ ํ™” ํ”Œ๋ž˜๊ทธ ์„ค์ •์„ ๊ฒฐํ•ฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋ฒ• 1: ์ˆ˜์‹  ๋ฐ ์ˆ˜์ง‘ ์Šค๋ ˆ๋“œ ๋ถ„๋ฆฌ & Ring Buffer ์•„ํ‚คํ…์ฒ˜

๋ฐ์ดํ„ฐ ์ˆ˜์‹ ๋ถ€(Worker Thread)์—์„œ UI ์Šค๋ ˆ๋“œ๋กœ ๊ณง๋ฐ”๋กœ ์‹œ๊ทธ๋„์„ ๋ณด๋‚ด์ง€ ์•Š๊ณ , ๊ณ ์† **๋ง ๋ฒ„ํผ(Ring Buffer / Circular Buffer)** ๋˜๋Š” Lock-free Queue์— ๋ฐ์ดํ„ฐ๋ฅผ ์Œ“์•„๋‘ก๋‹ˆ๋‹ค.

UI ์Šค๋ ˆ๋“œ๋Š” ๋…๋ฆฝ์ ์ธ QTimer๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ •ํ•ด์ง„ ํ”„๋ ˆ์ž„ ์ฃผ๊ธฐ(์˜ˆ: 60Hz = ์•ฝ 16ms ๋งˆ๋‹ค)๋งˆ๋‹ค ๋ฒ„ํผ์— ์Œ“์ธ ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ ์ผ๊ด„(Batch) ์ถ”์ถœํ•˜์—ฌ ํ™”๋ฉด์„ ๊ฐฑ์‹ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ ์ž…๋ ฅ ๋นˆ๋„(Inbound Frequency)์™€ UI ๋ Œ๋”๋ง ๋นˆ๋„(Render Frequency)๋ฅผ **๋””์ปคํ”Œ๋ง(Decoupling)**์‹œํ‚ต๋‹ˆ๋‹ค.

๊ธฐ๋ฒ• 2: ์‹œ๊ทธ๋„ ์“ฐ๋กœํ‹€๋ง(Throttling) ๋ฐ ์ฒญํฌ ๋‹จ์œ„ ์—…๋ฐ์ดํŠธ

๋ฐ์ดํ„ฐ 1๊ฑด๋‹น ์‹œ๊ทธ๋„ 1ํšŒ๋ฅผ ๋ฐœ์†กํ•˜๋Š” ๊ฒƒ์€ ์ด๋ฒคํŠธ ํ ๊ณ ๊ฐˆ์˜ ์ง€๋ฆ„๊ธธ์ž…๋‹ˆ๋‹ค. ํŒŒ์ดํ”„๋ผ์ธ ๋‚ด๋ถ€์—์„œ 100~1,000๊ฐœ์˜ ๋ฐ์ดํ„ฐ ํฌ์ธํŠธ๋ฅผ ํ•˜๋‚˜์˜ **๋ฐฐ์น˜ ์ฒญํฌ(Batch Chunk; ์˜ˆ: NumPy Array)** ํ˜•ํƒœ๋กœ ํŒจํ‚นํ•˜์—ฌ ๋‹จ์ผ ์‹œ๊ทธ๋„๋กœ ์ „๋‹ฌํ•˜๋„๋ก ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋ฒ• 3: QGraphicsView ๋ฐ QGraphicsScene ์ตœ์ ํ™” ํ”Œ๋ž˜๊ทธ ์ ์šฉ

Qt๊ฐ€ ๊ธฐ๋ณธ ์ œ๊ณตํ•˜๋Š” ์˜ต์…˜๋“ค์„ ๋Œ€์šฉ๋Ÿ‰ ์‹œ๊ฐํ™” ํ™˜๊ฒฝ์— ๋งž๊ฒŒ ํŠœ๋‹ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

# QGraphicsView ์„ค์ • ์ตœ์ ํ™” ์˜ˆ์‹œ
view.setViewportUpdateMode(QGraphicsView.ViewportUpdateMode.FullViewportUpdate)
view.setOptimizationFlags(
    QGraphicsView.OptimizationFlag.DontSavePainterState |
    QGraphicsView.OptimizationFlag.DontAdjustForAntialiasing
)
# Scene์˜ Spatial Indexing์„ ๋„๊ฑฐ๋‚˜ ์ตœ์†Œํ™” (์•„์ดํ…œ์ด ๋งค์šฐ ๋งŽ๊ณ  ์ž์ฃผ ์›€์ง์ด๋Š” ๊ฒฝ์šฐ)
scene.setItemIndexMethod(QGraphicsScene.ItemIndexMethod.NoIndex)
view.setCacheMode(QGraphicsView.CacheModeFlag.CacheBackground)
  • FullViewportUpdate: ๊ตญ์†Œ ์˜์—ญ ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ๊ณ„์‚ฐ ๋ฐ ์ง€์šฐ๊ธฐ ์˜ค๋ฒ„ํ—ค๋“œ๋ฅผ ์ค„์ด๊ณ  ์ „์ฒด ๋ทฐํฌํŠธ๋ฅผ ํ•œ ๋ฒˆ์— ์žฌ๊ทธ๋ฆฌ๋Š” ๊ฒƒ์ด ๋งŽ์€ ๊ฐœ์ˆ˜์˜ ์•„์ดํ…œ ๋ณ€๊ฒฝ ์‹œ ํ›จ์”ฌ ๋น ๋ฆ…๋‹ˆ๋‹ค.
  • NoIndex: ์•„์ดํ…œ์˜ ์œ„์น˜ ๋ณ€๊ฒฝ์ด ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ผ์–ด๋‚  ๋•Œ BSP ํŠธ๋ฆฌ ์—…๋ฐ์ดํŠธ ๋น„์šฉ์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋ฒ• 4: ๊ฐœ๋ณ„ ์•„์ดํ…œ ์˜ค๋ฒ„ํ—ค๋“œ ์ œ๊ฑฐ - ๋‹จ์ผ Custom QGraphicsItem ๋ณ‘ํ•ฉ

์ˆ˜๋งŒ ๊ฐœ์˜ ๋ฐ์ดํ„ฐ ํฌ์ธํŠธ๋ฅผ ๊ฐœ๋ณ„ QGraphicsEllipseItem์œผ๋กœ ๋“ฑ๋กํ•˜๋Š” ๋Œ€์‹ , **๋‹จ ํ•˜๋‚˜์˜ Custom QGraphicsItem**์„ ์„ ์–ธํ•˜๊ณ  ๊ทธ ๋‚ด๋ถ€์˜ paint() ๋ฉ”์„œ๋“œ์—์„œ ๋ชจ๋“  ์ ๊ณผ ์„ ์„ ์ผ๊ด„ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.

class RealtimeChartItem(QGraphicsItem):
    def __init__(self):
        super().__init__()
        self.data_points = [] # QPolygonF ๋˜๋Š” NumPy ๊ธฐ๋ฐ˜ ์ขŒํ‘œ
        self._boundingRect = QRectF(0, 0, 1000, 600)

    def boundingRect(self):
        return self._boundingRect

    def update_data(self, new_points):
        self.data_points = new_points
        self.update() # ํ™”๋ฉด ์žฌ๊ทธ๋ฆฌ๊ธฐ ์š”์ฒญ

    def paint(self, painter, option, widget=None):
        painter.setRenderHint(QPainter.RenderHint.Antialiasing, False) # ์•ˆํ‹ฐ์•จ๋ฆฌ์–ด์‹ฑ ๋น„ํ™œ์„ฑํ™”๋กœ ์†๋„ ํ™•๋ณด
        painter.setPen(QPen(Qt.GlobalColor.green, 1))
        
        # ๋‹จ์ผ ํ˜ธ์ถœ๋กœ ์ „์ฒด ํด๋ฆฌ๋ผ์ธ ๋ Œ๋”๋ง
        if len(self.data_points) > 1:
            painter.drawPolyline(self.data_points)

๊ธฐ๋ฒ• 5: QOpenGLWidget ํ†ตํ•ฉ์„ ํ†ตํ•œ GPU ํ•˜๋“œ์›จ์–ด ๊ฐ€์†

Qt์˜ ๊ธฐ๋ณธ ์†Œํ”„ํŠธ์›จ์–ด ๋ผ์Šคํ„ฐ๋ผ์ด์ €(Software Rasterizer) ๋Œ€์‹  **OpenGL ํ•˜๋“œ์›จ์–ด ๊ฐ€์†(GPU Acceleration)**์„ ํ™œ์„ฑํ™”ํ•ฉ๋‹ˆ๋‹ค. QGraphicsView์˜ ๋ทฐํฌํŠธ๋ฅผ QOpenGLWidget์œผ๋กœ ๊ต์ฒดํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ ์—„์ฒญ๋‚œ ์„ฑ๋Šฅ ํ–ฅ์ƒ์„ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

from PySide6.QtOpenGLWidgets import QOpenGLWidget

# OpenGL ๋ทฐํฌํŠธ ์„ค์ •
opengl_widget = QOpenGLWidget()
view.setViewport(opengl_widget)

4. ์‹ค๋ฌด ๊ตฌํ˜„ ํŒจํ„ด: ๊ณ ์„ฑ๋Šฅ ์‹ค์‹œ๊ฐ„ ์ฐจํŠธ ํŒŒ์ดํ”„๋ผ์ธ

์œ„์˜ ์ตœ์ ํ™” ๊ธฐ๋ฒ•์„ ์ด๋ง๋ผํ•œ ๊ตฌ์กฐ์  ํŒŒ์ดํ”„๋ผ์ธ ์ฝ”๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ํ๋ฆ„์œผ๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค.

import sys
import numpy as np
from collections import deque
from PySide6.QtCore import QThread, Signal, QTimer, QRectF, Qt
from PySide6.QtWidgets import QApplication, QGraphicsView, QGraphicsScene, QGraphicsItem
from PySide6.QtGui import QPainter, QPolygonF, QPointF, QPen
from PySide6.QtOpenGLWidgets import QOpenGLWidget

# 1. ๊ณ ์ฃผํŒŒ ๋ฐ์ดํ„ฐ ์ˆ˜์‹  ์›Œ์ปค ์Šค๋ ˆ๋“œ
class DataIngestionWorker(QThread):
    chunk_received = Signal(np.ndarray)

    def run(self):
        while not self.isInterruptionRequested():
            # 1kHz(1ms) ๊ฐ„๊ฒฉ์œผ๋กœ ๋ฐ์ดํ„ฐ ๋ฐœ์ƒ ๊ฐ€์ƒ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
            self.msleep(1)
            # 50๊ฐœ์˜ ์„ผ์„œ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
            raw_data = np.random.normal(size=(50, 2))
            self.chunk_received.emit(raw_data)

# 2. ์ปค์Šคํ…€ ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ๋ Œ๋”๋ง ์•„์ดํ…œ
class FastPlotItem(QGraphicsItem):
    def __init__(self):
        super().__init__()
        self.polygon = QPolygonF()
        self.bounds = QRectF(0, -10, 1000, 20)

    def boundingRect(self):
        return self.bounds

    def set_data(self, points):
        self.polygon = QPolygonF([QPointF(x, y) for x, y in points])
        self.update()

    def paint(self, painter, option, widget=None):
        painter.setPen(QPen(Qt.GlobalColor.cyan, 1))
        painter.drawPolyline(self.polygon)

# 3. ๋ฉ”์ธ ์‹œ๊ฐํ™” ์œˆ๋„์šฐ
class FastVisualizationView(QGraphicsView):
    def __init__(self):
        super().__init__()
        self.scene = QGraphicsScene(self)
        self.setScene(self.scene)
        
        # OpenGL GPU ๊ฐ€์† ํ™œ์„ฑํ™” ๋ฐ ๋ทฐํฌํŠธ ์„ค์ •
        self.setViewport(QOpenGLWidget())
        self.setViewportUpdateMode(QGraphicsView.ViewportUpdateMode.FullViewportUpdate)
        self.scene.setItemIndexMethod(QGraphicsScene.ItemIndexMethod.NoIndex)
        
        # ๋‹จ์ผ Plot ์•„์ดํ…œ ์ถ”๊ฐ€
        self.plot_item = FastPlotItem()
        self.scene.addItem(self.plot_item)

        # Ring Buffer ์„ค์ • (์ตœ๋Œ€ 1000๊ฐœ ํฌ์ธํŠธ ์œ ์ง€)
        self.buffer = deque(maxlen=1000)
        
        # ๋””์ปคํ”Œ๋ง ํƒ€์ด๋จธ (60 FPS = ์•ฝ 16ms ๋งˆ๋‹ค UI ์—…๋ฐ์ดํŠธ)
        self.render_timer = QTimer(self)
        self.render_timer.setInterval(16)
        self.render_timer.timeout.connect(self.flush_buffer_to_ui)
        self.render_timer.start()

        # ๋ฐ์ดํ„ฐ ์ˆ˜์‹  ์Šค๋ ˆ๋“œ ์‹คํ–‰
        self.worker = DataIngestionWorker()
        self.worker.chunk_received.connect(self.on_data_received)
        self.worker.start()

    def on_data_received(self, chunk):
        # UI ์Šค๋ ˆ๋“œ์˜ ํ•ธ๋“ค๋Ÿฌ์—์„œ๋Š” ๋‹จ์ˆœํžˆ ๋ฒ„ํผ์— ๋ฐ์ดํ„ฐ ์ €์žฅ๋งŒ ์ˆ˜ํ–‰ (์—ฐ์‚ฐ ์ตœ์†Œํ™”)
        self.buffer.extend(chunk)

    def flush_buffer_to_ui(self):
        if not self.buffer:
            return
        
        # NumPy ๋˜๋Š” QPolygon ์—ฐ์‚ฐ์„ ํ†ตํ•œ ๋ฐฐ์น˜ ์ขŒํ‘œ ๋ณ€ํ™˜
        data_arr = np.array(self.buffer)
        x_coords = np.linspace(0, 1000, len(data_arr))
        y_coords = data_arr[:, 0] * 50
        
        points = np.column_stack((x_coords, y_coords))
        self.plot_item.set_data(points)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    view = FastVisualizationView()
    view.resize(1024, 600)
    view.show()
    sys.exit(app.exec())

5. ์‹ค๋ฌด ์ ์šฉ ์‹œ ์œ ์˜ํ•ด์•ผ ํ•  ์—ฃ์ง€ ์ผ€์ด์Šค (Edge Cases)

์ฃผ์˜: ์ตœ์ ํ™” ๊ธฐ๋ฒ•์„ ์ ์šฉํ•˜๋Š” ๊ณผ์ •์—์„œ ์˜ˆ๊ธฐ์น˜ ์•Š์€ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜(Memory Leak)๋‚˜ ์Šค๋ ˆ๋“œ ๋™๊ธฐํ™” ์ด์Šˆ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜ ์—ฃ์ง€ ์ผ€์ด์Šค๋“ค์„ ๋ฐ˜๋“œ์‹œ ์ ๊ฒ€ํ•˜์„ธ์š”.
  • QueuedConnection ๋ฉ”๋ชจ๋ฆฌ ํญ์ฃผ: Worker ์Šค๋ ˆ๋“œ์—์„œ ์ƒ์„ฑํ•˜๋Š” ๋ฐ์ดํ„ฐ ์†๋„๊ฐ€ UI ์Šค๋ ˆ๋“œ๊ฐ€ Consumer๋กœ์„œ ์†Œ๋น„ํ•˜๋Š” ์†๋„๋ณด๋‹ค ๋น ๋ฅผ ๊ฒฝ์šฐ, Qt์˜ QueuedConnection ์ด๋ฒคํŠธ ํ ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ๊ณ„์† ๋ˆ„์ ๋ฉ๋‹ˆ๋‹ค. ์ˆ˜์‹  ์Šค๋ ˆ๋“œ ์ชฝ์— Max Buffer Limit์„ ๋‘๊ณ  ํ•œ๊ณ„๋ฅผ ์ดˆ๊ณผํ•˜๋ฉด ์ด์ „ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฎ์–ด์“ฐ๊ฑฐ๋‚˜ ๋“œ๋กญ(Drop)ํ•˜๋Š” ์ „๋žต์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
  • OpenGL Context ์Šค๋ ˆ๋“œ ์•ˆ์„ ์„ฑ: QOpenGLWidget์„ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ QPainter ๋ Œ๋”๋ง ์—ฐ์‚ฐ์€ ๋ฐ˜๋“œ์‹œ **UI Main Thread** ์ƒ์—์„œ ์‹คํ–‰๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. Worker ์Šค๋ ˆ๋“œ ๋‚ด์—์„œ OpenGL ํ•จ์ˆ˜๋‚˜ QPainter๋ฅผ ์ง์ ‘ ์กฐ์ž‘ํ•˜๋ฉด ์‹œ๊ทธ๋„ ์˜ค๋ฅ˜ ๋ฐ ํฌ๋ž˜์‹œ(Crash)๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.
  • ๊ฐ€๋น„์ง€ ์ปฌ๋ ‰ํ„ฐ(GC) ๋ ˆ์ดํ„ด์‹œ: ํŒŒ์ด์ฌ gc.collect()๊ฐ€ UI ์‹คํ–‰ ๋„์ค‘ ๋™์ž‘ํ•˜์—ฌ ์ˆœ๊ฐ„์ ์ธ ์Šคํ„ฐํ„ฐ๋ง(Stuttering)์ด ์œ ๋ฐœ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ˆ˜์‹  ์‹œ์—๋Š” ๋ถˆํ•„์š”ํ•œ ๊ฐ์ฒด ์ƒ์„ฑ์„ ์ž์ œํ•˜๊ณ , ๋ฐฐ์—ด์„ ์žฌ์‚ฌ์šฉ(Pre-allocation)ํ•˜๋Š” ๊ตฌ์กฐ๋กœ ๊ตฌํ˜„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


๊ฒฐ๋ก  ๋ฐ ์„ฑ๋Šฅ ๊ฐœ์„  ์š”์•ฝ

PySide6 ๊ธฐ๋ฐ˜ ๋Œ€์šฉ๋Ÿ‰ ์‹ค์‹œ๊ฐ„ ์‹œ๊ฐํ™” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ UI ๋ณ‘๋ชฉ์€ ํ”„๋ ˆ์ž„์›Œํฌ ์ž์ฒด์˜ ํ•œ๊ณ„๊ฐ€ ์•„๋‹ˆ๋ผ **์ด๋ฒคํŠธ ๋ฃจํ”„ ์ œ์–ด ๋ฐ ๊ทธ๋ž˜ํ”ฝ์Šค ํŒŒ์ดํ”„๋ผ์ธ ์˜ค๋ฒ„ํ—ค๋“œ**์—์„œ ๋น„๋กฏ๋ฉ๋‹ˆ๋‹ค. ๋ณธ ๊ธ€์—์„œ ์ œ์‹œํ•œ ํ•ต์‹ฌ ํ•ด๊ฒฐ์ฑ…์„ ์š”์•ฝํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

  1. Inbound Data vs Render Clock ๋ถ„๋ฆฌ: Ring Buffer์™€ QTimer๋ฅผ ํ™œ์šฉํ•ด ๋ Œ๋”๋ง ์ฃผ๊ธฐ๋ฅผ 60FPS๋กœ ์ œ์–ดํ•ฉ๋‹ˆ๋‹ค.
  2. QGraphicsItem ์ตœ์†Œํ™”: ์•„์ดํ…œ ๊ฐœ์ˆ˜๋ฅผ ์ตœ์†Œํ™”ํ•˜๊ณ  Custom Item์˜ paint() ๋‚ด์—์„œ ์ผ๊ด„ ๋ Œ๋”๋ง(Batch Drawing)์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
  3. ํ•˜๋“œ์›จ์–ด ๊ฐ€์† ํ™œ์šฉ: QOpenGLWidget์„ ๋ทฐํฌํŠธ๋กœ ์ง€์ •ํ•˜์—ฌ ๋ผ์Šคํ„ฐ๋ผ์ด์ œ์ด์…˜ ๋ถ€๋‹ด์„ GPU๋กœ ์ด๊ด€ํ•ฉ๋‹ˆ๋‹ค.
  4. Qt ๊ทธ๋ž˜ํ”ฝ ์˜ต์…˜ ์ตœ์ ํ™”: FullViewportUpdate ๋ฐ NoIndex ์˜ต์…˜์œผ๋กœ C++ ๋ฐ Spatial Indexing Overhead๋ฅผ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ์ฒด๊ณ„์ ์ธ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ์ ‘๊ทผ๋ฒ•์„ ์ ์šฉํ•˜๋ฉด, ํŒŒ์ด์ฌ PySide6 ํ™˜๊ฒฝ์—์„œ๋„ ์ดˆ๋‹น ์ˆ˜๋งŒ ๊ฑด ์ด์ƒ์˜ ๊ณ ์ฃผํŒŒ ๋ฐ์ดํ„ฐ๋ฅผ ํ”„๋ ˆ์ž„ ๋“œ๋กญ ์—†์ด ๋งค๋„๋Ÿฝ๊ฒŒ(Smooth 60FPS) ์‹ค์‹œ๊ฐ„ ์‹œ๊ฐํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฐ˜์‘ํ˜•