eval 및 new Function과 함께 VM 난독화 사용하기
VM 난독화가 동적 코드 생성(직접 eval과 Function 생성자)을 처리하는 방식, 바이트코드로 변환되는 대상과 건너뛰는 대상, 난독화 도구가 내보내는 경고, 그리고 런타임 ReferenceError를 진단하는 방법을 설명합니다.
왜 중요한가요
VM 난독화는 함수 본문을 바이트코드로 컴파일해 런타임 내부의 인터프리터를 통해 디스패치합니다. 주변 스코프의 식별자 이름도 함께
바뀝니다. 이 두 변환은 런타임에 문자열로부터 만들어지는 코드, 즉 eval(s), new Function(...s), Function(...s)와 잘
어울리지 않습니다. 런타임에 생성된 코드가 난독화 도구가 이름을 바꾼 식별자를 참조하면, 생성된 함수가 처음 실행될 때
Uncaught ReferenceError: <renamed-name> is not defined가 발생합니다.
난독화 도구는 각 패턴을 서로 다르게 처리합니다. 아래 표는 요약이며, 이 문서의 나머지 부분에서 각 항목을 설명합니다.
난독화 도구의 동작 한눈에 보기
| 소스의 패턴 | 어떻게 되나요 |
|---|---|
eval('literal string') (본문이 문자열 리터럴) | 정상 동작합니다. 다만 이 호출을 포함한 함수는 VM 바이트코드 변환에서 제외됩니다(직접 eval은 주변 지역 변수를 읽는데, 함수가 바이트코드로 컴파일되면 VM이 이를 보존하지 않기 때문입니다). |
eval(dynamicExpression) | 런타임에 ReferenceError로 중단될 수 있습니다. 이 호출을 포함한 함수와 그 안에 정의된 모든 함수 역시 VM 바이트코드 변환에서 제외됩니다. |
(0, eval)(s) / window.eval(s) (간접 호출) | 정상 동작합니다. 이 호출을 포함한 함수는 평소대로 VM 바이트코드로 변환됩니다. 간접 eval은 주변 지역 변수를 볼 수 없으므로 이름 변경으로 깨질 일이 없습니다. |
new Function('a', 'b', 'return a + b') (모든 인자가 문자열 리터럴) | 정상 동작합니다. 이 호출을 포함한 함수는 평소대로 VM 바이트코드로 변환됩니다. |
new Function(dynamicBody) / Function(dynamicBody) | 런타임에 ReferenceError로 중단될 수 있습니다. 이 호출을 포함한 함수와 그 안에 정의된 모든 함수 역시 VM 바이트코드 변환에서 제외됩니다. |
이 패턴들은 모두 난독화 결과에 치명적이지 않은 경고로도 표시됩니다. 경고의 형태와 CI 예제는 아래 런타임 전에 문제를 발견하기를 참고하세요.
정적 코드와 동적 코드를 다르게 다루는 이유
eval(s)는 자신이 호출된 함수의 지역 변수를 읽고 쓸 수 있습니다. s가 문자열 리터럴이면 난독화 도구가 난독화 시점에
본문을 파싱해 주변 코드와 일관되게 식별자 이름을 바꿀 수 있습니다. 반면 s가 동적 표현식이면 파싱이 런타임에야 일어나는데, 그
시점에는 이미 식별자 이름이 바뀌어 있으므로 런타임에 만들어진 소스는 더 이상 존재하지 않는 옛 이름을 참조하게 됩니다.
new Function(s)는 다르게 동작합니다. 본문은 항상 파일 최상단에 정의된 것처럼 실행되어 전역 변수에만 접근할 수 있고, 호출
주변의 지역 변수에는 결코 접근하지 못합니다. 이 자체는 안전합니다. 다만 이름이 바뀐 식별자를 이어 붙여 본문을 만든다면(예:
난독화 도구가 내부를 다시 작성한 함수의 func.toString()을 사용하는 경우) 런타임에 컴파일된 함수도 같은 종류의
ReferenceError를 맞닥뜨리게 됩니다.
정적인 new Function('return 42')에는 이런 위험이 전혀 없습니다. 본문은 이름 변경기가 들여다보지 않는 평범한 문자열이고,
런타임에도 전역만 보면 되기 때문입니다. 난독화 도구는 이 호출을 그대로 두며, 이를 감싸는 함수도 여전히 VM 바이트코드 변환
대상으로 남습니다.
런타임에 보게 되는 오류
가장 흔한 증상은 동적으로 만들어진 함수가 처음 실행될 때 발생하는 ReferenceError입니다.
여기서 TU는 난독화 도구가 번들의 IIFE 스코프 안에 도입한, 이름이 바뀐 식별자입니다. 동적 eval이나 Function 생성자 호출이
이를 참조하는 본문을 평가하지만, 그 본문은 TU가 정의되어 있지 않은 스코프에서 실행됩니다.
런타임 전에 문제를 발견하기
난독화 도구는 API를 통해 치명적이지 않은 경고를 내보내므로, 배포 전에 CI에서 이러한 패턴을 잡아낼 수 있습니다. 관련된 경고 유형은 두 가지입니다.
DynamicCodeRenameRisk— 함수에 본문이 런타임에 만들어지는 동적eval/new Function/Function호출이 들어 있습니다.VMDynamicCodeSkipped— 위 패턴 중 하나 때문에 해당 함수의 VM 바이트코드 변환이 건너뛰어졌습니다. 함수 이름(확인 가능한 경우)과 제외를 유발한 구문의 종류가 함께 담깁니다.
빌드에서 특정 경고 유형이 나오는 것이 예상된 일이라 CI에서 걸러내기보다 아예 발생 지점에서 끄고 싶다면, warnings
옵션(v7.8.0+)으로 getWarnings()가 내보내는 대상을 제어할 수 있습니다. 'none'은 전부 억제하고,
{ VMDynamicCodeSkipped: false } 같은 유형별 맵은 나머지는 남긴 채 특정 유형만 끕니다.
해결 방법
간접 eval(
(0, eval)(s))로 바꾸기eval인 경우에만 유용합니다. 간접 eval은 전역 스코프에서 실행되므로 주변 지역 변수를 볼 수 없고, 바로 그 때문에 이름이 바뀐 식별자를 참조할 일도 없습니다. 이 호출을 포함한 함수는 VM 바이트코드 변환 상태를 유지합니다.본문을 완전히 정적으로 만들기
new Function의 경우, 본문을 보간 없는 하나의 문자열 리터럴이나 템플릿 리터럴로 표현할 수 있다면 그 호출에는 이름 변경 위험이 없고 이를 감싸는 함수도 계속 바이트코드로 변환됩니다.new Function('a', 'b', 'return a + b')은 괜찮지만new Function('return ' + expr)은 그렇지 않습니다.호출을 별도의 최상위 함수로 옮기고 IIFE를 해제하기
최상위 함수는 각각 독립적으로 검사됩니다. 동적 코드 생성 호출을 별도의 최상위 함수로 끌어올리면, 번들 전체를 감싸는 최상위 IIFE 전반으로 제외가 번지는 대신 그 함수 하나만 VM 바이트코드 변환에서 빠집니다.
vmTargetFunctionsMode: 'comment'로 전환하기선택적으로 적용하는 모드로,
/* javascript-obfuscator:vm */이 표시된 함수만 바이트코드로 변환됩니다. 동적 코드 호출이 들어 있는 함수에는 주석을 붙이지 않고 나머지만 바이트코드로 변환하세요. 함수 지정하기를 참고하세요.vmForceCompileDynamicCode: true로 제외를 무시하기 (v6.14.0+)최후의 수단입니다. 이 옵션을 켜면 난독화 도구가 감싸는 함수를 그래도 바이트코드로 변환하고
VMDynamicCodeSkipped경고를 억제합니다. 런타임에 만들어지는 본문이 난독화 도구가 이름을 바꾸는 식별자를 결코 참조하지 않는다고 보장할 수 있을 때만 사용하세요. 그렇지 않으면 깔끔한 난독화를 얻는 대가로 런타임ReferenceError를 얻게 됩니다.DynamicCodeRenameRisk는 계속 발생하므로 CI에서 이를 기준으로 차단할 수 있습니다. 대시보드에서는 VM 섹션의 Overrides 그룹에 있는 "Force Compile Dynamic Code" 스위치입니다.
관련 문서
- VM 난독화 — 직접 eval 동작 방식 — 제외의 영향 범위를 좁히는 IIFE 해제 패턴입니다.
- VM 난독화 — 함수 지정하기 —
vmTargetFunctionsMode로 함수 단위로 적용 여부를 선택하는 방법입니다.
