C++ 크로스 플랫폼 GUI | Qt 기초 완벽 가이드 [#36-2]

C++ 크로스 플랫폼 GUI | Qt 기초 완벽 가이드 [#36-2]

이 글의 핵심

C++ 크로스 플랫폼 GUI에 대한 실전 가이드입니다. Qt 기초 완벽 가이드 [#36-2] 등을 예제와 함께 상세히 설명합니다.

들어가며: C++로 창 띄우고 버튼 넣고 싶어요

”버튼 클릭해도 반응이 없어요” — Qt가 해결하는 문제

C++로 GUI를 만들려고 하면 플랫폼별 API(Win32, Cocoa, X11)가 달라서 코드가 복잡해지고, 이벤트 처리·메모리 관리도 직접 해야 합니다. Qt는 이런 문제를 한 번에 해결합니다. Windows, macOS, Linux에서 같은 코드로 빌드하고, 시그널/슬롯으로 “버튼 클릭 → 함수 호출”을 타입 안전하게 연결하며, 부모-자식 관계로 위젯 메모리를 자동 관리합니다.

이 글에서 다루는 것:

  • Qt가 무엇인지, 왜 데스크톱 C++ GUI에 쓰이는지
  • 프로젝트 구성: CMake로 Qt 앱 빌드
  • 최소 창: QMainWindow, QWidget
  • 위젯과 레이아웃: QPushButton, QLabel, QVBoxLayout, QHBoxLayout, QGridLayout
  • 시그널/슬롯: 버튼 클릭 → 슬롯 연결 (람다, 멤버 함수)
  • 자주 하는 실수해결법
  • 베스트 프랙티스프로덕션 패턴

문제 시나리오: Qt 없이 겪는 어려움

시나리오 1: 플랫폼별 코드 분기

// ❌ Qt 없이: 플랫폼마다 다른 API
#ifdef _WIN32
    HWND hwnd = CreateWindowEx(...);
#elif __APPLE__
    NSWindow* window = [[NSWindow alloc] init...];
#else
    Display* display = XOpenDisplay(NULL);
    Window window = XCreateWindow(...);
#endif

Qt로 해결: QMainWindow 하나로 모든 플랫폼에서 동일하게 동작합니다.

시나리오 2: 버튼 클릭 시 반응 없음

콜백을 등록했는데 클릭해도 호출되지 않는 경우. Win32에서는 WndProc에서 WM_COMMAND를 처리하고, Cocoa에서는 @selector를 연결해야 합니다. Qtconnect(button, &QPushButton::clicked, ...) 한 줄로 해결합니다.

시나리오 3: 위젯 메모리 누수

new QPushButton()만 하고 부모를 지정하지 않으면, 창을 닫아도 버튼이 해제되지 않습니다. Qt는 부모 위젯이 소멸할 때 자식 위젯을 자동 삭제하므로, 부모만 지정하면 됩니다.

시나리오 4: 레이아웃이 창 크기에 맞지 않음

절대 좌표로 배치하면 창을 리사이즈할 때 UI가 깨집니다. Qt 레이아웃(QVBoxLayout, QHBoxLayout 등)을 쓰면 자동으로 비율에 맞춰 재배치됩니다.


Qt 아키텍처 개요

flowchart TB
    subgraph app["QApplication"]
        eventLoop[이벤트 루프]
    end

    subgraph mainWindow["QMainWindow"]
        menuBar[메뉴바]
        toolbar[툴바]
        central[중앙 위젯]
        statusBar[상태바]
    end

    subgraph widgets["위젯 계층"]
        QWidget
        QPushButton
        QLabel
        QLineEdit
    end

    subgraph layout["레이아웃"]
        VBox[QVBoxLayout]
        HBox[QHBoxLayout]
        Grid[QGridLayout]
    end

    subgraph signals["시그널/슬롯"]
        signal[시그널 발생]
        slot[슬롯 실행]
        signal --> slot
    end

    app --> mainWindow
    mainWindow --> central
    central --> layout
    layout --> widgets
    widgets --> signals

핵심 흐름: QApplication::exec()가 이벤트 루프를 돌리며 마우스·키보드 이벤트를 처리하고, 위젯이 시그널을 발생시키면 연결된 슬롯이 호출됩니다.

개념을 잡는 비유

이 글의 주제는 여러 부품이 맞물리는 시스템으로 보시면 이해가 빠릅니다. 한 레이어(저장·네트워크·관측)의 선택이 옆 레이어에도 영향을 주므로, 본문에서는 트레이드오프를 숫자와 패턴으로 정리합니다.


목차

  1. Qt란
  2. 프로젝트 만들기 (CMake 예시)
  3. 최소 창 띄우기
  4. 위젯과 레이아웃
  5. 시그널과 슬롯
  6. 완전한 예제: 버튼·라벨·레이아웃 통합
  7. 자주 발생하는 에러와 해결법
  8. 베스트 프랙티스
  9. 프로덕션 패턴
  10. 성능 및 UI 반응성 팁
  11. Q_OBJECT와 moc (다중 파일)
  12. 정리

1. Qt란

역할

  • GUI: 버튼, 리스트, 테이블, 다이얼로그 등 위젯레이아웃을 제공합니다.
  • 크로스 플랫폼: Windows, macOS, Linux, Android, iOS 등에서 같은 소스로 빌드할 수 있습니다.
  • 시그널/슬롯: 위젯 간 “이벤트가 났을 때 이 함수를 호출”하는 연결을 타입 안전하게 표현합니다.
  • 추가 모듈: 네트워크(Qt Network), DB(Qt SQL), 멀티미디어, 차트 등이 있어 데스크톱 앱에 필요한 기능을 한 번에 다룰 수 있습니다.

라이선스

  • Qt는 상업용·오픈소스 모두 사용 가능합니다. 상업용은 라이선스 조건을 확인해 사용하면 됩니다. 오픈소스 프로젝트는 LGPL 등으로 사용할 수 있습니다.

2. 프로젝트 만들기 (CMake 예시)

CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(MyQtApp LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
find_package(Qt6 REQUIRED COMPONENTS Widgets)  # Qt5면 Qt5

add_executable(MyQtApp main.cpp)
target_link_libraries(MyQtApp PRIVATE Qt6::Widgets)
  • Qt6 (또는 Qt5)의 Widgets 모듈을 링크하면 기본 창·위젯을 쓸 수 있습니다.
  • main.cpp에서 QApplication과 메인 윈도우를 만들고 exec() 로 이벤트 루프를 돌립니다.

빌드

mkdir build && cd build
cmake .. -DCMAKE_PREFIX_PATH=/path/to/Qt/6.x/gcc_64  # Qt 경로
cmake --build .
./MyQtApp

Qt 경로가 PATH나 CMAKE_PREFIX_PATH에 잡혀 있어야 합니다. Qt Creator를 쓰면 자동으로 설정됩니다.

Qt 설치 방법 (플랫폼별)

Windows:

# Qt 온라인 인스톤러 다운로드
# https://www.qt.io/download-qt-installer
# MSVC 2022 + Qt 6.x 선택

macOS:

brew install qt@6
# CMAKE_PREFIX_PATH 설정
export CMAKE_PREFIX_PATH="/opt/homebrew/opt/qt@6"

Linux (Ubuntu/Debian):

sudo apt install qt6-base-dev qt6-tools-dev
# 또는 온라인 인스톤러 사용

3. 최소 창 띄우기

main.cpp (최소)

QApplication 은 Qt 앱당 하나만 두고, exec() 가 이벤트 루프를 돌리며 창 이벤트·그리기를 처리합니다. QMainWindow 로 메인 창을 만들고 setWindowTitle, resize 로 제목과 크기를 정한 뒤 show() 로 보이게 합니다.

#include <QApplication>
#include <QMainWindow>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    QMainWindow window;
    window.setWindowTitle("My Qt App");
    window.resize(400, 300);
    window.show();
    return app.exec();
}
  • QApplication: 한 번만 생성하고, exec() 로 이벤트 루프를 돌립니다.
  • QMainWindow: 메뉴·툴바·중앙 위젯을 넣을 수 있는 메인 창입니다. show() 로 표시합니다.
  • app.exec(): 종료 시까지 이벤트를 처리합니다.

4. 위젯과 레이아웃

기본 위젯

위젯용도
QPushButton클릭 가능한 버튼
QLabel텍스트·이미지 표시
QLineEdit한 줄 입력
QTextEdit여러 줄 텍스트 편집
QComboBox드롭다운 선택
QCheckBox체크박스
QSpinBox숫자 입력

QVBoxLayout (세로 배치)

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

QWidget* central = new QWidget(&window);
QVBoxLayout* layout = new QVBoxLayout(central);
QLabel* label = new QLabel("Hello, Qt!");
QPushButton* button = new QPushButton("Click Me");
layout->addWidget(label);
layout->addWidget(button);
window.setCentralWidget(central);
  • setCentralWidgetQMainWindow의 중앙에 위젯을 설정하면, 그 위젯이 레이아웃의 부모가 되어 창 크기에 맞춰 배치됩니다.
  • addWidget은 위에서부터 순서대로 추가됩니다.

QHBoxLayout (가로 배치)

QHBoxLayout* hLayout = new QHBoxLayout();
hLayout->addWidget(new QLabel("이름:"));
hLayout->addWidget(new QLineEdit());
hLayout->addWidget(new QPushButton("확인"));
  • addWidget은 왼쪽에서 오른쪽 순으로 추가됩니다.

QGridLayout (그리드 배치)

QGridLayout* grid = new QGridLayout();
grid->addWidget(new QLabel("이름:"), 0, 0);      // row 0, col 0
grid->addWidget(new QLineEdit(), 0, 1);          // row 0, col 1
grid->addWidget(new QLabel("나이:"), 1, 0);    // row 1, col 0
grid->addWidget(new QSpinBox(), 1, 1);         // row 1, col 1
grid->addWidget(new QPushButton("저장"), 2, 0, 1, 2);  // row 2, col 0, rowSpan 1, colSpan 2
  • (행, 열) 좌표로 위젯을 배치합니다.
  • addWidget(widget, row, col, rowSpan, colSpan)으로 여러 셀을 합칠 수 있습니다.

QFormLayout (폼 스타일)

QFormLayout* form = new QFormLayout();
form->addRow("이름:", new QLineEdit());
form->addRow("이메일:", new QLineEdit());
form->addRow("", new QPushButton("제출"));  // 라벨 없이 위젯만
  • 라벨-입력 쌍을 자동으로 정렬합니다.

레이아웃 중첩

QVBoxLayout* mainLayout = new QVBoxLayout();
QHBoxLayout* topRow = new QHBoxLayout();
topRow->addWidget(new QLabel("검색:"));
topRow->addWidget(new QLineEdit());
topRow->addWidget(new QPushButton("찾기"));
mainLayout->addLayout(topRow);  // addLayout으로 하위 레이아웃 추가
mainLayout->addWidget(new QTextEdit());
  • addLayout으로 하위 레이아웃을 추가하면 복잡한 UI를 구성할 수 있습니다.

레이아웃 시각화

flowchart TB
    subgraph VBox["QVBoxLayout"]
        L1[QLabel]
        B1[QPushButton]
    end

    subgraph HBox["QHBoxLayout (중첩)"]
        L2[QLabel]
        E1[QLineEdit]
        B2[QPushButton]
    end

    VBox --> L1
    VBox --> HBox
    VBox --> B1
    HBox --> L2
    HBox --> E1
    HBox --> B2

5. 시그널과 슬롯

개념

  • 시그널: “무언가 일어났다” (클릭, 텍스트 변경 등). 위젯이 발생시킵니다.
  • 슬롯: “그럴 때 이걸 실행한다.” 일반 함수, 멤버 함수, 람다가 될 수 있습니다.
  • connect(발신자, 시그널, 수신자, 슬롯) 또는 connect(발신자, 시그널, 람다) 로 연결합니다.

람다로 연결

#include <QObject>

QObject::connect(button, &QPushButton::clicked, [label]() {
    label->setText("Button clicked!");
});
  • clicked 시그널에 람다를 연결해, 클릭 시 라벨 텍스트를 바꿉니다.
  • 주의: [&]로 캡처하면 람다가 버튼보다 오래 살 수 있어 위험합니다. [label]처럼 필요한 포인터만 캡처하거나, [this](객체가 살아 있는 경우)를 사용하세요.

멤버 함수를 슬롯으로

class MyWindow : public QMainWindow {
    Q_OBJECT
public:
    MyWindow() {
        QPushButton* btn = new QPushButton("클릭", this);
        connect(btn, &QPushButton::clicked, this, &MyWindow::onButtonClicked);
    }
private slots:
    void onButtonClicked() {
        qDebug() << "버튼 클릭됨";
    }
};
  • private slots: (또는 public slots:)로 선언한 멤버 함수를 슬롯으로 사용합니다.
  • Q_OBJECT 매크로가 있어야 moc(Meta-Object Compiler)가 시그널/슬롯 메타데이터를 생성합니다.

시그널에 인자 전달

// QSpinBox::valueChanged(int) 시그널
connect(spinBox, &QSpinBox::valueChanged,  {
    qDebug() << "값 변경:" << value;
});

// 커스텀 시그널 (클래스 내부)
signals:
    void dataReady(const QString& data);

Qt5 vs Qt6 연결 문법

// Qt5 (구 문법, 여전히 동작)
connect(button, SIGNAL(clicked()), this, SLOT(onClicked()));

// Qt6 권장 (타입 안전)
connect(button, &QPushButton::clicked, this, &MyWindow::onClicked);
  • Qt6에서는 함수 포인터 문법을 권장합니다. 컴파일 시점에 시그널/슬롯 시그니처를 검사할 수 있습니다.

6. 완전한 예제: 버튼·라벨·레이아웃 통합

예제 1: 클릭 카운터

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

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    QMainWindow window;
    QWidget* central = new QWidget(&window);
    QVBoxLayout* layout = new QVBoxLayout(central);

    QLabel* label = new QLabel("클릭 횟수: 0");
    QPushButton* button = new QPushButton("클릭");

    int count = 0;
    QObject::connect(button, &QPushButton::clicked, [&label, &count]() {
        ++count;
        label->setText(QString("클릭 횟수: %1").arg(count));
    });

    layout->addWidget(label);
    layout->addWidget(button);
    window.setCentralWidget(central);
    window.setWindowTitle("클릭 카운터");
    window.resize(300, 150);
    window.show();
    return app.exec();
}
  • 람다에서 count[&]로 캡처했지만, mainapp.exec() 동안 살아 있으므로 안전합니다.
  • QString::arg()로 포맷 문자열을 만듭니다.

예제 2: 입력 폼 + 버튼

#include <QApplication>
#include <QMainWindow>
#include <QWidget>
#include <QVBoxLayout>
#include <QFormLayout>
#include <QPushButton>
#include <QLabel>
#include <QLineEdit>

class FormWindow : public QMainWindow {
    Q_OBJECT
public:
    FormWindow() {
        QWidget* central = new QWidget(this);
        QVBoxLayout* mainLayout = new QVBoxLayout(central);

        QFormLayout* form = new QFormLayout();
        nameEdit = new QLineEdit();
        emailEdit = new QLineEdit();
        form->addRow("이름:", nameEdit);
        form->addRow("이메일:", emailEdit);
        mainLayout->addLayout(form);

        resultLabel = new QLabel("입력 대기 중...");
        mainLayout->addWidget(resultLabel);

        QPushButton* submitBtn = new QPushButton("제출");
        connect(submitBtn, &QPushButton::clicked, this, &FormWindow::onSubmit);
        mainLayout->addWidget(submitBtn);

        setCentralWidget(central);
        setWindowTitle("입력 폼");
        resize(400, 200);
    }
private slots:
    void onSubmit() {
        QString text = QString("이름: %1, 이메일: %2")
            .arg(nameEdit->text())
            .arg(emailEdit->text());
        resultLabel->setText(text);
    }
private:
    QLineEdit* nameEdit;
    QLineEdit* emailEdit;
    QLabel* resultLabel;
};

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    FormWindow window;
    window.show();
    return app.exec();
}
  • QFormLayout으로 라벨-입력 쌍을 깔끔하게 배치합니다.
  • connect로 버튼 클릭 시 onSubmit 슬롯을 호출합니다.

예제 3: 간단한 텍스트 에디터

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

class SimpleEditor : public QMainWindow {
    Q_OBJECT
public:
    SimpleEditor() {
        editor = new QTextEdit(this);
        setCentralWidget(editor);

        QMenu* fileMenu = menuBar()->addMenu("파일");
        fileMenu->addAction("열기", this, &SimpleEditor::openFile);
        fileMenu->addAction("저장", this, &SimpleEditor::saveFile);

        setWindowTitle("간단한 에디터");
        resize(600, 400);
    }

private slots:
    void openFile() {
        QString filename = QFileDialog::getOpenFileName(this);
        if (!filename.isEmpty()) {
            QFile file(filename);
            if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
                editor->setPlainText(QTextStream(&file).readAll());
            }
        }
    }

    void saveFile() {
        QString filename = QFileDialog::getSaveFileName(this);
        if (!filename.isEmpty()) {
            QFile file(filename);
            if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
                QTextStream(&file) << editor->toPlainText();
            }
        }
    }

private:
    QTextEdit* editor;
};

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    SimpleEditor editor;
    editor.show();
    return app.exec();
}
  • QMenuBaraddAction으로 메뉴를 추가하고, 시그널/슬롯으로 연결합니다.
  • QFileDialog로 파일 선택, QFileQTextStream으로 읽기/쓰기를 합니다.

7. 자주 발생하는 에러와 해결법

에러 1: Could not find Qt6

원인: CMake가 Qt 설치 경로를 찾지 못함.

해결법:

# Qt 경로를 CMAKE_PREFIX_PATH에 지정
cmake .. -DCMAKE_PREFIX_PATH="/opt/homebrew/opt/qt@6"   # macOS
cmake .. -DCMAKE_PREFIX_PATH="C:/Qt/6.5.0/msvc2019_64"  # Windows

에러 2: undefined reference to 'vtable for MyClass'

원인: Q_OBJECT를 사용하는 클래스인데 moc가 실행되지 않음.

해결법: CMake에서 Qt6::Widgets를 링크하면 자동으로 moc가 실행됩니다. add_executable에 해당 소스가 포함되어 있는지 확인하세요. 헤더만 있는 경우:

set(CMAKE_AUTOMOC ON)
add_executable(MyQtApp main.cpp mywindow.cpp)
target_link_libraries(MyQtApp PRIVATE Qt6::Widgets)

에러 3: 버튼 클릭해도 슬롯이 호출되지 않음

원인 1: connect에서 this가 이미 소멸됨. (예: 로컬 변수로 창을 만들고 connect(btn, ..., this, ...)에서 this가 스코프를 벗어남)

해결법: 수신자를 nullptr로 두고 람다만 사용하거나, 수신자 객체의 생명주기를 확인하세요.

원인 2: Qt::QueuedConnection이 필요한 경우(다른 스레드)에 기본 Qt::AutoConnection이 동기로 동작함.

해결법:

connect(worker, &Worker::resultReady, this, &MainWindow::handleResult, Qt::QueuedConnection);

에러 4: 메모리 누수 — 위젯이 삭제되지 않음

원인: new QWidget()로 만들고 부모를 지정하지 않음.

해결법:

// ❌ 잘못된 예
QPushButton* btn = new QPushButton("클릭");

// ✅ 올바른 예: 부모 지정
QPushButton* btn = new QPushButton("클릭", this);
// 또는 레이아웃에 추가하면 레이아웃의 부모 위젯이 자동으로 부모가 됨
layout->addWidget(new QPushButton("클릭"));

에러 5: 람다 캡처로 인한 dangling reference

원인: [&]로 캡처한 지역 변수가 람다보다 먼저 소멸.

해결법:

// ❌ 위험: label이 먼저 삭제될 수 있음
QObject::connect(button, &QPushButton::clicked, [&]() {
    label->setText("...");
});

// ✅ 값/포인터로 필요한 것만 캡처 (Qt가 연결 해제 시 람다도 제거)
QObject::connect(button, &QPushButton::clicked, [label]() {
    label->setText("...");
});

에러 6: QWidget: Must construct a QApplication before a QPaintDevice

원인: QApplication 생성 전에 위젯을 생성함.

해결법:

// ✅ 순서 지키기
int main(int argc, char *argv[]) {
    QApplication app(argc, argv);  // 반드시 먼저
    QMainWindow window;
    // ...
}

8. 베스트 프랙티스

1. 부모-자식 관계 일관성

  • 모든 위젯에 부모를 지정하세요. 레이아웃에 addWidget하면 해당 레이아웃의 부모 위젯이 자동으로 부모가 됩니다.
  • 부모가 소멸할 때 자식이 자동 삭제되므로 delete를 호출하지 마세요.

2. 시그널/슬롯은 함수 포인터 문법 사용

// ✅ Qt6 권장
connect(btn, &QPushButton::clicked, this, &MyWindow::onClicked);

// ❌ Qt5 구 문법 (문자열이라 오타 시 런타임 에러)
connect(btn, SIGNAL(clicked()), this, SLOT(onClicked()));

3. 슬롯 네이밍

  • on<Widget><Signal> 형식을 쓰면 가독성이 좋습니다. 예: onButtonClicked, onLineEditTextChanged.

4. UI 스레드에서만 위젯 접근

  • Qt 위젯은 메인 스레드에서만 생성·수정해야 합니다. 워커 스레드에서 결과를 전달할 때는 시그널을 Qt::QueuedConnection으로 연결하세요.

5. 리소스 경로

  • 이미지 등 리소스는 Qt Resource System(.qrc)을 사용하면 실행 파일에 포함할 수 있습니다.
// :/images/icon.png 형태로 접근
label->setPixmap(QPixmap(":/images/icon.png"));

9. 프로덕션 패턴

패턴 1: MVC 분리

  • Model: 데이터 (QAbstractItemModel 등)
  • View: QTableView, QListView
  • Controller: 시그널/슬롯으로 Model과 View 연결
// Model-View 사용 예
QStandardItemModel* model = new QStandardItemModel(this);
QTableView* view = new QTableView(this);
view->setModel(model);

패턴 2: 다이얼로그 모달/모달리스

// 모달: 다이얼로그가 닫힐 때까지 메인 창 조작 불가
QDialog dialog(this);
if (dialog.exec() == QDialog::Accepted) {
    // 사용자가 OK 클릭
}

// 모달리스: 다이얼로그와 메인 창 동시 조작
QDialog* dialog = new QDialog(this);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->show();

패턴 3: 설정 저장 (QSettings)

void MainWindow::closeEvent(QCloseEvent* event) {
    QSettings settings("MyCompany", "MyApp");
    settings.setValue("geometry", saveGeometry());
    settings.setValue("windowState", saveState());
    QMainWindow::closeEvent(event);
}

// 생성자에서
QSettings settings("MyCompany", "MyApp");
restoreGeometry(settings.value("geometry").toByteArray());
restoreState(settings.value("windowState").toByteArray());

패턴 4: 스타일시트 (QSS)

// 다크 테마 예시
setStyleSheet(
    "QMainWindow { background-color: #2d2d2d; }"
    "QPushButton { background-color: #404040; color: white; border-radius: 4px; }"
    "QPushButton:hover { background-color: #505050; }"
);

패턴 5: 로깅 및 에러 처리

#include <QMessageBox>

void MainWindow::onSaveFailed(const QString& error) {
    QMessageBox::critical(this, "저장 실패", error);
}

패턴 6: 시그널/슬롯 시퀀스 (다중 스레드)

sequenceDiagram
    participant Worker as 워커 스레드
    participant Signal as 시그널
    participant Main as 메인 스레드
    participant Slot as 슬롯

    Worker->>Signal: emit resultReady(data)
    Signal->>Main: QueuedConnection
    Main->>Slot: handleResult(data)
    Note over Slot: UI 업데이트 안전
  • 워커 스레드에서 emit한 시그널은 Qt::QueuedConnection으로 메인 스레드의 이벤트 큐에 넣어지고, 메인 스레드가 슬롯을 실행합니다. 따라서 UI 위젯을 직접 건드리지 않고도 안전하게 업데이트할 수 있습니다.

성능 및 UI 반응성 팁

1. 무거운 작업은 워커 스레드로

// ❌ 메인 스레드에서 대용량 처리 → UI 멈춤
void onProcessClicked() {
    processHugeData();  // 5초 걸림 → 창이 5초간 응답 없음
}

// ✅ QThread 또는 QtConcurrent 사용
void onProcessClicked() {
    QtConcurrent::run([this]() {
        auto result = processHugeData();
        QMetaObject::invokeMethod(this, [this, result]() {
            updateUI(result);  // 메인 스레드에서 UI 업데이트
        }, Qt::QueuedConnection);
    });
}

2. 대량 위젯은 가상화

  • QListWidget에 수천 개 항목을 넣으면 느려집니다. QListView + QAbstractItemModel을 사용하면 화면에 보이는 항목만 그립니다.

3. 스타일시트 최소화

  • setStyleSheet를 과도하게 쓰면 매 프레임마다 스타일 재계산이 발생할 수 있습니다. 공통 스타일은 한 번만 적용하고, QPalette로 테마를 바꾸는 것이 효율적입니다.

Q_OBJECT와 moc: 다중 파일 프로젝트

Q_OBJECT를 사용하는 클래스는 moc(Meta-Object Compiler)가 처리해야 합니다. CMake에서 AUTOMOC가 켜져 있으면 자동으로 처리됩니다.

단일 파일 예제에서 FormWindowSimpleEditor를 쓰려면, 해당 클래스를 헤더(.h)소스(.cpp) 로 분리하는 것이 좋습니다:

// formwindow.h
#pragma once
#include <QMainWindow>
#include <QLineEdit>
#include <QLabel>

class FormWindow : public QMainWindow {
    Q_OBJECT
public:
    FormWindow();
private slots:
    void onSubmit();
private:
    QLineEdit* nameEdit;
    QLineEdit* emailEdit;
    QLabel* resultLabel;
};
// formwindow.cpp
#include "formwindow.h"
#include <QVBoxLayout>
#include <QFormLayout>
#include <QPushButton>

FormWindow::FormWindow() {
    // ... 생성자 구현
}

void FormWindow::onSubmit() {
    // ... 슬롯 구현
}
# CMakeLists.txt
add_executable(MyQtApp main.cpp formwindow.cpp formwindow.h)
target_link_libraries(MyQtApp PRIVATE Qt6::Widgets)
  • formwindow.hQ_OBJECT가 있으면 CMake가 moc_formwindow.cpp를 자동 생성하고 링크합니다.

10. 정리

  • Qt는 C++ 크로스 플랫폼 GUI 프레임워크로, 데스크톱 앱에서 사실상 표준처럼 쓰입니다.
  • QApplication + QMainWindow + setCentralWidget으로 창을 만들고, QVBoxLayout, QHBoxLayout, QGridLayout 등으로 위젯을 배치할 수 있습니다.
  • 시그널/슬롯으로 “버튼 클릭 → 함수 호출”을 connect로 연결하면, 타입 안전하게 이벤트 처리가 됩니다.
  • 부모-자식 관계를 지키면 메모리 관리가 자동으로 됩니다.
  • 자주 하는 실수(Qt 경로, moc, 부모 누락, 람다 캡처)를 피하고, 베스트 프랙티스프로덕션 패턴을 적용하면 실무에 바로 활용할 수 있습니다.

구현 체크리스트

  • CMAKE_PREFIX_PATH에 Qt 경로 설정
  • 모든 위젯에 부모 지정 (또는 레이아웃에 addWidget)
  • connect 시 함수 포인터 문법 사용 (Qt6)
  • Q_OBJECT 클래스는 moc 처리 확인
  • UI 스레드에서만 위젯 접근
  • 설정 저장 시 QSettings 사용

실무 활용 사례

산업용 제어 소프트웨어: 공장 자동화, 로봇 제어 시스템의 모니터링 UI는 대부분 Qt로 개발됩니다. Windows와 Linux 임베디드 시스템에서 같은 코드로 실행되고, 실시간 센서 데이터를 QTableWidget이나 QChart로 표시합니다.

금융 트레이딩 플랫폼: Bloomberg Terminal, 증권사 HTS(Home Trading System) 같은 고성능 트레이딩 툴은 Qt로 개발되어 밀리초 단위 반응성과 복잡한 차트·테이블을 동시에 처리합니다.

과학/엔지니어링 툴: MATLAB, LabVIEW 대안으로 데이터 수집·분석 툴을 만들 때 Qt를 사용하면, 그래프·테이블·파일 I/O를 한 프레임워크에서 해결할 수 있습니다.

다음 단계로 나아가기

  • Qt Designer: UI를 비주얼하게 디자인하고 .ui 파일로 저장
  • Model/View: QTableView, QListView로 대량 데이터 표시
  • 네트워크: QNetworkAccessManager로 HTTP 통신

관련 글: 파일 I/O(#11-1), HTTP 클라이언트(#21-1)


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

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

  • C++ GUI | “Qt 프레임워크” 초보자 가이드
  • C++ CMake 고급 | 멀티 타겟·외부 라이브러리 관리 (대규모 프로젝트 빌드)
  • C++ 현대적인 C++ GUI: Dear ImGui로 디버깅 툴·대시보드 만들기 [#36-1]

실전 체크리스트

실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.

코드 작성 전

  • 이 기법이 현재 문제를 해결하는 최선의 방법인가?
  • 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
  • 성능 요구사항을 만족하는가?

코드 작성 중

  • 컴파일러 경고를 모두 해결했는가?
  • 엣지 케이스를 고려했는가?
  • 에러 처리가 적절한가?

코드 리뷰 시

  • 코드의 의도가 명확한가?
  • 테스트 케이스가 충분한가?
  • 문서화가 되어 있는가?

이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.


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

Qt 튜토리얼, Qt 시작하기, C++ GUI 프레임워크, 크로스 플랫폼 앱, Qt 예제, QWidget 사용법, 시그널 슬롯, Qt CMake, C++ 데스크톱 앱, Qt6 입문, QVBoxLayout, QPushButton 등으로 검색하시면 이 글이 도움이 됩니다.

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. C++로 데스크톱 앱을 만들 때 사실상 표준인 Qt입니다. 프로젝트 생성, 위젯, 시그널/슬롯, 레이아웃까지 이 글의 예제를 따라 하면 최소 동작 앱을 만들 수 있고, 베스트 프랙티스와 프로덕션 패턴을 적용해 확장할 수 있습니다.

Q. Qt5와 Qt6 중 무엇을 써야 하나요?

A. 신규 프로젝트는 Qt6를 권장합니다. Qt6는 C++17 기반이고, 모듈 구조가 정리되었으며, 시그널/슬롯이 더 효율적입니다. 레거시 프로젝트는 Qt5를 유지하다가 마이그레이션 계획을 세우면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. Qt 공식 문서Qt 예제를 참고하세요. Qt Creator로 예제 프로젝트를 열어 보면 실전 코드를 학습할 수 있습니다.

한 줄 요약: Qt로 크로스 플랫폼 데스크톱 GUI를 개발할 수 있습니다. 위젯·레이아웃·시그널/슬롯을 익히고, 자주 하는 실수를 피하면 실무에 바로 적용할 수 있습니다.

이전 글: C++ GUI #36-1: Dear ImGui

다음 글: [C++23 프리뷰 #37-1] 남들보다 먼저 써보는 C++23 핵심 기능


관련 글

  • C++ 현대적인 C++ GUI: Dear ImGui로 디버깅 툴·대시보드 만들기 [#36-1]
  • C++ Python과 C++의 만남 | pybind11으로 고성능 엔진 만들기 [#35-1]
  • C++ WebAssembly(Wasm)와 Emscripten | C++을 브라우저에서 돌리기 [#35-2]
  • C++ 남들보다 먼저 써보는 C++23 핵심 기능 [#37-1]
  • C++ std::filesystem 완벽 가이드 | 경로·디렉토리·파일·권한 한 번에 정리