PySide QTimer singleShot와 interval 정확도 드리프트 스레드 어피니티 완벽 가이드
⏱️ QTimer 동작 원리부터 정확도 최적화와 스레드 어피니티까지 한 번에 정리합니다
GUI의 자연스러운 반응성과 백그라운드 작업의 안정성을 동시에 잡으려면 타이밍 제어가 탄탄해야 합니다.
PySide(Qt for Python)의 QTimer는 간단한 인터벌 반복부터 지연 실행까지 폭넓게 쓰이지만, 정확도와 드리프트, 그리고 스레드 어피니티를 이해하지 못하면 기대와 다른 결과를 종종 마주하게 됩니다.
이 글은 실전에서 자주 부딪히는 질문을 기준으로 QTimer의 singleShot과 interval 사용법, 타이머 타입에 따른 특성, 이벤트 루프 요구 사항, 그리고 멀티스레드 환경에서의 어피니티 규칙을 사람 말로 풀어 정리합니다.
코드 스니펫과 체크리스트 중심으로 빠르게 적용할 수 있도록 구성했습니다.
핵심만 먼저 짚어보면 이렇습니다.
singleShot은 한 번만 실행되는 지연 호출에 적합하고, interval은 주기 반복에 쓰입니다.
QTimer의 정확도는 운영체제와 타이머 타입의 영향을 받으며 기본은 CoarseTimer이고, 더 촘촘한 타이밍이 필요하면 PreciseTimer를 명시해 지연을 줄일 수 있습니다.
타이머는 자신이 속한 QObject의 스레드 어피니티에서 시그널을 방출하므로, 해당 스레드에 이벤트 루프가 돌아가야 정상 동작합니다.
이 기본기를 잡아두면 초 단위 작업부터 서브초 타이밍까지 원하는 수준으로 컨트롤할 수 있습니다.
📋 목차
⏳ QTimer 기본 개념과 동작 조건
QTimer는 Qt 이벤트 루프에 의해 구동되는 타이머 객체로, 지정한 시간 간격이 흐르면 시그널을 발생시켜 콜백을 실행합니다.
GUI 프레임워크 특성상 메인 스레드의 이벤트 루프가 멈추면 타이머도 멈춘다는 점이 핵심입니다.
즉, 블로킹 I/O나 무거운 연산으로 이벤트 루프가 지연되면 타이머 콜백도 늦게 호출됩니다.
정확한 타이밍을 원한다면 이벤트 루프를 항상 유동적으로 유지하고, 시간이 오래 걸리는 작업은 워커 스레드로 분리하는 것이 기본 원칙입니다.
QTimer는 두 가지 방식으로 쓰입니다.
하나는 한 번만 지연 실행하는 singleShot, 다른 하나는 주기적으로 반복 호출하는 interval 모드입니다.
두 모드 모두 이벤트 루프가 살아 있어야 정상 동작하며, 타이머의 시그널은 타이머 객체가 속한 스레드 어피니티에서 방출됩니다.
따라서 객체를 만들거나 moveToThread로 옮길 때, 해당 스레드에 이벤트 루프(QThread.run 내 exec())가 돌아가도록 설계해야 합니다.
🧠 타이머가 ‘언제’ 불리는가, 이벤트 루프와의 관계
Qt의 타이머는 내부적으로 시스템 타이머와 이벤트 루프 큐를 활용합니다.
만료 시점이 도래하면 콜백이 즉시 실행되는 것이 아니라, 다음 이벤트 루프 사이클에서 처리됩니다.
그 사이 메인 스레드가 긴 작업으로 바쁘면 만료가 밀리고, 이 지연이 누적되면 “드리프트”처럼 느껴질 수 있습니다.
UI 프레임 드랍이나 입력 지연이 보인다면, 타이머 자체의 문제가 아니라 이벤트 루프가 막혀 있는지부터 점검하는 습관이 좋습니다.
🔧 singleShot과 interval의 기본 패턴
from PySide6.QtCore import QTimer, Qt
from PySide6.QtWidgets import QApplication
app = QApplication([])
# 1) singleShot: 한 번만 지연 실행
QTimer.singleShot(500, lambda: print("500ms 후 한 번 실행"))
# 2) interval: 주기적으로 반복 실행
timer = QTimer()
timer.setInterval(1000) # 1000ms
timer.timeout.connect(lambda: print("초당 한 번 실행"))
timer.start()
# (선택) 더 정확한 타이밍 힌트
timer.setTimerType(Qt.PreciseTimer) # Coarse(기본), Precise, VeryCoarse 중 선택
app.exec()
singleShot은 지연 호출을 간결하게 처리할 수 있고, interval은 상태 갱신·폴링·애니메이션 타이밍 등에 적합합니다.
반복 타이머는 start/stop으로 수명 주기를 관리하고, 일시 정지가 필요하면 stop() 후 다시 start()로 재개하는 구조가 명확합니다.
정밀도가 중요하다면 setTimerType으로 Qt.PreciseTimer를 힌트로 제공할 수 있지만, 최종 정확도는 운영체제의 스케줄링과 시스템 타이머 해상도에 의존한다는 점을 기억하세요.
🧭 타이머 타입 개요와 선택 기준
| 타입 | 특징/용도 |
|---|---|
| Qt.CoarseTimer | 기본값. 전력 효율 우선. 지터가 상대적으로 큼. |
| Qt.PreciseTimer | 정밀도 우선. 짧은 인터벌에 적합. 시스템 부하에 민감. |
| Qt.VeryCoarseTimer | 여러 초 단위 등 대략적 스케줄. 배터리/절전 모드에 유리. |
- 🧭이벤트 루프가 항상 동작하도록 긴 작업은 워커 스레드로 이동한다.
- ⏱️짧은 인터벌 또는 지연이 민감하면 Qt.PreciseTimer 사용을 고려한다.
- 🧵타이머가 속한 객체의 스레드 어피니티와 해당 스레드의 이벤트 루프 존재를 확인한다.
💡 TIP: UI 업데이트는 메인 스레드에서만 안전합니다.
워커 스레드에서 계산하고, 결과만 시그널/슬롯으로 메인 스레드 타이머 콜백에 전달하면 지연과 경쟁 상태를 동시에 줄일 수 있습니다.
⚠️ 주의: time.sleep을 메인 스레드에서 호출하면 이벤트 루프가 멈추고 타이머가 밀립니다.
지연이 필요하면 QTimer.singleShot을 사용하거나, 워커 스레드에서 처리하세요.
🧩 singleShot와 interval 차이와 올바른 선택
PySide의 QTimer.singleShot()과 QTimer 객체 기반의 interval 방식은 외형상 비슷해 보여도 내부 동작과 용도가 다릅니다.
이 차이를 이해하면 불필요한 메모리 점유나 실행 타이밍 꼬임을 방지할 수 있습니다.
singleShot은 ‘지연 한 번 실행’, interval은 ‘주기적 반복 실행’이라는 차이가 핵심입니다.
그리고 이 두 가지는 이벤트 루프의 부하 상황과 맞물려 동작 특성이 크게 달라집니다.
⏱️ singleShot: 짧고 간단한 지연 호출에 최적
singleShot은 한 번만 실행되고 자동으로 소멸되는 타이머를 생성합니다.
일회성 UI 효과나 특정 시간 뒤 함수 호출에 쓰기 좋습니다.
QTimer 객체를 별도로 관리할 필요가 없어 코드가 간결하고, 함수 단위로 바로 적용할 수 있습니다.
다만, 지연 시간 동안 이벤트 루프가 멈추면 콜백도 늦어지므로 절대적인 지연 보장은 아닙니다.
from PySide6.QtCore import QTimer
# 2초 뒤에 한 번만 실행
QTimer.singleShot(2000, lambda: print("2초 후 실행"))
이 방식은 UI 초기화 이후 특정 동작을 살짝 지연시키거나, 로딩 후 후속 코드를 실행할 때 자주 사용됩니다.
특히 앱 시작 시 로딩 애니메이션 이후 초기화 코드를 실행하거나, 일정 시간 후 알림을 띄울 때 유용합니다.
🔁 interval: 주기적 반복과 상태 모니터링에 최적
interval 방식은 QTimer 객체를 인스턴스화하여 setInterval()과 start()를 사용합니다.
timeout 시그널이 주기적으로 발생해 지정된 함수를 계속 호출합니다.
센서 데이터 폴링, UI 상태 업데이트, 주기적 저장 등 지속적인 루프를 만들 때 유용합니다.
단, 콜백 함수가 수행되는 동안 인터벌이 겹치지 않도록 설계해야 합니다.
from PySide6.QtCore import QTimer
timer = QTimer()
timer.setInterval(1000)
timer.timeout.connect(lambda: print("매초 실행"))
timer.start()
interval은 stop()을 호출하지 않는 이상 계속 실행되며, 객체가 삭제되거나 이벤트 루프가 종료되면 자동으로 멈춥니다.
주기적 동작이 필요하지만 루프문으로 CPU를 잡아두고 싶지 않을 때 이상적인 구조입니다.
💡 어떤 상황에 어떤 타이머를 써야 할까?
- 🚀앱 시작 시 초기화 지연 → QTimer.singleShot
- 🔄데이터 갱신, 폴링, 애니메이션 → QTimer() + setInterval()
- 🧭반복 타이머의 콜백이 긴 경우 → 워커 스레드 분리
💎 핵심 포인트:
singleShot은 ‘한 번만’, interval은 ‘계속 반복’.
그리고 두 방식 모두 이벤트 루프 의존적이므로 UI 응답성 확보가 타이머 신뢰도에 직결됩니다.
🎯 정확도와 드리프트 관리 전략
QTimer는 기본적으로 OS의 타이머 해상도와 Qt 이벤트 루프의 상태에 따라 정확도가 달라집니다.
이 말은, 지정한 시간마다 콜백이 “정확히” 실행된다고 단정할 수 없다는 뜻입니다.
특히 밀리초 단위의 짧은 인터벌에서는 누적 지연, 즉 드리프트(Drift) 현상이 발생하기 쉽습니다.
이 구간에서는 QTimer의 타입, 이벤트 루프 부하, CPU 스케줄러 상태를 함께 고려해야 합니다.
⏱️ 정확도에 영향을 주는 요인
QTimer의 정확도는 세 가지 주요 요인으로 좌우됩니다.
첫째, 운영체제의 타이머 해상도입니다.
Windows는 기본적으로 약 15ms 해상도를 갖고 있으며, timeBeginPeriod() 등으로 변경할 수 있지만 이는 시스템 전체에 영향을 주므로 권장되지 않습니다.
둘째, 이벤트 루프의 부하 상태입니다.
루프가 과부하되면 타이머 시그널이 큐에 밀려 실행이 늦어집니다.
셋째, QTimer의 타입입니다.
Qt는 CoarseTimer(대략적), PreciseTimer(정확), VeryCoarseTimer(느슨) 세 가지 타이머 타입을 제공합니다.
💬 QTimer의 PreciseTimer는 OS가 보장하는 최소 지연에 근접하지만, CPU 점유율이 높을수록 정확도는 다시 떨어집니다.
🧮 드리프트 누적과 보정 방법
드리프트는 타이머 이벤트의 간격이 점점 밀리면서 목표 주기보다 늦어지는 현상입니다.
예를 들어 초당 1회 호출을 원했는데 10분 뒤에는 몇 초씩 밀리는 상황이 생길 수 있습니다.
이를 완화하기 위해 “실행 기준 시간”을 고정하고, 각 루프마다 다음 목표 시점을 재계산하는 방식이 효과적입니다.
from PySide6.QtCore import QTimer, QElapsedTimer
timer = QTimer()
timer.setInterval(1000)
base = QElapsedTimer()
base.start()
def tick():
elapsed = base.elapsed()
print(f"경과시간: {elapsed}ms")
# 다음 주기 보정 가능
# 예: 목표 시간 - 실제 경과 차이 계산
timer.timeout.connect(tick)
timer.start()
이처럼 주기마다 시간 오차를 확인하고, 필요 시 내부에서 보정값을 계산하면 장기적인 드리프트를 줄일 수 있습니다.
물론, 하드 리얼타임이 필요한 환경이라면 QTimer 대신 QElapsedTimer나 외부 타임베이스를 직접 활용하는 것이 더 안정적입니다.
⚙️ 타이머 정확도 향상을 위한 실전 팁
- 🧭100ms 이하 주기는 PreciseTimer로 지정.
- 🚦이벤트 루프 내 블로킹 호출 금지 (time.sleep 대신 QTimer.singleShot 사용).
- 💡정확한 시간 기반 연산은 QElapsedTimer로 검증.
- 🔋배터리 기반 환경에서는 CoarseTimer를 유지해 전력 소모 절감.
💎 핵심 포인트:
정확도를 높이려면 PreciseTimer를 쓰고, 드리프트를 줄이려면 기준 시간을 고정해 오차를 누적 보정하세요.
CPU가 놀고 있어도 이벤트 루프가 막히면 타이머는 멈춥니다.
🧵 스레드 어피니티와 이벤트 루프의 법칙
QTimer를 사용할 때 종종 겪는 혼란 중 하나가 “타이머가 왜 실행되지 않지?”라는 문제입니다.
대부분의 원인은 스레드 어피니티(Thread Affinity)와 이벤트 루프(Event Loop) 구조를 정확히 이해하지 못한 데 있습니다.
QTimer는 자신이 속한 객체의 스레드에서 동작하며, 해당 스레드에 이벤트 루프가 반드시 존재해야 합니다.
즉, QTimer는 단순히 스레드 안에 생성됐다고 해서 자동으로 작동하지 않습니다.
🔄 스레드 어피니티란?
Qt의 모든 QObject 기반 클래스는 자신이 속한 “스레드 어피니티” 정보를 가집니다.
QTimer도 QObject를 상속하므로, 자신이 속한 스레드의 이벤트 루프가 살아 있어야 timeout 시그널을 발행할 수 있습니다.
만약 다른 스레드에서 QTimer를 만들고, 해당 스레드에서 QThread.exec()이 실행되지 않았다면 타이머는 작동하지 않습니다.
from PySide6.QtCore import QObject, QThread, QTimer
class Worker(QObject):
def __init__(self):
super().__init__()
self.timer = QTimer(self)
self.timer.timeout.connect(lambda: print("워커 타이머 tick"))
self.timer.start(1000)
class MyThread(QThread):
def run(self):
worker = Worker()
self.exec() # 이벤트 루프 시작, 필수!
thread = MyThread()
thread.start()
이 예시처럼, 워커 객체가 QThread 내에서 동작하려면 self.exec()으로 이벤트 루프를 실행해야 합니다.
이 루프가 있어야 QTimer가 시그널을 방출할 수 있으며, 이벤트 루프 없는 스레드에서는 QTimer는 무용지물입니다.
🧠 moveToThread()로 어피니티 제어하기
QObject 계열 객체는 기본적으로 생성된 스레드에 속하지만, moveToThread()를 통해 다른 스레드로 옮길 수 있습니다.
이를 통해 타이머를 특정 워커 스레드에서 작동시키는 구조를 만들 수 있습니다.
worker = Worker()
worker.moveToThread(thread) # 어피니티 이동
이 때, 반드시 스레드의 이벤트 루프가 실행 중이어야 하며, moveToThread() 이후에 QTimer.start()를 호출해야 정상적으로 작동합니다.
타이머의 timeout 시그널은 해당 스레드의 루프에서 처리됩니다.
⚙️ 스레드 안전하게 타이머를 제어하는 방법
- 🧭QTimer는 생성된 스레드의 이벤트 루프에서만 동작.
- 🔗
moveToThread()로 타이머를 옮길 경우, start()는 반드시 이동 후 호출. - 🧯스레드 종료 시 타이머를
stop()후deleteLater()로 안전하게 정리.
⚠️ 주의: 타이머를 잘못된 스레드에서 제어하면 QObject::startTimer: timers cannot be started from another thread 경고가 발생합니다.
이는 잘못된 어피니티로 인한 전형적인 오류입니다.
💎 핵심 포인트:
QTimer는 객체가 속한 스레드의 이벤트 루프에서만 동작합니다.
스레드 간 이동 시에는 반드시 moveToThread() 후에 start()를 호출해야 하며, 이벤트 루프가 살아 있어야 시그널이 발행됩니다.
🛠️ 실전 패턴과 안전한 코드 스니펫
QTimer를 제대로 활용하려면 단순히 타이머를 생성하고 시작하는 것을 넘어서, 실행 시점, 예외 처리, 스레드와의 조합까지 고려한 패턴이 필요합니다.
실제 애플리케이션에서 자주 사용되는 구조를 중심으로, 안전하고 유지보수가 쉬운 QTimer 활용 예시를 정리했습니다.
⚙️ 주기적 갱신 패턴 (UI 업데이트)
UI 상태를 주기적으로 갱신할 때, QTimer를 활용하면 메인 스레드를 점유하지 않으면서 자연스러운 업데이트가 가능합니다.
아래 예시는 라벨에 현재 시간을 1초마다 표시하는 기본 패턴입니다.
from PySide6.QtWidgets import QLabel, QApplication
from PySide6.QtCore import QTimer, QDateTime
app = QApplication([])
label = QLabel()
label.show()
timer = QTimer()
timer.setInterval(1000)
timer.timeout.connect(lambda: label.setText(QDateTime.currentDateTime().toString()))
timer.start()
app.exec()
이 코드는 이벤트 루프 기반 갱신의 전형적인 구조입니다.
UI 스레드가 블로킹되지 않으므로 끊김 없이 업데이트가 이어지며, CPU 부하도 최소화됩니다.
🧵 백그라운드 작업에서 안전하게 타이머 쓰기
워커 스레드 내부에서도 QTimer를 사용할 수 있지만, 반드시 QThread.exec()를 호출해야 합니다.
이벤트 루프가 돌지 않으면 타이머가 작동하지 않기 때문입니다.
아래 코드는 워커 스레드에서 데이터를 주기적으로 갱신하는 예시입니다.
from PySide6.QtCore import QObject, QThread, QTimer
class Worker(QObject):
def __init__(self):
super().__init__()
self.timer = QTimer(self)
self.timer.timeout.connect(self.process)
self.timer.start(2000) # 2초마다
def process(self):
print("데이터 갱신 중...")
class Thread(QThread):
def run(self):
worker = Worker()
self.exec()
thread = Thread()
thread.start()
이 구조는 UI와 데이터 처리를 완전히 분리하면서도, 스레드 안전한 타이머 기반 주기 작업을 구현할 수 있습니다.
단, 스레드 종료 시 stop()으로 타이머를 명시적으로 중단하는 습관을 들이는 것이 좋습니다.
🧩 실무에서 자주 쓰는 QTimer 활용 팁
- 🧭짧은 딜레이 함수 대체로 QTimer.singleShot()을 사용해 sleep을 피한다.
- 🧩복잡한 UI 갱신은 interval 기반 타이머로 주기 제어.
- ⚙️정확도가 중요할 땐 Qt.PreciseTimer로 설정.
- 🧵멀티스레드 환경에선 이벤트 루프 존재를 항상 점검.
- 🧯프로그램 종료 전 타이머를
stop()후 정리하면 리소스 누수 방지.
💎 핵심 포인트:
QTimer는 단순한 반복 호출 도구가 아니라, 이벤트 루프에 의존하는 비동기 스케줄러입니다.
UI 프리징 없이 안정적으로 주기를 관리하려면, 블로킹 코드 대신 QTimer.singleShot이나 interval 구조를 적극적으로 활용하세요.
❓ 자주 묻는 질문 (FAQ)
QTimer.singleShot과 QTimer 객체 방식 중 어떤 게 더 좋나요?
반면, 반복적인 주기 제어나 중단·재시작이 필요한 경우엔 QTimer 객체를 생성하는 방식이 적합합니다.
PreciseTimer를 써도 정확한 주기가 안 나오는 이유는?
타이머 타입보다 이벤트 루프의 부하 관리가 더 중요합니다.
QTimer를 스레드 안에서 쓸 때 꼭 exec()을 호출해야 하나요?
QThread 내 run() 함수에서 self.exec()을 호출해야 타이머의 timeout 시그널이 정상적으로 발생합니다.
타이머 정확도를 높이려면 어떤 방법이 가장 효과적인가요?
추가로 QElapsedTimer로 기준 시간을 보정하는 기법을 함께 쓰면 드리프트를 최소화할 수 있습니다.
멀티스레드에서 여러 QTimer를 동시에 써도 괜찮나요?
단, 각 스레드에 이벤트 루프가 존재해야 하며, 타이머를 서로 다른 스레드 간에 옮길 경우 moveToThread() 후 start()를 다시 호출해야 합니다.
time.sleep() 대신 QTimer.singleShot을 쓰는 이유는?
QTimer.singleShot은 루프를 유지한 채 지연을 수행하므로 UI가 멈추지 않고 자연스럽게 작동합니다.
QTimer가 실행되지 않거나 즉시 종료되는 이유는?
타이머는 반드시 멤버 변수나 전역 변수로 참조를 유지해야 합니다.
PySide와 PyQt의 QTimer는 완전히 동일한가요?
API는 거의 동일하므로 코드 차이는 거의 없습니다.
🧭 PySide QTimer로 안정적 타이밍을 구현하는 핵심 요약
QTimer는 단순한 지연 실행 도구를 넘어, PySide에서 비동기 이벤트를 조율하는 핵심 타이밍 컨트롤러입니다.
singleShot으로 짧은 지연을, interval 기반으로 반복 실행을 수행할 수 있으며, 정확도를 보정하기 위해 타이머 타입을 조정하고 드리프트를 관리하는 것이 중요합니다.
스레드 어피니티와 이벤트 루프의 관계를 이해하면, 복잡한 멀티스레드 환경에서도 안전하고 정밀한 타이밍 제어가 가능합니다.
QTimer의 정확도는 OS와 이벤트 루프 부하에 의존하므로, 절대 시간 기준(QElapsedTimer)으로 주기적 보정 로직을 추가하면 장기 안정성이 향상됩니다.
또한 UI 스레드에서는 블로킹 호출을 피하고, 백그라운드 스레드에서는 이벤트 루프를 반드시 실행해야 합니다.
이 원칙만 지켜도 PySide에서 발생하는 대부분의 타이머 관련 문제를 예방할 수 있습니다.
🏷️ 관련 태그 : PySide, QTimer, Qt for Python, 이벤트루프, PreciseTimer, 스레드어피니티, 드리프트, singleShot, interval, QElapsedTimer