메뉴 닫기

PySide Qt for Python 시그널 슬롯 기본 사용법 connect emit 연결 유형 Auto Direct Queued

PySide Qt for Python 시그널 슬롯 기본 사용법 connect emit 연결 유형 Auto Direct Queued

🔌 실무에서 바로 쓰는 시그널과 슬롯 연결 패턴을 핵심만 쏙 정리합니다

GUI 이벤트를 다루다 보면 버튼 클릭이나 상태 변화처럼 작은 동작을 깔끔하게 연결하는 방법이 성패를 좌우합니다.
Qt for Python(PySide)은 이를 위해 시그널과 슬롯이라는 명확한 모델을 제공합니다.
특히 obj.signal.connect(slot) 형태로 함수나 메서드를 연결하고, 필요할 때 emit(args)로 인자를 전달해 반응시키는 흐름이 핵심입니다.
연결 방식도 상황별로 달라서 Auto, Direct, Queued 유형을 이해해야 스레드 경계를 안전하게 넘기고 UI 프리즈 없이 동작시킬 수 있습니다.
개념만 알면 끝날 문제 같지만, 실제 코드에서는 어느 순간 신호가 사라지거나 슬롯이 기대와 다르게 실행되는 등 미묘한 버그를 만나게 됩니다.
그럴 때 구조를 다시 세우는 출발점이 바로 올바른 연결 원칙과 실행 컨텍스트의 이해입니다.

이 글은 PySide의 객체 모델 중 시그널과 슬롯 기본을 중심으로 실용적인 가이드를 제공합니다.
가장 먼저 시그널과 슬롯의 동작 원리를 간단한 그림으로 잡은 뒤, obj.signal.connect(slot) 작성 규칙과 슬롯 시그니처 매칭 포인트를 정리합니다.
이어 emit(args)로 안전하게 인자를 전달하는 패턴을 살펴보고, Auto, Direct, Queued 연결 유형이 언제 어떤 스레드 컨텍스트에서 실행되는지 명확히 구분합니다.
마지막으로 워커 스레드와 UI 스레드를 아우르는 예제를 통해 큐드 호출로 병목을 피하는 방법을 보여주고, 자주 묻는 질문에서 메모리 관리와 디버깅 팁을 정리해 헷갈리기 쉬운 부분을 말끔히 정돈합니다.



🔎 PySide 시그널과 슬롯의 개념

PySide(Qt for Python)의 객체 모델에서 시그널과 슬롯은 사용자 동작이나 내부 상태 변화를 서로 다른 컴포넌트로 전달하는 표준 메커니즘입니다.
버튼이 눌리는 것처럼 이벤트를 발생시키는 쪽이 시그널을 내보내고, 그 신호를 받아 처리하는 함수가 슬롯입니다.
핵심 흐름은 간단합니다.
객체의 시그널을 슬롯에 obj.signal.connect(slot)으로 연결하고, 필요 시 시그널 측에서 emit(args)로 데이터를 함께 발사합니다.
이 구조 덕분에 위젯 간 결합도는 낮아지고, UI 코드와 작업 로직을 명확히 분리할 수 있습니다.

시그널은 타입 안정성을 유지하기 위해 사전에 정의된 시그니처를 갖고, 슬롯은 그 시그니처를 수용할 수 있어야 합니다.
슬롯은 일반 함수, 인스턴스 메서드, 람다 등 호출 가능한(callable) 모든 대상을 지원합니다.
연결 자체는 동적으로 여러 개를 허용하므로 한 시그널에서 여러 슬롯을 호출하거나, 한 슬롯이 여러 시그널을 처리하도록 구성할 수 있습니다.
또한 PySide는 실행 컨텍스트를 제어하기 위한 연결 유형을 제공합니다.
기본값은 Auto이며 상황에 따라 Direct 또는 Queued로 동작하도록 결정되거나 명시적으로 선택할 수 있습니다.

CODE BLOCK
from PySide6.QtCore import QObject, Signal

class Worker(QObject):
    progressed = Signal(int)        # 시그널 정의: 정수 1개 전달
    finished   = Signal()           # 시그널 정의: 인자 없음

    def run(self):
        for i in range(0, 101, 10):
            self.progressed.emit(i) # 시그널 발사: emit(args)
        self.finished.emit()        # 완료 알림

def on_progress(value):
    print("progress:", value)

def on_finish():
    print("done")

w = Worker()
w.progressed.connect(on_progress)   # obj.signal.connect(slot)
w.finished.connect(on_finish)       # 다중 연결도 가능

핵심 요소 설명
Signal 이벤트를 알리는 인터페이스로, emit(args)를 통해 데이터와 함께 발생합니다.
Slot 시그널을 수신해 실행되는 콜러블입니다.
시그널 시그니처와 호환되어야 합니다.
Connection obj.signal.connect(slot)으로 연결합니다.
여러 슬롯을 한 시그널에, 여러 시그널을 한 슬롯에 묶을 수 있습니다.
Type 실행 컨텍스트를 제어하는 연결 유형으로 Auto, Direct, Queued가 있습니다.

💬 시그널은 관심사의 분리를 돕습니다.
위젯은 ‘무엇이 일어났다’만 알리고, 실제 처리는 슬롯에 위임합니다.
이 덕분에 테스트 용이성과 재사용성이 크게 높아집니다.

  • 🧭시그널 시그니처와 슬롯 파라미터 호환을 먼저 확인합니다.
  • 🧷해당 객체의 생명주기 동안 연결이 유효한지 점검합니다.
    부모-자식 관계가 끊기면 슬롯이 호출되지 않을 수 있습니다.
  • 🧪간단한 로그 슬롯을 추가해 연결 여부를 빠르게 검증합니다.

💡 TIP: 슬롯이 불필요하게 강한 참조를 잡아 메모리 누수가 생길 수 있습니다.
람다를 사용할 때 외부 변수를 캡처한다면, 필요 시 disconnect()로 명시 해제하거나 약한 참조 패턴을 검토하세요.

⚠️ 주의: 긴 작업을 슬롯에서 직접 처리하면 UI 스레드를 막아 프리즈가 발생합니다.
연결 유형과 스레드 모델을 고려해 작업은 워커로 보내고, 상태 업데이트만 시그널로 교환하세요.

🧩 obj.signal.connect 사용법

PySide에서 시그널과 슬롯을 연결하는 기본 문법은 obj.signal.connect(slot) 형태입니다.
이 구문은 직관적이지만 내부적으로는 타입 검사, 슬롯 유효성, 그리고 연결 유형에 대한 설정까지 모두 수행합니다.
즉, 연결이 한 번만 성공해도 PySide는 이벤트 루프 내에서 해당 슬롯을 자동으로 관리합니다.
이때 슬롯이 존재하지 않거나 호출 불가 상태면 예외가 발생하므로 초기 테스트 시 로그 출력이나 람다를 통해 동작을 점검하는 것이 좋습니다.

슬롯에는 단순한 함수뿐 아니라 클래스의 인스턴스 메서드, 정적 메서드, 또는 lambda 표현식도 전달할 수 있습니다.
여러 시그널을 하나의 슬롯에 연결해 통합적으로 처리하거나, 하나의 시그널을 여러 슬롯에 연결해 다중 동작을 동시에 수행하도록 설정하는 것도 가능합니다.
이 모든 연결은 PySide가 자동으로 추적하며, 객체가 삭제될 경우 관련된 연결은 자동으로 해제됩니다.

CODE BLOCK
from PySide6.QtWidgets import QApplication, QPushButton
from PySide6.QtCore import Slot

@Slot()
def say_hello():
    print("Hello, Qt!")

app = QApplication([])
button = QPushButton("Click me")

# 시그널과 슬롯 연결
button.clicked.connect(say_hello)

button.show()
app.exec()

위 예제에서 버튼의 clicked 시그널은 파라미터 없이 발생하며, 연결된 say_hello() 슬롯이 즉시 실행됩니다.
만약 슬롯이 인자를 받는 형태라면, 시그널이 내보내는 인자의 수와 타입이 일치해야 정상적으로 호출됩니다.
이 규칙이 맞지 않으면 런타임에 호출되지 않거나 경고 메시지가 표시됩니다.

🧱 슬롯으로 람다 함수 사용하기

간단한 작업이라면 별도의 함수 정의 대신 lambda로 슬롯을 작성할 수도 있습니다.
특히 버튼의 상태를 즉석에서 변경하거나, 단일 인자를 로그로 출력하는 경우 자주 활용됩니다.

CODE BLOCK
button.clicked.connect(lambda: print("Button clicked!"))

💎 핵심 포인트:
람다 슬롯은 간단하지만 디버깅이 어렵습니다.
복잡한 로직이나 긴 실행 흐름을 포함하는 경우, 별도의 명시적 함수나 @Slot 데코레이터를 사용하는 편이 안정적입니다.

🔗 여러 슬롯 연결과 연결 해제

하나의 시그널에 여러 슬롯을 연결하면, 시그널이 발생할 때 연결된 슬롯들이 순차적으로 모두 호출됩니다.
이때 호출 순서는 보장되지 않지만, UI 이벤트의 경우 일반적으로 등록된 순서대로 동작합니다.
반대로 더 이상 필요하지 않은 슬롯은 disconnect()를 사용해 안전하게 해제할 수 있습니다.

CODE BLOCK
button.clicked.connect(lambda: print("첫 번째 슬롯"))
button.clicked.connect(lambda: print("두 번째 슬롯"))

# 해제도 가능
button.clicked.disconnect()

💡 TIP: 동일한 슬롯을 여러 번 연결하면 중복 호출됩니다.
테스트 중 중복 연결을 방지하려면 연결 전 disconnect()로 초기화하거나, 조건문을 통해 실행 여부를 제어하세요.



🚀 emit 로직과 인자 전달

시그널이 정의되면, 이를 실제로 발생시키는 것은 emit() 메서드입니다.
PySide에서 시그널 객체는 클래스 수준에서 선언되며, 인스턴스 내부에서 self.signal.emit(args) 형태로 호출합니다.
이때 args는 시그널 시그니처에 정의된 타입과 수에 맞게 전달되어야 하며, 그렇지 않으면 런타임 오류가 발생합니다.
emit은 일반적인 함수 호출과 달리, 내부적으로 연결된 모든 슬롯을 순회하며 호출을 전달하는 구조입니다.

예를 들어, 진행 상황을 업데이트하는 progressed 시그널은 정수 값을 전달하도록 정의할 수 있고, 슬롯은 이를 받아 UI에 표시할 수 있습니다.
이 구조는 이벤트를 즉시 반영해야 하는 GUI 프로그래밍에서 매우 효율적입니다.

CODE BLOCK
from PySide6.QtCore import QObject, Signal

class Task(QObject):
    progressed = Signal(int)
    completed = Signal(str)

    def run(self):
        for i in range(0, 101, 20):
            self.progressed.emit(i)
        self.completed.emit("작업 완료!")

def report_progress(value):
    print(f"진행률: {value}%")

def report_done(msg):
    print(msg)

t = Task()
t.progressed.connect(report_progress)
t.completed.connect(report_done)

t.run()

위 코드를 실행하면 0, 20, 40, 60, 80, 100의 값이 순차적으로 progressed 슬롯에 전달되며,
모든 루프가 끝나면 completed 시그널이 “작업 완료!” 메시지와 함께 emit됩니다.
즉, emit은 단일 함수 호출이 아니라 등록된 모든 슬롯으로 이벤트를 브로드캐스트하는 구조입니다.

🧮 여러 인자 전달과 시그니처 주의점

시그널은 여러 인자를 동시에 보낼 수도 있습니다.
이 경우 Signal(int, str, float)처럼 정의하며, 슬롯도 같은 수의 파라미터를 받아야 합니다.
만약 슬롯이 인자보다 적게 받도록 설계된다면, 초과 인자는 단순히 무시됩니다.
하지만 반대로 슬롯이 더 많은 인자를 요구하면 예외가 발생하므로 항상 시그니처를 맞추는 것이 중요합니다.

CODE BLOCK
class MultiSignal(QObject):
    updated = Signal(int, str)

    def trigger(self):
        self.updated.emit(42, "데이터 전송")

def receive_data(num, text):
    print(f"{num} / {text}")

ms = MultiSignal()
ms.updated.connect(receive_data)
ms.trigger()

이처럼 emit의 인자와 슬롯의 파라미터 수가 맞으면 안전하게 이벤트를 주고받을 수 있습니다.
또한 PySide는 내부적으로 C++ Qt와 동일한 시그널 슬롯 매커니즘을 사용하므로,
파이썬 인터페이스에서도 Qt의 스레드 안전성과 이벤트 큐 로직이 그대로 유지됩니다.

💎 핵심 포인트:
emit 호출은 동기 방식이지만, 연결 유형에 따라 슬롯이 실행되는 시점이 달라집니다.
특히 스레드 간 통신에서는 Auto와 Queued의 차이를 이해하는 것이 필수입니다.

⚠️ 주의: emit에 전달되는 인자는 복사되어 슬롯으로 전달됩니다.
대용량 객체나 이미지 데이터를 직접 전달하면 성능이 저하될 수 있으므로, 포인터나 참조 대신 경로 문자열 등을 넘겨 처리하는 것이 좋습니다.

🔗 Auto Direct Queued 연결 유형

PySide의 시그널 슬롯 시스템에서는 실행 방식이 중요한데, 동일한 connect 구문이라도 내부적으로 어떤 스레드에서 호출되는지에 따라 다르게 작동합니다.
이 차이를 제어하는 요소가 바로 연결 유형(Connection Type)입니다.
PySide(Qt for Python)는 세 가지 기본 유형을 제공합니다.
각각의 연결 방식은 실행 타이밍과 스레드 컨텍스트에 큰 차이를 만들기 때문에, 적절한 선택이 프로그램 안정성과 성능을 좌우합니다.

⚙️ AutoConnection (기본)

Qt.AutoConnection은 기본 설정이며, 호출하는 객체와 슬롯이 같은 스레드에 있으면 Direct로, 서로 다른 스레드에 있으면 Queued로 자동 전환됩니다.
즉, 대부분의 GUI 코드에서는 Auto로 설정하는 것만으로 충분하며, Qt가 내부적으로 최적의 방식을 선택합니다.
UI 업데이트가 메인 스레드에서 안전하게 수행되고, 백그라운드 스레드의 연산도 안정적으로 분리됩니다.

⚡ DirectConnection (직접 호출)

Qt.DirectConnection은 emit이 호출된 즉시 슬롯을 실행합니다.
이 방식은 같은 스레드 내에서 매우 빠르게 작동하지만, 다른 스레드의 객체에 직접 접근하면 경쟁 상태나 크래시를 유발할 수 있습니다.
따라서 DirectConnection은 계산 중심의 로직이나 이벤트를 즉시 처리해야 하는 경우에만 제한적으로 사용해야 합니다.

CODE BLOCK
from PySide6.QtCore import Qt

signal.connect(slot, type=Qt.DirectConnection)

위 설정은 emit이 호출된 스레드에서 슬롯이 즉시 실행됨을 의미합니다.
즉, 이벤트 큐를 거치지 않고 바로 함수가 호출됩니다.
이로 인해 스레드 안전성이 보장되지 않으므로, UI 접근이 포함된 경우 반드시 메인 스레드에서만 사용해야 합니다.

🧵 QueuedConnection (큐잉 호출)

Qt.QueuedConnection은 emit을 즉시 실행하지 않고, 수신자의 이벤트 루프로 전달하여 나중에 실행되도록 예약합니다.
즉, 시그널을 보낸 스레드와 슬롯이 속한 스레드가 다를 경우 안전하게 메시지를 전달할 수 있습니다.
GUI 프레임워크의 스레드 안전 규칙을 깨지 않으면서도 병렬 작업을 수행하기에 적합합니다.

CODE BLOCK
signal.connect(slot, type=Qt.QueuedConnection)

Queued 연결은 백그라운드 워커 스레드가 데이터를 처리하고, UI 스레드에서 안전하게 그 결과를 반영하는 구조에 가장 자주 사용됩니다.
Qt는 내부적으로 메시지 큐를 사용하므로, 실제 슬롯 호출은 다음 이벤트 루프 순환 시점에서 수행됩니다.

💎 핵심 포인트:
AutoConnection은 대부분의 상황에서 가장 안전하고 효율적입니다.
하지만 멀티스레드 구조에서 UI를 갱신할 때는 반드시 QueuedConnection을 사용해야 합니다.
Direct는 실시간 제어나 동일 스레드 내 처리에만 제한적으로 활용하세요.

⚠️ 주의: Auto와 Direct, Queued의 차이를 명확히 이해하지 않으면 예상치 못한 스레드 충돌이 발생할 수 있습니다.
특히 emit 시점에서 객체의 소유 스레드가 이미 종료된 경우, 슬롯이 실행되지 않거나 앱이 비정상 종료될 수 있습니다.



🧪 스레드와 큐드 시나리오 예제

PySide에서 GUI를 다루는 애플리케이션이라면 메인 스레드(UI 스레드)는 항상 사용자 인터페이스만 담당해야 합니다.
시간이 오래 걸리는 연산은 별도의 스레드로 보내야 하며, 그 결과를 다시 UI에 반영할 때는 반드시 QueuedConnection을 이용해야 합니다.
이 방식은 PySide가 Qt의 이벤트 루프를 통해 안전하게 메시지를 전달하도록 해줍니다.
즉, UI는 멈추지 않고 워커 스레드는 독립적으로 동작하면서도 데이터를 교환할 수 있습니다.

CODE BLOCK
from PySide6.QtCore import QObject, QThread, Signal, Slot, Qt
from PySide6.QtWidgets import QApplication, QLabel

class Worker(QObject):
    progress = Signal(int)
    done = Signal()

    def run(self):
        for i in range(0, 101, 20):
            self.progress.emit(i)
        self.done.emit()

class Window(QLabel):
    @Slot(int)
    def update_label(self, value):
        self.setText(f"진행률: {value}%")

    @Slot()
    def finished(self):
        self.setText("완료!")

app = QApplication([])
label = Window("시작")
thread = QThread()
worker = Worker()

worker.moveToThread(thread)
worker.progress.connect(label.update_label, type=Qt.QueuedConnection)
worker.done.connect(label.finished, type=Qt.QueuedConnection)

thread.started.connect(worker.run)
thread.start()
label.show()
app.exec()

위 예제는 전형적인 PySide 멀티스레드 패턴입니다.
워커는 독립된 스레드에서 실행되며, UI를 직접 수정하지 않습니다.
대신 progressdone 시그널을 통해 결과를 전달하고, 메인 스레드의 슬롯이 이를 수신하여 UI를 갱신합니다.
이때 QueuedConnection이 없었다면 QLabel이 다른 스레드에서 직접 접근되어 충돌이 발생했을 것입니다.

💬 PySide는 시그널 슬롯 시스템을 통해 스레드 간 안전한 통신을 제공합니다.
QueuedConnection은 비동기 이벤트 루프 기반의 메시지 전달이므로, UI 프리즈 없이 안정적인 업데이트가 가능합니다.

📡 스레드 종료 및 리소스 관리

스레드 작업이 끝나면 반드시 thread.quit()thread.wait()로 종료를 관리해야 합니다.
PySide의 QThread는 이벤트 루프를 가진 독립 실행 단위이므로, 명시적으로 중단하지 않으면 애플리케이션이 종료되지 않을 수 있습니다.
또한 객체 삭제 전에 연결된 시그널을 해제하여 메모리 누수를 방지하는 것이 좋습니다.

  • 🔌UI 업데이트는 항상 메인 스레드의 슬롯에서 처리합니다.
  • 🧩Worker 객체는 moveToThread()로 별도 스레드에 소속시킵니다.
  • ⚙️QueuedConnection을 명시해 스레드 간 안전성을 확보합니다.
  • 🧹스레드 종료 후 quit()wait() 호출로 자원을 해제합니다.

💎 핵심 포인트:
PySide에서 멀티스레드 환경을 다룰 때 가장 중요한 원칙은 UI는 오직 메인 스레드에서만 접근한다는 것입니다.
이를 지키기 위해 시그널 슬롯과 QueuedConnection을 적절히 활용하면 안정적이고 반응성 높은 프로그램을 만들 수 있습니다.

자주 묻는 질문 (FAQ)

시그널을 emit했는데 슬롯이 실행되지 않습니다. 왜 그럴까요?
슬롯 함수가 존재하지 않거나 시그니처가 일치하지 않으면 호출되지 않습니다.
특히 시그널에 인자를 정의했는데 슬롯이 해당 인자를 받지 못하면 연결이 실패합니다.
connect() 이후 로그로 연결 여부를 확인하는 것이 좋습니다.
emit 호출 시 오류 없이 아무 일도 안 일어납니다.
시그널과 슬롯이 서로 다른 객체 소유 스레드에 있을 때, 연결 유형이 잘못 지정되면 실행이 지연되거나 누락될 수 있습니다.
Qt.QueuedConnection을 명시적으로 설정해 이벤트 루프를 통해 안전하게 전달하도록 하세요.
시그널에 여러 슬롯을 연결할 때 순서를 제어할 수 있나요?
Qt는 슬롯 호출 순서를 보장하지 않습니다.
순서가 중요한 경우, 하나의 슬롯에서 여러 작업을 순차적으로 수행하거나 별도의 신호 체인을 구성하는 것이 바람직합니다.
lambda 슬롯을 사용하면 메모리 누수가 생길 수 있나요?
람다 내부에서 외부 변수를 캡처하면 참조 순환이 생길 수 있습니다.
필요할 경우 disconnect()로 명시 해제하거나, 약한 참조(weakref)를 사용하면 안전합니다.
emit에 전달할 수 있는 인자에는 어떤 제약이 있나요?
파이썬 객체 대부분은 가능하지만, Qt의 내부 시리얼라이저가 처리하지 못하는 복잡한 객체는 전달할 수 없습니다.
일반적으로 숫자, 문자열, 리스트, 딕셔너리 등 기본형을 사용하는 것이 안전합니다.
스레드 간 통신 시 DirectConnection을 써도 되나요?
DirectConnection은 emit을 호출한 스레드에서 즉시 슬롯을 실행합니다.
스레드 간에서는 위험하므로 반드시 QueuedConnection을 사용해야 합니다.
Direct는 같은 스레드 내부에서만 권장됩니다.
AutoConnection과 QueuedConnection의 차이는 뭔가요?
AutoConnection은 Qt가 자동으로 판단합니다.
시그널과 슬롯이 같은 스레드에 있으면 Direct처럼 즉시 실행되고, 다른 스레드에 있으면 Queued로 동작합니다.
명시적 제어가 필요한 경우 QueuedConnection을 직접 지정하면 됩니다.
시그널 슬롯 연결을 해제하지 않아도 되나요?
PySide는 QObject의 수명주기를 추적하므로, 객체가 삭제되면 자동으로 연결도 해제됩니다.
하지만 테스트 코드나 임시 객체의 반복 연결 시에는 명시적으로 disconnect()를 호출해주는 것이 안전합니다.

🧭 PySide 시그널 슬롯의 핵심 요약과 실무 팁

PySide(Qt for Python)의 시그널 슬롯 메커니즘은 단순히 이벤트를 연결하는 도구가 아니라, 애플리케이션의 구조적 안정성과 유지보수성을 결정하는 핵심 설계 요소입니다.
객체 간 결합도를 낮추고, UI와 로직을 분리하며, 멀티스레드 환경에서도 안전한 데이터 교환을 가능하게 만듭니다.
시그널은 emit()을 통해 발생하고, 슬롯은 connect()로 연결되어 실행되는 단순한 규칙을 따르지만, 그 내부에서는 Qt의 이벤트 루프와 스레드 시스템이 긴밀히 협력합니다.

실무에서는 기본 AutoConnection만으로도 대부분의 상황을 커버할 수 있으나, 별도의 스레드에서 UI를 갱신하거나 대용량 연산을 수행할 때는 반드시 QueuedConnection을 사용해야 합니다.
DirectConnection은 같은 스레드 내에서만 안전하게 작동하므로, 타 스레드 접근 시에는 피해야 합니다.
또한, 연결 해제나 객체 삭제 시점에서 메모리 관리가 중요하며, 람다를 사용한 간단한 슬롯은 편리하지만 참조 순환에 주의해야 합니다.

PySide는 C++ Qt의 강력한 신호-슬롯 구조를 그대로 계승하고 있습니다.
따라서 이벤트 루프 기반의 비동기 처리, 사용자 인터랙션, 백그라운드 작업 제어 등 다양한 시나리오에서 활용할 수 있습니다.
정확한 연결 패턴과 실행 컨텍스트를 이해한다면, PySide를 이용한 Python GUI 개발은 훨씬 더 직관적이고 견고해집니다.


🏷️ 관련 태그 : PySide, QtforPython, 시그널슬롯, emit, connect, AutoConnection, QueuedConnection, DirectConnection, QThread, 파이썬GUI