new Function로 함수 만들기
JS 를 다시 공부하게 된 이유는..
결국 새롭게 나오는 프레임워크들은 대부분 JS 로 되어있고,
새로운 프레임워크를 새로운 언어 배우듯이 배우고 싶지 않아서, 조금 더 본질에 집중해보려고 공부를 시작했다.
ko.javascript.info를 보면서 처음부터 공부중이고, 실행 컨텍스트가 머릿 속에 정리되고 나서부터 재미가 붙기 시작했던 것 같다.
Function 생성자 부분 보는데 흥미로워서 추가로 찾아본 것들을 정리하려고 끄적여본당 ..
문법
let func = new Function([arg1, arg2, ...argN], functionBody);
함수의 인자와 본문을 문자열로 받는다. 런타임에 문자열로부터 함수 객체를 만들어낸다.
const sum = new Function('a', 'b', 'return a + b');
sum(1, 2); // 3
인수 표기 방식은 세 가지 모두 동일하게 동작한다.
new Function('a', 'b', 'return a + b');
new Function('a,b', 'return a + b');
new Function('a , b', 'return a + b');
기존 함수 선언과 다른 점은 소스코드 작성 시점이 아니라 런타임에 함수를 만들 수 있다는 것이다. 서버에서 코드 문자열을 받아서 함수로 변환하는 것도 가능하다.
const codeFromServer = await fetch('/api/formula').then(r => r.text());
// codeFromServer = "return a * b + offset"
const fn = new Function('a', 'b', 'offset', codeFromServer);
fn(3, 4, 10); // 22
[[Environment]]와 렉시컬 환경
JS의 모든 함수는 내부 슬롯 [[Environment]]를 갖는다. 이 슬롯에는 함수가 생성된 시점의 렉시컬 환경에 대한 참조가 저장된다. 클로저가 동작하는 이유가 이것이다.
function outer() {
const msg = 'hello';
const inner = function() {
console.log(msg); // 'hello' ✅
};
return inner;
}
inner가 outer 바깥에서 호출되더라도 msg에 접근할 수 있는 건, [[Environment]]가 outer의 렉시컬 환경을 붙잡고 있기 때문이다.
new Function은 다르게 동작한다.
function outer() {
const msg = 'hello';
const fn = new Function('console.log(msg)');
return fn;
}
outer()(); // ReferenceError: msg is not defined ❌
new Function으로 만들어진 함수의 [[Environment]]는 생성 위치와 관계없이 항상 전역 렉시컬 환경을 가리킨다.
왜 전역 스코프만 볼까
압축기(minifier) 문제
프로덕션 배포 전 코드는 minifier를 통과한다. minifier는 지역 변수 이름을 짧게 바꾼다.
// 원본
function processUser() {
let userName = 'John';
// ...
}
// minify 후
function processUser() {
let a = 'John';
// ...
}
userName을 a로 교체하는 건 문제없다. userName은 지역변수고 함수 외부에서 접근할 수 없으니까. minifier는 코드 구조를 분석해서 이 과정을 안전하게 수행한다.
function processUser() {
let userName = 'John';
// 이 문자열 안의 'userName'은 minifier가 추적하지 않는다
const fn = new Function('return userName');
fn(); // minify 후: userName을 찾을 수 없음 ❌
}
그런데 new Function의 본문은 문자열이라 minifier가 내부 식별자를 추적하지 않는다. userName이 a로 바뀐 후에도 문자열 안은 여전히 'return userName'으로 남아있기 때문에 런타임에 에러가 난다.
아키텍처적 이유
외부에서 받아온 코드가 현재 스코프의 지역변수를 읽을 수 있으면 그 자체가 보안 취약점이 된다. 전역 스코프만 접근하도록 제한하는 것이 더 예측 가능하고 안전한 설계다.
외부 값이 필요하면 인수로 넘기기
function processUser() {
let userName = 'John';
const fn = new Function('name', 'return `Hello, ${name}!`');
fn(userName); // ✅
}
minifier가 userName을 a로 바꿔도 fn(a)로 바뀔 뿐이고, 문자열 안의 name은 매개변수 이름이라 일관되게 처리된다.
실무 사용 패턴
튜토리얼에선 실무에서 유용하게 사용된다고 해서 좀 찾아봤지만 앱 코드에서 직접 쓸 일은 거의 없다고 한다.(특히 FE에선..)
내가 가진 레포들에도 검색해보니 프레임워크나 라이브러리 내부에서 쓰인다.
Vue 템플릿 컴파일러가 대표적인 사례다. Vue의 <template>은 빌드 타임에 render 함수로 컴파일되는데, CDN 빌드처럼 런타임 컴파일이 필요한 경우 new Function()을 사용해 템플릿 문자열을 render 함수로 변환한다. Vue 공식 문서에 compileToFunction이 "CSP-compliant하지 않다"는 경고를 달아둔 이유가 이것이다.
JSON 스키마 기반 검증·직렬화 라이브러리 중 일부도 스키마 구조에 최적화된 함수를 런타임에 동적으로 생성하는 패턴에서 사용한다.
CSP에서 막히는 Function() ㅠㅠ
CSP(Content Security Policy)는 HTTP 헤더로 브라우저에게 이 페이지에서 허용되는 것을 알려주는 보안 정책이다. script-src에 'unsafe-eval'이 명시되지 않으면 문자열로부터 코드를 실행하는 API들이 차단된다.
MDN script-src 문서에 차단되는 항목들이 나와있다.
eval()Function()- 문자열 인자를 받는
setTimeout(),setInterval(),setImmediate()
Content-Security-Policy: script-src 'self'
// → new Function() 차단됨
Content-Security-Policy: script-src 'self' 'unsafe-eval'
// → new Function() 허용됨
금융이나 기업 내부 시스템처럼 보안 정책이 엄격한 환경에서는 'unsafe-eval'을 허용하지 않는 경우가 많다. Vue CDN 빌드가 이런 환경에서 동작하지 않는 이유도 여기 있다.
정리
- 런타임에 문자열로 함수를 만드는 방법이다
[[Environment]]가 전역 렉시컬 환경을 가리켜서 외부 지역변수에 접근할 수 없다- minifier 호환성과 보안을 위한 의도된 설계다
- 외부 값이 필요하면 인수로 명시적으로 넘겨야 한다
- CSP
'unsafe-eval'없이는 브라우저에서 차단된다