2023년 1월 5일

new Function 문법

함수 표현식과 함수 선언문 이외에 함수를 만들 수도 있는 방법이 하나 더 있습니다. 잘 사용하는 방법은 아니지만, 이 방법 외에는 대안이 없을 때 사용합니다.

문법

new Function 문법을 사용하면 함수를 만들 수 있습니다.

let func = new Function ([arg1, arg2, ...argN], functionBody);

새로 만들어지는 함수는 인수 arg1...argN과 함수 본문 functionBody로 구성됩니다.

인수 두 개가 있는 함수를 직접 만들어 보면서 new Function 문법에 대해 이해해보도록 합시다.

let sum = new Function('a', 'b', 'return a + b');

alert( sum(1, 2) ); // 3

인수가 없고 함수 본문만 있는 함수를 만들어보겠습니다.

let sayHi = new Function('alert("Hello")');

sayHi(); // Hello

기존에 사용하던 방법과 new Function을 사용해 함수를 만드는 방법의 가장 큰 차이는 런타임에 받은 문자열을 사용해 함수를 만들 수 있다는 점입니다.

함수 표현식과 함수 선언문은 개발자가 직접 스크립트를 작성해야만 함수를 만들 수 있었죠.

그러나 new Function이라는 문법을 사용하면 어떤 문자열도 함수로 바꿀 수 있습니다. 서버에서 전달받은 문자열을 이용해 새로운 함수를 만들고 이를 실행하는 것도 가능합니다.

let str = ... 서버에서 동적으로 전달받은 문자열(코드 형태) ...

let func = new Function(str);
func();

서버에서 코드를 받거나 템플릿을 사용해 함수를 동적으로 컴파일해야 하는 경우, 복잡한 웹 애플리케이션을 구현할 때와 같이 아주 특별한 경우에 new Function을 사용할 수 있습니다.

클로저

함수는 특별한 프로퍼티 [[Environment]]에 저장된 정보를 이용해 자기 자신이 태어난 곳을 기억합니다. [[Environment]]는 함수가 만들어진 렉시컬 환경을 참조합니다(자세한 내용은 변수의 유효범위와 클로저에서 다루었습니다).

그런데 new Function을 이용해 함수를 만들면 함수의 [[Environment]] 프로퍼티가 현재 렉시컬 환경이 아닌 전역 렉시컬 환경을 참조하게 됩니다.

따라서 new Function을 이용해 만든 함수는 외부 변수에 접근할 수 없고, 오직 전역 변수에만 접근할 수 있습니다.

function getFunc() {
  let value = "test";

  let func = new Function('alert(value)');

  return func;
}

getFunc()(); // ReferenceError: value is not defined

일반적인 방법을 사용해 함수를 정의한 예시와 비교해봅시다.

function getFunc() {
  let value = "test";

  let func = function() { alert(value); };

  return func;
}

getFunc()(); // getFunc의 렉시컬 환경에 있는 값 "test"가 출력됩니다.

지금 당장은 new Function이 제공하는 특수한 기능이 익숙하지 않을 수 있는데, 실무에선 이 기능이 아주 유용하게 쓰입니다.

문자열을 사용해서 함수를 만들어야 한다고 가정해봅시다. 스크립트를 작성하는 시점엔 어떻게 함수를 짤지 알 수 없어서 기존의 함수 선언 방법은 사용하지 못하는데, 스크립트 실행 시점 즈음엔 함수를 어떻게 짜야 할 지 아이디어가 떠오를 수 있을 겁니다. 이때, 서버를 비롯한 외부 출처를 통해 코드를 받아올 수 있겠죠.

그런데 이렇게 만들어진 새 함수는 기존 스크립트와 문제없이 상호작용할 수 있어야 합니다.

new Function으로 만든 새로운 함수 내부에서 외부 변수에 접근하려 할 때, 기존 함수 선언 방식으로 작성한 함수와 동일한 동작이 보장되어야 하죠.

그런데 스크립트가 프로덕션 서버에 반영되기 전, 압축기(minifier) 에 의해 압축될 때 문제가 발생합니다. 압축기는 스크립트에서 주석이나 여분의 공백 등을 없애 코드 크기를 줄여주는 특수한 프로그램인데 압축기가 지역 변수 이름을 짧게 바꾸면서 문제가 발생하죠.

구체적으로 어떤 부분이 문제가 되는지 예시를 통해 알아봅시다. 함수 내부에 let userName라는 변수가 있으면 이 지역변수는 압축기에 의해 let a 등(짧은 이름)으로 대체되는데, 이때 userName 모두가 a로 교체됩니다. userName은 지역변수이고, 함수 외부에선 함수 내부에 있는 변수에 접근할 수 없기 때문에 이렇게 해도 전혀 문제가 없죠. 압축기는 단순히 변수를 찾아서 바꾸지 않고 코드 구조를 분석해 기존에 작성한 코드의 기능을 망가뜨리지 않으면서 영리하게 제 역할을 수행합니다.

이런 동작 방식 때문에 new Function 문법으로 만든 함수 내부에서 외부 변수에 접근하려고 하면 userName은 이미 이름이 변경되었기 때문에 찾을 수 없게 됩니다.

압축기가 동작한 이후엔, new Function으로 만든 함수 내부에서 외부 렉시컬 환경에 접근하려고 할 때 문제가 발생할 수 있죠.

함수 내부에서 외부 변수에 접근하는 것은 아키텍처 관점에서도 좋지 않고 에러에 취약합니다.

new Function으로 만든 함수에 무언갈 넘겨주고 싶다면 인수를 사용하세요.

요약

문법:

let func = new Function ([arg1, arg2, ...argN], functionBody);

인수를 한꺼번에 모아(쉼표로 구분) 전달할 수도 있습니다.

아래 세 선언 방식은 동일하게 동작하죠.

new Function('a', 'b', 'return a + b'); // 기본 문법
new Function('a,b', 'return a + b'); // 쉼표로 구분
new Function('a , b', 'return a + b'); // 쉼표와 공백으로 구분

new Function을 이용해 만든 함수의 [[Environment]]는 외부 렉시컬 환경이 아닌 전역 렉시컬 환경을 참조하므로 외부 변수를 사용할 수 없습니다. 단점 같아 보이는 특징이긴 하지만 에러를 예방해 준다는 관점에선 장점이 되기도 합니다. 구조상으론 매개변수를 사용해 값을 받는 게 더 낫습니다. 압축기에 의한 에러도 방지할 수 있죠.

튜토리얼 지도