C++ 현대적인 C++ GUI: Dear ImGui로 디버깅 툴·대시보드 만들기 [#36-1]
이 글의 핵심
C++ 현대적인 C++ GUI: Dear ImGui로 디버깅 툴·대시보드 만들기 [#36-1]에 대한 실전 가이드입니다.
들어가며: 콘솔만 보다가, 화면에 뭔가 띄우고 싶어요
눈에 보이는 결과물 — ImGui의 매력
서버·콘솔 로그만 보던 개발에서 한 단계 나아가, 실시간으로 값이 바뀌는 창을 띄우고 싶을 때가 있습니다. 게임 엔진 내부 값, 서버 상태, 물리 파라미터, 네트워크 통계 등을 창과 버튼·슬라이더로 보면서 조정하는 툴이 있으면 디버깅과 튜닝이 훨씬 수월해집니다.
Dear ImGui는 즉시 모드(Immediate Mode) GUI(Graphical User Interface—그래픽으로 사용자와 상호작용하는 인터페이스) 라이브러리입니다. 즉시 모드는 “매 프레임 위젯을 그리면서 입력을 처리하는” 방식이라 상태를 따로 저장하지 않아도 됩니다. 가볍고, 빌드가 쉽고, C++ 한 곳에 렌더링 백엔드만 붙이면 되는 구조라 게임 개발자·툴 제작자들이 많이 씁니다. 이 글은 ImGui를 써서 나만의 디버깅 툴·상태 모니터링 대시보드를 만드는 첫 걸음입니다.
문제 시나리오: 왜 ImGui가 필요한가
시나리오 1: 물리 파라미터 튜닝 지옥
물리 엔진의 중력, 마찰력, 반발 계수를 조정할 때마다 코드 수정 → 재컴파일 → 실행 → 확인을 반복하면 시간이 너무 많이 듭니다. 브레이크포인트로 값을 바꿔도 다음 실행 시 초기화됩니다. 슬라이더로 실시간 조정하면서 결과를 바로 보는 툴이 필요합니다.
시나리오 2: 서버 상태가 콘솔 로그에 묻힌다
printf나 std::cout으로 CPU 사용률, 연결 수, 레이턴시를 출력하면 로그가 쌓여서 최신 값만 보기 어렵습니다. 실시간 대시보드에서 숫자와 그래프로 한눈에 보이면 병목을 즉시 파악할 수 있습니다.
시나리오 3: Qt/Win32는 너무 무겁다
독립된 데스크톱 앱이 아니라 게임/엔진 안에 붙이는 내장 디버그 UI만 필요할 때, Qt나 Win32는 의존성·빌드 시간·바이너리 크기가 부담됩니다. 헤더 몇 개만 넣으면 되는 가벼운 라이브러리가 필요합니다.
시나리오 4: 프로토타입 UI를 빠르게
”이 값이 바뀌면 어떻게 되지?”를 확인하는 임시 UI를 만들 때, 이벤트 콜백·위젯 ID·레이아웃 매니저를 신경 쓰기 싫습니다. 매 프레임 “여기 버튼 그려”만 호출하면 되는 방식이 개발 속도를 높입니다.
ImGui로 해결:
- 슬라이더·체크박스로 변수에 참조를 넘기면, 화면에서 바꾼 값이 곧바로 로직에 반영됩니다.
PlotLines로 시계열 데이터를 실시간 그래프로 표시할 수 있습니다.- GLFW + OpenGL 백엔드만 붙이면 수백 줄로 완전한 디버그 창을 만들 수 있습니다.
이 글에서 다루는 것:
- Dear ImGui란 무엇인지, 즉시 모드의 의미
- 최소 윈도우: 한 개 창에 라벨·버튼·슬라이더
- 백엔드: GLFW + OpenGL 또는 SDL 등과의 연결
- 실전 활용: 변수 노출·간단한 대시보드
- 자주 발생하는 에러와 해결법
- 프로덕션 패턴과 베스트 프랙티스
개념을 잡는 비유
이 글의 주제는 여러 부품이 맞물리는 시스템으로 보시면 이해가 빠릅니다. 한 레이어(저장·네트워크·관측)의 선택이 옆 레이어에도 영향을 주므로, 본문에서는 트레이드오프를 숫자와 패턴으로 정리합니다.
목차
- Dear ImGui란
- 즉시 모드(Immediate Mode)란
- 최소 예제: 창 하나 띄우기
- 백엔드 연동 (GLFW + OpenGL)
- 완전한 Dear ImGui 예제
- 실전: 디버깅 툴·대시보드
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스
- 프로덕션 패턴
1. Dear ImGui란
특징
- 헤더 기반에 가까운 구조로, 소스를 프로젝트에 넣고 백엔드만 붙이면 됩니다.
- 즉시 모드: 매 프레임 “지금 그릴 위젯”을 코드로 호출합니다. 레이아웃·상태는 라이브러리가 내부에서 처리합니다.
- 의존성 최소: 윈도우 생성·입력·렌더링은 백엔드가 담당합니다. GLFW+OpenGL, SDL, DirectX, Vulkan 등 여러 백엔드가 공식 예제로 제공됩니다.
- 용도: 에디터 내부 툴, 디버그 오버레이, 프로파일러 UI, 설정 패널, 프로토타입 UI에 적합합니다. “독립된 데스크톱 앱”보다는 엔진/앱 안에 붙이는 내장 UI에 맞습니다.
아키텍처 개요
flowchart TB
subgraph app["애플리케이션"]
loop["메인 루프"]
ui["UI 코드\nImGui Begin ~ End"]
end
subgraph imgui["Dear ImGui"]
core["imgui.cpp/h\n핵심 로직"]
backend["imgui_impl_*.cpp\n백엔드"]
end
subgraph platform["플랫폼"]
glfw["GLFW\n윈도우·입력"]
gl["OpenGL\n렌더링"]
end
loop --> ui
ui --> core
core --> backend
backend --> glfw
backend --> gl
왜 ImGui인가
- 빠르게 UI를 만들 수 있고, C++ 한 곳에서 로직과 UI를 같이 짤 수 있어 게임·툴 쪽에서 표준처럼 쓰입니다. “눈에 보이는 결과물”을 빠르게 만들고 싶을 때 적합합니다.
2. 즉시 모드(Immediate Mode)란
전통 GUI vs 즉시 모드
flowchart LR
subgraph retained["유지 모드 (Qt, Win32)"]
R1[버튼 객체 생성] --> R2[이벤트 콜백 등록]
R2 --> R3[클릭 시 콜백 호출]
end
subgraph immediate["즉시 모드 (ImGui)"]
I1[매 프레임 Button 호출] --> I2[반환값으로 클릭 여부 확인]
I2 --> I3["if(Button) ..."]
end
- 전통(유지 모드): 버튼 등을 “객체”로 만들어 두고, 이벤트 콜백으로 반응합니다. Qt, Win32 컨트롤 등이 이 방식입니다.
- 즉시 모드: 매 프레임 “여기에 버튼을 그려라”라고 호출합니다. 버튼이 눌렸는지는 그 호출의 반환값으로 알 수 있습니다. 상태는 라이브러리나 사용자 변수로 유지합니다.
- ImGui는 즉시 모드이므로, 매 프레임
ImGui::Button("Click")같은 코드가 실행되고, 클릭 여부는if (ImGui::Button("Click")) { ... }로 처리합니다. 코드가 직관적이고, C++ 한 스레드에서 UI와 로직을 같이 짜기 좋습니다.
프레임별 호출 흐름
sequenceDiagram
participant App as 애플리케이션
participant ImGui as ImGui
participant Backend as 백엔드(OpenGL)
loop 매 프레임
App->>Backend: NewFrame() (입력 처리)
App->>ImGui: NewFrame()
App->>ImGui: Begin("창") ~ 위젯 ~ End()
App->>ImGui: Render()
App->>Backend: RenderDrawData()
end
3. 최소 예제: 창 하나 띄우기
기본 흐름 (백엔드가 준비된 상태 가정)
매 프레임 ImGui_ImplXXX_NewFrame() (백엔드별), ImGui::NewFrame() 로 프레임을 시작한 뒤 Begin(“My Window”) ~ End() 사이에 위젯을 호출합니다. SliderFloat(“Value”, &value, 0, 1) 은 value 변수를 0~1 범위의 슬라이더로 그리며, 사용자가 드래그하면 value 가 실시간으로 바뀝니다. Render() 와 ImGui_ImplXXX_RenderDrawData(…) 로 그리기 데이터를 백엔드에 넘겨 실제로 화면에 그립니다.
#include "imgui.h"
// 매 프레임
ImGui_ImplXXX_NewFrame(); // 백엔드별 (OpenGL, SDL 등)
ImGui::NewFrame();
ImGui::Begin("My Window");
ImGui::Text("Hello, ImGui!");
if (ImGui::Button("Click Me")) {
// 클릭 시 처리
}
float value = 0.5f;
ImGui::SliderFloat("Value", &value, 0.0f, 1.0f);
ImGui::End();
ImGui::Render();
ImGui_ImplXXX_RenderDrawData(ImGui::GetDrawData());
- Begin/End 사이에 위젯을 넣으면 그 창에 그려집니다.
- Text: 문자열 표시.
- Button: 버튼. 반환값이 true면 이번 프레임에 클릭된 것.
- SliderFloat: float 변수를 슬라이더로 바꿉니다.
&value로 실시간 연동됩니다. - Render 후 백엔드가 RenderDrawData로 실제 그리기를 수행합니다.
주의: SliderFloat에 지역 변수 참조
위 예제에서 float value가 지역 변수이면, 매 프레임 0.5f로 초기화되므로 슬라이더를 움직여도 다음 프레임에 리셋됩니다. 실제 사용 시에는 value를 클래스 멤버나 static 변수로 두어야 합니다.
// ❌ 잘못된 예: 매 프레임 0.5로 리셋됨
void render() {
float value = 0.5f;
ImGui::SliderFloat("Value", &value, 0.0f, 1.0f);
}
// ✅ 올바른 예: 상태 유지
static float value = 0.5f;
void render() {
ImGui::SliderFloat("Value", &value, 0.0f, 1.0f);
}
4. 백엔드 연동 (GLFW + OpenGL)
구성
- 윈도우·입력: GLFW (또는 SDL 등)로 창을 만들고 마우스/키보드 이벤트를 받습니다.
- ImGui는 imgui.cpp/imgui.h 등 소스를 프로젝트에 포함합니다.
- imgui_impl_glfw.cpp, imgui_impl_opengl3.cpp를 같이 빌드해, ImGui_ImplGlfw_InitForOpenGL, ImGui_ImplOpenGL3_Init으로 초기화하고, 매 프레임 NewFrame → 위젯 호출 → Render → RenderDrawData 순서로 돌립니다.
초기화 순서 (개념)
- GLFW 창 생성.
- OpenGL 컨텍스트 생성.
- ImGui::CreateContext(), ImGui_ImplGlfw_InitForOpenGL(…), ImGui_ImplOpenGL3_Init(…).
- 메인 루프: ImGui_ImplGlfw_NewFrame(), ImGui::NewFrame() → UI 코드 → ImGui::Render(), ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()).
- 종료 시 ImGui_Impl*_Shutdown(), ImGui::DestroyContext().
공식 저장소의 examples/ 에서 사용하는 백엔드와 동일한 파일을 프로젝트에 넣으면 됩니다.
빠른 시작: 공식 예제로 5분 안에 실행하기
# ImGui 저장소 클론
git clone https://github.com/ocornut/imgui.git
cd imgui/examples/example_glfw_opengl3
# 빌드 (Linux/macOS)
make
# 실행
./example_glfw_opengl3
Windows에서는 Visual Studio 솔루션 파일(.sln)을 열어 빌드하면 됩니다. 5분 안에 창이 뜨고 위젯을 조작할 수 있어, “눈으로 확인”하는 경험을 빠르게 할 수 있습니다.
5. 완전한 Dear ImGui 예제
전체 main.cpp (GLFW + OpenGL 3)
아래 코드를 복사해 프로젝트에 넣고, ImGui 소스 및 백엔드 파일을 포함하면 바로 실행할 수 있습니다.
// main_imgui_minimal.cpp
// 빌드: g++ -std=c++17 main_imgui_minimal.cpp imgui.cpp imgui_draw.cpp imgui_tables.cpp
// imgui_widgets.cpp imgui_impl_glfw.cpp imgui_impl_opengl3.cpp
// -I. -lglfw -lGL -ldl
// -o imgui_demo
#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include <GLFW/glfw3.h>
#include <stdio.h>
#include <vector>
static void glfw_error_callback(int error, const char* description) {
fprintf(stderr, "GLFW Error %d: %s\n", error, description);
}
int main(int, char**) {
glfwSetErrorCallback(glfw_error_callback);
if (!glfwInit())
return 1;
const char* glsl_version = "#version 130";
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
GLFWwindow* window = glfwCreateWindow(1280, 720, "Dear ImGui Demo", nullptr, nullptr);
if (window == nullptr)
return 1;
glfwMakeContextCurrent(window);
glfwSwapInterval(1);
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
ImGui::StyleColorsDark();
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init(glsl_version);
// 애플리케이션 상태 (매 프레임 유지되어야 함)
float slider_value = 0.5f;
bool checkbox_value = false;
int click_count = 0;
std::vector<float> plot_data;
for (int i = 0; i < 100; ++i)
plot_data.push_back(0.5f + 0.3f * sinf(i * 0.1f));
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
// === 메인 창 ===
ImGui::Begin("디버그 툴");
ImGui::Text("Hello, Dear ImGui!");
ImGui::Separator();
if (ImGui::Button("클릭 카운트 증가")) {
click_count++;
}
ImGui::SameLine();
ImGui::Text("클릭 수: %d", click_count);
ImGui::Checkbox("옵션 활성화", &checkbox_value);
ImGui::SliderFloat("슬라이더", &slider_value, 0.0f, 1.0f, "%.2f");
ImGui::Separator();
ImGui::Text("시계열 그래프");
plot_data.erase(plot_data.begin());
plot_data.push_back(slider_value + 0.1f * sinf(glfwGetTime()));
ImGui::PlotLines("값", plot_data.data(), (int)plot_data.size(),
0, nullptr, 0.0f, 1.5f, ImVec2(0, 80));
ImGui::End();
// === 두 번째 창 (데모) ===
ImGui::Begin("추가 정보");
ImGui::Text("슬라이더 현재값: %.3f", slider_value);
ImGui::Text("체크박스: %s", checkbox_value ? "ON" : "OFF");
ImGui::End();
ImGui::Render();
int display_w, display_h;
glfwGetFramebufferSize(window, &display_w, &display_h);
glViewport(0, 0, display_w, display_h);
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
glfwSwapBuffers(window);
}
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
glfwDestroyWindow(window);
glfwTerminate();
return 0;
}
예제 코드 설명
| 코드 | 설명 |
|---|---|
glfw_error_callback | GLFW 에러 시 stderr로 출력 |
IMGUI_CHECKVERSION() | ImGui 버전 불일치 시 경고 |
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard | 키보드 네비게이션 활성화 |
slider_value, checkbox_value, click_count | 클래스 멤버 또는 static처럼 프레임 간 유지되는 변수 |
plot_data | 시계열 데이터; 매 프레임 한 칸씩 밀어 넣어 스크롤 효과 |
ImGui::SameLine() | 이전 위젯과 같은 줄에 배치 |
ImGui::Separator() | 구분선 |
CMake로 빌드하기
직접 g++ 명령을 쓰기 어렵다면 CMake로 프로젝트를 구성할 수 있습니다.
# CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(imgui_demo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
find_package(glfw3 REQUIRED)
find_package(OpenGL REQUIRED)
set(IMGUI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/imgui)
set(IMGUI_SOURCES
${IMGUI_DIR}/imgui.cpp
${IMGUI_DIR}/imgui_draw.cpp
${IMGUI_DIR}/imgui_tables.cpp
${IMGUI_DIR}/imgui_widgets.cpp
${IMGUI_DIR}/imgui_impl_glfw.cpp
${IMGUI_DIR}/imgui_impl_opengl3.cpp
)
add_executable(imgui_demo main.cpp ${IMGUI_SOURCES})
target_include_directories(imgui_demo PRIVATE ${IMGUI_DIR})
target_link_libraries(imgui_demo glfw OpenGL::GL)
# 빌드
mkdir build && cd build
cmake ..
make
./imgui_demo
6. 실전: 디버깅 툴·대시보드
변수 노출
게임/서버의 숫자·플래그를 전역이나 클래스 멤버로 두고, ImGui 창에서 SliderFloat, Checkbox, InputInt 등으로 참조를 넘기면, 화면에서 바꾼 값이 곧바로 로직에 반영됩니다. 브레이크포인트 없이 실시간 튜닝이 가능해집니다.
완전한 예제: 네트워크 서버 모니터링 대시보드
#include "imgui.h"
#include <vector>
#include <cmath>
struct ServerStats {
int active_connections = 0;
float cpu_usage = 0.0f;
float memory_mb = 0.0f;
std::vector<float> latency_history;
static constexpr size_t MAX_HISTORY = 100;
};
void renderServerDashboard(ServerStats& stats) {
ImGui::Begin("Server Monitor");
ImGui::Text("Active Connections: %d", stats.active_connections);
ImGui::Text("CPU Usage: %.1f%%", stats.cpu_usage);
ImGui::Text("Memory: %.1f MB", stats.memory_mb);
if (stats.latency_history.size() > 0) {
ImGui::PlotLines("Latency (ms)",
stats.latency_history.data(),
(int)stats.latency_history.size(),
0, nullptr, 0.0f, 100.0f,
ImVec2(0, 80));
}
if (ImGui::Button("Reset Stats")) {
stats.active_connections = 0;
stats.cpu_usage = 0.0f;
stats.memory_mb = 0.0f;
stats.latency_history.clear();
}
ImGui::End();
}
// 호출 예: 매 프레임 서버에서 stats를 갱신한 뒤
void updateAndRender(ServerStats& stats) {
// stats.active_connections = server.getConnectionCount();
// stats.cpu_usage = getCpuUsage();
// stats.memory_mb = getMemoryUsageMB();
// stats.latency_history.push_back(getLatencyMs());
// if (stats.latency_history.size() > ServerStats::MAX_HISTORY)
// stats.latency_history.erase(stats.latency_history.begin());
renderServerDashboard(stats);
}
이 패턴을 사용하면 서버 상태를 실시간으로 보면서 디버깅할 수 있습니다.
물리 파라미터 튜닝 패널
struct PhysicsParams {
float gravity = -9.8f;
float friction = 0.5f;
float restitution = 0.8f;
bool enable_collision = true;
};
void renderPhysicsPanel(PhysicsParams& params) {
ImGui::Begin("Physics Tuner");
ImGui::SliderFloat("Gravity", ¶ms.gravity, -20.0f, 0.0f, "%.1f");
ImGui::SliderFloat("Friction", ¶ms.friction, 0.0f, 1.0f, "%.2f");
ImGui::SliderFloat("Restitution", ¶ms.restitution, 0.0f, 1.0f, "%.2f");
ImGui::Checkbox("Collision", ¶ms.enable_collision);
ImGui::End();
}
간단한 대시보드
- Begin으로 창을 여러 개 만들고, 각각 “FPS”, “네트워크 통계”, “물리 파라미터” 등으로 나누면 상태 모니터링 대시보드가 됩니다.
- PlotLines / PlotHistogram으로 시계열·분포를 그릴 수 있어, 프로파일러나 통계 툴에 잘 맞습니다.
실무 활용 사례
게임 엔진 디버그 오버레이: Unreal Engine, Unity 등 상용 게임 엔진들이 내부적으로 ImGui를 사용해 FPS, 메모리 사용량, 렌더링 통계를 실시간으로 화면에 표시합니다. 게임 실행 중 F1 키를 누르면 나오는 디버그 창이 대부분 ImGui로 구현됩니다.
서버 모니터링 툴: 실시간 서버 메트릭(CPU, 메모리, 네트워크 대역폭)을 ImGui 대시보드로 시각화하면, 웹 대시보드 없이도 로컬에서 빠르게 상태를 확인할 수 있습니다. PlotLines로 시계열 그래프를 그려 병목을 즉시 파악할 수 있습니다.
물리 시뮬레이션 파라미터 튜닝: 물리 엔진의 중력, 마찰력, 반발 계수 등을 ImGui 슬라이더로 실시간 조정하면서 시뮬레이션 결과를 바로 확인할 수 있어, 브레이크포인트 없이 빠르게 최적값을 찾을 수 있습니다.
7. 자주 발생하는 에러와 해결법
문제 1: NewFrame/Render 순서 오류로 크래시
증상: ImGui::Begin() 호출 시 크래시, 또는 화면에 아무것도 그려지지 않음.
원인: ImGui::NewFrame() 없이 위젯을 호출하거나, ImGui::Render() 없이 프레임을 끝냄.
// ❌ 잘못된 예
while (running) {
ImGui::Begin("Window"); // NewFrame() 없음 → 크래시 가능
ImGui::Text("Hello");
ImGui::End();
// Render() 없음 → 그려지지 않음
}
해결법:
// ✅ 올바른 예
while (running) {
ImGui_ImplGlfw_NewFrame();
ImGui_ImplOpenGL3_NewFrame();
ImGui::NewFrame();
ImGui::Begin("Window");
ImGui::Text("Hello");
ImGui::End();
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
}
문제 2: 백엔드 초기화 누락
증상: 창은 뜨지만 마우스/키보드 입력이 안 되거나, 화면이 검게만 보임.
원인: ImGui_ImplGlfw_InitForOpenGL()과 ImGui_ImplOpenGL3_Init() 중 하나만 호출하거나, 둘 다 호출하지 않음.
// ❌ 잘못된 예
ImGui::CreateContext();
ImGui_ImplGlfw_InitForOpenGL(window, true);
// ImGui_ImplOpenGL3_Init() 누락 → 렌더링 안 됨
해결법:
// ✅ 올바른 예
ImGui::CreateContext();
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init("#version 130");
문제 3: 매 프레임 호출 누락 — UI가 사라짐
증상: 첫 프레임에만 UI가 보이고, 다음 프레임부터 사라짐.
원인: ImGui는 즉시 모드이므로 위젯 코드를 매 프레임 호출해야 함. 조건문 안에 넣어서 한 번만 호출한 경우.
// ❌ 잘못된 예
bool first_frame = true;
if (first_frame) {
ImGui::Begin("Window");
ImGui::Text("Hello");
ImGui::End();
first_frame = false;
}
해결법:
// ✅ 올바른 예: 매 프레임 호출
ImGui::Begin("Window");
ImGui::Text("Hello");
ImGui::End();
문제 4: SliderFloat/InputText에 지역 변수 참조
증상: 슬라이더를 움직여도 값이 바뀌지 않거나, 다음 프레임에 0으로 리셋됨.
원인: float value = 0.5f처럼 매 프레임 새로 초기화되는 지역 변수에 참조를 넘김.
// ❌ 잘못된 예
void render() {
float value = 0.5f;
ImGui::SliderFloat("Value", &value, 0.0f, 1.0f);
use(value); // 매 프레임 0.5만 전달됨
}
해결법:
// ✅ 올바른 예: static 또는 멤버 변수
static float value = 0.5f;
void render() {
ImGui::SliderFloat("Value", &value, 0.0f, 1.0f);
use(value);
}
문제 5: Begin/End 불일치
증상: 레이아웃 깨짐, 크래시, 또는 “Begin/End mismatch” 경고.
원인: ImGui::Begin() 호출 후 ImGui::End()를 호출하지 않거나, early return 시 End 누락.
// ❌ 잘못된 예
void render() {
ImGui::Begin("Window");
if (error) return; // End() 호출 안 됨!
ImGui::Text("Hello");
ImGui::End();
}
해결법:
// ✅ 올바른 예: RAII 또는 항상 End 호출
void render() {
if (ImGui::Begin("Window")) {
if (!error) {
ImGui::Text("Hello");
}
ImGui::End(); // Begin이 true를 반환했으면 항상 End
}
}
문제 6: PlotLines에 빈 벡터 전달
증상: PlotLines 호출 시 크래시.
원인: data가 nullptr이거나 values_count가 0인데 PlotLines 호출.
// ❌ 잘못된 예
std::vector<float> data; // 비어 있음
ImGui::PlotLines("Label", data.data(), data.size(), ...); // size=0, data.data() 유효하지 않을 수 있음
해결법:
// ✅ 올바른 예
if (!data.empty()) {
ImGui::PlotLines("Label", data.data(), (int)data.size(), ...);
}
문제 7: UTF-8 한글 깨짐
증상: ImGui::Text("한글") 출력 시 깨짐.
원인: 소스 파일이 UTF-8이 아니거나, 폰트에 한글 글리프가 없음.
해결법:
- 소스 파일을 UTF-8로 저장.
- 한글 폰트 로드:
io.Fonts->AddFontFromFileTTF("NotoSansKR-Regular.ttf", 18.0f, nullptr, io.Fonts->GetGlyphRangesKorean());
8. 베스트 프랙티스
1. 창 표시/숨김은 플래그로
디버그 창을 F1 키로 토글할 때, 창을 “삭제”하지 말고 bool show_debug = true 같은 플래그로 제어합니다.
static bool show_debug = true;
if (ImGui::IsKeyPressed(ImGuiKey_F1))
show_debug = !show_debug;
if (show_debug) {
ImGui::Begin("Debug", &show_debug);
// ...
ImGui::End();
}
2. 고유 ID로 위젯 충돌 방지
같은 레이블을 가진 위젯이 여러 개 있으면 ID가 겹쳐서 동작이 꼬일 수 있습니다. ImGui::PushID() / PopID() 또는 ##id 접미사로 구분합니다.
for (int i = 0; i < items.size(); ++i) {
ImGui::PushID(i);
ImGui::SliderFloat("Value", &items[i].value, 0.0f, 1.0f);
ImGui::PopID();
}
// 또는
ImGui::SliderFloat("Value##1", &v1, 0.0f, 1.0f);
ImGui::SliderFloat("Value##2", &v2, 0.0f, 1.0f);
3. CollapsingHeader로 섹션 접기
위젯이 많을 때 CollapsingHeader로 섹션을 나누면 가독성이 좋아집니다.
if (ImGui::CollapsingHeader("네트워크")) {
ImGui::Text("Connections: %d", stats.connections);
ImGui::PlotLines("Latency", ...);
}
if (ImGui::CollapsingHeader("메모리")) {
ImGui::Text("Usage: %.1f MB", stats.memory_mb);
}
4. 테이블은 BeginTable/EndTable
여러 열을 정렬해 표시할 때 Columns보다 BeginTable이 권장됩니다.
if (ImGui::BeginTable("Stats", 3)) {
ImGui::TableSetupColumn("Name");
ImGui::TableSetupColumn("Value");
ImGui::TableSetupColumn("Unit");
ImGui::TableHeadersRow();
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::Text("CPU");
ImGui::TableSetColumnIndex(1); ImGui::Text("%.1f", cpu);
ImGui::TableSetColumnIndex(2); ImGui::Text("%%");
ImGui::EndTable();
}
5. 스타일은 한 곳에서 설정
색상·폰트 등은 초기화 시 한 번만 설정하고, 런타임에 자주 바꾸지 않습니다.
void setupImguiStyle() {
ImGui::StyleColorsDark();
ImGuiStyle& style = ImGui::GetStyle();
style.WindowRounding = 5.0f;
style.FrameRounding = 3.0f;
}
6. InputText에 std::string 사용
ImGui::InputText는 char buf[256] 대신 ImGui::InputText의 오버로드로 std::string을 쓸 수 있습니다. C++17 이상에서는 ImGui::InputText와 함께 ImGuiInputTextFlags_CallbackResize를 사용해 동적 버퍼를 관리합니다.
// ImGui 1.80+ 에서는 InputTextWithHint, InputText 등이 std::string 지원
static std::string text_buffer;
char buf[256];
strncpy(buf, text_buffer.c_str(), sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
if (ImGui::InputText("Label", buf, sizeof(buf))) {
text_buffer = buf;
}
7. 툴팁으로 설명 추가
위젯에 마우스를 올리면 설명이 나오게 하려면 ImGui::SetItemTooltip 또는 ImGui::BeginTooltip/EndTooltip을 사용합니다.
ImGui::SliderFloat("Gravity", &gravity, -20.0f, 0.0f);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("중력 가속도 (m/s^2). 음수면 아래 방향.");
9. 프로덕션 패턴
패턴 1: 디버그 UI 레이어 분리
게임/엔진에서는 “항상 보이는 UI”와 “디버그 전용 UI”를 분리합니다. 디버그 UI는 #ifdef DEBUG 또는 show_debug 플래그로 빌드/실행 시에만 포함합니다.
void renderUI() {
renderGameHUD(); // 항상 표시 (체력, 점수 등)
#ifdef IMGUI_DEBUG
if (g_showDebugOverlay)
renderDebugOverlay();
#endif
}
패턴 2: 설정 저장/로드 (ini)
ImGui는 io.IniFilename을 설정하면 창 위치·크기를 자동으로 ini 파일에 저장합니다.
ImGuiIO& io = ImGui::GetIO();
io.IniFilename = "imgui.ini"; // nullptr이면 저장 비활성화
패턴 3: 멀티뷰포트 (Docking)
에디터처럼 여러 창을 탭/도킹하려면 ImGuiConfigFlags_DockingEnable을 켜고, DockSpaceOverViewport()를 사용합니다.
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
// ...
ImGui::DockSpaceOverViewport(ImGui::GetMainViewport());
패턴 4: 스레드 안전성
ImGui는 스레드 안전하지 않습니다. UI 코드는 반드시 메인 스레드에서만 호출하고, 다른 스레드에서 수집한 데이터는 락/큐를 통해 메인 스레드로 전달한 뒤 UI에서 표시합니다.
// 워커 스레드
void workerThread() {
float cpu = getCpuUsage();
g_statsQueue.push(cpu); // 락 보호 큐
}
// 메인 스레드 (렌더 루프)
void mainThread() {
float cpu;
if (g_statsQueue.try_pop(cpu))
g_displayCpu = cpu;
ImGui::Text("CPU: %.1f%%", g_displayCpu);
}
패턴 5: 성능 — 클리핑
창이 화면 밖에 있거나 최소화되어 있으면 ImGui가 자동으로 그리기를 건너뜁니다. 수천 개 위젯이 있을 때는 ImGuiListClipper로 가상 스크롤을 적용해 보이지 않는 항목은 그리지 않습니다.
ImGuiListClipper clipper;
clipper.Begin(1000); // 1000개 항목
while (clipper.Step()) {
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) {
ImGui::Text("Item %d", i);
}
}
패턴 6: 에러 처리 및 복구
ImGui 자체는 예외를 던지지 않지만, 백엔드(OpenGL 등)에서 문제가 생기면 크래시할 수 있습니다. 초기화 실패 시 명확한 에러 메시지를 출력하고 종료합니다.
if (!ImGui_ImplOpenGL3_Init(glsl_version)) {
fprintf(stderr, "ImGui OpenGL3 초기화 실패. OpenGL 3.0+ 필요.\n");
return 1;
}
패턴 7: 플랫폼별 백엔드 선택
Windows에서는 DirectX 11, macOS에서는 Metal, Linux에서는 OpenGL 또는 Vulkan을 쓰는 식으로 플랫폼에 맞는 백엔드를 선택할 수 있습니다. ImGui는 동일한 API를 제공하므로, 백엔드만 교체하면 됩니다.
#ifdef _WIN32
#include "imgui_impl_win32.h"
#include "imgui_impl_dx11.h"
#elif __APPLE__
#include "imgui_impl_osx.h"
#include "imgui_impl_metal.h"
#else
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#endif
다음 단계로 나아가기
이 글을 마스터했다면:
- 고급 위젯: Tree, Table, Docking, Multi-viewport
- 커스텀 렌더링: DrawList API로 직접 그리기
- 스타일링: 색상, 폰트, 테마 커스터마이징
관련 글: 비동기 이벤트 루프(#29-2), 멀티스레드 서버(#29-3)
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 크로스 플랫폼 GUI | Qt 기초 완벽 가이드 [#36-2]
- C++ Asio 데드락 디버깅 | 비동기 콜백 실전 [#49-3]
- C++ GDB/LLDB | cout 100개 찍어도 못 찾은 버그, 디버거로 5분 만에 해결
이 글에서 다루는 키워드 (관련 검색어)
Dear ImGui 튜토리얼, ImGui 사용법, C++ GUI 라이브러리, 즉시 모드 GUI, 게임 개발 UI, C++ 디버깅 툴, ImGui 예제, GLFW ImGui, OpenGL ImGui, C++ 대시보드 등으로 검색하시면 이 글이 도움이 됩니다.
정리
- Dear ImGui는 즉시 모드 GUI로, 매 프레임 위젯을 호출하고 반환값·참조로 상호작용합니다. 가볍고, 백엔드만 붙이면 C++ 한 곳에서 툴 UI를 만들 수 있습니다.
- GLFW + OpenGL 등 공식 예제 백엔드를 쓰면, Begin/End 안에 Text, Button, SliderFloat 등을 넣어 디버깅 툴·상태 대시보드를 빠르게 만들 수 있습니다.
- NewFrame → 위젯 → Render 순서를 반드시 지키고, 슬라이더/입력에는 프레임 간 유지되는 변수의 참조를 넘깁니다.
- 게임·엔진·서버 쪽에서 “눈에 보이는 결과물”이 필요할 때 ImGui로 시작하면 체류 시간과 만족도 모두 올리기 좋습니다.
구현 체크리스트
- ImGui 소스 및 백엔드 파일 프로젝트에 포함
- GLFW 창 생성 및 OpenGL 컨텍스트
-
ImGui::CreateContext(),ImGui_ImplGlfw_InitForOpenGL,ImGui_ImplOpenGL3_Init호출 - 매 프레임
NewFrame→ 위젯 →Render→RenderDrawData순서 준수 - SliderFloat/InputText 등에 static 또는 멤버 변수 참조 사용
-
Begin/End쌍 일치, early return 시End누락 주의 - PlotLines에 빈 벡터 전달 전
empty()체크
실전 체크리스트
실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.
코드 작성 전
- 이 기법이 현재 문제를 해결하는 최선의 방법인가?
- 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
- 성능 요구사항을 만족하는가?
코드 작성 중
- 컴파일러 경고를 모두 해결했는가?
- 엣지 케이스를 고려했는가?
- 에러 처리가 적절한가?
코드 리뷰 시
- 코드의 의도가 명확한가?
- 테스트 케이스가 충분한가?
- 문서화가 되어 있는가?
이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 게임·툴 제작자들이 쓰는 가볍고 빠른 ImGui. 나만의 디버깅 툴이나 상태 모니터링 대시보드를 C++로 만드는 과정을 연재합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. Qt와 ImGui 중 뭘 써야 하나요?
A. 독립된 데스크톱 앱(설정 창, 메인 윈도우 등)이 필요하면 Qt가 적합합니다. 게임/엔진 내부에 붙이는 디버그 UI, 프로파일러, 툴 패널이면 ImGui가 더 가볍고 빠릅니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. Dear ImGui 공식 저장소의 imgui_demo.cpp에 모든 위젯 예제가 있습니다. cppreference와 해당 라이브러리 공식 문서를 참고하세요.
한 줄 요약: Dear ImGui로 가벼운 툴·대시보드 UI를 빠르게 만들 수 있습니다. 다음으로 Qt 기초(#36-2)를 읽어보면 좋습니다.
다음 글: [C++ GUI #36-2] 크로스 플랫폼 GUI: Qt 기초 찍어먹기
이전 글: [C++ 실무 융합 #35-2] WebAssembly(Wasm)와 Emscripten: C++을 브라우저에서 돌리기
관련 글
- C++ 크로스 플랫폼 GUI | Qt 기초 완벽 가이드 [#36-2]
- C++ Python과 C++의 만남 | pybind11으로 고성능 엔진 만들기 [#35-1]
- C++ WebAssembly(Wasm)와 Emscripten | C++을 브라우저에서 돌리기 [#35-2]
- C++ 남들보다 먼저 써보는 C++23 핵심 기능 [#37-1]
- C++ std::filesystem 완벽 가이드 | 경로·디렉토리·파일·권한 한 번에 정리