C++ JavaScript 스크립팅 완벽 가이드 | V8 임베딩·컨텍스트·C++↔JS 바인딩 [실전]

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++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

목차

  1. 문제 시나리오: JavaScript 스크립팅이 필요한 순간
  2. V8 핵심 개념
  3. V8 임베딩 기본: 초기화·컨텍스트·스크립트 실행
  4. C++ 함수를 JavaScript에 노출
  5. JavaScript에서 C++ 객체 바인딩
  6. 완전한 JavaScript 스크립팅 예제
  7. 자주 발생하는 에러와 해결법
  8. 베스트 프랙티스
  9. 프로덕션 패턴
  10. 구현 체크리스트

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 객체로 노출하려면 ObjectTemplateInternal 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 제외
  • 스크립트 캐시 (반복 실행 시)

정리

항목설명
IsolateV8 VM 인스턴스, 자체 힙
ContextJS 실행 환경, 전역 객체
HandleScope로컬 핸들 수명 관리
FunctionTemplateC++ 함수를 JS에 노출
ObjectTemplateC++ 객체를 JS 객체로 래핑
Internal FieldC++ 포인터 저장 슬롯
TryCatchJS 예외 처리

핵심 원칙:

  1. 생명주기를 엄격히 지킵니다 (초기화 → 사용 → 해제).
  2. HandleScope로 핸들 수명을 관리하고, 반환 시 Escape합니다.
  3. Context를 명시하고, Context::Scope로 활성화합니다.
  4. 에러 처리ToLocalTryCatch로 합니다.

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. 웹·Node.js와 동일한 JS 로직 공유, 게임 UI 스크립팅, 설정·규칙 엔진, 플러그인 시스템 등에 활용합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. V8 대신 다른 JS 엔진을 쓸 수 있나요?

A. QuickJS, Duktape 등 경량 엔진도 있습니다. 웹 호환성이 중요하면 V8, 경량이 중요하면 QuickJS를 고려하세요.


참고 자료


한 줄 요약: V8 Isolate·Context·FunctionTemplate·ObjectTemplate을 활용하면 C++와 JavaScript 간 양방향 바인딩을 구현해 웹·Node와 동일한 로직을 네이티브 앱에서 재사용할 수 있습니다.


관련 글

  • C++ Lua 스크립팅 완벽 가이드 | Lua C API·스택·테이블·바인딩 [실전]
  • C++ Python 스크립팅 완벽 가이드 | pybind11 모듈·클래스·NumPy·예외 처리 [실전]
  • C++ 스크립팅 엔진 통합 | Lua·Python·JavaScript 바인딩 완벽 가이드 [#55-3]
... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3