C++ 코드를 입력하면 실제 g++ 컴파일러로 컴파일 및 실행하고, GDB MI를 통해 런타임 메모리 연산을 추적하여 자료구조의 변화를 실시간 애니메이션으로 시각화하는 웹 애플리케이션입니다.
C++ 코드 --> g++ 컴파일 --> GDB MI 세션 --> 라인별 스냅샷 --> gdbMapper --> TraceStep[] --> 시각화
(-g -O0) (--interpreter=mi2) (지역변수 + 구조체 필드) (ALLOC/SET_PTR/ (React + Framer Motion)
SET_FIELD/LOCAL_VAR)
- 사용자가 C++ 코드를 작성 (커스텀 struct / 포인터 포함)
- 백엔드가 디버그 심볼 포함으로 컴파일 (
g++ -g -O0) - GDB가 MI 모드로 바이너리를 한 줄씩 실행하며 지역변수와 struct 필드값을 매 단계마다 캡처
gdbMapper가 GDB 스냅샷을TraceStep[]이벤트로 변환 (ALLOC, SET_PTR, SET_FIELD, LOCAL_VAR)- 런타임 패턴 분석 엔진이 실제 포인터 그래프를 구축하고, 그래프 토폴로지(분기도, 순환, 깊이)로 자료구조를 자동 감지
- 프론트엔드에서 커맨드를 애니메이션으로 재생
두 번째 추적 경로는 없습니다. 프로그램을 컴파일해 실행하고 그 출력만 돌려줍니다. steps는 비어 있고 시각화는 아무것도 그리지 않으며, 응답의 notice가 그 사실을 알립니다.
예전에는 사용자 소스에 추적 함수를 주입하는 계측기(instrumenter) 폴백이 있었습니다. 혼자서는 멀쩡히 컴파일되는 프로그램에 대해 컴파일되지 않는 C++를 뱉는 일이 있어 제거했습니다.
- 실제 C++ 실행: g++ 백엔드를 통해 실제 C++ 코드를 컴파일하고 실행합니다. 시뮬레이션이나 의사코드 파싱이 아닙니다.
- GDB 기반 추적: GDB Machine Interface(MI)로 코드를 한 줄씩 스텝하며 정확한 메모리 상태를 캡처합니다. 소스 코드 변환이 필요 없습니다.
- 스마트 자동 감지: 2단계 감지 시스템:
- 정적 분석: 메서드명 (
push/pop= Stack) 및 자기 타입 포인터 수 (2+ = Tree, 1 = Linked List) - 런타임 분석: 실행 트레이스에서 실제 포인터 그래프를 구축하고, 그래프 속성(분기, 순환, 깊이)으로 재분류
- 정적 분석: 메서드명 (
- 메모리 그래프 시각화: 포인터(
->), 할당(new), 해제(delete)를 추적하여 메모리 관계를 그래프로 표현 - 단계별 실행: Run / Pause / Step / Prev 컨트롤로 정밀한 상태 추적
- 속도 조절: 0.25x ~ 4x 재생 속도
- 리사이즈 패널: 에디터, 시각화, 터미널 패널 크기 조절 가능
- 인터랙티브 뷰: 드래그로 패닝, Ctrl+스크롤로 확대/축소
- C++ 자동완성: C++ 키워드, 타입, STL 컨테이너에 대한 IDE 스타일 코드 완성
- 터미널 출력:
cout실시간 출력 및cin입력 지원
| 자료구조 | 색상 | 감지 방식 | 시각화 |
|---|---|---|---|
| Stack | Purple | push() + pop() 메서드, 또는 std::stack/vector |
수직 플레이트 적층, TOP 표시 |
| Queue | Cyan | enqueue() + dequeue() 메서드, 또는 std::queue/deque/priority_queue |
수평 컨베이어 벨트, FRONT/BACK 라벨 |
| Tree (BST / N-ary) | Green | 자기 타입 포인터 2+개 또는 런타임 분기 패턴 (parent 포인터 허용) | 계층 버킷 레이아웃, ROOT 뱃지, 드래그 패닝 |
| Linked List | Purple | 자기 타입 포인터 1개, 선형 체인 | 화살표 연결 그래프 뷰 |
| 이중 연결 리스트(Doubly LL) | Cyan / Amber | next/prev 양방향 페어를 가진 선형 체인 |
수평 체인 — solid cyan next + dashed amber prev |
| 원형 연결 리스트 | Amber | 런타임 사이클 감지 (출차수 1 + 루프) | 다각형 배치, HEAD 뱃지, amber 점선 cycle-back 엣지 |
| 일반 그래프(General Graph) | Rose | back-edge 제거 후에도 분기+사이클이 남는 경우 | 모든 엣지가 표시되는 그래프 뷰 |
| Memory (Heap) | Purple | 포인터 구조 폴백 | 메모리 주소 기반 그래프 뷰 |
정적 힌트 (컴파일 타임) 런타임 분석 (실행 후, GDB 모드)
push/pop 메서드 → Stack (이미 분류됨)
enqueue/dequeue → Queue (이미 분류됨)
std::stack/vector → Stack
std::queue/deque/priority_queue → Queue
┌─ 한 필드의 양방향 페어를 제거 ─┐
│ (= back-edge / prev / parent) │
└────────────────┬──────────────┘
▼
primary 그래프 사이클 + 분기 → Graph
primary 그래프 사이클 + 출차수 ≤ 1 → 원형 연결 리스트
primary 그래프 비순환 + 분기 → Tree
primary 그래프 비순환 + back-edge 존재 → Doubly Linked List
primary 그래프 비순환 + 단순 체인 → Linked List
필드명 무관: left/right, a/b, child1/child2, ptr1/ptr2, next/prev, child/parent — 실제 런타임 포인터 토폴로지에서 구조를 감지합니다.
지원 C++ 패턴: struct, class, private/public/protected, friend class, 생성자 초기화 리스트, const/static 한정자, 배열 필드, 다중 변수 선언, delete[].
지원 값 타입: int, double, string, bool, char
지원 STL 컨테이너:
std::stack, std::queue, std::priority_queue, std::vector, std::deque. 매 스냅샷마다 .size() / .top() / .back()을 평가하고 그 차이에서 PUSH / POP 커맨드를 합성합니다.
#include <iostream>
using namespace std;
struct Stack {
int data[100];
int top;
Stack() : top(-1) {}
void push(int val) { data[++top] = val; }
void pop() { if (top >= 0) top--; }
};
int main() {
Stack s;
s.push(10);
s.push(20);
s.push(30);
s.pop();
s.push(40);
return 0;
}#include <iostream>
using namespace std;
class Node {
private:
int data;
Node* next;
Node(int d) : data(d), next(nullptr) {}
friend class LLQueue;
};
class LLQueue {
private:
Node* front;
Node* rear;
public:
LLQueue() : front(nullptr), rear(nullptr) {}
void enqueue(int val) {
Node* node = new Node(val);
if (!rear) { front = rear = node; return; }
rear->next = node;
rear = node;
}
void dequeue() {
if (!front) return;
Node* temp = front;
front = front->next;
if (!front) rear = nullptr;
delete temp;
}
};
int main() {
LLQueue q;
q.enqueue(100);
q.enqueue(200);
q.enqueue(300);
q.dequeue();
q.enqueue(400);
return 0;
}#include <iostream>
using namespace std;
struct BBB {
int val;
BBB* a;
BBB* b;
BBB(int v) : val(v), a(nullptr), b(nullptr) {}
};
int main() {
BBB* root = new BBB(42);
root->a = new BBB(10);
root->b = new BBB(99);
root->a->a = new BBB(5);
root->a->b = new BBB(25);
return 0;
}#include <iostream>
using namespace std;
struct Node {
int data;
Node* next;
Node(int v) : data(v), next(nullptr) {}
};
int main() {
Node* head = new Node(1);
head->next = new Node(2);
head->next->next = new Node(3);
head->next->next->next = new Node(4);
// 중간 노드 삭제
Node* temp = head->next;
head->next = temp->next;
delete temp;
return 0;
}#include <iostream>
using namespace std;
struct Node {
int data;
Node* next;
Node(int v) : data(v), next(nullptr) {}
};
int main() {
Node* head = new Node(1);
head->next = new Node(2);
head->next->next = new Node(3);
head->next->next->next = head; // head로 다시 연결 (사이클)
return 0;
}#include <iostream>
using namespace std;
struct Node {
int data;
Node* next;
Node* prev;
Node(int v) : data(v), next(nullptr), prev(nullptr) {}
};
int main() {
Node* a = new Node(1);
Node* b = new Node(2);
Node* c = new Node(3);
a->next = b; b->prev = a;
b->next = c; c->prev = b;
return 0;
}#include <stack>
#include <queue>
using namespace std;
int main() {
stack<int> s;
s.push(10); s.push(20); s.push(30);
s.pop();
queue<int> q;
q.push(1); q.push(2); q.push(3);
q.pop();
return 0;
}- Node.js 22 (
.nvmrc에 고정된 버전이며 CI도 이걸 씁니다) - npm 9+
- g++ (Windows: MSYS2, Linux/Mac: 시스템 g++)
- GDB (Windows MSYS2:
pacman -S mingw-w64-ucrt-x86_64-gdb, Linux:apt install gdb, Mac:brew install gdb)
GDB는 필수입니다. 트레이스를 만드는 건 GDB뿐이라, 없으면 시각화할 것 자체가 없습니다. GDB가 없어도 서버는 프로그램을 컴파일해 실행하고 출력을 돌려주지만
steps는 빈 배열이고 화면에는 아무것도 그려지지 않습니다. 폴백 추적 경로는 더 이상 없습니다 — 계측기는 제거됐습니다.
# 프론트엔드
npm install
npm run dev
# 백엔드 (별도 터미널)
cd server
npm install
npm run dev브라우저에서 http://localhost:5173 접속.
숫자 값은 기동 시점에 검증됩니다. 순수한 양의 정수가 아니면(
256M,8s,-1,1e6) 예외를 던지고 서버가 뜨지 않습니다. 바이트 수는 전부 풀어 쓰세요 —RLIMIT_AS_BYTES=268435456,256M은 안 됩니다. 값을 비우거나 지정하지 않으면 기본값을 씁니다. 이건 의도된 동작입니다: 예전 파서는256M을256으로 읽었고, 그 결과 모든 추적 대상이 주소공간 256바이트로 즉사한 뒤 엉뚱한 GDB 오류로 보고됐습니다.
| 변수 | 기본값 | 설명 |
|---|---|---|
VITE_COMPILER_API_URL |
http://localhost:3001 |
백엔드 API URL (프론트엔드) |
PORT |
3001 |
서버 포트 (백엔드) |
GPP_PATH |
자동 감지 | g++ 컴파일러 경로 |
GDB_PATH |
자동 감지 | GDB 디버거 경로 |
USE_GDB |
true |
false면 추적 없이 실행 — 출력만 나오고 steps는 빈 배열이라 아무것도 그려지지 않습니다 |
TEMP_BASE |
자동 탐지 | 컴파일된 바이너리를 쓰고 실행할 디렉터리. /dev/shm이 exec 가능하면 그걸, 아니면 OS 임시 디렉터리를 씁니다 |
FRONTEND_URL |
— | 프로덕션 CORS 허용 도메인 |
MAX_STDIN_BYTES |
65536 |
요청당 stdin 최대 크기 |
MAX_OUTPUT_BYTES |
1048576 |
캡처하는 프로그램 출력 상한 |
GDB_SESSION_BUDGET_MS |
45000 |
추적 세션 하나의 벽시계 예산 |
COMPILE_TIMEOUT_MS |
15000 |
디버그 빌드가 강제 종료되기까지의 시간 |
MAX_CONCURRENT_JOBS |
2 |
동시에 도는 컴파일+추적 작업 수. 각각이 컴파일러 + GDB + 추적 대상 프로그램입니다 |
MAX_QUEUED_JOBS |
8 |
슬롯을 기다릴 수 있는 작업 수. 넘으면 어차피 타임아웃 날 줄에 세우는 대신 503으로 즉시 거절합니다 |
QUEUE_WAIT_MS |
20000 |
대기 중인 작업이 포기하기까지의 시간 |
PISTON_URL |
— | 로컬 g++ 대신 Piston 서버에 실행 위임. 이 경로에는 추적이 없습니다 |
VERBOSE_STEP_LOG |
— | true면 생성된 추적 스텝을 전부 로깅 |
VERBOSE_MI_LOG |
— | true면 GDB/MI 전 라인 출력. 매우 시끄럽고 사용자 소스와 변수 값이 로그에 남습니다 |
RATE_LIMIT_COMPILE |
20 |
IP당 분당 컴파일 요청 수 |
RATE_LIMIT_GENERAL |
120 |
IP당 분당 일반 /api 요청 수 |
RATE_LIMIT_WINDOW_MS |
60000 |
레이트 리밋 윈도(밀리초) |
TRUST_PROXY |
— | 로드밸런서 뒤에 있을 때 실제 클라이언트 IP를 추출하기 위한 Express trust-proxy 값 (1 또는 CIDR 리스트) |
RLIMIT_CPU_SEC |
8 |
프로그램당 CPU 시간 한도(초) — Linux에서 prlimit로 적용 |
RLIMIT_AS_BYTES |
268435456 |
프로그램당 가상 메모리 한도 (기본 256 MiB) |
RLIMIT_STACK_BYTES |
16777216 |
프로그램당 스택 한도 (기본 16 MiB) |
RLIMIT_FSIZE_BYTES |
8388608 |
프로그램이 쓸 수 있는 파일 최대 크기 (8 MiB) |
RLIMIT_NOFILE |
64 |
최대 열린 파일 디스크립터 수 |
RLIMIT_NPROC |
— | 더 이상 쓰지 않습니다. RLIMIT_NPROC은 컨테이너가 아니라 UID 기준으로 시스템 전역에서 세므로, 옆 프로세스가 소진하면 컴파일러가 cc1plus를 fork하지 못합니다. 컨테이너 단위로 거세요 — compose의 pids_limit 또는 docker run --pids-limit |
PRLIMIT_PATH |
/usr/bin/prlimit |
prlimit 경로. 리눅스에서 없으면 서버가 기동을 거부합니다 — util-linux를 설치하거나, 경로를 고치거나, DISABLE_RLIMIT=true로 의도적으로 우회하세요 |
DISABLE_RLIMIT |
— | true면 사용자 코드에 CPU·메모리·파일크기·프로세스 수 제한이 전혀 없는 상태로 기동합니다. cgroup이나 VM으로 이미 갇힌 호스트를 위한 탈출구이고, 그 밖에서는 이 서비스를 무제한 원격 실행 엔드포인트로 만듭니다 |
- GitHub 레포를 Vercel에 연결
VITE_COMPILER_API_URL에 백엔드 URL 설정- 배포
백엔드는 관리형 PaaS가 잘 내주지 않는 두 가지를 필요로 합니다.
- ptrace. 트레이스를 만드는 건 GDB뿐입니다.
- exec 가능한 임시 디렉터리. 컴파일된 바이너리를 쓰고 나서 실행해야 하는데, Docker는
/dev/shm을 기본적으로noexec로 마운트합니다. 그러면 쓰기는 성공하고 실행에서 막힙니다. 서버가 기동 시 이걸 탐지해 OS 임시 디렉터리로 물러나며,TEMP_BASE로 직접 지정할 수도 있습니다.
ptrace가 없으면 프로그램을 컴파일·실행해 출력만 돌려주는 서버가 됩니다. steps는 비어 있고 화면에는 아무것도 안 그려집니다. 시각화 도구가 아니라 실행기입니다. 계측기 폴백은 더 이상 없습니다 — 어떤 플랫폼이 ptrace를 막는다면 답은 다른 설정이 아니라 다른 플랫폼입니다.
이 저장소에는 render.yaml도 Procfile도 없습니다. 대시보드로 설정하는 PaaS에 배포하면 빌드 명령·시작 명령·환경변수가 전부 버전 관리 밖에 있게 됩니다.
이 이미지에 필요한 플래그를 전부 거는 건 server/docker-compose.yml 하나뿐입니다. 위에서 말한 exec tmpfs 마운트를 포함해서요:
cd server
docker compose up compilerread_only, no-new-privileges, 번들된 seccomp 프로파일, mem_limit, cpus, pids_limit을 적용하고 /tmp과 /dev/shm을 exec 가능한 tmpfs로 마운트합니다. 외부에 노출하기 전에 FRONTEND_URL을 프론트엔드 오리진으로 설정하세요. 동등한 docker run 형태는 server/Dockerfile 끝 주석에 있습니다 — 거기 seccomp 경로는 호스트 경로입니다. 컨테이너가 생기기 전에 Docker CLI가 읽기 때문입니다.
서버는 HTTP로 받은 임의의 C++를 컴파일·실행하므로 이 플래그들은 장식이 아니라 필수입니다. prlimit이 프로그램별 CPU·메모리·파일크기·프로세스 수를 막고, 컨테이너 플래그가 prlimit이 못 막는 것을 막습니다. /api/compile에는 레이트 리밋(기본 20 req/min/IP)도 걸려 있고 RATE_LIMIT_*로 조정합니다.
번들 프로파일의 알려진 구멍 — 나중에 발견하지 말고 지금 결정하시라고 적어둡니다. seccomp 프로파일이 socket/connect를 허용하므로 추적 대상 코드가 네트워크에 나갈 수 있습니다. 문제가 된다면 compose 서비스에 network_mode: none을 추가하세요. ptrace도 무조건 허용인데 GDB에 필요해서입니다. 다만 kernel.yama.ptrace_scope=0인 호스트에서는 사용자 코드가 Node 프로세스에 붙을 수도 있습니다 — 둘이 같은 uid로 돌기 때문입니다. compose의 piston 프로파일은 privileged 컨테이너를 띄우며 기본으로 꺼져 있습니다. 네트워크를 격리하기 전에는 켜지 마세요.
src/
├── types.ts # 타입 정의 (Command, State 등)
├── App.tsx # 메인 레이아웃 (리사이즈 패널)
├── api/
│ └── compilerApi.ts # 프론트엔드 -> 백엔드 API 클라이언트
├── engine/
│ └── stepMapper.ts # TraceStep[] -> 커맨드 변환 + 런타임 패턴 분석
├── hooks/
│ └── useVisualizer.ts # 상태 관리 (useReducer)
├── utils/
│ └── ids.ts # ID 생성 유틸리티
└── components/
├── CodeInput.tsx # Monaco C++ 에디터 (자동완성)
├── Controls.tsx # 실행 컨트롤 & 속도 슬라이더
├── Terminal.tsx # 출력 / 입력 / 커맨드 로그 탭
├── Visualizer.tsx # 자료구조별 시각화 라우터
└── DataStructures/
├── StackPlate.tsx # Stack: 수직 플레이트 적층
├── QueueBlock.tsx # Queue: 수평 컨베이어 벨트
├── GraphView.tsx # Memory / Linked List / 일반 그래프: 화살표 그래프
├── TreeChart.tsx # Tree: 계층 버킷 레이아웃 (N-ary)
├── CircularListView.tsx # 원형 연결 리스트: 다각형 레이아웃
└── DoublyListView.tsx # 이중 연결 리스트: forward/back 화살표
server/
├── Dockerfile # 배포용 Docker 이미지 (Node.js + g++ + gdb + util-linux)
├── docker-compose.yml # 지원되는 실행 방법 — 모든 보안 플래그를 적용
├── seccomp-profile.json # `--security-opt seccomp=...` 용 syscall 화이트리스트
└── src/
├── index.ts # Express 서버 (CORS + 레이트 리밋)
├── env.ts # 숫자 설정 엄격 파싱, 잘못된 값이면 기동 시 예외
├── routes/compile.ts # POST /api/compile 엔드포인트
└── services/
├── compiler.ts # g++ 컴파일 + Linux용 prlimit 래퍼
├── childEnv.ts # 환경변수 허용목록 — 사용자 코드가 서버 비밀을 못 봄
├── tempBase.ts # 컴파일된 바이너리를 실행할 exec 가능 디렉터리 선택
├── gdbDriver.ts # GDB MI 드라이버: 스폰/라인 스텝/스냅샷 + exec-wrapper rlimit
└── gdbMapper.ts # GDB 스냅샷 → TraceStep[] (STL 컨테이너 포함)
이 프로젝트는 다음 오픈소스 라이브러리를 사용합니다:
| 라이브러리 | 라이선스 | 설명 |
|---|---|---|
| React | MIT | UI 컴포넌트 라이브러리 |
| React DOM | MIT | 웹용 React 렌더러 |
| TypeScript | Apache-2.0 | JavaScript의 타입 확장 |
| Vite | MIT | 차세대 프론트엔드 빌드 도구 |
| Tailwind CSS | MIT | 유틸리티 우선 CSS 프레임워크 |
| Framer Motion | MIT | React 애니메이션 라이브러리 |
| Monaco Editor (React) | MIT | VS Code 에디터 React 컴포넌트 |
| ESLint | MIT | JavaScript/TypeScript 린터 |
| 라이브러리 | 라이선스 | 설명 |
|---|---|---|
| Express | MIT | Node.js 웹 프레임워크 |
| CORS | MIT | Cross-Origin Resource Sharing 미들웨어 |
| Node.js | MIT | JavaScript 런타임 |
| tsx | MIT | Node.js용 TypeScript 실행기 |
| express-rate-limit | MIT | /api IP별 레이트 리밋 |
| dotenv | BSD-2-Clause | server/.env를 환경변수로 로드 |
| 도구 | 라이선스 | 설명 |
|---|---|---|
| g++ (GCC) | GPL-3.0 | C++ 컴파일러 (런타임 의존성, 번들에 미포함) |
| GDB | GPL-3.0 | 라인별 추적용 GNU 디버거 (런타임 의존성, 번들에 미포함) |
MIT License - 자세한 내용은 LICENSE 참조.