C++ JavaScript 스크립팅 완벽 가이드 | V8 임베딩·컨텍스트·C++↔JS 바인딩 [실전]
이 글의 핵심
V8 엔진 임베딩: Isolate·Context 생성, 스크립트 실행, C++ 함수 노출, JS 객체 바인딩, 양방향 호출. 문제 시나리오, 완전한 예제, 흔한 에러, 베스트 프랙티스, 프로덕션 패턴.
들어가며: “웹과 네이티브 앱에서 같은 로직을 두 번 짜고 있어요”
실제 겪는 문제 시나리오
서버(Node.js)와 클라이언트(브라우저)에서 같은 비즈니스 로직을 쓰고, 네이티브 앱(C++)에서도 동일한 검증·계산이 필요할 때가 있습니다. 예를 들어 결제 금액 검증, 할인 규칙, 포맷 변환 같은 로직을 세 군데에 중복 구현하면 유지보수가 어렵습니다. 비유하면 “한 레시피(JS 로직)를 주방(C++), 식당(브라우저), 배달(Node)에서 각각 다른 언어로 다시 쓰는 것”과 같습니다.
flowchart TD
subgraph wrong[❌ 중복 구현]
W1[JS 로직] --> W2[Node.js 서버]
W1 --> W3[브라우저]
W1 --> W4[C++ 네이티브]
W2 -.->|각각 따로 구현| W5[버그·불일치 위험]
end
subgraph right[✅ V8 임베딩]
R1[JS 로직 1곳] --> R2[Node·브라우저]
R1 --> R3[C++ + V8]
R3 --> R4[동일 엔진·동일 결과]
end
문제의 핵심:
- C++에 JavaScript를 실행할 수 있으면 한 번 작성한 JS 코드를 여러 환경에서 재사용할 수 있습니다.
- V8은 Chrome·Node.js에서 쓰는 JavaScript 엔진으로, C++에 임베드해 JS를 실행할 수 있습니다.
- Isolate·Context·HandleScope 등 V8 개념을 이해해야 안정적으로 사용할 수 있습니다.
이 글에서 다루는 것:
- 문제 시나리오: JavaScript 스크립팅이 필요한 실제 상황
- V8 임베딩: Isolate, Context, 스크립트 실행
- C++ → JS: C++ 함수를 JavaScript에 노출
- JS → C++: JavaScript에서 C++ 객체·메서드 호출
- 객체 바인딩: C++ 클래스를 JS 객체로 래핑
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스와 프로덕션 패턴
요구 환경: C++17 이상, V8 (depot_tools 또는 vcpkg로 빌드)
이 글을 읽으면:
- V8 임베딩의 핵심 개념을 이해할 수 있습니다.
- C++와 JavaScript 간 양방향 바인딩을 구현할 수 있습니다.
- 실전에서 바로 활용할 수 있는 완전한 코드를 얻을 수 있습니다.
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
목차
- 문제 시나리오: JavaScript 스크립팅이 필요한 순간
- V8 핵심 개념
- V8 임베딩 기본: 초기화·컨텍스트·스크립트 실행
- C++ 함수를 JavaScript에 노출
- JavaScript에서 C++ 객체 바인딩
- 완전한 JavaScript 스크립팅 예제
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스
- 프로덕션 패턴
- 구현 체크리스트
1. 문제 시나리오: JavaScript 스크립팅이 필요한 순간
시나리오 1: 웹·Node·네이티브에서 동일 로직 공유
문제: 결제 금액 검증, 할인 규칙, 날짜 포맷 같은 로직이 서버(Node.js), 웹(브라우저), 네이티브 앱(C++)에 각각 구현되어 있습니다. 규칙이 바뀔 때마다 세 군데를 수정해야 하고, 한 곳만 수정하면 불일치 버그가 발생합니다.
해결: JavaScript로 로직을 한 번만 작성하고, C++에 V8을 임베드해 동일한 JS 코드를 실행합니다. Node·브라우저·C++ 모두 같은 엔진(또는 호환 엔진)으로 동일한 결과를 보장합니다.
시나리오 2: 게임 UI·이벤트 스크립팅
문제: 게임 UI 버튼 동작, 퀘스트 조건, 이벤트 시퀀스가 C++에 하드코딩되어 있습니다. 기획자가 “이 버튼 클릭 시 점수 2배” 같은 수정을 요청할 때마다 C++ 수정 → 빌드 → 배포가 반복됩니다.
해결: UI 동작·이벤트를 JavaScript로 작성하고, C++ 게임 엔진이 V8으로 스크립트를 실행합니다. 스크립트만 수정해 재배포하면 됩니다.
시나리오 3: 설정·규칙 엔진
문제: 비즈니스 규칙(예: “주문 금액 5만 원 이상이면 무료 배송”)이 복잡해지면서 C++에 if-else가 난립합니다. 규칙 변경 시마다 재빌드가 필요합니다.
해결: 규칙을 JavaScript 표현식이나 함수로 정의하고, C++가 V8으로 평가합니다. 규칙만 수정하면 재시작 없이 적용 가능합니다.
시나리오 4: 플러그인·확장 스크립트
문제: 에디터·도구에서 사용자가 커스텀 동작을 추가하고 싶어 합니다. C++ 플러그인 DLL은 빌드 환경이 복잡하고, 보안 위험도 있습니다.
해결: JavaScript로 플러그인 API를 노출하면, 사용자가 스크립트만 작성해 확장할 수 있습니다. 샌드박스로 제한된 API만 제공해 안전하게 합니다.
시나리오 5: V8 빌드·초기화 실패
문제: V8을 임베드하려 했는데, 초기화 순서가 잘못되어 세그멘테이션 폴트가 발생합니다. 또는 Isolate를 Dispose한 뒤 사용해 크래시가 납니다.
해결: V8 생명주기(Platform → Initialize → Isolate → Context → 사용 → Dispose)를 엄격히 지키고, HandleScope로 핸들 수명을 관리합니다.
2. V8 핵심 개념
Isolate, Context, HandleScope
Isolate: V8 가상 머신 인스턴스. 자체 힙을 갖고, 다른 Isolate와 메모리를 공유하지 않습니다. 스레드당 하나 또는 애플리케이션당 하나로 사용합니다.
Context: JavaScript 실행 환경. 전역 객체, 빌트인 객체가 Context에 묶여 있습니다. 여러 Context를 같은 Isolate에서 만들 수 있어, 서로 다른 스크립트를 격리할 수 있습니다.
HandleScope: V8 객체 핸들의 수명을 관리합니다. HandleScope가 끝나면 그 안에서 만든 Local 핸들은 GC 대상이 됩니다. 함수 진입 시 HandleScope를 만들고, 반환값은 Escape로 밖으로 빼냅니다.
flowchart TB
subgraph v8[V8 아키텍처]
I[Isolate] --> C1[Context 1]
I --> C2[Context 2]
C1 --> H1[HandleScope]
C2 --> H2[HandleScope]
H1 --> S1[Script 실행]
H2 --> S2[Script 실행]
end
subgraph cpp[C++]
A[애플리케이션] --> I
S1 --> A
S2 --> A
end
핸들 타입
| 타입 | 설명 | 수명 |
|---|---|---|
Local<T> | 스택에 있는 핸들 | HandleScope 범위 내 |
Persistent<T> | 힙에 있는 핸들 | 명시적으로 Reset할 때까지 |
Global<T> | Persistent의 스마트 포인터 버전 | C++11 이후 권장 |
주의: Local은 HandleScope가 끝나면 무효화됩니다. 함수 반환 시 EscapableHandleScope::Escape()로 반환해야 합니다.
3. V8 임베딩 기본: 초기화·컨텍스트·스크립트 실행
V8 초기화 및 스크립트 실행 (완전한 예제)
// v8_basic.cpp — V8 초기화·스크립트 실행
#include <v8.h>
#include <libplatform/libplatform.h>
#include <string>
#include <iostream>
#include <memory>
class V8Engine {
public:
V8Engine() {
v8::V8::InitializeICUDefaultLocation("");
v8::V8::InitializeExternalStartupData("");
platform_ = v8::platform::NewDefaultPlatform();
v8::V8::InitializePlatform(platform_.get());
v8::V8::Initialize();
v8::Isolate::CreateParams params;
params.array_buffer_allocator =
v8::ArrayBuffer::Allocator::NewDefaultAllocator();
isolate_ = v8::Isolate::New(params);
}
~V8Engine() {
isolate_->Dispose();
v8::V8::Dispose();
v8::V8::ShutdownPlatform();
delete isolate_->GetArrayBufferAllocator();
}
std::string runScript(const std::string& js_code) {
v8::Isolate::Scope isolate_scope(isolate_);
v8::HandleScope handle_scope(isolate_);
v8::Local<v8::Context> context = v8::Context::New(isolate_);
v8::Context::Scope context_scope(context);
v8::Local<v8::String> source =
v8::String::NewFromUtf8(isolate_, js_code.c_str()).ToLocalChecked();
v8::Local<v8::Script> script;
if (!v8::Script::Compile(context, source).ToLocal(&script)) {
return "[Compile Error]";
}
v8::Local<v8::Value> result;
if (!script->Run(context).ToLocal(&result)) {
return "[Runtime Error]";
}
v8::String::Utf8Value utf8(isolate_, result);
return std::string(*utf8, utf8.length());
}
private:
std::unique_ptr<v8::Platform> platform_;
v8::Isolate* isolate_;
};
int main() {
V8Engine engine;
std::string result = engine.runScript("'Hello, ' + 'V8!'");
std::cout << result << "\n"; // Hello, V8!
return 0;
}
핵심 포인트:
- 초기화 순서: ICU → ExternalStartupData → Platform → V8 → Isolate
- 해제 순서: Isolate::Dispose → V8::Dispose → ShutdownPlatform
- HandleScope: 스크립트 실행 전에 반드시 생성
- 에러 처리:
ToLocalChecked()대신ToLocal(&var)로 실패 시 처리
TryCatch로 예외 처리
// 예외 처리 예제
v8::TryCatch try_catch(isolate_);
v8::Local<v8::Value> result;
if (!script->Run(context).ToLocal(&result)) {
if (try_catch.HasCaught()) {
v8::Local<v8::Message> message = try_catch.Message();
v8::String::Utf8Value msg_str(isolate_, message->Get());
std::cerr << "JS Error: " << *msg_str << "\n";
}
return "[Runtime Error]";
}
4. C++ 함수를 JavaScript에 노출
FunctionTemplate으로 C++ 함수 바인딩
JavaScript에서 print("hello")처럼 C++ 함수를 호출하려면 FunctionTemplate으로 콜백을 등록하고, Context의 전역 객체에 바인딩합니다.
// C++ 콜백: JavaScript에서 호출됨
void PrintCallback(const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = args.GetIsolate();
for (int i = 0; i < args.Length(); i++) {
v8::String::Utf8Value str(isolate, args[i]);
std::cout << (i > 0 ? " " : "") << *str;
}
std::cout << "\n";
}
// 전역 객체에 print 함수 등록
void setupGlobalObject(v8::Isolate* isolate, v8::Local<v8::ObjectTemplate>& global) {
global->Set(
isolate,
"print",
v8::FunctionTemplate::New(isolate, PrintCallback)
);
}
// Context 생성 시 사용
v8::Local<v8::ObjectTemplate> global = v8::ObjectTemplate::New(isolate);
setupGlobalObject(isolate, global);
v8::Local<v8::Context> context = v8::Context::New(isolate, nullptr, global);
JavaScript에서 사용:
print("Hello", "from", "JavaScript!");
// 출력: Hello from JavaScript!
인자 받기 및 반환값 전달
void AddCallback(const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = args.GetIsolate();
if (args.Length() < 2 || !args[0]->IsNumber() || !args[1]->IsNumber()) {
isolate->ThrowException(v8::String::NewFromUtf8Literal(isolate, "Need two numbers"));
return;
}
double a = args[0].As<v8::Number>()->Value();
double b = args[1].As<v8::Number>()->Value();
args.GetReturnValue().Set(v8::Number::New(isolate, a + b));
}
JavaScript에서 사용:
const sum = add(10, 20);
print(sum); // 30
데이터를 콜백에 전달 (External Data)
struct AppState {
int counter = 0;
};
void IncrementCallback(const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = args.GetIsolate();
v8::Local<v8::External> data = args.Data().As<v8::External>();
AppState* state = static_cast<AppState*>(data->Value());
state->counter++;
args.GetReturnValue().Set(v8::Integer::New(isolate, state->counter));
}
// 등록 시
AppState state;
global->Set(isolate, "increment",
v8::FunctionTemplate::New(isolate, IncrementCallback,
v8::External::New(isolate, &state)));
5. JavaScript에서 C++ 객체 바인딩
ObjectTemplate과 Internal Field로 C++ 객체 래핑
C++ 클래스 인스턴스를 JavaScript 객체로 노출하려면 ObjectTemplate에 Internal Field를 두고, C++ 포인터를 저장합니다.
// C++ 클래스
class Point {
public:
Point(int x, int y) : x_(x), y_(y) {}
int x() const { return x_; }
int y() const { return y_; }
void setX(int x) { x_ = x; }
void setY(int y) { y_ = y; }
private:
int x_, y_;
};
// Getter 콜백
void GetPointX(v8::Local<v8::Name> property,
const v8::PropertyCallbackInfo<v8::Value>& info) {
v8::Local<v8::Object> self = info.Holder();
v8::Local<v8::External> wrap = self->GetInternalField(0).As<v8::External>();
Point* p = static_cast<Point*>(wrap->Value());
info.GetReturnValue().Set(v8::Integer::New(info.GetIsolate(), p->x()));
}
// Setter 콜백
void SetPointX(v8::Local<v8::Name> property, v8::Local<v8::Value> value,
const v8::PropertyCallbackInfo<void>& info) {
v8::Local<v8::Object> self = info.Holder();
v8::Local<v8::External> wrap = self->GetInternalField(0).As<v8::External>();
Point* p = static_cast<Point*>(wrap->Value());
p->setX(value->Int32Value(info.GetContext()).FromJust());
}
void GetPointY(v8::Local<v8::Name> property,
const v8::PropertyCallbackInfo<v8::Value>& info) {
v8::Local<v8::Object> self = info.Holder();
v8::Local<v8::External> wrap = self->GetInternalField(0).As<v8::External>();
Point* p = static_cast<Point*>(wrap->Value());
info.GetReturnValue().Set(v8::Integer::New(info.GetIsolate(), p->y()));
}
void SetPointY(v8::Local<v8::Name> property, v8::Local<v8::Value> value,
const v8::PropertyCallbackInfo<void>& info) {
v8::Local<v8::Object> self = info.Holder();
v8::Local<v8::External> wrap = self->GetInternalField(0).As<v8::External>();
Point* p = static_cast<Point*>(wrap->Value());
p->setY(value->Int32Value(info.GetContext()).FromJust());
}
// ObjectTemplate 생성
v8::Local<v8::ObjectTemplate> point_templ = v8::ObjectTemplate::New(isolate);
point_templ->SetInternalFieldCount(1);
point_templ->SetAccessor(
v8::String::NewFromUtf8Literal(isolate, "x"), GetPointX, SetPointX);
point_templ->SetAccessor(
v8::String::NewFromUtf8Literal(isolate, "y"), GetPointY, SetPointY);
C++ 객체를 JS로 래핑하는 헬퍼
v8::Local<v8::Object> wrapPoint(v8::Isolate* isolate,
v8::Local<v8::ObjectTemplate> templ,
Point* p) {
v8::Local<v8::Object> obj = templ->NewInstance(isolate->GetCurrentContext())
.ToLocalChecked();
obj->SetInternalField(0, v8::External::New(isolate, p));
return obj;
}
JavaScript에서 사용:
// C++에서 point 객체를 전역에 등록했다고 가정
print(point.x, point.y); // 10 20
point.x = 100;
print(point.x); // 100
FunctionTemplate으로 메서드 바인딩
void PointToString(const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = args.GetIsolate();
v8::Local<v8::Object> self = args.Holder();
v8::Local<v8::External> wrap = self->GetInternalField(0).As<v8::External>();
Point* p = static_cast<Point*>(wrap->Value());
std::string str = "Point(" + std::to_string(p->x()) + ", "
+ std::to_string(p->y()) + ")";
args.GetReturnValue().Set(
v8::String::NewFromUtf8(isolate, str.c_str()).ToLocalChecked());
}
// PrototypeTemplate에 메서드 추가
v8::Local<v8::FunctionTemplate> point_ctor = v8::FunctionTemplate::New(isolate);
point_ctor->InstanceTemplate()->SetInternalFieldCount(1);
point_ctor->PrototypeTemplate()->Set(
isolate, "toString",
v8::FunctionTemplate::New(isolate, PointToString));
6. 완전한 JavaScript 스크립팅 예제
예제 1: 게임 엔진 API (create_entity, set_position, add_score)
// game_js_engine.cpp
#include <v8.h>
#include <libplatform/libplatform.h>
#include <string>
#include <unordered_map>
#include <memory>
#include <iostream>
struct Entity {
int id;
float x, y;
};
class EntityManager {
int next_id_ = 0;
std::unordered_map<int, Entity> entities_;
public:
int createEntity() {
int id = next_id_++;
entities_[id] = Entity{id, 0, 0};
return id;
}
Entity* getEntity(int id) {
auto it = entities_.find(id);
return it != entities_.end() ? &it->second : nullptr;
}
void setPosition(int id, float x, float y) {
auto* e = getEntity(id);
if (e) { e->x = x; e->y = y; }
}
void destroyEntity(int id) { entities_.erase(id); }
};
class GameJSEngine {
std::unique_ptr<v8::Platform> platform_;
v8::Isolate* isolate_;
v8::Global<v8::Context> context_; // runScript·fireCollision에서 공유
std::unique_ptr<EntityManager> entities_;
int score_ = 0;
static void JsCreateEntity(const v8::FunctionCallbackInfo<v8::Value>& args) {
auto* self = static_cast<GameJSEngine*>(
args.Data().As<v8::External>()->Value());
int id = self->entities_->createEntity();
args.GetReturnValue().Set(v8::Integer::New(args.GetIsolate(), id));
}
static void JsSetPosition(const v8::FunctionCallbackInfo<v8::Value>& args) {
auto* self = static_cast<GameJSEngine*>(
args.Data().As<v8::External>()->Value());
if (args.Length() < 3) return;
int id = args[0]->Int32Value(args.GetIsolate()->GetCurrentContext()).FromJust();
float x = args[1]->NumberValue(args.GetIsolate()->GetCurrentContext()).FromJust();
float y = args[2]->NumberValue(args.GetIsolate()->GetCurrentContext()).FromJust();
self->entities_->setPosition(id, x, y);
}
static void JsAddScore(const v8::FunctionCallbackInfo<v8::Value>& args) {
auto* self = static_cast<GameJSEngine*>(
args.Data().As<v8::External>()->Value());
if (args.Length() < 1) return;
int delta = args[0]->Int32Value(args.GetIsolate()->GetCurrentContext()).FromJust();
self->score_ += delta;
}
static void JsPrint(const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = args.GetIsolate();
for (int i = 0; i < args.Length(); i++) {
v8::String::Utf8Value str(isolate, args[i]);
std::cout << (i > 0 ? " " : "") << *str;
}
std::cout << "\n";
}
public:
GameJSEngine() : entities_(std::make_unique<EntityManager>()) {
v8::V8::InitializeICUDefaultLocation("");
v8::V8::InitializeExternalStartupData("");
platform_ = v8::platform::NewDefaultPlatform();
v8::V8::InitializePlatform(platform_.get());
v8::V8::Initialize();
v8::Isolate::CreateParams params;
params.array_buffer_allocator = v8::ArrayBuffer::Allocator::NewDefaultAllocator();
isolate_ = v8::Isolate::New(params);
}
~GameJSEngine() {
isolate_->Dispose();
v8::V8::Dispose();
v8::V8::ShutdownPlatform();
delete isolate_->GetArrayBufferAllocator();
}
v8::Local<v8::Context> createContext() {
v8::HandleScope handle_scope(isolate_);
v8::Local<v8::ObjectTemplate> global = v8::ObjectTemplate::New(isolate_);
global->Set(isolate_, "print", v8::FunctionTemplate::New(isolate_, JsPrint));
global->Set(isolate_, "create_entity",
v8::FunctionTemplate::New(isolate_, JsCreateEntity,
v8::External::New(isolate_, this)));
global->Set(isolate_, "set_position",
v8::FunctionTemplate::New(isolate_, JsSetPosition,
v8::External::New(isolate_, this)));
global->Set(isolate_, "add_score",
v8::FunctionTemplate::New(isolate_, JsAddScore,
v8::External::New(isolate_, this)));
v8::Local<v8::Context> ctx = v8::Context::New(isolate_, nullptr, global);
context_.Reset(isolate_, ctx);
return ctx;
}
bool runScript(const std::string& js_code) {
v8::Isolate::Scope isolate_scope(isolate_);
v8::HandleScope handle_scope(isolate_);
v8::Local<v8::Context> context;
if (context_.IsEmpty()) {
context = createContext();
} else {
context = context_.Get(isolate_);
}
v8::Context::Scope context_scope(context);
v8::Local<v8::String> source =
v8::String::NewFromUtf8(isolate_, js_code.c_str()).ToLocalChecked();
v8::Local<v8::Script> script;
if (!v8::Script::Compile(context, source).ToLocal(&script)) {
return false;
}
v8::Local<v8::Value> result;
return script->Run(context).ToLocal(&result);
}
int getScore() const { return score_; }
};
JavaScript 게임 스크립트:
// game_script.js
const id = create_entity();
print("Created entity:", id);
set_position(id, 100, 200);
add_score(50);
print("Score added. Total:", 50);
예제 2: C++에서 JavaScript 함수 호출 (콜백)
GameJSEngine::fireCollision(a, b)는 위 예제 1에 포함된 메서드입니다. C++ 게임에서 충돌 발생 시 on_collision이 스크립트에 정의되어 있으면 호출합니다.
// main 또는 게임 루프에서
GameJSEngine engine;
engine.runScript(R"(
function on_collision(a, b) {
print("Collision:", a, b);
add_score(10);
}
)");
engine.fireCollision(1, 2); // on_collision(1, 2) 호출
JavaScript 콜백:
function on_collision(entityA, entityB) {
print("Collision between", entityA, "and", entityB);
add_score(10);
}
예제 3: CMake 빌드 (V8 링크)
# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(GameJSDemo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
# V8 경로 (depot_tools로 빌드한 경우)
set(V8_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/v8/out.gn/x64.release/obj")
include_directories(${V8_ROOT}/../.. ${V8_ROOT}/../../include)
link_directories(${V8_ROOT})
add_executable(game_js_demo game_js_engine.cpp main.cpp)
target_link_libraries(game_js_demo
v8_monolith
v8_libbase
v8_libplatform
pthread
dl
)
target_compile_options(game_js_demo PRIVATE -fno-rtti)
예제 4: main.cpp
// main.cpp
#include "game_js_engine.cpp" // 또는 헤더 분리
int main() {
GameJSEngine engine;
const char* script = R"(
const id = create_entity();
print("Entity ID:", id);
set_position(id, 50, 75);
add_score(100);
print("Done.");
)";
if (!engine.runScript(script)) {
std::cerr << "Script failed\n";
return 1;
}
std::cout << "Final score: " << engine.getScore() << "\n";
return 0;
}
7. 자주 발생하는 에러와 해결법
에러 1: “Isolate가 이미 dispose된 후 사용”
원인: Isolate::Dispose() 호출 후 해당 Isolate를 사용했습니다.
// ❌ 잘못된 예
isolate_->Dispose();
runScript("1+1"); // 크래시!
// ✅ 올바른 예 — 사용 완료 후 Dispose
{
v8::Isolate::Scope scope(isolate_);
runScript("1+1");
}
isolate_->Dispose();
에러 2: HandleScope 없이 Local 반환
원인: HandleScope 범위를 벗어나면 Local 핸들이 무효화됩니다.
// ❌ 잘못된 예
v8::Local<v8::String> getString() {
v8::HandleScope scope(isolate_);
return v8::String::NewFromUtf8Literal(isolate_, "hello");
} // scope 종료 시 "hello" 핸들 무효화 → 반환값 댕글링
// ✅ 올바른 예 — EscapableHandleScope
v8::Local<v8::String> getString() {
v8::EscapableHandleScope scope(isolate_);
v8::Local<v8::String> s = v8::String::NewFromUtf8Literal(isolate_, "hello");
return scope.Escape(s);
}
에러 3: Context 없이 스크립트 실행
원인: Script::Compile·Run에 Context를 넘기지 않았거나, Context::Scope가 없습니다.
// ❌ 잘못된 예
v8::Local<v8::Script> script = v8::Script::Compile(source).ToLocalChecked();
script->Run(); // Context 필요!
// ✅ 올바른 예
v8::Local<v8::Context> context = v8::Context::New(isolate_);
v8::Context::Scope context_scope(context);
v8::Local<v8::Script> script;
v8::Script::Compile(context, source).ToLocal(&script);
script->Run(context);
에러 4: ICU 데이터 파일 누락
원인: V8은 icudtl.dat 파일이 필요합니다. 없으면 초기화 시 실패하거나 런타임 에러가 발생합니다.
해결:
# V8 빌드 출력에서 icudtl.dat 복사
cp out.gn/x64.release/icudtl.dat /path/to/your/binary/
# 또는 실행 디렉터리와 동일한 위치에
에러 5: “undefined symbol” 링크 에러
원인: V8 라이브러리 링크 순서 또는 누락.
해결:
# 링크 순서: v8_monolith → v8_libbase → v8_libplatform
target_link_libraries(game_js_demo
v8_monolith
v8_libbase
v8_libplatform
pthread
dl
)
에러 6: Internal Field 인덱스 오류
원인: SetInternalFieldCount(1)을 했는데 GetInternalField(1)을 접근하거나, Internal Field에 값을 설정하지 않았습니다.
// ❌ 잘못된 예
point_templ->SetInternalFieldCount(1);
// ...
obj->GetInternalField(1); // 인덱스 1은 없음 (0만 유효)
// ✅ 올바른 예
obj->SetInternalField(0, v8::External::New(isolate, p));
// GetInternalField(0)만 사용
에러 7: 스레드 안전성 — Isolate는 스레드당 하나
원인: 여러 스레드에서 같은 Isolate를 동시에 사용하면 크래시합니다.
해결: Isolate는 스레드당 하나만 사용합니다. 멀티스레드에서는 스레드별 Isolate를 만들거나, 락으로 접근을 직렬화합니다.
에러 8: UTF-8 변환 실패
원인: ToLocalChecked()가 실패 시(예: 잘못된 UTF-8) 어설트로 종료합니다.
// ❌ 위험
v8::Local<v8::String> s = v8::String::NewFromUtf8(isolate_, data).ToLocalChecked();
// ✅ 안전
v8::Local<v8::String> s;
if (!v8::String::NewFromUtf8(isolate_, data).ToLocal(&s)) {
return; // 에러 처리
}
8. 베스트 프랙티스
1. HandleScope는 함수 진입 시 생성
void myFunction() {
v8::HandleScope handle_scope(isolate_);
// ... 모든 V8 작업
} // scope 종료 시 로컬 핸들 정리
2. 반환할 핸들은 EscapableHandleScope로 Escape
v8::Local<v8::Object> createObject() {
v8::EscapableHandleScope scope(isolate_);
v8::Local<v8::Object> obj = v8::Object::New(isolate_);
return scope.Escape(obj);
}
3. TryCatch로 예외 처리
v8::TryCatch try_catch(isolate_);
if (!script->Run(context).ToLocal(&result)) {
if (try_catch.HasCaught()) {
v8::Local<v8::Message> msg = try_catch.Message();
// 로깅·에러 전파
}
}
4. C++ 객체 소유권 명확히
- Internal Field에 넣은 C++ 포인터의 소유권을 정합니다.
- JS 객체가 GC될 때 C++ 객체도 해제하려면
WeakCallback으로 정리합니다.
5. Context 재사용
- 매 스크립트 실행마다 Context를 새로 만들면 비용이 큽니다.
- 동일 Context를 재사용하고, 필요 시
Context::Scope만 갱신합니다.
6. 스크립트 사전 컴파일
// 반복 실행 시 컴파일 한 번만
v8::Local<v8::Script> script;
v8::Script::Compile(context, source).ToLocal(&script);
for (int i = 0; i < N; i++) {
script->Run(context); // 컴파일 없이 실행만
}
9. 프로덕션 패턴
패턴 1: Isolate 풀 (멀티스레드)
요청별로 Isolate를 재사용해 초기화 비용을 줄입니다.
class IsolatePool {
std::vector<v8::Isolate*> pool_;
std::mutex mtx_;
public:
v8::Isolate* acquire();
void release(v8::Isolate* isolate);
};
패턴 2: 스크립트 캐시
파일 경로 → 컴파일된 Script 매핑으로 반복 로드 시 컴파일을 생략합니다.
std::unordered_map<std::string, v8::Global<v8::Script>> script_cache_;
패턴 3: 샌드박스 — 제한된 전역 객체
사용자 스크립트에 require, fetch, eval 등을 노출하지 않고, 허용된 API만 전역에 등록합니다.
// 위험한 API 제외
// global->Set("eval", ...); // 하지 않음
// global->Set("require", ...); // 하지 않음
global->Set("log", v8::FunctionTemplate::New(isolate_, SafeLog));
패턴 4: 타임아웃 (실행 시간 제한)
무한 루프 방지를 위해 V8의 TerminateExecution을 사용합니다.
isolate_->TerminateExecution();
// 별도 스레드에서 일정 시간 후 호출
패턴 5: 메모리 제한
v8::ResourceConstraints로 힙 크기를 제한합니다.
v8::ResourceConstraints constraints;
constraints.set_max_old_space_size(64); // MB
v8::SetResourceConstraints(isolate_, &constraints);
패턴 6: 에러 로깅 및 모니터링
void logV8Error(v8::Isolate* isolate, v8::TryCatch& try_catch) {
v8::Local<v8::Message> msg = try_catch.Message();
v8::String::Utf8Value msg_str(isolate, msg->Get());
v8::String::Utf8Value stack_str(isolate,
try_catch.StackTrace(isolate->GetCurrentContext()).ToLocalChecked());
LOG_ERROR("V8: %s\nStack: %s", *msg_str, *stack_str);
}
10. 구현 체크리스트
V8 JavaScript 스크립팅 도입 시 확인할 항목:
- V8 초기화 순서 (ICU → Platform → V8 → Isolate)
- Isolate 해제 순서 (Dispose → V8::Dispose → ShutdownPlatform)
- HandleScope / EscapableHandleScope 사용
- Context::Scope로 Context 활성화
- TryCatch로 예외 처리
- C++ 함수 노출 시
FunctionTemplate+External데이터 - C++ 객체 바인딩 시
SetInternalFieldCount+SetInternalField -
ToLocalChecked()대신ToLocal(&var)에러 처리 - icudtl.dat 배포
- 스레드당 Isolate 하나 또는 락
- 샌드박스: 위험 API 제외
- 스크립트 캐시 (반복 실행 시)
정리
| 항목 | 설명 |
|---|---|
| Isolate | V8 VM 인스턴스, 자체 힙 |
| Context | JS 실행 환경, 전역 객체 |
| HandleScope | 로컬 핸들 수명 관리 |
| FunctionTemplate | C++ 함수를 JS에 노출 |
| ObjectTemplate | C++ 객체를 JS 객체로 래핑 |
| Internal Field | C++ 포인터 저장 슬롯 |
| TryCatch | JS 예외 처리 |
핵심 원칙:
- 생명주기를 엄격히 지킵니다 (초기화 → 사용 → 해제).
- HandleScope로 핸들 수명을 관리하고, 반환 시
Escape합니다. - Context를 명시하고,
Context::Scope로 활성화합니다. - 에러 처리는
ToLocal과TryCatch로 합니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 웹·Node.js와 동일한 JS 로직 공유, 게임 UI 스크립팅, 설정·규칙 엔진, 플러그인 시스템 등에 활용합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. V8 대신 다른 JS 엔진을 쓸 수 있나요?
A. QuickJS, Duktape 등 경량 엔진도 있습니다. 웹 호환성이 중요하면 V8, 경량이 중요하면 QuickJS를 고려하세요.
참고 자료
- V8 Embedding Guide
- V8 API Reference
- Node.js V8 바인딩
- C++ 시리즈 #55-3: 스크립팅 통합
- C++ 시리즈 #35-2: WebAssembly·Emscripten
한 줄 요약: V8 Isolate·Context·FunctionTemplate·ObjectTemplate을 활용하면 C++와 JavaScript 간 양방향 바인딩을 구현해 웹·Node와 동일한 로직을 네이티브 앱에서 재사용할 수 있습니다.
관련 글
- C++ Lua 스크립팅 완벽 가이드 | Lua C API·스택·테이블·바인딩 [실전]
- C++ Python 스크립팅 완벽 가이드 | pybind11 모듈·클래스·NumPy·예외 처리 [실전]
- C++ 스크립팅 엔진 통합 | Lua·Python·JavaScript 바인딩 완벽 가이드 [#55-3]