문서
/
레시피
/

호스트 측 템플릿 치환

호스트 측 템플릿 치환

v6.10.0+

서버에서 렌더링한 값(예: Go html/template의 `{{ .Field }}` 표현식)을 난독화 파이프라인을 깨뜨리지 않고 VM 난독화된 JavaScript에 주입합니다.

문제

백엔드(Go, Rails, Django, PHP 등)가 요청마다 일부를 렌더링해야 하는 JavaScript 파일을 제공하는 상황입니다. API 엔드포인트, 기능 플래그 목록, 초기 상태 값, 빌드 ID, 논스 같은 것들이죠. vmObfuscation: true로 JS를 난독화하면서도, 난독화가 끝난 뒤에 템플릿 엔진이 결과물의 자리표시자를 치환할 수 있어야 합니다. 자리표시자가 VM 바이트코드에 흡수되어 버리면(문자열과 식별자의 기본 동작입니다) 템플릿 엔진이 치환할 대상이 남지 않습니다.

어떤 옵션을 골라야 하나요?

서버가 주는 값이…사용할 방법
원시 JS 표현식(배열 리터럴, 객체, 숫자, 불리언, 함수 호출)reservedNames + 식별자 자리표시자
문자열이고, 템플릿 구분자를 직접 정할 수 있는 경우reservedStrings + 템플릿 리터럴 자리표시자
문자열이고, 템플릿 엔진이 특정 구분자를 강제하는 경우(예: Go의 {{ .Field }})reservedStrings + 문자열 또는 템플릿 리터럴 자리표시자

패턴 1 - 식별자 자리표시자와 reservedNames

적합한 경우: 따옴표 이스케이프를 전혀 신경 쓰지 않고 원시 JS 표현식(배열, 객체, 숫자 등)을 주입하고 싶을 때.

실제 코드와 절대 겹치지 않을 독특한 식별자를 골라 직접 참조한 뒤, 그 식별자에 일치하는 정규식을 reservedNames에 넣으세요. VM 난독화에서 이 식별자는 예약 표현식 배열을 거쳐 결과물에 그대로 나타납니다.

소스

(function () {
    var initialState = __INITIAL_STATE__;
    bootstrap(initialState);
})();

난독화 옵션

JavaScriptObfuscator.obfuscate(source, {
    vmObfuscation: true,
    reservedNames: ['^__INITIAL_STATE__$']
});

난독화 후

결과물 어딘가에서 다음과 비슷한 예약 표현식 배열을 발견하게 됩니다.

let _r = [__INITIAL_STATE__, /* …other entries… */];

__INITIAL_STATE__ 토큰은 식별자 이름 변경에서 살아남으며 VM 바이트코드에 흡수되지 않습니다.

호스트 측 치환 (Go 예시)

tpl := template.Must(template.New("obf.js").Parse(obfuscated))
tpl.Execute(w, map[string]any{
    "InitialState": map[string]any{"user": "alice", "theme": "dark"},
})

…여기서 Go 템플릿이 해당 토큰을 원시 JS 표현식으로 바꿉니다.

{{ `__INITIAL_STATE__` }} → {{ .InitialState | toJSON }}

결과:

let _r = [{"user":"alice","theme":"dark"}, /* … */];

따옴표도 이스케이프도 필요 없습니다. 주입된 값은 JS 엔진이 평범한 표현식으로 파싱합니다.

패턴 2 - 템플릿 리터럴 자리표시자와 reservedStrings

적합한 경우: {{ .Field }} 같은 구분자를 요구하는 Go / Jinja 계열 템플릿 엔진을 사용하는데, 그 형태가 백틱으로 감싸면 유효한 JS 텍스트가 되는 경우.

자리표시자는 단일 조각으로 이루어진, 보간이 없는 템플릿 리터럴 안에 놓입니다. VM 난독화에서는 예약 표현식 배열을 거치며, 결과물에 백틱 형태 그대로 보존됩니다.

소스

(function () {
    var featureFlags = JSON.parse(`{{.Config.FeatureFlags}}`);
    applyFlags(featureFlags);
})();

난독화 옵션

UI

위 소스의 {{.Config.FeatureFlags}} 자리표시자를 예약하려면 Reserved Strings 필드에 다음 정규식을 백슬래시 하나로 입력하세요. UI는 값을 입력한 그대로 저장하므로 JS 코드에서와 달리 백슬래시를 두 번 쓰지 않습니다.

\{\{[^}]+\}\}

백슬래시를 하나만 사용해 정규식을 입력한 난독화 도구 UI의 Reserved Strings 필드

API

JavaScriptObfuscator.obfuscate(source, {
    vmObfuscation: true,
    reservedStrings: ['\\{\\{[^}]+\\}\\}']
});

난독화 후

let _r = [`{{.Config.FeatureFlags}}`, /* … */];

자리표시자는 감싸는 백틱까지 포함해 바이트 단위로 그대로 보존됩니다.

호스트 측 치환

{{.Config.FeatureFlags}}를 유효한 JSON 문자열로 바꾸세요. 런타임에는 백틱 안에 놓이므로 내부 따옴표를 이스케이프할 필요가 없습니다.

flagsJSON, _ := json.Marshal(config.FeatureFlags) // e.g. {"newCheckout":true,"darkMode":false}
out := strings.ReplaceAll(obfuscated, "{{.Config.FeatureFlags}}", string(flagsJSON))

런타임에는 JSON.parse(`{"newCheckout":true,"darkMode":false}`)가 되어 정상 동작합니다.

패턴 3 - 따옴표로 감싼 문자열 자리표시자와 reservedStrings

적합한 경우: 소스 코드가 ES5를 유지해야 하거나(템플릿 리터럴 사용 불가), 주변 API가 일반 문자열 리터럴을 요구하는 경우.

자리표시자는 작은따옴표 또는 큰따옴표로 감싼 문자열 리터럴입니다. VM 난독화에서는 예약 문자열 배열을 거치는데, 이 배열은 JSON.stringify로 직렬화된 JS 배열로 출력되므로 입력의 따옴표 종류와 관계없이 항상 큰따옴표가 됩니다.

소스

(function () {
    var tags = JSON.parse("{{.Page.Tags}}");
    renderTags(tags);
})();

난독화 옵션

UI

위 소스의 {{.Page.Tags}} 자리표시자를 예약하려면 Reserved Strings 필드에 다음 정규식을 백슬래시 하나로 입력하세요. UI는 값을 입력한 그대로 저장하므로 JS 코드에서와 달리 백슬래시를 두 번 쓰지 않습니다.

\{\{[^}]+\}\}

백슬래시를 하나만 사용해 정규식을 입력한 난독화 도구 UI의 Reserved Strings 필드

API

JavaScriptObfuscator.obfuscate(source, {
    vmObfuscation: true,
    reservedStrings: ['\\{\\{[^}]+\\}\\}']
});

난독화 후

var _rs = ["{{.Page.Tags}}", /* … */];

호스트 측 치환

자리표시자가 큰따옴표로 감싼 JS 문자열 안에 놓이므로, 주입하는 JSON 페이로드의 내부 " 문자는 \"로 이스케이프해야 합니다.

raw, _ := json.Marshal(page.Tags) // e.g. ["news","tech","release"]
// Escape " for embedding inside a JS double-quoted string.
escaped := strings.ReplaceAll(string(raw), `"`, `\"`)
out := strings.ReplaceAll(obfuscated, "{{.Page.Tags}}", escaped)

결과 출력:

var _rs = ["[\"news\",\"tech\",\"release\"]", /* … */];

런타임에는 JSON.parse("[\"news\",\"tech\",\"release\"]")["news","tech","release"]가 됩니다.

이스케이프를 잊으면 브라우저가 짝이 맞지 않는 따옴표를 만나 SyntaxError를 던집니다. 패턴 2는 백틱을 사용해 이 문제를 아예 비켜 갑니다.

한 프로그램에 여러 자리표시자 쓰기

세 가지 패턴은 서로 조합할 수 있습니다. 대안을 포함한 reservedStrings 정규식 하나로 템플릿 엔진이 내보내는 모든 자리표시자 형태를 잡아낼 수 있습니다.

JavaScriptObfuscator.obfuscate(source, {
    vmObfuscation: true,
    reservedNames: ['^__INITIAL_STATE__$'],
    reservedStrings: ['\\{\\{[^}]+\\}\\}']
});

같은 소스 안에서 식별자 자리표시자(원시 JS 값용)와 문자열 자리표시자(렌더링된 JSON용)를 섞어 쓸 수 있습니다. 서버가 실제로 무엇을 주입할지에 따라 자리표시자마다 알맞은 방식을 고르세요.

호환성 참고 사항

  • 식별자와 문자열에 같은 자리표시자를 쓰지 마세요. __TOKEN__이라는 예약 이름과 __TOKEN__에 일치하는 예약 문자열은 서로 다른 코드 경로(예약 표현식 배열 대 예약 문자열 배열)를 가리킵니다. 각각 확실히 구분되는 형태를 사용하세요. 예를 들어 식별자 자리표시자에는 UPPER_SNAKE 규칙을, 문자열 자리표시자에는 구분자로 감싼 형태({{ ... }}, %{...}, <<<...>>>)를 쓰면 됩니다. 그러면 한쪽 정규식의 실수가 다른 쪽에 조용히 일치하는 일을 막을 수 있습니다.
  • reservedStrings 정규식은 문자열의 원시 값에 대해 동작합니다. 정규식은 소스 텍스트가 아니라 문자열의 런타임 값과 대조됩니다. \{\{[^}]+\}\}{{.something}}(또는 다른 {{...}} 형태)을 포함하는 문자열에 일치합니다. 자리표시자가 추가 내용에 감싸여 있을 수 있다면("prefix-{{.Field}}-suffix") 정규식은 여전히 일치하지만 문자열 전체가 보존되므로, 그에 맞춰 치환 계획을 세우세요.
  • 어떤 템플릿 엔진과도 함께 쓸 수 있습니다. 예시는 Go text/template 문법을 사용하지만, javascript-obfuscator 연동에는 Go에만 해당하는 부분이 전혀 없습니다. 난독화 결과물에 문자열 수준의 치환을 할 수 있는 것이라면 무엇이든 동작합니다. Rails ERB, Django, PHP 짧은 태그, CI 파이프라인의 sed 등이 그렇습니다. 사용하는 엔진이 자연스럽게 내보내면서 실제 JS 문법과 겹치지 않는 구분자를 고르세요.