C++ 현대적인 GUI: Dear ImGui로 디버깅 툴·대시보드 만들기
들어가며: 콘솔만 보다가, 화면에 무언가 띄우고 싶습니다
서버·콘솔 로그만 보던 프로그램에 실시간으로 값이 바뀌는 창을 띄우고 싶을 때가 있습니다. 게임 엔진 내부 값, 서버 상태, 물리 파라미터, 네트워크 통계를 창과 버튼·슬라이더로 보면서 바로 조정할 수 있으면 디버깅과 튜닝이 훨씬 수월해집니다. 물리 엔진의 중력이나 마찰 계수를 바꿀 때마다 코드 수정, 재컴파일, 실행을 반복하는 대신 슬라이더로 값을 움직이며 결과를 보는 식입니다. printf로 찍은 CPU 사용률·연결 수가 로그에 묻혀 최신 값을 찾기 어려운 문제도, 숫자와 그래프를 한 창에 띄우면 사라집니다.
Dear ImGui는 이런 내장 디버그 UI를 만들기 위한 즉시 모드(Immediate Mode) GUI 라이브러리입니다. 위젯 객체를 만들어 두고 이벤트 콜백을 연결하는 대신, 매 프레임 “여기에 버튼을 그려라”라고 함수를 호출하고 그 반환값으로 클릭 여부를 확인합니다. 소스 파일 몇 개를 프로젝트에 넣고 렌더링 백엔드만 연결하면 되므로, Qt 같은 프레임워크를 들이기 부담스러운 게임·엔진·툴 안에 붙이기 좋습니다.
이 글은 즉시 모드 개념을 짚은 뒤, GLFW + OpenGL 3 백엔드로 창을 띄우고 버튼·슬라이더·그래프로 변수를 노출하는 디버그 대시보드를 만들고, 처음 붙일 때 흔히 겪는 문제를 정리합니다.
Dear ImGui란
특징
Dear ImGui는 헤더 전용 라이브러리는 아니지만, 핵심이 imgui.cpp, imgui_draw.cpp, imgui_tables.cpp, imgui_widgets.cpp(데모가 필요하면 imgui_demo.cpp) 몇 개의 소스 파일이라 별도 빌드 시스템 없이 프로젝트에 그대로 넣어 컴파일합니다. 창 생성·입력·실제 그리기는 하지 않고, 이 부분은 backends/ 디렉터리의 플랫폼 백엔드(GLFW, SDL, Win32 등)와 렌더러 백엔드(OpenGL, DirectX, Vulkan, Metal 등)가 맡습니다. 그래서 에디터 내부 툴, 디버그 오버레이, 프로파일러 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
즉시 모드(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 컨트롤 같은 유지 모드(retained mode) GUI는 버튼을 객체로 만들어 두고 이벤트 콜백으로 반응합니다. UI 상태가 위젯 객체 안에 있으므로, 애플리케이션 데이터와 위젯 상태를 서로 동기화하는 코드가 필요합니다. 즉시 모드에서는 매 프레임 if (ImGui::Button("Click")) { ... }처럼 위젯을 그리는 호출과 입력 처리가 한 줄에 함께 있고, 슬라이더 값 같은 상태는 사용자가 넘긴 변수 자체입니다. 창 위치나 펼침 여부 같은 일부 UI 상태만 라이브러리가 ID별로 내부에 기억합니다. 그래서 데이터와 화면이 어긋날 일이 없는 대신, 매 프레임 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
최소 예제: 창 하나 띄우기
기본 흐름 (백엔드가 준비된 상태 가정)
매 프레임 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 사이에 호출한 위젯은 그 창에 그려집니다. Button은 이번 프레임에 클릭되었으면 true를 반환하고, SliderFloat은 넘겨받은 float 변수를 직접 읽고 씁니다. ImGui::Render()는 그리기 명령 목록(draw data)을 만들기만 하고, 실제 GPU 호출은 렌더러 백엔드의 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);
}
백엔드 연동 (GLFW + OpenGL)
GLFW로 창과 OpenGL 컨텍스트를 만들고 입력을 받으며, ImGui 핵심 소스와 함께 backends/imgui_impl_glfw.cpp, backends/imgui_impl_opengl3.cpp를 빌드합니다.
초기화 순서
- 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/ 에서 사용하는 백엔드와 동일한 파일을 프로젝트에 넣으면 됩니다.
공식 예제로 먼저 확인하기
# ImGui 저장소 클론
git clone https://github.com/ocornut/imgui.git
cd imgui/examples/example_glfw_opengl3
# 빌드 (Linux/macOS)
make
# 실행
./example_glfw_opengl3
Linux·macOS에서는 GLFW가 설치되어 있어야 합니다(예: sudo apt install libglfw3-dev, brew install glfw). Windows에서는 examples/imgui_examples.sln을 Visual Studio로 열어 빌드하면 됩니다. 이 예제가 돌아가면 개발 환경과 그래픽 드라이버 쪽 문제는 없는 것이므로, 직접 만든 코드가 안 될 때 비교 기준으로 쓸 수 있습니다.
GLFW + OpenGL 3 전체 main.cpp와 CMake 빌드
전체 main.cpp (GLFW + OpenGL 3)
아래 코드를 복사해 프로젝트에 넣으며, ImGui 소스 및 백엔드 파일을 포함하면 바로 실행할 수 있습니다.
// main_imgui_minimal.cpp
// 빌드 (Linux, imgui 저장소 루트에서):
// g++ -std=c++17 main_imgui_minimal.cpp imgui.cpp imgui_draw.cpp imgui_tables.cpp
// imgui_widgets.cpp backends/imgui_impl_glfw.cpp backends/imgui_impl_opengl3.cpp
// -I. -Ibackends -lglfw -lGL -ldl -o imgui_demo
#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include <GLFW/glfw3.h>
#include <cmath>
#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;
}
slider_value, checkbox_value, click_count는 렌더 루프 밖에 선언되어 프레임이 바뀌어도 값이 유지됩니다. plot_data는 매 프레임 맨 앞을 지우고 뒤에 새 값을 넣어 그래프가 흘러가는 효과를 냅니다(데이터가 많다면 vector::erase(begin()) 대신 원형 버퍼와 PlotLines의 offset 인자를 쓰는 편이 낫습니다). IMGUI_CHECKVERSION()은 헤더와 컴파일된 ImGui 소스의 버전·구조체 크기가 맞는지 확인합니다.
두 가지를 주의해야 합니다. 첫째, 예제의 창 제목과 라벨은 한글인데 ImGui 기본 폰트에는 한글 글리프가 없어서 이대로 실행하면 ?로 표시됩니다. 아래 “한글 깨짐” 항목처럼 한글 폰트를 로드하거나 라벨을 영문으로 바꿔야 합니다. 둘째, macOS는 OpenGL 3.2 Core 프로필 이상만 지원하므로, 공식 예제처럼 GLFW_CONTEXT_VERSION 3.2, GLFW_OPENGL_CORE_PROFILE, GLFW_OPENGL_FORWARD_COMPAT를 지정하고 GLSL 버전을 "#version 150"으로 바꿔야 창이 뜹니다.
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}/backends/imgui_impl_glfw.cpp
${IMGUI_DIR}/backends/imgui_impl_opengl3.cpp
)
add_executable(imgui_demo main.cpp ${IMGUI_SOURCES})
target_include_directories(imgui_demo PRIVATE ${IMGUI_DIR} ${IMGUI_DIR}/backends)
target_link_libraries(imgui_demo PRIVATE glfw OpenGL::GL)
cmake -B build -S .
cmake --build build
./build/imgui_demo
ImGui 1.80부터 백엔드 파일은 저장소 루트가 아니라 backends/ 디렉터리에 있습니다. 오래된 글의 경로를 그대로 쓰면 imgui_impl_glfw.cpp를 찾지 못합니다.
서버 모니터링 대시보드와 파라미터 튜닝 패널
변수 노출
게임/서버의 숫자·플래그를 전역이나 클래스 멤버로 두며, 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);
}
renderServerDashboard는 렌더 스레드에서 호출되므로, 서버 스레드가 ServerStats를 갱신한다면 둘 사이에 락이나 스냅샷 복사가 필요합니다(아래 스레드 안전성 항목 참고).
물리 파라미터 튜닝 패널
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으로 시계열과 분포를 그릴 수 있습니다. 더 본격적인 그래프가 필요하면 ImGui 위에서 동작하는 별도 라이브러리인 ImPlot을 씁니다.
NewFrame/Render 순서, Begin/End 불일치, 한글 깨짐 같은 에러
문제 1: NewFrame/Render 순서 오류로 크래시
ImGui::NewFrame() 없이 위젯을 호출하거나 ImGui::Render() 없이 프레임을 끝내면, 크래시가 나거나 화면에 아무것도 그려지지 않습니다. 매 프레임 순서는 입력 처리, 백엔드 NewFrame, ImGui::NewFrame, 위젯, Render, RenderDrawData, 버퍼 교체입니다.
// 잘못된 예
while (running) {
ImGui::Begin("Window"); // NewFrame() 없음 → 크래시 가능
ImGui::Text("Hello");
ImGui::End();
// Render() 없음 → 그려지지 않음
}
// 올바른 예
while (running) {
glfwPollEvents();
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
ImGui::Begin("Window");
ImGui::Text("Hello");
ImGui::End();
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
glfwSwapBuffers(window);
}
NewFrame() 없이 위젯을 호출하면 디버그 빌드에서는 assert로 바로 멈춥니다. ImGui는 잘못된 사용을 예외 대신 IM_ASSERT로 알려 주므로, 디버그 빌드에서 assert 메시지를 읽는 것이 원인을 찾는 가장 빠른 길입니다.
문제 2: 백엔드 초기화 누락
ImGui는 플랫폼 백엔드(입력·창)와 렌더러 백엔드(그리기)를 각각 초기화해야 합니다. ImGui_ImplGlfw_InitForOpenGL()과 ImGui_ImplOpenGL3_Init() 중 하나라도 빠지면 입력이 안 먹거나 아무것도 그려지지 않고, 디버그 빌드에서는 백엔드가 없다는 assert가 납니다.
// 잘못된 예
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가 사라집니다.
// 잘못된 예
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: Begin/End 불일치
Begin() 뒤에 End()를 빠뜨리면(특히 중간의 early return에서) 창 스택이 어긋나, 디버그 빌드에서 “Missing End()” 계열 assert가 나거나 다른 창의 위젯이 엉뚱한 창에 그려집니다.
// 잘못된 예
void render() {
ImGui::Begin("Window");
if (error) return; // End() 호출 안 됨!
ImGui::Text("Hello");
ImGui::End();
}
// 올바른 예: Begin의 반환값과 관계없이 End는 항상 호출
void render() {
if (ImGui::Begin("Window")) { // false면 창이 접혔거나 가려진 것: 내용만 건너뜀
if (!error) {
ImGui::Text("Hello");
}
}
ImGui::End();
}
Begin()이 false를 반환해도 End()는 반드시 호출해야 합니다. 반환값은 “내용을 그릴 필요가 있는가”만 알려 줄 뿐입니다. 반대로 BeginTable, BeginPopup, BeginMenu, TreeNode 같은 함수는 true를 반환했을 때만 짝이 되는 End*/TreePop을 호출해야 해서 규칙이 서로 다르므로 헷갈리기 쉽습니다. (BeginChild는 1.90 이전에는 Begin처럼 항상 EndChild를 호출해야 했습니다.)
문제 5: 한글 깨짐
ImGui::Text("한글")이 ?나 깨진 글자로 나온다면 소스 문자열이 UTF-8이 아니거나, 로드한 폰트에 한글 글리프가 없는 경우입니다.
ImGui는 모든 문자열을 UTF-8로 받습니다. 먼저 소스 파일을 UTF-8로 저장하고, MSVC라면 /utf-8 옵션으로 컴파일해 문자열 리터럴이 CP949로 바뀌지 않게 합니다. 그다음 한글 글리프가 있는 폰트를 로드합니다.
ImGuiIO& io = ImGui::GetIO();
io.Fonts->AddFontFromFileTTF("NotoSansKR-Regular.ttf", 18.0f, nullptr,
io.Fonts->GetGlyphRangesKorean());
1.92 이전 버전은 폰트 아틀라스를 미리 굽기 때문에 글리프 범위를 지정해야 하고, 한글 전체 범위는 아틀라스가 꽤 커집니다. 1.92부터는 필요한 글리프를 동적으로 굽는 방식으로 바뀌어 범위 지정이 필요 없습니다. 사용하는 버전의 docs/FONTS.md를 확인하세요.
고유 ID, CollapsingHeader, BeginTable로 UI 정리하기
창 표시/숨김은 플래그로
디버그 창을 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();
}
고유 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);
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);
}
테이블은 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();
}
스타일은 한 곳에서 설정
색상·폰트 등은 초기화 시 한 번만 설정하며, 런타임에 자주 바꾸지 않습니다.
void setupImguiStyle() {
ImGui::StyleColorsDark();
ImGuiStyle& style = ImGui::GetStyle();
style.WindowRounding = 5.0f;
style.FrameRounding = 3.0f;
}
InputText에 std::string 사용
핵심 API의 ImGui::InputText는 char 버퍼와 크기를 받습니다. std::string을 직접 쓰려면 저장소에 함께 들어 있는 misc/cpp/imgui_stdlib.cpp를 빌드에 추가하고 misc/cpp/imgui_stdlib.h를 포함합니다. 이 파일이 ImGuiInputTextFlags_CallbackResize를 이용해 입력 길이에 맞춰 문자열 크기를 늘려 주는 오버로드를 제공합니다.
#include "misc/cpp/imgui_stdlib.h"
static std::string name;
ImGui::InputText("Name", &name); // 포인터로 넘김
툴팁으로 설명 추가
위젯에 마우스를 올리면 설명이 나오게 하려면 ImGui::SetItemTooltip 또는 ImGui::BeginTooltip/EndTooltip을 사용합니다.
ImGui::SliderFloat("Gravity", &gravity, -20.0f, 0.0f);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("중력 가속도 (m/s^2). 음수면 아래 방향.");
디버그 UI 레이어 분리, ini 저장, Docking, 스레드 안전성
패턴 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: 도킹
에디터처럼 여러 창을 탭으로 묶거나 붙이려면 도킹 기능이 필요합니다. 도킹은 master 브랜치가 아니라 docking 브랜치에 있으므로 그 브랜치를 받아야 하고, ImGuiConfigFlags_DockingEnable을 켠 뒤 매 프레임 메인 뷰포트 전체를 도킹 영역으로 만듭니다.
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
// 매 프레임, NewFrame() 직후
ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport()); // 1.90.x 이후 시그니처
DockSpaceOverViewport의 인자 순서는 버전에 따라 바뀌었으므로 사용하는 버전의 헤더를 확인하세요. ImGui 창을 OS 창 밖으로 꺼내는 멀티 뷰포트는 별도 플래그(ImGuiConfigFlags_ViewportsEnable)이며, 백엔드 쪽 추가 처리가 필요합니다.
패턴 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: 성능 — 클리핑
창이 접혀 있거나 완전히 가려지면 Begin()이 false를 반환하므로 그 안의 위젯 호출을 통째로 건너뛸 수 있습니다. 하지만 펼쳐진 창 안에서는 화면 밖에 있는 위젯이라도 호출 비용은 듭니다. 수천 개 항목이 있을 때는 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
다음 단계
트리(TreeNode), 테이블, 도킹 같은 고급 위젯과 ImDrawList로 직접 도형을 그리는 커스텀 렌더링, 색상·폰트 테마 조정이 다음 주제입니다. 저장소의 imgui_demo.cpp는 모든 위젯의 사용 예를 담고 있어, ImGui::ShowDemoWindow()를 띄워 놓고 원하는 위젯의 소스를 찾아보는 것이 가장 빠른 학습 방법입니다.
관련 글: 비동기 이벤트 루프(#29-2), 멀티스레드 서버(#29-3)
같이 보면 좋은 글
- Qt로 C++ 첫 GUI 만들기
- C++ Asio 데드락 디버깅 | 비동기 콜백 실전 [#49-3]
- C++ 디버깅 기초 | GDB·LLDB 브레이크포인트·워치포인트·단계 실행
- C++ Python과 C++의 만남 | pybind11으로 고성능 엔진 만들기 [#35-1]
- C++ WebAssembly(Wasm)와 Emscripten | C++을 브라우저에서 돌리기 [#35-2]
- C++ 미리 사용해 보는 C++23 핵심 기능 [#37-1]
- Windows에서만 파일을 못 찾을 때