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를 연결해야 합니다. Qt는 connect(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()가 이벤트 루프를 돌리며 마우스·키보드 이벤트를 처리하고, 위젯이 시그널을 발생시키면 연결된 슬롯이 호출됩니다.
개념을 잡는 비유
이 글의 주제는 여러 부품이 맞물리는 시스템으로 보시면 이해가 빠릅니다. 한 레이어(저장·네트워크·관측)의 선택이 옆 레이어에도 영향을 주므로, 본문에서는 트레이드오프를 숫자와 패턴으로 정리합니다.
목차
- Qt란
- 프로젝트 만들기 (CMake 예시)
- 최소 창 띄우기
- 위젯과 레이아웃
- 시그널과 슬롯
- 완전한 예제: 버튼·라벨·레이아웃 통합
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스
- 프로덕션 패턴
- 성능 및 UI 반응성 팁
- Q_OBJECT와 moc (다중 파일)
- 정리
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);
- setCentralWidget로 QMainWindow의 중앙에 위젯을 설정하면, 그 위젯이 레이아웃의 부모가 되어 창 크기에 맞춰 배치됩니다.
- 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를[&]로 캡처했지만,main이app.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();
}
- QMenuBar와 addAction으로 메뉴를 추가하고, 시그널/슬롯으로 연결합니다.
- QFileDialog로 파일 선택, QFile과 QTextStream으로 읽기/쓰기를 합니다.
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가 켜져 있으면 자동으로 처리됩니다.
단일 파일 예제에서 FormWindow나 SimpleEditor를 쓰려면, 해당 클래스를 헤더(.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.h에Q_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 완벽 가이드 | 경로·디렉토리·파일·권한 한 번에 정리