시그널과 슬롯
소개
GUI 프로그래밍에서 한 위젯을 변경할 때, 다른 위젯에도 이를 알리고 싶은 경우가 종종 있습니다. 더 일반적으로는 어떤 종류의 객체든 서로 통신할 수 있기를 원합니다. 예를 들어, 사용자가 ' Close ' 버튼을 클릭하면, 해당 창의 ` close()` 함수가 호출되기를 원할 것입니다.
다른 툴킷들은 콜백을 사용하여 이러한 종류의 통신을 구현합니다. 콜백은 함수에 대한 포인터이므로, 처리 함수가 특정 이벤트에 대해 알림을 보내도록 하려면 다른 함수(콜백)에 대한 포인터를 처리 함수에 전달하면 됩니다. 그러면 처리 함수는 적절한 시점에 콜백을 호출합니다. 이 방법을 성공적으로 사용하는 프레임워크도 존재하지만, 콜백은 직관적이지 않을 수 있으며 콜백 인자의 유형 일관성을 보장하는 데 문제가 발생할 수 있습니다.
시그널과 슬롯
Qt에서는 콜백 기법에 대한 대안으로 신호(signal)와 슬롯(slot)을 사용합니다. 신호는 특정 이벤트가 발생할 때 발송됩니다. Qt Widgets에는 미리 정의된 신호가 많이 있지만, Widgets를 서브클래스화하여 사용자 정의 신호를 추가할 수도 있습니다. 슬롯은 특정 신호에 반응하여 호출되는 함수입니다. Qt Widgets에는 미리 정의된 슬롯이 많이 있지만, 관심 있는 신호를 처리할 수 있도록 위젯을 서브클래스화하여 자체 슬롯을 추가하는 것이 일반적인 관행입니다.
신호와 슬롯 메커니즘은 유형 안전성을 보장합니다. 신호의 시그니처는 이를 수신하는 슬롯의 시그니처와 일치해야 합니다. (사실 슬롯은 추가 인수를 무시할 수 있으므로, 수신하는 신호보다 시그니처가 더 짧을 수도 있습니다.) 시그니처가 호환되므로, 함수 포인터 기반 구문을 사용할 때 컴파일러가 유형 불일치를 감지하는 데 도움을 줄 수 있습니다. 문자열 기반의 SIGNAL 및 SLOT 구문은 런타임에 유형 불일치를 감지합니다. 신호와 슬롯은 느슨하게 결합되어 있습니다. 즉, 신호를 발산하는 클래스는 어떤 슬롯이 해당 신호를 수신하는지 알지도 못하고 신경 쓰지도 않습니다. Qt의 시그널 및 슬롯 메커니즘은 시그널을 슬롯에 연결하면, 적절한 시점에 해당 시그널의 매개변수를 사용하여 슬롯이 호출되도록 보장합니다. 시그널과 슬롯은 유형에 상관없이 임의의 개수의 인자를 받을 수 있습니다. 이들은 완전히 유형 안전합니다.
QObject 이나 그 하위 클래스(예: QWidget) 중 하나를 상속하는 모든 클래스는 신호와 슬롯을 포함할 수 있습니다. 신호는 객체의 상태가 다른 객체에게 흥미로울 수 있는 방식으로 변경될 때 해당 객체에 의해 발신됩니다. 이것이 객체가 통신을 위해 수행하는 전부입니다. 객체는 자신이 발신한 신호를 어떤 객체가 수신하는지 알지도, 신경 쓰지도 않습니다. 이것이 진정한 정보 캡슐화이며, 이를 통해 객체를 소프트웨어 구성 요소로 사용할 수 있게 됩니다.
슬롯은 신호를 수신하는 데 사용될 수 있지만, 동시에 일반적인 멤버 함수이기도 합니다. 객체가 자신의 신호를 수신하는 대상이 있는지 알지 못하는 것과 마찬가지로, 슬롯도 자신에게 연결된 신호가 있는지 알지 못합니다. 이를 통해 Qt를 사용하여 진정으로 독립적인 컴포넌트를 생성할 수 있습니다.
단일 슬롯에 원하는 만큼 신호를 연결할 수 있으며, 하나의 신호는 필요한 만큼 여러 슬롯에 연결될 수 있습니다. 심지어 신호를 다른 신호에 직접 연결하는 것도 가능합니다. (이 경우 첫 번째 신호가 발신될 때마다 두 번째 신호가 즉시 발신됩니다.)
신호와 슬롯은 함께 강력한 컴포넌트 프로그래밍 메커니즘을 구성합니다.
신호
신호는 객체의 내부 상태가 변경되어 해당 객체의 클라이언트나 소유자에게 중요한 정보가 될 수 있을 때 객체에 의해 발생됩니다. 신호는 공개 액세스 함수이며 어디서나 발생시킬 수 있지만, 신호를 정의한 클래스와 그 하위 클래스에서만 발생시키는 것을 권장합니다.
신호가 발신되면, 일반적으로 일반적인 함수 호출과 마찬가지로 해당 신호에 연결된 슬롯이 즉시 실행됩니다. 이때 신호와 슬롯 메커니즘은 어떤 GUI 이벤트 루프와도 완전히 독립적으로 작동합니다. ` emit ` 문 뒤에 오는 코드의 실행은 모든 슬롯이 반환된 후에 이루어집니다. queued connections 를 사용할 때는 상황이 약간 다릅니다. 이 경우, emit 키워드 뒤에 오는 코드는 즉시 계속 실행되며, 슬롯은 나중에 실행됩니다.
하나의 신호에 여러 슬롯이 연결된 경우, 신호가 발신되면 슬롯들은 연결된 순서대로 차례로 실행됩니다.
신호는 moc에 의해 자동으로 생성되므로, .cpp 파일에서 구현해서는 안 됩니다.
인수에 대한 참고 사항: 경험상 신호와 슬롯은 특수한 타입을 사용하지 않을 때 재사용성이 더 높습니다. 만약 QScrollBar::valueChanged()이 가상의 QScrollBar::Range와 같은 특수한 타입을 사용한다면, 이는 QScrollBar 를 위해 특별히 설계된 슬롯에만 연결될 수 있을 것입니다. 서로 다른 입력 위젯을 서로 연결하는 것은 불가능해질 것입니다.
슬롯
슬롯은 연결된 신호가 발신될 때 호출됩니다. 슬롯은 일반적인 C++ 함수이며 정상적으로 호출될 수 있습니다. 유일한 특별한 점은 신호를 슬롯에 연결할 수 있다는 것입니다.
슬롯은 일반적인 멤버 함수이므로, 직접 호출될 때는 일반적인 C++ 규칙을 따릅니다. 그러나 슬롯으로서, 슬롯은 액세스 수준에 관계없이 신호-슬롯 연결을 통해 어떤 컴포넌트에서든 호출될 수 있습니다. 즉, 임의의 클래스 인스턴스에서 방출된 신호가 관련이 없는 다른 클래스의 인스턴스에 있는 비공개 슬롯을 호출하게 할 수도 있습니다.
슬롯을 가상(virtual)으로 정의할 수도 있는데, 이는 실무에서 매우 유용한 것으로 나타났습니다.
콜백에 비해 시그널과 슬롯은 제공하는 유연성이 더 크기 때문에 속도가 약간 느리지만, 실제 애플리케이션에서는 그 차이가 미미합니다. 일반적으로, 일부 슬롯에 연결된 시그널을 발신하는 것은 비가상 함수 호출을 통해 수신자를 직접 호출하는 것보다 약 10배 정도 느립니다. 이는 연결 객체를 찾고, 모든 연결을 안전하게 반복 처리하며(즉, 신호 발신 중에 후속 수신자가 소멸되지 않았는지 확인), 모든 매개변수를 일반적인 방식으로 마샬링하는 데 필요한 오버헤드입니다. 10회의 비가상 함수 호출이 많아 보일 수 있지만, 예를 들어 ` new `이나 ` delete ` 연산에 비해 오버헤드는 훨씬 적습니다. 내부적으로 ` new `이나 ` delete`을 필요로 하는 문자열, 벡터 또는 리스트 연산을 수행하는 즉시, 신호와 슬롯의 오버헤드는 전체 함수 호출 비용에서 극히 작은 부분만을 차지합니다. 슬롯 내에서 시스템 호출을 수행하거나, 간접적으로 10개 이상의 함수를 호출할 때도 마찬가지입니다. 시그널과 슬롯 메커니즘이 제공하는 단순성과 유연성은 이 오버헤드를 감수할 만한 가치가 충분하며, 사용자들은 이를 전혀 눈치채지 못할 것입니다.
signals 또는 slots 라는 변수를 정의하는 다른 라이브러리는 Qt 기반 애플리케이션과 함께 컴파일할 때 컴파일러 경고나 오류를 유발할 수 있습니다. 이 문제를 해결하려면 #undef 를 사용하여 문제가 되는 전처리기 심볼을 비활성화하십시오.
간단한 예제
최소한의 C++ 클래스 선언은 다음과 같을 수 있습니다:
class Counter
{
public:
Counter() { m_value = 0; }
int value() const { return m_value; }
void setValue(int value);
private:
int m_value;
};QObject 를 기반으로 한 간단한 클래스는 다음과 같이 작성될 수 있습니다:
#include <QObject>
class Counter : public QObject
{
Q_OBJECT
// Note. The Q_OBJECT macro starts a private section.
// To declare public members, use the 'public:' access modifier.
public:
Counter() { m_value = 0; }
int value() const { return m_value; }
public slots:
void setValue(int value);
signals:
void valueChanged(int newValue);
private:
int m_value;
};QObject 를 기반으로 한 버전은 동일한 내부 상태를 가지며, 해당 상태에 접근할 수 있는 공개 메서드를 제공하지만, 그 외에도 시그널과 슬롯을 이용한 컴포넌트 프로그래밍을 지원합니다. 이 클래스는 시그널( valueChanged())을 발신하여 외부에 상태가 변경되었음을 알릴 수 있으며, 다른 객체가 시그널을 보낼 수 있는 슬롯을 가지고 있습니다.
신호나 슬롯을 포함하는 모든 클래스는 선언의 맨 위에 ` Q_OBJECT `을 명시해야 합니다. 또한 ` QObject`을 (직접 또는 간접적으로) 상속받아야 합니다.
슬롯은 애플리케이션 프로그래머가 구현합니다. 다음은 Counter::setValue() 슬롯의 가능한 구현 예시입니다:
void Counter::setValue(int value)
{
if (value != m_value) {
m_value = value;
emit valueChanged(value);
}
}emit 줄은 새로운 값을 인자로 사용하여 객체에서 valueChanged() 신호를 발생시킵니다.
다음 코드 예제에서는 두 개의 ` Counter ` 객체를 생성하고, ` QObject::connect()`를 사용하여 첫 번째 객체의 ` valueChanged() ` 신호를 두 번째 객체의 ` setValue() ` 슬롯에 연결합니다:
Counter a, b;
QObject::connect(&a, &Counter::valueChanged,
&b, &Counter::setValue);
a.setValue(12); // a.value() == 12, b.value() == 12
b.setValue(48); // a.value() == 12, b.value() == 48a.setValue(12) 를 호출하면 a 가 valueChanged(12) 신호를 방출하고, b 는 setValue() 슬롯에서 이 신호를 수신합니다. 즉, b.setValue(12) 가 호출됩니다. 그런 다음 b 도 동일한 valueChanged() 신호를 방출하지만, b 의 valueChanged() 신호에 연결된 슬롯이 없기 때문에 이 신호는 무시됩니다.
setValue() 함수는 value != m_value 인 경우에만 값을 설정하고 신호를 발생시킨다는 점에 유의하십시오. 이는 순환 연결(예: b.valueChanged() 가 a.setValue() 에 연결된 경우)에서 무한 루프가 발생하는 것을 방지합니다.
기본적으로 연결을 하나 만들 때마다 신호가 하나 발생하며, 중복된 연결의 경우 두 개의 신호가 발생합니다. disconnect()를 한 번 호출하는 것만으로 이러한 모든 연결을 끊을 수 있습니다. Qt::UniqueConnection 로 type 을 전달하면, 중복이 아닌 경우에만 연결이 이루어집니다. 이미 중복(동일한 객체의 정확히 동일한 슬롯에 대한 정확히 동일한 신호)이 존재하는 경우, 연결은 실패하고 connect는 false 를 반환합니다.
이 예제는 객체들이 서로에 대한 정보를 알 필요 없이 함께 작동할 수 있음을 보여줍니다. 이를 가능하게 하려면 객체들을 서로 연결하기만 하면 되며, 이는 간단한 ` QObject::connect()` 함수 호출이나 uic의 자동 연결 기능을 통해 달성할 수 있습니다.
실제 예제
다음은 멤버 함수가 없는 간단한 위젯 클래스의 헤더 예제입니다. 이 예제는 사용자 자신의 애플리케이션에서 신호와 슬롯을 어떻게 활용할 수 있는지 보여주기 위한 것입니다.
#ifndef LCDNUMBER_H
#define LCDNUMBER_H
#include <QFrame>
class LcdNumber : public QFrame
{
Q_OBJECTLcdNumber 이 클래스는 QFrame 및 QWidget 를 통해 신호-슬롯 관련 지식의 대부분을 포함하고 있는 ` QObject`을 상속받습니다. 이는 내장된 ` QLCDNumber ` 위젯과 다소 유사합니다.
Q_OBJECT 매크로는 전처리기에서 확장되어 moc 에 의해 구현되는 여러 멤버 함수를 선언합니다. 만약 “undefined reference to vtable for LcdNumber ”와 같은 컴파일러 오류가 발생한다면, 아마도 moc을 실행하는 것을 잊었거나 링크 명령어에 moc 출력을 포함시키는 것을 빠뜨렸을 가능성이 높습니다.
public:
LcdNumber(QWidget *parent = nullptr);
signals:
void overflow();클래스 생성자 및 ` public ` 멤버 뒤에, ` signals` 클래스를 선언합니다. ` LcdNumber ` 클래스는 표시할 수 없는 값을 요청받으면 ` overflow()` 신호를 발생시킵니다.
오버플로우를 신경 쓰지 않거나 오버플로우가 발생할 수 없다는 것을 알고 있다면, ` overflow() ` 신호를 무시할 수 있습니다. 즉, 이 신호를 어떤 슬롯에도 연결하지 않으면 됩니다.
반대로 숫자가 오버플로우될 때 두 개의 서로 다른 오류 함수를 호출하고 싶다면, 해당 신호를 두 개의 서로 다른 슬롯에 연결하기만 하면 됩니다. Qt는 두 함수를 모두 (연결된 순서대로) 호출합니다.
public slots:
void display(int num);
void display(double num);
void display(const QString &str);
void setHexMode();
void setDecMode();
void setOctMode();
void setBinMode();
void setSmallDecimalPoint(bool point);
};
#endif슬롯(slot)은 다른 위젯의 상태 변경에 대한 정보를 얻기 위해 사용되는 수신 함수입니다. 위 코드에서 볼 수 있듯이, ` LcdNumber `는 표시될 숫자를 설정하기 위해 슬롯을 사용합니다. ` display() `는 클래스와 프로그램의 나머지 부분 간의 인터페이스의 일부이므로, 이 슬롯은 public으로 선언됩니다.
몇 가지 예제 프로그램에서는 QScrollBar 의 valueChanged() 신호를 display() 슬롯에 연결하여, LCD 화면에 스크롤바의 값이 지속적으로 표시되도록 합니다.
display()가 오버로드되어 있다는 점에 유의하십시오. Qt의 신호와 슬롯은 강력한 타입 지정 연결을 제공합니다. 이는 컴파일 시점에 기존 콜백보다 더 안전합니다. 콜백을 사용할 경우 서로 다른 함수 이름을 사용하고 수동으로 타입을 추적해야 합니다. 하지만 오버로드된 함수를 사용할 때는 어떤 버전을 사용할지 명시해야 합니다. 다음 섹션에서는 그 방법을 보여줍니다.
오버로드된 시그널 및 슬롯에 연결하기
신호나 슬롯이 오버로드된 경우(매개변수가 다른 여러 버전이 있는 경우), 함수 포인터 구문을 사용하여 연결할 버전을 명시적으로 지정해야 합니다. qOverload() 또는 static_cast 를 사용하여 모호함을 해소할 수 있습니다:
// Connect to the QString overload of QCompleter::activated(const QString &)
connect(completer, qOverload<const QString &>(&QCompleter::activated),
this, &MyClass::onActivatedText);
// Or select QLCDNumber::display(int) when connecting from QSlider::valueChanged(int)
connect(slider, &QSlider::valueChanged,
lcd, qOverload<int>(&QLCDNumber::display));
// Using static_cast (more verbose):
connect(completer, static_cast<void(QCompleter::*)(const QString &)>(&QCompleter::activated),
this, &MyClass::onActivatedText);
// Or using a lambda to call the correct overload:
connect(slider, &QSlider::valueChanged,
this, [lcd](int value) { lcd->display(value); });자동 연결 관리
Qt XML은 ` QObject`에서 파생된 유형 간의 연결 수명을 자동으로 관리합니다. 송신자 또는 수신자 객체 중 하나가 소멸되면 연결이 자동으로 제거되어, 삭제된 객체에 대한 호출이 방지됩니다. 이는 함수 포인터 구문과 문자열 기반 SIGNAL/SLOT 구문 모두에 적용됩니다.
람다 연결의 경우, 컨텍스트 객체(일반적으로 ` this`)를 제공하여 컨텍스트가 소멸될 때 람다의 연결이 해제되도록 해야 합니다:
connect(button, &QPushButton::clicked, this, [this]{ handleClick(); });Qt는 완전히 소멸된 객체에 신호가 전달되는 것을 방지하지만, 파생 클래스의 소멸자가 완료된 후이지만 ~QObject 에 도달하기 전인 객체 소멸 중에 신호가 전달될 수 있습니다. 이 제한 사항은 특히 함수 포인터 구문을 사용하여 생성된 연결에 적용됩니다. 이는 기저 클래스의 소멸자가 이미 소멸된 파생 클래스에 연결된 신호를 발산할 때 발생할 수 있습니다. 이러한 상황이 해당 클래스에 문제를 일으킬 수 있다면, 소멸자 내에서 해당 신호를 명시적으로 해제하는 것을 고려하십시오.
기본 인자를 사용하는 신호 및 슬롯
신호와 슬롯의 시그니처에는 인자가 포함될 수 있으며, 인자에는 기본값을 지정할 수 있습니다. ` QObject::destroyed()`를 예로 들어 보겠습니다:
void destroyed(QObject* = nullptr);QObject 가 삭제되면 이 QObject::destroyed() 신호를 방출합니다. 삭제된 QObject 에 대한 매달린 참조가 있는 곳이라면 어디에서든 이 신호를 포착하여 정리할 수 있도록 하고자 합니다. 이에 적합한 슬롯 시그니처는 다음과 같을 수 있습니다:
void objectDestroyed(QObject* obj = nullptr);신호를 슬롯에 연결하려면 ` QObject::connect()`를 사용합니다. 신호와 슬롯을 연결하는 방법에는 여러 가지가 있습니다. 첫 번째 방법은 함수 포인터를 사용하는 것입니다:
connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed);함수 포인터와 함께 QObject::connect()을 사용하면 몇 가지 장점이 있습니다. 첫째, 컴파일러가 신호의 인자와 슬롯의 인자가 호환되는지 확인할 수 있습니다. 또한 필요한 경우 컴파일러가 인자를 암시적으로 변환할 수도 있습니다.
펑크터나 C++11 람다를 사용하여 연결할 수도 있습니다:
connect(sender, &QObject::destroyed, this, [=](){ this->m_objects.remove(sender); });이 두 경우 모두 connect() 호출 시 this 를 컨텍스트로 제공합니다. 컨텍스트 객체는 리시버가 어떤 스레드에서 실행되어야 하는지에 대한 정보를 제공합니다. 컨텍스트를 제공하면 리시버가 컨텍스트 스레드에서 실행되도록 보장하므로 이는 중요합니다.
송신자나 컨텍스트가 소멸되면 람다는 연결이 해제됩니다. 신호가 발송될 때 펑크터 내부에서 사용되는 모든 객체가 여전히 유효한 상태인지 주의해야 합니다.
신호를 슬롯에 연결하는 또 다른 방법은 ` QObject::connect()`와 ` SIGNAL `, ` SLOT ` 매크로를 사용하는 것입니다. 인자에 기본값이 있는 경우 ` SIGNAL() ` 및 ` SLOT() ` 매크로에 인자를 포함할지 여부에 대한 규칙은, ` SIGNAL() ` 매크로에 전달되는 시그니처의 인자 수가 ` SLOT() ` 매크로에 전달되는 시그니처의 인자 수보다 적어서는 안 된다는 것입니다.
다음과 같은 모든 코드가 정상적으로 작동합니다:
connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed(Qbject*)));
connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed()));
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed()));하지만 다음 코드는 작동하지 않습니다:
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed(QObject*)));...슬롯은 신호가 전송하지 않는 ` QObject `를 기대하기 때문입니다. 이 연결은 런타임 오류를 발생시킬 것입니다.
이 ` QObject::connect()` 오버로드를 사용할 때, 컴파일러는 신호와 슬롯의 인수를 검사하지 않는다는 점에 유의하십시오.
신호 및 슬롯의 고급 사용법
신호의 발신자에 대한 정보가 필요한 경우를 위해, Qt는 신호를 보낸 객체에 대한 포인터를 반환하는 ` QObject::sender()` 함수를 제공합니다.
람다 표현식은 슬롯에 사용자 정의 인자를 전달하는 편리한 방법입니다:
connect(action, &QAction::triggered, engine,
[=]() { engine->processAction(action->text()); });타사 신호 및 슬롯과 함께 Qt 사용하기
Qt를 타사 신호/슬롯 메커니즘과 함께 사용할 수 있습니다. 심지어 동일한 프로젝트에서 두 메커니즘을 모두 사용할 수도 있습니다. 이를 위해서는 CMake 프로젝트 파일에 다음 내용을 작성하십시오:
target_compile_definitions(my_app PRIVATE QT_NO_KEYWORDS)qmake 프로젝트(.pro) 파일에는 다음 내용을 작성해야 합니다:
CONFIG += no_keywords이는 Qt가 moc 키워드인 signals, slots, emit 를 정의하지 않도록 지시하는 것으로, 이러한 이름들은 Boost와 같은 타사 라이브러리에서 사용될 예정이기 때문입니다. 그런 다음 ` no_keywords ` 플래그를 사용하여 Qt 신호와 슬롯을 계속 사용하려면, 소스 코드에서 Qt moc 키워드가 사용된 모든 부분을 해당 Qt 매크로인 Q_SIGNALS (또는 Q_SIGNAL), Q_SLOTS (또는 Q_SLOT), Q_EMIT 로 간단히 대체하면 됩니다.
Qt 기반 라이브러리의 시그널과 슬롯
Qt 기반 라이브러리의 공개 API는 signals 및 slots 대신 Q_SIGNALS 및 Q_SLOTS 키워드를 사용해야 합니다. 그렇지 않으면 QT_NO_KEYWORDS 를 정의한 프로젝트에서 해당 라이브러리를 사용하기 어렵습니다.
이 제한 사항을 적용하기 위해, 라이브러리 제작자는 라이브러리를 빌드할 때 QT_NO_SIGNALS_SLOTS_KEYWORDS 이라는 전처리기 정의(define)를 설정할 수 있습니다.
이 정의는 라이브러리 구현에서 다른 Qt 전용 키워드의 사용 여부에 영향을 주지 않으면서 신호와 슬롯을 제외합니다.
QLCDNumber, QObject::connect(), 메타 객체 시스템 및 Qt의 속성 시스템도 참조하십시오 .
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.