6. 함수 심화학습(2)

protect-me·2021년 6월 9일
0
post-thumbnail

6.5 전역 객체


요약

  • 전역 객체를 사용하면 어디서든 접근 가능한 변수를 만들 수 있습니다.
    전역 객체엔 Array와 같은 내장 객체, window.innerHeight(뷰포트의 높이를 반환함)같은 브라우저 환경 전용 변수 등이 저장되어 있습니다.

  • 전역 객체는 globalThis라는 보편적인 이름으로 불립니다.
    하지만 '관습’에 따라 브라우저에서는 window, 'Node.js'에서는 global이라는 이름으로 불릴 때가 많습니다. globalThis는 제안 목록에 추가 된 지 얼마 안 된 기능이기 때문에, 비 크로미움 기반 브라우저에선 지원하지 않습니다(폴리필을 구현하면 사용할 수 있습니다).

  • 프로젝트 전체에서 꼭 필요한 변수만 전역 객체에 저장하도록 하고, 전역 변수는 가능한 한 최소한으로 사용합시다.

  • 모듈을 사용하고 있지 않다면, 브라우저에서 var로 선언한 전역 변수는 전역 객체의 프로퍼티가 됩니다.

  • 이해하기 쉽고 요구사항 변경에 쉽게 대응할 수 있는 코드를 구현하려면, window.x처럼 전역 객체의 프로퍼티에 직접 접근합시다.

  • 브라우저 환경에선 전역 객체를 window, Node.js 환경에선 global라고 부르는데, 각 호스트 환경마다 부르는 이름은 다릅니다.
  • 본 튜토리얼은 브라우저 환경에서 구동되기 때문에 window라는 전역 객체를 사용하도록 하겠습니다. 다른 호스트 환경에서 작업하고 계신다면 window대신 globalThis를 사용하시면 됩니다.
alert("Hello");
// 위와 동일하게 동작합니다.
window.alert("Hello");
  • 브라우저에서 let이나 const가 아닌 var로 선언한 전역 함수나 전역 변수는 전역 객체의 프로퍼티가 됩니다.
var gVar = 5;
alert(window.gVar);
  • var 대신 let을 사용하면 위 예시와는 달리 전역 객체를 통해 변수에 접근할 수 없습니다.
let gLet = 5;
alert(window.gLet);
  • 중요한 변수라서 모든 곳에서 사용할 수 있게 하려면, 아래와 같이 전역 객체에 직접 프로퍼티를 추가해 주시기 바랍니다.
window.currentUser = {
  name: "John"
};
alert(currentUser.name);  // John
// 지역 변수 'currentUser'가 있다면
// 지역 변수와 충돌 없이 전역 객체 window에서 이를 명시적으로 가져올 수 있음
alert(window.currentUser.name); // John
  • 전역 변수는 되도록 사용하지 않는 것이 좋습니다. 함수를 만들 땐 외부 변수나 전역 변수를 사용하는 것보다 ‘인풋’ 변수를 받고 이를 이용해 '아웃풋’을 만들어내게 해야 테스트도 쉽고, 에러도 덜 만들어냅니다.

폴리필 사용하기

  • 내장 객체 Promise를 지원하는지 여부를 아래와 같이 테스트할 수 있죠. 구식 브라우저는 Promise 객체를 지원하지 않기 때문에 alert 창이 뜰 겁니다.
if (!window.Promise) {
  alert("구식 브라우저를 사용 중이시군요!");
}
  • 명세에는 있는 기능이지만 해당 기능을 지원하지 않는 오래된 브라우저를 사용하고 있다면 직접 함수를 만들어 전역 객체에 추가하는 방식으로 "폴리필"을 만들 수 있습니다.
if (!window.Promise) {
  window.Promise = ... // 모던 자바스크립트에서 지원하는 기능을 직접 구현함
}


6.6 객체로서의 함수와 기명 함수 표현식


요약
함수는 객체입니다.
이번 챕터에선 객체로서의 함수에서 사용 할 수 있는 프로퍼티 두 가지를 다뤄보았습니다.

  • name – 함수의 이름이 저장됩니다. 대개는 함수 선언부에서 이름을 가져오는데, 선언부에 이름이 없는 경우엔 자바스크립트 엔진이 컨텍스트(할당 등)를 이용해 이름을 추론합니다.
  • length – 함수 선언부에 있는 인수의 수로 나머지 매개변수는 포함되지 않습니다.
    함수 표현식으로 함수를 정의하였는데 이름이 있다면 이를 기명 함수 표현식이라 부릅니다. 기명 함수 표현식의 이름은 재귀 호출과 같이 함수 내부에서 자기 자신을 호출하고자 할 때 사용할 수 있습니다.

함수엔 다양한 프로퍼티를 추가할 수 있습니다. 널리 쓰이는 자바스크립트 라이브러리 상당수에서 이런 커스텀 프로퍼티를 잘 활용하고 있습니다.

이런 라이브러리들은 ‘주요’ 함수 하나를 만들고 여기에 다양한 ‘헬퍼’ 함수를 붙이는 식으로 구성됩니다. jQuery는 이름이 $인 주요 함수로 이루어져 있습니다. lodash는 주요 함수 __.clone, _.keyBy등의 프로퍼티를 추가하는 식으로 구성되죠. 자세한 정보는 lodash 공식 문서에서 찾아볼 수 있습니다. 이렇게 함수 하나에 다양한 헬퍼 함수를 붙여 라이브러리를 만들면 라이브러리 하나가 전역 변수 하나만 사용하므로 전역 공간을 더럽히지 않는다는 장점이 있습니다. 이름 충돌도 방지할 수 있죠.

이렇게 객체로서의 함수 특징을 이용해 커스텀 프로퍼티를 만들면 함수는 자기 자신을 이용해 원하는 일을 수행할 수 있고, 함수 자기 자신에 딸린 프로퍼티의 기능도 사용할 수 있다는 장점이 있습니다.

  • 함수는 호출이 가능한(callable) '행동 객체’라고 이해하면 쉽습니다. 우리는 함수를 호출 할 수 있을 뿐만 아니라 객체처럼 함수에 프로퍼티를 추가·제거하거나 참조를 통해 전달할 수도 있습니다.

‘name’ 프로퍼티

  • name 프로퍼티를 사용하면 함수 이름을 가져올 수 있습니다.
function sayHi() {
  alert("Hi");
}
alert(sayHi.name); // sayHi

let sayHello = function() {
  alert("Hello");
};
alert(sayHello.name); 
// sayHello (익명 함수이지만 이름이 있네요!)

let user = {
  sayHi() {
    // ...
  }
}
alert(user.sayHi.name); // sayHi
// 객체 메서드의 이름도 가져올 수 있습니다
  • 자바스크립트 명세서에서 정의된 이 기능을 'contextual name’이라고 부릅니다. 이름이 없는 함수의 이름을 지정할 땐 컨텍스트에서 이름을 가져오죠.

‘length’ 프로퍼티

  • 내장 프로퍼티 length는 함수 매개변수의 개수를 반환합니다. 예시를 살펴봅시다.
function f1(a) {}
function f2(a, b) {}
function many(a, b, ...more) {}

alert(f1.length); // 1
alert(f2.length); // 2
alert(many.length); // 2

커스텀 프로퍼티

  • 함수에 자체적으로 만든 프로퍼티를 추가할 수도 있습니다.
function sayHi() {
  alert("Hi");
  // 함수를 몇 번 호출했는지 세봅시다.
  sayHi.counter++;
}
sayHi.counter = 0; // 초깃값

sayHi(); // Hi
sayHi(); // Hi

alert( `호출 횟수: ${sayHi.counter}` ); // 호출 횟수: 2회

프로퍼티는 변수가 아닙니다.

  • sayHi.counter = 0와 같이 함수에 프로퍼티를 할당해도 함수 내에 지역변수 counter가 만들어지지 않습니다. counter 프로퍼티와 변수 let counter는 전혀 관계가 없습니다.
  • 프로퍼티를 저장하는 것처럼 함수를 객체처럼 다룰 수 있지만, 이는 실행에 아무 영향을 끼치지 않습니다. 변수는 함수 프로퍼티가 아니고 함수 프로퍼티는 변수가 아니기 때문입니다. 둘 사이에는 공통점이 없습니다.
  • 클로저는 함수 프로퍼티로 대체할 수 있습니다. 변수의 유효범위와 클로저 챕터에서 살펴본 바 있는 counter 함수를 함수 프로퍼티를 사용해 바꿔보도록 하겠습니다.
function makeCounter() {
  function counter() {
    return counter.count++;
  };
  counter.count = 0;
  return counter;
}

let counter = makeCounter();
counter.count = 10;
alert( counter() ); // 10
  • 두 방법의 차이점은 count 값이 외부 변수에 저장되어있는 경우 드러납니다. 클로저를 사용한 경우엔 외부 코드에서 count에 접근할 수 없습니다. 오직 중첩함수 내에서만 count 값을 수정할 수 있습니다. 반면 함수 프로퍼티를 사용해 count를 함수에 바인딩시킨 경우엔 다음 예시와 같이 외부에서 값을 수정할 수 있습니다.

기명 함수 표현식

  • 먼저, 일반 함수 표현식
let sayHi = function(who) {
  alert(`Hello, ${who}`);
};
  • 함수표현식 + 이름 붙이기(함수 선언문은 아님)
let sayHi = function func(who) {
  alert(`Hello, ${who}`);
};
sayHi("John"); // Hello, John
  1. 이름을 사용해 함수 표현식 내부에서 자기 자신을 참조할 수 있습니다.
  2. 기명 함수 표현식 외부에선 그 이름을 사용할 수 없습니다.
let sayHi = function func(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    func("Guest"); // func를 사용해서 자신을 호출합니다.
  }
};

sayHi(); // Hello, Guest

// 하지만 아래와 같이 func를 호출하는 건 불가능합니다.
func(); // Error, func is not defined (기명 함수 표현식 밖에서는 그 이름에 접근할 수 없습니다.)
  • 외부 코드에 의해 sayHi가 변경될 수 있다는 문제가 생깁니다. 함수 표현식을 새로운 변수에 할당하고, 기존 변수에 null을 할당하면 에러가 발생하죠.
  • 에러는 함수가 sayHi를 자신의 외부 렉시컬 환경에서 가지고 오기 때문에 발생합니다. 지역(local) 렉시컬 환경엔 sayHi가 없기 때문에 외부 렉시컬 환경에서 sayHi를 찾는데, 함수 호출 시점에 외부 렉시컬 환경의 sayHi엔 null이 저장되어있기 때문에 에러가 발생합니다.
let sayHi = function(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    sayHi("Guest"); // TypeError: sayHi is not a function
  }
};

let welcome = sayHi;
sayHi = null;

welcome(); // 중첩 sayHi 호출은 더 이상 불가능합니다!
  • 수정된 코드
  • "func"이라는 이름은 함수 지역 수준(function-local)에 존재하므로 외부 렉시컬 환경에서 찾지 않아도 됩니다. 외부 렉시컬 환경에선 보이지도 않죠. 함수 표현식에 붙인 이름은 현재 함수만 참조하도록 명세서에 정의되어있기 때문입니다.
let sayHi = function func(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    func("Guest"); // 원하는 값이 제대로 출력됩니다.
  }
};

let welcome = sayHi;
sayHi = null;

welcome(); // Hello, Guest (중첩 호출이 제대로 동작함)


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

문법

let func = new Function ([arg1, arg2, ...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();

클로저

  • 함수는 특별한 프로퍼티 [[Environment]]에 저장된 정보를 이용해 자기 자신이 태어난 곳을 기억합니다. [[Environment]]는 함수가 만들어진 렉시컬 환경을 참조합니다
  • 그런데 new Function을 이용해 함수를 만들면 함수의 [[Environment]] 프로퍼티가 현재 렉시컬 환경이 아닌 전역 렉시컬 환경을 참조하게 됩니다.
  • 따라서 new Function을 이용해 만든 함수는 외부 변수에 접근할 수 없고, 오직 전역 변수에만 접근할 수 있습니다.
  • 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으로 만든 함수에 무언갈 넘겨주고 싶다면 인수를 사용하세요.


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


요약

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

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

  • CPU가 과부하 상태인 경우
  • 브라우저 탭이 백그라운드 모드인 경우
  • 노트북이 배터리에 의존해서 구동 중인 경우
    이런 상황에서 타이머의 최소 지연 시간은 300밀리초에서 심하면 1,000밀리초까지 늘어납니다. 연장 시간은 브라우저나 구동 중인 운영 체제의 성능 설정에 따라 다릅니다.
  • 일정 시간이 지난 후에 원하는 함수를 예약 실행(호출)할 수 있게 하는 것을 '호출 스케줄링(scheduling a call)'이라고 합니다.
    - setTimeout을 이용해 일정 시간이 지난 후에 함수를 실행하는 방법
    - setInterval을 이용해 일정 시간 간격을 두고 함수를 실행하는 방법

setTimeout

let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)
  • func|code
    실행하고자 하는 코드로, 함수 또는 문자열 형태입니다. 대개는 이 자리에 함수가 들어갑니다. 하위 호환성을 위해 문자열도 받을 수 있게 해놓았지만 추천하진 않습니다.
  • delay
    실행 전 대기 시간으로, 단위는 밀리초(millisecond, 1000밀리초 = 1초)이며 기본값은 0입니다.
  • arg1, arg2…
    함수에 전달할 인수들로, IE9 이하에선 지원하지 않습니다.
function sayHi() {
  alert('안녕하세요.');
}
setTimeout(sayHi, 1000);

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

setTimeout("alert('안녕하세요.')", 1000); // 지양
setTimeout(() => alert('안녕하세요.'), 1000);

함수를 실행하지 말고 넘기세요.
초보 개발자는 setTimeout에 함수를 넘길 때, 함수 뒤에 () 을 붙이는 실수를 하곤 합니다.

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

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

clearTimeout으로 스케줄링 취소하기

  • setTimeout을 호출하면 '타이머 식별자(timer identifier)'가 반환됩니다. 스케줄링을 취소하고 싶을 땐 이 식별자(아래 예시에서 timerId)를 사용하면 됩니다.
let timerId = setTimeout(...);
clearTimeout(timerId);

setInterval

  • setInterval 메서드는 setTimeout과 동일한 문법을 사용합니다. 다만, setTimeout이 함수를 단 한 번만 실행하는 것과 달리 setInterval은 함수를 주기적으로 실행하게 만듭니다.
  • 함수 호출을 중단하려면 clearInterval(timerId)을 사용하면 됩니다.
let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...);

                          
// 2초 간격으로 메시지를 보여줌
let timerId = setInterval(() => alert('째깍'), 2000);
// 5초 후에 정지
setTimeout(() => { clearInterval(timerId); alert('정지'); }, 5000);

중첩 setTimeout

  • 다섯 번째 줄의 setTimeout은 (*)로 표시한 줄의 실행이 종료되면 다음 호출을 스케줄링합니다.
/** setInterval을 이용하지 않고 아래와 같이 중첩 setTimeout을 사용함
let timerId = setInterval(() => alert('째깍'), 2000);
*/

let timerId = setTimeout(function tick() {
  alert('째깍');
  timerId = setTimeout(tick, 2000); // (*)
}, 2000);
  • 중첩 setTimeout을 이용하는 방법은 setInterval을 사용하는 방법보다 유연합니다. 호출 결과에 따라 다음 호출을 원하는 방식으로 조정해 스케줄링 할 수 있기 때문입니다.
let delay = 5000;

let timerId = setTimeout(function request() {
  ...요청 보내기...

  if (서버 과부하로 인한 요청 실패) {
    // 요청 간격을 늘립니다.
    delay *= 2;
  }

  timerId = setTimeout(request, delay);

}, delay);
  • CPU 소모가 많은 작업을 주기적으로 실행하는 경우에도 setTimeout을 재귀 실행하는 방법이 유용합니다. 작업에 걸리는 시간에 따라 다음 작업을 유동적으로 계획할 수 있기 때문입니다.
  • 중첩 setTimeout을 이용하는 방법은 지연 간격을 보장하지만 setInterval은 이를 보장하지 않습니다.
  • setInterval (지연 간격보장 X)
let i = 1;
setInterval(function() {
  func(i++);
}, 100);

  • setTimeout을(지연 간격보장 O)
let i = 1;
setTimeout(function run() {
  func(i++);
  setTimeout(run, 100);
}, 100);

가비지 컬렉션과 setInterval·setTimeout

  • setInterval이나 setTimeout에 함수를 넘기면, 함수에 대한 내부 참조가 새롭게 만들어지고 이 참조 정보는 스케줄러에 저장됩니다. 따라서 해당 함수를 참조하는 것이 없어도 setInterval과 setTimeout에 넘긴 함수는 가비지 컬렉션의 대상이 되지 않습니다.
  • setInterval의 경우는, clearInterval이 호출되기 전까지 함수에 대한 참조가 메모리에 유지됩니다.
  • 그런데 이런 동작 방식에는 부작용이 하나 있습니다. 외부 렉시컬 환경을 참조하는 함수가 있다고 가정해 봅시다. 이 함수가 메모리에 남아있는 동안엔 외부 변수 역시 메모리에 남아있기 마련입니다. 그런데 이렇게 되면 실제 함수가 차지했어야 하는 공간보다 더 많은 메모리 공간이 사용됩니다. 이런 부작용을 방지하고 싶다면 스케줄링할 필요가 없어진 함수는 아무리 작더라도 취소하도록 합시다.

대기 시간이 0인 setTimeout

  • setTimeout(func, 0)이나 setTimeout(func)을 사용하면 setTimeout의 대기 시간을 0으로 설정할 수 있습니다.
  • 이렇게 대기 시간을 0으로 설정하면 func을 ‘가능한 한’ 빨리 실행할 수 있습니다. 다만, 이때 스케줄러는 현재 실행 중인 스크립트의 처리가 종료된 이후에 스케줄링한 함수를 실행합니다.
  • 이런 특징을 이용하면 현재 스크립트의 실행이 종료된 ‘직후에’ 원하는 함수가 실행될 수 있게 할 수 있습니다.
setTimeout(() => alert("World"));
alert("Hello");
  • 예시에서 첫 번째 줄은 '0밀리초 후에 함수 호출하기’라는 할 일을 '계획표에 기록’해주는 역할을 합니다. 그런데 스케줄러는 현재 스크립트(alert 함수)의 실행이 종료되고 나서야 '계획표에 어떤 할 일이 적혀있는지 확인’하므로, Hello가 먼저, World은 그다음에 출력됩니다.

브라우저 환경에서 실제 대기 시간은 0이 아닙니다.
브라우저는 HTML5 표준에서 정한 중첩 타이머 실행 간격 관련 제약을 준수합니다. 해당 표준엔 "다섯 번째 중첩 타이머 이후엔 대기 시간을 최소 4밀리초 이상으로 강제해야 한다."라는 제약이 명시되어있습니다.




📚 참고 : javascript.info

profile
protect me from what i want

0개의 댓글