본문으로 건너뛰기
Previous
Next
C++ GUI | Qt 프레임워크: 위젯부터 MOC·시그널/슬롯·이벤트 루프·Graphics View까지

C++ GUI | Qt 프레임워크: 위젯부터 MOC·시그널/슬롯·이벤트 루프·Graphics View까지

C++ GUI | Qt 프레임워크: 위젯부터 MOC·시그널/슬롯·이벤트 루프·Graphics View까지

이 글의 핵심

Qt Widgets 입문 예제에 더해, 메타 객체 시스템(MOC), 시그널/슬롯·이벤트 루프의 내부 동작, Graphics View 아키텍처, 프로덕션에서 통용되는 소유권·스레딩·테스트 패턴까지 한 번에 정리합니다.

첫 번째 Qt 애플리케이션

#include <QApplication>
#include <QPushButton>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    
    QPushButton button("Hello Qt!");
    button.resize(200, 100);
    button.show();
    
    return app.exec();
}

빌드:

터미널에서 다음 명령어를 실행합니다.

# qmake 사용
qmake -project
qmake
make

# CMake 사용
find_package(Qt6 REQUIRED COMPONENTS Widgets)
target_link_libraries(myapp Qt6::Widgets)

시그널과 슬롯

#include <QApplication>
#include <QPushButton>
#include <QMessageBox>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    
    QPushButton button("클릭하세요");
    
    // 시그널-슬롯 연결
    QObject::connect(&button, &QPushButton::clicked,  {
        QMessageBox::information(nullptr, "알림", "버튼이 클릭되었습니다!");
    });
    
    button.show();
    
    return app.exec();
}

레이아웃

수직 레이아웃

#include <QApplication>
#include <QWidget>
#include <QPushButton>
#include <QVBoxLayout>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    
    QWidget window;
    QVBoxLayout *layout = new QVBoxLayout(&window);
    
    layout->addWidget(new QPushButton("버튼 1"));
    layout->addWidget(new QPushButton("버튼 2"));
    layout->addWidget(new QPushButton("버튼 3"));
    
    window.show();
    
    return app.exec();
}

수평 레이아웃

QHBoxLayout *layout = new QHBoxLayout(&window);
layout->addWidget(new QPushButton("왼쪽"));
layout->addWidget(new QPushButton("중간"));
layout->addWidget(new QPushButton("오른쪽"));

그리드 레이아웃

C/C++ 예제 코드입니다.

QGridLayout *layout = new QGridLayout(&window);
layout->addWidget(new QPushButton("1"), 0, 0);
layout->addWidget(new QPushButton("2"), 0, 1);
layout->addWidget(new QPushButton("3"), 1, 0);
layout->addWidget(new QPushButton("4"), 1, 1);

커스텀 위젯

#include <QWidget>
#include <QPushButton>
#include <QLabel>
#include <QVBoxLayout>

class CounterWidget : public QWidget {
    Q_OBJECT
    
private:
    int count;
    QLabel *label;
    QPushButton *button;
    
public:
    CounterWidget(QWidget *parent = nullptr) : QWidget(parent), count(0) {
        label = new QLabel("카운트: 0", this);
        button = new QPushButton("증가", this);
        
        QVBoxLayout *layout = new QVBoxLayout(this);
        layout->addWidget(label);
        layout->addWidget(button);
        
        connect(button, &QPushButton::clicked, this, &CounterWidget::increment);
    }
    
private slots:
    void increment() {
        count++;
        label->setText(QString("카운트: %1").arg(count));
    }
};

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    
    CounterWidget widget;
    widget.show();
    
    return app.exec();
}

메뉴와 툴바

#include <QMainWindow>
#include <QMenuBar>
#include <QToolBar>
#include <QAction>
#include <QMessageBox>

class MainWindow : public QMainWindow {
    Q_OBJECT
    
public:
    MainWindow() {
        // 메뉴 생성
        QMenu *fileMenu = menuBar()->addMenu("파일");
        QMenu *editMenu = menuBar()->addMenu("편집");
        
        // 액션 생성
        QAction *newAction = new QAction("새 파일", this);
        QAction *openAction = new QAction("열기", this);
        QAction *saveAction = new QAction("저장", this);
        
        // 메뉴에 액션 추가
        fileMenu->addAction(newAction);
        fileMenu->addAction(openAction);
        fileMenu->addSeparator();
        fileMenu->addAction(saveAction);
        
        // 툴바 생성
        QToolBar *toolbar = addToolBar("메인 툴바");
        toolbar->addAction(newAction);
        toolbar->addAction(openAction);
        toolbar->addAction(saveAction);
        
        // 시그널 연결
        connect(newAction, &QAction::triggered, this, &MainWindow::newFile);
        connect(openAction, &QAction::triggered, this, &MainWindow::openFile);
    }
    
private slots:
    void newFile() {
        QMessageBox::information(this, "알림", "새 파일 생성");
    }
    
    void openFile() {
        QMessageBox::information(this, "알림", "파일 열기");
    }
};

실전 예시

예시 1: 간단한 텍스트 에디터

#include <QMainWindow>
#include <QTextEdit>
#include <QMenuBar>
#include <QFileDialog>
#include <QFile>
#include <QTextStream>

class TextEditor : public QMainWindow {
    Q_OBJECT
    
private:
    QTextEdit *textEdit;
    
public:
    TextEditor() {
        textEdit = new QTextEdit(this);
        setCentralWidget(textEdit);
        
        createMenus();
        
        setWindowTitle("간단한 텍스트 에디터");
        resize(800, 600);
    }
    
private:
    void createMenus() {
        QMenu *fileMenu = menuBar()->addMenu("파일");
        
        QAction *openAction = fileMenu->addAction("열기");
        connect(openAction, &QAction::triggered, this, &TextEditor::openFile);
        
        QAction *saveAction = fileMenu->addAction("저장");
        connect(saveAction, &QAction::triggered, this, &TextEditor::saveFile);
        
        fileMenu->addSeparator();
        
        QAction *exitAction = fileMenu->addAction("종료");
        connect(exitAction, &QAction::triggered, this, &QWidget::close);
    }
    
private slots:
    void openFile() {
        QString filename = QFileDialog::getOpenFileName(this, "파일 열기");
        
        if (!filename.isEmpty()) {
            QFile file(filename);
            if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
                QTextStream in(&file);
                textEdit->setText(in.readAll());
                file.close();
            }
        }
    }
    
    void saveFile() {
        QString filename = QFileDialog::getSaveFileName(this, "파일 저장");
        
        if (!filename.isEmpty()) {
            QFile file(filename);
            if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
                QTextStream out(&file);
                out << textEdit->toPlainText();
                file.close();
            }
        }
    }
};

예시 2: 계산기

#include <QWidget>
#include <QLineEdit>
#include <QPushButton>
#include <QGridLayout>

class Calculator : public QWidget {
    Q_OBJECT
    
private:
    QLineEdit *display;
    double currentValue;
    char currentOp;
    
public:
    Calculator() : currentValue(0), currentOp('\0') {
        display = new QLineEdit("0", this);
        display->setReadOnly(true);
        display->setAlignment(Qt::AlignRight);
        
        QGridLayout *layout = new QGridLayout(this);
        layout->addWidget(display, 0, 0, 1, 4);
        
        // 숫자 버튼
        for (int i = 0; i < 10; i++) {
            QPushButton *btn = new QPushButton(QString::number(i), this);
            connect(btn, &QPushButton::clicked, this, &Calculator::digitClicked);
            int row = (9 - i) / 3 + 1;
            int col = (i - 1) % 3;
            if (i == 0) {
                layout->addWidget(btn, 4, 1);
            } else {
                layout->addWidget(btn, row, col);
            }
        }
        
        // 연산자 버튼
        QPushButton *addBtn = new QPushButton("+", this);
        connect(addBtn, &QPushButton::clicked, this, &Calculator::operatorClicked);
        layout->addWidget(addBtn, 1, 3);
        
        QPushButton *equalBtn = new QPushButton("=", this);
        connect(equalBtn, &QPushButton::clicked, this, &Calculator::equalClicked);
        layout->addWidget(equalBtn, 4, 3);
        
        resize(300, 400);
    }
    
private slots:
    void digitClicked() {
        QPushButton *btn = qobject_cast<QPushButton*>(sender());
        QString digit = btn->text();
        
        if (display->text() == "0") {
            display->setText(digit);
        } else {
            display->setText(display->text() + digit);
        }
    }
    
    void operatorClicked() {
        QPushButton *btn = qobject_cast<QPushButton*>(sender());
        currentOp = btn->text().at(0).toLatin1();
        currentValue = display->text().toDouble();
        display->setText("0");
    }
    
    void equalClicked() {
        double secondValue = display->text().toDouble();
        double result = 0;
        
        switch (currentOp) {
            case '+': result = currentValue + secondValue; break;
            case '-': result = currentValue - secondValue; break;
            case '*': result = currentValue * secondValue; break;
            case '/': result = currentValue / secondValue; break;
        }
        
        display->setText(QString::number(result));
        currentOp = '\0';
    }
};

예시 3: 이미지 뷰어

#include <QMainWindow>
#include <QLabel>
#include <QMenuBar>
#include <QFileDialog>
#include <QPixmap>
#include <QScrollArea>

class ImageViewer : public QMainWindow {
    Q_OBJECT
    
private:
    QLabel *imageLabel;
    QScrollArea *scrollArea;
    
public:
    ImageViewer() {
        imageLabel = new QLabel;
        imageLabel->setScaledContents(true);
        
        scrollArea = new QScrollArea;
        scrollArea->setWidget(imageLabel);
        setCentralWidget(scrollArea);
        
        createMenus();
        
        setWindowTitle("이미지 뷰어");
        resize(800, 600);
    }
    
private:
    void createMenus() {
        QMenu *fileMenu = menuBar()->addMenu("파일");
        
        QAction *openAction = fileMenu->addAction("열기");
        connect(openAction, &QAction::triggered, this, &ImageViewer::openImage);
    }
    
private slots:
    void openImage() {
        QString filename = QFileDialog::getOpenFileName(
            this, "이미지 열기", "", "Images (*.png *.jpg *.bmp)");
        
        if (!filename.isEmpty()) {
            QPixmap image(filename);
            imageLabel->setPixmap(image);
            imageLabel->resize(image.size());
        }
    }
};

메타 객체 시스템과 MOC

Qt의 핵심은 C++ 언어 사양에 없는 리플렉션(reflection)에 가까운 기능을, 별도의 전처리기인 moc(meta-object compiler) 로 보완한다는 점입니다. QObject를 상속한 클래스에 Q_OBJECT 매크로를 넣으면, moc는 해당 클래스에 대해 메타 객체 코드를 생성합니다. 이 코드는 시그널 인덱스, 슬롯 시그니처 문자열, 프로퍼티 메타데이터 등을 담은 정적 구조체와, 런타임에 이를 조회하는 진입점을 제공합니다.

빌드 관점에서는 일반적인 C++ 컴파일 파이프라인 앞단에 “.h를 읽고 moc_*.cpp를 추가”하는 단계가 끼어듭니다. CMake에서는 set(CMAKE_AUTOMOC ON) 또는 qt_add_executable(...) 계열 타깃 속성으로 자동화하는 것이 표준입니다. qmake 사용자는 .pro에 해당 헤더가 포함되면 자동으로 moc가 호출됩니다.

설계상 제약도 이해해야 합니다. moc는 클래스 정의를 정적으로 분석하므로, Q_OBJECT가 들어간 클래스에 템플릿 매개변수만 다른 여러 정의를 두거나, 다중 상속에서 QObject가 두 번 이상 나타나는 형태는 지원하지 않습니다. 또한 시그널/슬롯 선언은 클래스 본문에 직접 두는 것이 안전합니다. 이런 제약은 “C++만으로는 부족한 부분을 코드 생성으로 메우는” 트레이드오프의 결과입니다.

런타임에는 QMetaObject를 통해 클래스 이름, 열거형, 동적 프로퍼티, Q_INVOKABLE 메서드 등을 조회할 수 있고, 이는 스크립트 바인딩, 직렬화, 원격 프로시저 호출 같은 상위 레이어의 토대가 됩니다. 예를 들어 QMetaObject::invokeMethod는 문자열 이름으로 슬롯을 호출할 수 있어, UI XML 로딩이나 플러그인 경계에서 유용합니다.

// CMake: target에 Qt6::Core 연결 후 AUTOMOC ON 가정
#include <QObject>
#include <QDebug>

class Worker : public QObject {
    Q_OBJECT
public:
    explicit Worker(QObject *parent = nullptr) : QObject(parent) {}

public slots:
    void onTask(int id) { qDebug() << "task" << id; }
};

// 문자열 기반 호출(런타임 바인딩). 프로덕션에서는 타입 안전한 connect를 우선한다.
void invokeByName(Worker *w) {
    QMetaObject::invokeMethod(w, "onTask", Qt::QueuedConnection, Q_ARG(int, 42));
}

위 예에서 invokeMethod는 메타 객체에 등록된 슬롯 이름을 해석합니다. 큐 연결(Qt::QueuedConnection)을 쓰면 호출이 이벤트 큐를 통해 비동기로 전달되므로, 다른 스레드에 속한 객체로 안전하게 작업을 넘길 때 자주 씁니다. 다만 문자열 기반 호출은 오타나 시그니처 불일치를 런타임까지 늦출 수 있으므로, 가능하면 함수 포인터 기반 connect를 기본으로 하고, 플러그인·리스너 같은 경계에서만 invokeMethod를 쓰는 편이 안전합니다.

시그널과 슬롯의 내부 동작

Qt 5 이후 권장 방식은 함수 포인터 오버로드를 사용하는 connect(sender, &Sender::signal, receiver, &Receiver::slot) 형태입니다. 이 방식은 컴파일 타임에 시그널·슬롯 시그니처가 맞는지 검사할 수 있어, 과거의 SIGNAL/SLOT 매크로보다 유지보수성이 뛰어납니다.

연결의 저장 구조를 개략적으로 말하면, 각 QObject는 수신 연결 목록을 유지하고, 시그널이 발생하면 해당 시그널 인덱스에 매달린 연결들을 순회하며 슬롯을 호출합니다. 내부적으로는 QObjectPrivate와 연결 테이블이 깊게 얽혀 있으며, 시그널 하나에 다수의 슬롯이 연결되는 멀티캐스트가 자연스럽게 지원됩니다.

연결 타입이 핵심입니다.

  • Qt::DirectConnection: 시그널을 emit한 스레드에서 즉시 슬롯을 실행합니다. 같은 스레드 안에서의 동기 호출에 가깝습니다.
  • Qt::QueuedConnection: QEvent로 감싼 슬롯 호출을 수신자 객체가 속한 스레드의 이벤트 루프에 넣습니다. 워커 스레드에서 UI 객체를 직접 건드리지 않고 안전하게 갱신할 때 표준 패턴입니다.
  • Qt::BlockingQueuedConnection: 큐에 넣되 발신 스레드는 슬롯이 끝날 때까지 블로킹합니다. 데드락 위험이 있으므로 같은 스레드끼리는 절대 사용하지 말아야 합니다.

람다를 연결할 때는 컨텍스트 객체를 세 번째 인자로 넘기면, 해당 객체가 파괴될 때 연결이 자동 해제되어 댕글링 참조를 줄일 수 있습니다.

#include <QObject>
#include <QTimer>

class Counter : public QObject {
    Q_OBJECT
public:
    explicit Counter(QObject *parent = nullptr) : QObject(parent) {
        auto *t = new QTimer(this);
        t->setInterval(100);
        connect(t, &QTimer::timeout, this, [this]() { ++m_value; emit valueChanged(m_value); });
        t->start();
    }
signals:
    void valueChanged(int v);
private:
    int m_value = 0;
};

시그널 인자는 복사·이동 가능한 타입이어야 하며, 큐 연결 시 Q_DECLARE_METATYPE 등록이 필요한 경우가 있습니다. 큰 데이터를 매 틱마다 시그널로 밀어 넣기보다는, 공유 버퍼 + 변경 플래그, 또는 std::shared_ptr로 불변 스냅샷을 넘기는 식으로 설계를 조정하는 것이 좋습니다.

이벤트 루프와 QCoreApplication

GUI가 없는 서비스나 테스트 코드에서는 QCoreApplication만으로도 이벤트 루프를 돌릴 수 있습니다. QApplicationQGuiApplication을 거쳐 위젯·스타일·데스크톱 통합까지 포함합니다. 즉, 반드시 GUI가 필요한 것은 아니며, 네트워크·타이머·소켓 notifier만 쓰는 백그라운드 프로세스도 QCoreApplication 기반이 흔합니다.

exec()는 사실상 “큐에서 QEvent를 꺼내 QObject::event로 디스패치하는 루프”입니다. 윈도우 시스템에서 온 입력, 타이머, 소켓 준비 완료, 그리고 큐 연결로 예약된 슬롯 호출 모두 이 큐를 통해 처리됩니다.

중첩 이벤트 루프QEventLoop 로컬 객체로 만들 수 있습니다. 다이얼로그의 exec() 같은 것이 대표적입니다. 다만 processEvents()를 남용하면 재진입으로 인해 상태가 꼬이기 쉬우므로, 모달 진행률 창 등 명확한 용도가 아니면 지양하는 것이 좋습니다.

#include <QCoreApplication>
#include <QTimer>
#include <iostream>

int main(int argc, char *argv[]) {
    QCoreApplication app(argc, argv);
    QTimer::singleShot(0, &app, []() { std::cout << "이벤트 루프가 돈 뒤 한 번 실행\n"; });
    return app.exec(); // 이벤트 루프 진입; 종료 시까지 블로킹
}

QCoreApplication::quit() 또는 마지막 윈도우가 닫힐 때의 기본 동작 등으로 루프가 빠져나오면 exec()가 반환값을 돌려줍니다. 장시간 실행되는 작업은 이벤트 루프 스레드를 막지 말고, QThread+큐 연결 또는 QtConcurrent 등으로 오프로딩하는 것이 반응성 유지의 기본 원칙입니다.

Graphics View 프레임워크 아키텍처

QGraphicsView·QGraphicsScene·QGraphicsItem으로 구성되는 Graphics View는 대량의 2D 객체를 효율적으로 그리기 위한 장면 그래프(scene graph)입니다. 단순 QPainter로 위젯에 직접 그리는 방식과 달리, 항목 단위 좌표계·선택·충돌 검사·변환이 프레임워크에 논리적으로 분리되어 있습니다.

  • QGraphicsScene: 논리적 장면. 여러 QGraphicsItem을 보유하며, BSP 트리 등으로 가시 영역에 대한 항목 쿼리를 가속할 수 있습니다.
  • QGraphicsView: 뷰포트에 장면을 투영합니다. 여러 뷰가 하나의 장면을 공유할 수 있어, 동일 데이터에 대한 확대·분할 화면이 단순해집니다.
  • QGraphicsItem: 로컬 좌표계를 가지며, 부모-자식 변환으로 계층적 배치가 가능합니다.

좌표는 아이템 로컬 → 장면(scene) → 뷰(view) 순으로 변환되며, 줌·회전은 QGraphicsView의 변환 행렬로 처리하는 것이 일반적입니다. 대화형 편집기, 회로도, 간단한 CAD, 보드게임 맵 등 수천 개 이상의 객체가 화면에 존재할 때 위젯 하나에 전부 그리는 것보다 유지보수와 성능 면에서 유리한 경우가 많습니다.

#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    QGraphicsScene scene;
    scene.setSceneRect(0, 0, 400, 300);
    auto *item = scene.addRect(10, 10, 100, 60);
    item->setFlag(QGraphicsItem::ItemIsMovable); // 사용자 드래그
    QGraphicsView view(&scene);
    view.setRenderHint(QPainter::Antialiasing);
    view.show();
    return app.exec();
}

성능을 다룰 때는 항목 수뿐 아니라 페인트 복잡도·충돌 검사 빈도도 함께 봐야 합니다. QGraphicsItem::CacheMode로 캐싱을 켜거나, 변경이 적은 배경은 별도 레이어로 분리하는 식의 튜닝이 사용됩니다. 또한 QGraphicsPixmapItem 대량 배치 시 메모리와 텍스처 업로드 비용을 프로파일링하는 것이 좋습니다.

프로덕션에서 통용되는 Qt 패턴

객체 소유권은 Qt 데스크톱 앱의 메모리 안전성을 좌우합니다. 부모 QObject가 소멸할 때 자식 위젯과 자식 QObject가 순서대로 삭제되므로, 동적 할당 시 부모를 명시하는 습관이 중요합니다. 반대로 장수명 객체를 가리키는 포인터는 QPointer(또는 Qt 6의 QWeakPointer와 QObject 특화 조합)로 파괴 여부를 감지하는 편이 안전합니다.

스레드와 QObject 친화성(affinity) 을 지켜야 합니다. 특정 인스턴스는 생성된 스레드에 귀속되며, 다른 스레드에서 직접 메서드를 호출하면 데이터 레이스가 납니다. 워커 로직은 QObject 서브클래스로 만들고 moveToThread로 옮긴 뒤, 시그널/슬롯을 큐 연결로만 통신하는 패턴이 가장 흔한 정석입니다.

설정과 배포 측면에서는 QSettings로 OS별 저장 위치를 추상화하고, 로깅은 qDebug/qWarning을 넘어 애플리케이션 로거나 파일 싱크로 확장합니다. UI 코드와 비즈니스 로직을 분리하면 테스트에서 QSignalSpy로 시그널 발생 여부를 검증하기 쉬워집니다.

// 데이터 레이스를 피하기: 워커 QObject는 전용 스레드로만 moveToThread하고,
// UI 쪽에서는 시그널을 Qt::QueuedConnection(또는 기본 큐 연결)로 보낸다.
#include <QObject>
#include <QThread>

class ImageTask : public QObject {
    Q_OBJECT
public slots:
    void process(const QString &path) {
        Q_UNUSED(path);
        // 디스크·CPU 작업 — 이 슬롯은 worker가 속한 스레드에서 실행됨
    }
};

void attachWorkerToThread(ImageTask *worker, QThread *thread) {
    worker->moveToThread(thread);
    QObject::connect(thread, &QThread::finished, worker, &QObject::deleteLater);
    // UI: emit taskRequested(path) → connect(..., Qt::QueuedConnection) → ImageTask::process
}

실제 앱에서는 스레드를 start()한 뒤, UI나 조율기(coordinator)가 시그널로 경로를 넘기고 슬롯이 워커 스레드에서 실행되게 구성합니다. 요지는 UI 스레드에 귀속된 QWidget 등을 워커에서 직접 호출하지 않는 것입니다.

마지막으로, Qt 6에서는 일부 API와 모듈 구조가 정리되었으므로, 새 프로젝트는 Qt 6 기준 문서를 기본으로 삼고, 레거시 Qt 5 코드는 QStringRefQStringView 등 마이그레이션 가이드를 병행하는 것이 좋습니다. 이 글의 입문 예제는 Widgets 중심이지만, 같은 메타 객체·이벤트 루프 철학은 Qt Quick에도 공통으로 깔려 있어, 이후 QML로 확장할 때도 기반이 됩니다.

자주 발생하는 문제

문제 1: Q_OBJECT 매크로 에러

증상: moc 관련 에러

원인: Q_OBJECT 매크로 사용 시 moc 실행 필요

해결법: qmake 또는 CMake 사용

문제 2: 시그널-슬롯 연결 안됨

증상: 클릭해도 반응 없음

원인: 잘못된 시그널/슬롯 이름

해결법: 컴파일 타임 체크 사용

// ❌ 런타임 체크
connect(button, SIGNAL(clicked()), this, SLOT(onClicked()));

// ✅ 컴파일 타임 체크
connect(button, &QPushButton::clicked, this, &MyClass::onClicked);

문제 3: 메모리 누수

증상: 메모리 증가

원인: 위젯 삭제 안함

해결법: 부모 위젯 설정

// ✅ 부모 설정하면 자동 삭제
QPushButton *button = new QPushButton("버튼", parentWidget);

FAQ

Q1: Qt vs 다른 GUI 프레임워크?

A:

  • Qt: 크로스 플랫폼, 풍부한 기능
  • wxWidgets: 네이티브 룩앤필
  • GTK: 리눅스 중심

Q2: Qt 버전은?

A: Qt 6 최신 버전 사용 권장 (Qt 5도 여전히 많이 사용)

Q3: Qt Creator vs Visual Studio?

A: Qt Creator가 Qt 개발에 최적화되어 있습니다.

Q4: 상용 라이선스 필요한가요?

A: 오픈소스 프로젝트는 무료 (LGPL/GPL)

Q5: Qt Quick vs Qt Widgets?

A:

  • Qt Widgets: 전통적, 안정적
  • Qt Quick: 모던, 모바일 친화적

Q6: Qt 학습 리소스는?

A:

  • 공식 문서: doc.qt.io
  • Qt Examples
  • “C++ GUI Programming with Qt” 책

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

관련 글

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「C++ GUI | Qt 프레임워크: 위젯부터 MOC·시그널/슬롯·이벤트 루프·Graphics View까지」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「C++ GUI | Qt 프레임워크: 위젯부터 MOC·시그널/슬롯·이벤트 루프·Graphics View까지」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, GUI, Qt, 프레임워크, UI, MOC, GraphicsView 등으로 검색하시면 이 글이 도움이 됩니다.