자바스크립트 톺아보기 11

이지영·2025년 1월 14일

new Function 문법

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

문법

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

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

새로 만들어지는 함수는 인수 arg1...argN과 함수 본문 functionBody로 구성된다.
인수 두 개가 있는 함수를 직접 만들어보면서 이해해보자

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을 사용해 함수를 만드는건가?

new Function을 사용하면 문자열 형태로 전달받은 코드를 실행할 수 있는 함수 객체를 생성할 수 있다.

예시

// 서버에서 받은 데이터
// 서버에서 동적으로 생성된 코드 조각
let serverData = "return a+b;";

// new Function을 사용하여 함수를 생성
let dynamicFunction = new Function ('a','b', serverData);

// 생성된 함수 실행
let result = dynamicFunction(5,3);
console.log(result); // 8 출력된다.

이 방식은 동적으로 생성된 코드를 실행할 때 유용하지만, 보안상의 이유롷 사용에 주의해야한다.
신뢰할 수 없는 데이터를 실행하면 코드 인젝션 공격에 노출될 수 있으니 조심해야한다.

클로저

함수는 틀별한 프로퍼티 [[Environment]]에 저장된 정보를 이용해 자기 자신이 태어난 곳을 기억한다.
[[Environment]]는 함수가 만들어진 렉시컬 환경을 참조한다.

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

따라서 new Function을 이용해 만든 함수는 외부 변수에 접근할 수 없고 오직 전역 변수에만 접근할 수 있다.
지금 당장은 new Function이 제공하는 특수한 기능이 익숙하지 않을 수 있는데, 실무에서는 이 기능이 아주 유용하게 쓰인다.

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

setTimeout과 setInterval을 이용한 호출 스케줄링

일정 시간이 지난 후에 원하는 함수를 예약 실행(호출)할 수 있게 하는 것을 '호출 스케줄링'이라고 한다.
호출 스케줄링을 구현하는 방법은 두 가지가 있다.

  • setTimeout을 이용해 일정 시간이 지난 후에 함수를 실행하는 방법
  • setInterval을 이용해 일정 시간 간격을 두고 함수를 실행하는 방법

setTimeout

let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)

매개변수:

func|code

실행하고자 하는 코드로

함수 또는 문자열 형태이다.
대개는 이 자리에 함수가 들어간다. 하위 호환성을 위해 문자열도 받을 수 있게 해놨지만 추천하진 않는다.

delay
실행 전 대기 시간으로, 단위는 밀리초(millisecond, 1000밀리초 = 1초)이며 기본값은 0이다.

arg1, arg2
함수에 전달할 인수들로 IE9 이하에서는 지원하지 않는다.

아래 예시를 통해 setTimeout 사용법을 알아보자.

function sayHi(){
   alert('안녕하세요');
}
setTimeout(sayHi, 1000);

// 또는
function sayHi(who, phrase) {
   alert(who + ' 님' + phrase)
}
setTimeout(sayHi, 1000, "홍길동", "안녕하세요"); // 홍길동 님 안녕하세요

setTimeout의 첫 번째 인수가 문자열이면 자바스크립트는 이 문자열을 이용해 함수를 만든다.
아래 예시가 정상적으로 동작하는 이유이다.

setTimeout("alert('안녕')",1000);

하지만 이렇게 문자열을 사용하는 방식은 추천하지 않는다 되도록 다음 예시와 같이 익명 화살표 함수를 사용해라

setTimeout(() => alert('안녕하세요'),1000);

함수를 실행하지 말고 넘겨라

// 잘못된 코드
setTimeout(sayHi(),1000);

setTimeout은 함수의 참조 값을 받도록 정의되어 있는데 sayHi()를 인수로 전달하면 함수 실행 결과가 전달되어버린다.
그런데 sayHi()에는 반환문이 없다. 호출 결과는 undefined가 된다.
따라서 setTimeout은 스케줄링할 대상을 찾지 못해 원하는 대로 코드가 동작하지 않는다.

clearTimeout으로 스케줄링 취소하기

setTimeout을 호출하면 타이머 식별자가 반환된다. 스케줄링을 취소하고 싶을 땐 이 식별자를 사용하면 된다.

let timerId = setTimeout(...);
clearTimeout(timerId);

아래 예시는 함수 실행을 계획해 놓았다가 중간에 마음이 바꾸어 계획해 놓았던 것을 취소한 상황을 코드로 표현하고 있다.

let timerId = setTimeout(()=>alert("아무 일도 일어나지 않는다."),1000)
alert(timerId);

clearTimeout(timerId);
alert(timerId); // 취소 후에도 식별자의 값은 null이 되지 않고 위의 값과 동일하게 알럿창이 실행된다.

참고로 Node.js에서 setTimeout을 실행하면 타이머 객체가 반환된다.

setInterval

setTimeout과 동일한 문법을 사용한다. 인수 역시 동일하다
다만 setTimeout이 함수를 단 한번만 실행하는 것과 달리 setInterval은 함수를 주기적으로 실행하게 만든다.

함수 호출을 준단하려면 clearInterval(timerId)를 사용하면된다.

다음 예시를 실행하면 메시지가 2초 간격으로 보이다가 5초 이후에는 더 이상 메시지가 보이지 않는다.

let timerId = setInterval(()=>alert('째깍'),2000);

// 5초 후에 정지
setTimeout(()=>clearInterval(timerId),5000)

alert창이 떠있더라도 타이머는 멈추지 않는다.

중첩 setTimeout

무언가를 일정 간격을 두고 실행하는 방법에는 크게 2가지가 있다.
하나는 setInterval을 이용하는 방법이고 다른 하나는 중첩 setTimeout을 이용하는 방법이다.

let timerId = setTimeout(function tick(){
   alert('째깍');
   timerId = setTimeout(tick,2000);
},2000)

중첩 setTimeout을 이용하면 setInterval을 사용하는 방법보다 유연하다.
호출 결과에 따라 다음 호출을 원하는 방식으로 조정해 스케줄링 할 수 있기 떄문이다.

5초 간격으로 서버에 요청을 보내 데이터를 얻는다고 가정해보자
서버가 과부하 상태라면 요청 간격을 10초 20초 40초 등으로 증가시켜주는게 좋다.

let delay = 5000;
let timerId = setTimeout(function request(){
   ...요청 보내기...
   if(서버 과부하로 인한 요청 실패)
   delay *=2;
   timerId = setTimeout(request,delay);
},delay)

CPU소모가 많은 작업을 주기적으로 실행하는 경우에도 setTimeout을 재귀 실행하는 방법이 유용하다.
작업에 걸리는 시간에 따라 다음 작업을 유동적으로 계획할 수 있기 떄문이다.

중첩 setTimeout을 이용하는 방법은 지연 간격을 보장하짐나 setInterval은 이를 보장하지 않는다.
setInterval은 func를 실행하는데 소모되는 시간도 지연 간격에 포함시킨다.
따라서 func를 실행하는 데 걸리는 시간이 명시한 지연 간격보다 길 때
func의 실행이 종료될때까지 기다려준다.
실행이 종료되면 엔지는 스케줄러를 확인하고 지연 시간이 지났으면 다음 호출을 바로 시작한다.
따라서 함수 호출에 걸리는 시간이 매번 delay밀리초보다 길면 모든 함수가 쉼 없이 계속 연속 호출을 한다.

한편 중첩 setTimeout을 이용하면
명시한 지연시간이 보장된다.
이렇게 지연 간격이 보장되는 이유는 이전 함수의 실행이 종료된 이후에 다음 함수 호출에 대한 계획이 세워지기 떄문이다.

가비지 컬렉션과 setInterval setTimeout

setInterval이나 setTimeout에 함수를 넘기면, 함수에 대한 내부 참조가 새롭게 만들어지고 이 참조 정보는 스케줄러에 저장된다. 따라서 해당 함수를 참조하는 것이 없어도 setInterval 과 setTimeout에 넘긴 함수는
가비지 컬렉션의 대상이 되지 않는다.

setTimeout은 스케줄러가 함수를 호출할때까지 함수는 메모리에 유지된다.

setInterval의 경우는 clearInterval이 호출되기 전까지 함수에 대한 참조가 메모리에 유지된다.

그런데 이런 동작 방식에는 부작용이 있는데,

외부 렉시컬 환경을 참조하는 함수가 있다고 가정했을 때 이 함수가 메모리에 남아있는 동안엔 외부 변수 역시 메모리에 남아있기 마련이다. 그런데 이렇게 되면 실제 함수가 차지했어야 하는 공간보다 더 많은 메모리 공간이 사용된다.

이런 부작용을 방지하고 싶다면 스케줄링 할 필요가 없어진 함수는 아무리 작더라도 취소해야한다

대기시간이 0인 setTimeout

이렇게 대기 시간을 0으로 설정하면 func를 가능한 한 빨리 실행할 수 있다.
다만, 이떄 스케줄러는 현재 실행 중인 스크립트의 처리가 종료된 이후에 스케줄링한 함수를 실행한다.
이런 특징을 이용하면 현재 스크립트의 실행이 종료된 직후에 원하는 함수가 실행될 수 있게 할 수 있다.

브라우저 환경에서 실제 대기 시간은 0이 아니다

브라우저는 HTML5 표준에서 정한 중첩 타이머 실행 간격 제약을 준수한다.
해당 표준에는 다섯 번째 중첩 타이머 이후엔 대기 시간을 최소 4밀리미초 이상으로 강제해야 한다 라는 제약이 있다.

이런 제약은 setTImeout뿐만 아니라 setInterval에도 적용된다.
setInterval(f)도 처음 몇 번은 함수 f를 지연 없이 실행하지만
나중엔 지연 간격을 4밀리초 이상으로 늘려버린다.

한편 서버 측엔 이런 제약이 없다. node.js의 prcess.nextTick과 setImmediate를 이용하면
비동기 작업을 지연 없이 실행 할 수 있다.

위에서 언급된 제약은 브라우저에 한정된다.

summary

  • setInterval(func, delay, ...args)과 setTimeout(func, delay, ...args)은
    delay 밀리초 후에 func를 규칙적으로 또는 한 번 실행하도록 해준다.
  • setInterval, setTimeout을 호출하고 반환받은 값을 clearInterval, clearTimeout에 넘겨주면 스케줄링을 취소할 수 있다.
  • 중첩 setTimeout을 사용하면 setInterval을 사요한 것보다 유연하게 코드를 작성할 수 있다. 여기에 더하여 지연 간격 보장이라는 장점도 있다.
  • 대기 시간이 0인 setTimeout을 사용함녀 현재 스크립트의 실행이 완료된 후 가능한 빠르게 원하는 함수를 호출할 수 있다.
  • 지연 없이 중첩 setTimeout을 5회 이상 호출하거나 지연 없는 setInterval 에서 호출이 5회 이상 이루어지면 4밀리초 이상의 지연 간격이 강제로 더해진다. 이는 브라우저에만 적용되는 사항이며 하위 호환성을 위해 유지되고 있다.

스케줄링 메서드를 사용할 땐 명시한 지연 간격이 보장되지 않을 수 있다는 점에 유의해야한다.
아래와 같은 상황에서 브라우저 내 타이머가 느려지면 지연 간격이 보장되지 않는다

  • CPU가 과부하 상태인 경우
  • 브라우저 탭이 백그라운드 모드인 경우
  • 노트북이 배터리에 의존해서 구동 중인 경우
    이러한 상황에서 타이머의 최소 지연 시간은 300밀리초에서 심하면 1000밀리초까지 늘어난다.
    연장 시간은 브라우저나 구동중인 운영체제의 성능 설정에 따라 다르다.

과제

일초 간격으로 숫자 출력하기

function printNumbers(from,to){
   let current = from;
   let timerId = setInterval(function(){
      alert(current);
      if(current == to{
         clearInterval(timerId)
      }
      current ++
   },1000)
}
PrintNumbers(5,10)

function printNumbers(from,to){
   let current = from;
   let timerId = setTimout(function go(){
      alert(current);
      if(current < to){
         setTimeout(go,1000)
      }
      current ++;
   },1000)
}

두 방법 모두에서 최초 호출 이전에(첫 알럿창이 뜨기 전에) 1000ms의 지연 간격을 두고있다
초기 지연 없이 함수를 바로 실행하려면 아래와 같이 별도의 줄에서 함수를 호출해줘야한다.

function printNumbers(from,to){
   let current = from;
   function go(){
      alert(current);
      if(current == to) {
         clearInterver(timerId)
      }
   go();
   let timerId = setInterval(go,1000);
   }
}

call/apply와 데코레이터, 포워딩

자바스크립트는 함수를 다룰 때 탁월한 유연성을 제공한다. 함수는 이곳저곳 정달될 수 있고
객체로도 사용될 수 있다. 이번 챕터에선 함수 간에 호출을 어떻게 포워딩 하는지 함수를 어떻게 데코레이팅 하는지에 대해 알아보겠다.

코드 변경 없이 캐싱 기능 추가하기

CPU를 많이 잡아먹지만 결과는 안정적인 함수 slow(x)가 있다고 가정해보자.
결과가 안정적이라는 말은 x가 같으면 호출 결과도 같다는 것을 의미한다.
slow(x)가 자주 호출된다면 결과를 어딘가에 저장(캐싱)해 재연산에 걸리는 시간을 줄이고 싶을 것이다.
아래 예시에서는 slow()안에 캐싱 관련 코드를 추가하는ㄷ ㅐ신, 래퍼 함수를 만들어 캐싱 기능을 추가할 에정이다.
곧 정리하겠지만 이렇게 래퍼 함수를 만들면 여러 가지 이점이 있다.

0개의 댓글