본문으로 바로가기

즉시실행 함수 패턴

이 포스팅은 원문을 번역한 글을 차용하였습니다.

[What (function (window, document, undefined) {})(window, document); really means]




IIFE(immediately-invoked function expression)

요즘 저는 아래와 같은 IIFE(immediately-invoked function expression, 즉시실행함수 표현식) 형식에 대해 많은 질문을 받았습니다.

JavaScript
(function (window, document, undefined) {
  // 
})(window, document);

그래서 이와 관련된 포스팅을 하겠습니다. 

저는 이 포스팅에서 다양한 내용을 함께 다뤄보려 합니다. 차례대로 살펴보도록 하겠습니다.



Scope

JavaScript는 function scope를 가지기 때문에 이를통해 private scope(지역 범위)를 가질 수 있습니다. 

예를 들면:

JavaScript
(function (window, document, undefined) { 
  var name = ‘Todd’; 
})(window, document); 
console.log(name); // name is not defined, it’s in a different scope

간단합니다. (역자주: 위 코드에서 name은 함수 안에서만 유효한 scope을 가집니다.)



How it works(동작 방법)

평범한 함수 표현식은 아래처럼 생겼습니다. 

(역자주: 함수 표현식은 해당 코드블럭이 JavaScript 엔진의 parser에 의해 실행코드로서 해석되며 실행에 따른 결과값을 가지거나 특정 변수에 할당된 값으로 존재)

JavaScript
var logMyName = function (name) {
  console.log(name);
};

logMyName('Todd');

위 함수 표현식을 통해서 우리는 우리가 원하는 시점에 함수를 호출할 수 있습니다. 또한 어디에서든 우리가 원하는 scope을 제공할 수 있습니다. 

“IIFE”가 ()로 감싸진 이유는 즉시실행함수 표현식이기 때문입니다. 


이런 함수는 런타임 시에 즉시 호출되며 한번 실행 후에는 우리가 다시 호출할 수 없습니다.

JavaScript
var logMyName = (function (name) {
  console.log(name); // Todd
})('Todd');


위 예제에서 logMyName 변수에 할당했었던 코드모양을 살펴보시기 바랍니다.

JavaScript
(function () {
  
})();


만약 아래 코드처럼 괄호쌍 하나가 빠진다면 동작하지 않습니다. 

(역자주: 왜냐하면 JavaScript 엔진의 parser는 function 키워드가 처음으로 나오면 함수 선언문으로 인식하기 때문입니다.)

JavaScript
function () {
  
}();


하지만 JavaScript에서 이런 모습의 코드를 강제로 동작시키기 위한 몇 가지 트릭이 존재합니다. 

이 방법은 JavaScript 엔진의 parser가 코드 앞의 `!`문자를 보고 함수 선언이 아닌 표현으로 인식하게 하는 방법입니다.

JavaScript
!function () {
  
}();


마찬가지로 아래처럼 비슷하게 변형된 방법들도 있습니다.

JavaScript
function () {
  
}();
-function () {
  
}();
~function () {
  
}();

하지만 저는 이런 방법을 사용하지 않습니다. 

관련된 내용은 @mariusschulz의 Disassembling JavaScript’s IIFE Syntax를 확인해보시면 IIFE 문법과 변형된 모습들에 대해서 자세하게 살펴볼 수 있습니다.



Arguments

이제 우리는 IIFE가 동작하는 방법을 알았습니다. 

물론 IIFE에도 arguments를 전달할 수 있습니다.

JavaScript
(function (window) { 
})(window);

이 코드가 어떻게 동작할까요? 

마지막 부분의 (window);를 통해서 함수가 실행되며 이 시점에 우리는 window 객체를 넘겨주게 됩니다. 

넘겨 받은 객체는 함수 안에서도 window라는 이름으로 정의되어 있습니다. 

여러분은 window라는 파라미터 이름은 의미가 없으며 다른 이름으로 정의해도 되지 않냐고 따질 수 있습니다. 하지만 이 window라는 이름을 잘 사용해 보도록 합니다.


그래서 무엇을 해볼까요? document 객체를 넘겨봅시다.

JavaScript
(function (window, document) { 
  // we refer to window and document normally 
})(window, document);

지역 변수들은 글로벌 변수보다 빠르게 해석될 수 있습니다. 

하지만 엄청난 스케일의 코드가 아닌 이상 우리는 눈에띄는 속도 향상은 느낄 수는 없습니다. 

그래도 글로벌 변수들을 많이 참조한다면 충분히 고려해볼만 합니다.



What about undefined?

ECMAScript 3에 의하면 undefied는 mutable 합니다. 

이는 undefiend = true; 처럼 값을 재할당 할수 있다는 것을 의미합니다. 

놀랍죠? 감사하게도 ECMAScript 5의 strict 모드 `user strict;`를 활용하면 parser가 error를 뱉어줍니다. 

하지만 그 이전에 우리 스스로 IIFE를 아래 코드처럼 보호해야 합니다.

JavaScript
(function (window, document, undefined) { 
})(window, document);

이렇게 하면 누군가 아래처럼 실수를 하더라도 문제가 없습니다. 

IIFE argument로 세번째 인자를 넘기지 않았기 때문에 함수 scope 안의 undefined는 실제 undefined 의미로서 동작하게 되는 것입니다.

JavaScript
undefined = true;
(function (window, document, undefined) { 
  // undefined is a local undefined variable 
})(window, document);



Minifying

만약 함수 안으로 지역 변수들이 pass in 되면 변수명 자체는 중요하지 않고 우리가 원하는 새로운 이름으로 호출 할 수 있습니다. 

아래의 코드는 minifying을 거쳐

JavaScript
(function (window, document, undefined) { 
  console.log(window); // Object window 
})(window, document);


아래 코드로 바뀝니다.

JavaScript
(function (a, b, c) { 
  console.log(a); // Object window 
})(window, document);


여러분이 참조하는 라이브러리들, window, document 객체들이 멋지게 minified 된 모습을 상상해 보시기 바랍니다. 

이뿐만 아니라 jQuery 객체 역시 $ 형태로 전달 할 수 있으며 무엇이든지 lexical scope 안에서 전달 가능합니다.

JavaScript
(function ($, window, document, undefined) {
  // use $ to refer to jQuery
  // $(document).addClass('test');
})(jQuery, window, document);
(function (a, b, c, d) {
  // becomes
  // a(c).addClass('test');
 })(jQuery, window, document);

이는 우리가 jQuery.noConflict(); 처럼 호출할 필요가 없고 지역 모듈로서 $에 jQuery를 할당해서 사용할 수 있다는 의미입니다. 

JavaScript의 scope와 global/local 변수들에 대해서 공부한다면 보다 도움이 될 것입니다.


성능이 좋은 minifier라면 당신의 코드에 있는 undefined 단어를 샅샅이 뒤져서 `c`와 같은 이름으로 치환할 것입니다. 

여기서 undefined라는 이름 자체는 치환에 있어서 큰 관계가 없습니다. 

우리가 알아야 하는 것은 참조하는 객체(Object)가 undefined라는 것이며 특별한 의미를 가지는 것은 아닙니다. 

undefined는 선언은 되었지만 값이 할당된 적이 없다는 의미의 JavaScript에서 제공하는 데이터 형입니다. (역자주: 이 부분은 minifier가 단순히 undefined 이름을 보고 치환하는 것이 아니라 레퍼런스 관계를 고려해서 치환한다는 의미 같습니다.)



Non-browser global environments

Node.js 같은 도구들 때문에 브라우저는 항상 전역 객체가 아닙니다. 

여러 환경을 고려해서 작업해야 한다면 IIFE를 생성할 때 브라우저는 항상 글로벌 객체가 아니기 때문에 신경쓰일 수 있습니다. 

때문에 저는 IIFE 코드 형태를 아래 모습을 기본으로 생성하는 습관이 있습니다.

JavaScript
(function (root) {

})(this);

브라우저에서는 전역 환경(전역객체)은 window 객체에 레퍼런스 되어 있기 때문에 window를 꼭 넘길 필요는 없고 this를 통해 간결하게 넘길 수 있습니다. 

저는 root이라는 네이밍을 선호하는데 이는 브라우저는 물론 브라우저 환경이 아닌 곳에서도 root로 참조가 가능하기 때문입니다. 


만약 여러분이 universal solution(오픈소스 프로젝트를 생성할 때 저는 이 방식을 항상 사용합니다.)에 관심이 있다면 아래와 같은 UMD wrapper방식이 있습니다.

JavaScript
(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    define(factory);
  } else if (typeof exports === 'object') {
    module.exports = factory;
  } else {
    root.MYMODULE = factory();
  }
})(this, function () {
  // 
});

이 방법은 엄청 세련된 방법입니다. 

argument로 넘어온 함수가 factory 함수로 호출되고 있습니다. 

이런 방식을 통해서 우리는 환경에 따라 적절하게 외부로 할당할 수 있습니다. 

브라우저에서는 root.MYMODULE = factory(); 방식으로 우리의 IIFE 모듈을 할당할 수 있고 Node.js의 경우에는 module.exports 방식으로, requireJS 방식(typeof define === ‘function’ && define.amdresolves가 true인 경우)으로 할당할 수 있습니다.


이와 관련된 자세한 이야기는 조금 다른 내용이지만, UMD repo도 확인해볼 것을 추천합니다.






Jaehee's WebClub