8. IIFE, modules, namespaces
1. IIFE(Immediately-Invoked Function Expression) 즉시실행함수
함수를 정의하자마자 즉시 실행하는 함수
Self-Executing Annoymous Function으로 알려진 디자인 패턴
- 첫 번째 괄호(grouping Operator)로 둘러쌓인 익명함수 : 전역 스코프에 불필요한 변수를 추가해서 오염시키는 것을 방지할 수 있을 뿐 아니라 IIFE 내부의 변수에 접근하지 못하도록 막을 수 있는 방법
- 두 번째 괄호 : 즉시 실행 함수를 생성하는 괄호. 이를 통해 자바스크립트 엔진은 함수를 즉시 해석해서 실행한다.
사용 이유
초기화 : 한 번의 실행만 필요로하는 초기화 코드 부분에 많이 사용됨(변수를 전역으로 선언하는것을 피하기 위해서). 전역에 변수를 추가하지 않아도 되기 때문에 충돌없이 구현할 수 있어 플러그인이나 라이브러리등을 만들 때 많이 사용
내부 변수 보호 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// 외부 허용
var fn = (function() {
var name = "lee";
return {
name: name
}
}());
console.log(fn.name) // lee
// 외부 허용 X
(function(){
var name = "lee";
return {
name: name
}
}());
2. Modules
- reference :
2 - 1. 모듈이란?
모듈은 구현 세부 사항을 캡슐화하고 공개 API를 노출하여 재사용이 가능한 코드로 다른 코드에서 쉽게로드하고 사용할 수 있는 코드 조각
2 -2. 모듈이 왜 필요한가?
모듈없이도 코드를 쓸 수 있다. 모듈은 60~70년 대 부터 개발자들이 많은 다른 폼들과 프로그래밍 언어에서 쓰였다.
자바스크립트에서 모듈은 아래의 역할들을 해준다.
- abstract code(코드를 추상화시켜준다.): 우리가 복잡한 실제 작동 방식을 이해할 필요가 없도록 특정한 라이브러리에게 기능을 위임해준다.
- encapsulate code(코드를 캡슐화시켜준다.): 코드가 수정되지 않길 바란다면 모듈안의 코드를 숨길 수 있다.
- reuse code(코드를 재사용한다.): 같은 코드를 또 다시 쓰지 않도록 해준다.
- manage dependencies(의존성을 관리해준다.): 코드를 다시 쓰지 않아도 되도록 쉽게 의존성을 변경할 수 있다.
(dependency : 코드에서 두 모듈 간의 연결)
2 - 3. ES5에서의 모듈 패턴
es5와 그 이전 버전에서는 모듈을 염두해두지 않고 설계되었다. 시간이 지남에 따라 개발자는 JavaScript에서 모듈 식 디자인을 시뮬레이션하기 위해 다양한 패턴을 고안했다.
이 패턴들이 어떤 것인지 감을 주기 위해 2개의 쉬운 예를 보자 : IIFE 와 노출식 모듈 패턴
2 - 3 - 1. IIFE
즉시실행함수는 선언될 때 실행되는 익명 함수이다. 함수가 괄호로 어떻게 둘러쌓여있는지 보자. 자바스크립트에서 function
이라는 단어로 시작되는 줄은 함수 선언식으로 고려된다.
1 | function() { |
함수 선언식을 즉시 실행시키면 에러를 반환한다.
1 | function() { |
함수에 괄호를 추가하면 함수 표현식으로 만들어준다.
1 | (function(){ |
함수 표현식은 함수를 반환하기 때문에 우리는 함수를 즉시 실행할 수 있다.
1 | (function(){ |
- 즉시실행함수 안의 복잡한 코드를 캡슐화시켜주어서 우리가 즉시실행함수 코드의 내용을 이해할 필요가 없다.
- 즉시실행함수 안에 변수를 정의하여 전역 스코프를 오염시키지 않는다. (즉시실행함수 안에서
var
로 선언된 변수는 즉시실행함수의 closure로 남는다.)
하지만 즉시실행함수가 의존성 관리에 대한 원리를 제공하진 않는다.
2 - 3 - 2. Revealing Module pattern
노출식 모듈 패턴은 즉시실행함수와 비슷한데, 여기서는 변수에 리턴 값을 할당해준다.
1 | // Expose module as global variable |
주의할 점은 여기서는 괄호로 함수를 감싸줄 필요가 없다는 것인데, 코드가 function
으로 시작하지 않기 때문이다.
우리는 변수를 통해 모듈의 API에 접근할 수 있다.
1 | // Access module functionality |
singleton
대신 모듈은 생성자 함수를 노출시킬 수 있다.
1 | // Expose module as global variable |
우리가 선언할 때 함수를 실행하지 않는다는 것에 주목하자.
대신, 우리는 Module
이라는 생성자 함수를 사용해 모듈을 인스턴스화한다.
(인스턴스(instance): 객체와 유사하지만 “현재 생성된 바로 그 객체”라는 뉘앙스)
1 | var module = new Module(); |
공개 API에 접근하기 위해서는
1 | module.sayHello(); |
노출식 모듈 패턴은 즉시실행함수와 비슷한 장점을 제공하지만 이것 역시 의존성 관리를 위한 원리를 제공하진 않는다.
자바스크립트가 발전함에 따라 각각 고유의 장점과 단점이있는 모듈을 정의하기 위해 더 다양한 구문이 개발되었다.
우리는 그것을 모듈 포맷이라고 부른다.
2 - 4. Module formats
모듈 포맷은 모듈을 정의하기 위해 사용되는 문법이다.
ES6 이전, 자바스크립트는 모듈을 정의하기 위한 공식적인 문법이 없었다. 따라서 똑똑한 개발자들은 자바스크립트의 모듈을 정의하기 위해 다양한 포맷을 고안해냈다.
가장 광범위하게 받아들여지고 잘 알려진 포맷들은 :
- AMD (Asynchronous Module Definition)
- CommonJS
- UMD(Universal Moule Definition)
- System.register
- ES6 module format
다음의 문법들을 알아볼 수 있게 간단하게 살펴보자.
2 - 4 - 1. AMD(Asynchronous Module Definition)
AMD 포맷은 브라우저에서 사용되고 define
함수로 모듈을 정의한다.
1 | //의존성이 있는 모듈 정의 |
2 - 4 - 2. CommonJS format
CommonJS 포맷은 Node.js에서 사용되고 require
와 module.exports
를 사용해서 의존성과 모듈을 정의한다.
1 | var dep1 = require('./dep1'); |
2 - 4 - 3. UMD(Universal Module Definition)
UMD 포맷은 브라우저와 Node.js에서 둘 다 사용 가능하다.
1 | (function (root, factory) { |
2 - 4 - 4. System.register
system.register 포맷은 es5에서 es6 모듈을 지원하기위해 개발되었다.
1 | import { p as q } from './dep'; |
2 - 4 - 5. ES6 module format
ES6부터 JavaScript는 기본 모듈 형식도 지원한다.export
토큰을 사용하여 모듈의 공개 API를 내보낸다.
1 | // lib.js |
import
토큰으로 export된 모듈을 받아온다.
1 | import { sayHello } from './lib'; |
as
를 사용하여 별칭을 만들어 import해올 수도 있다.
1 | import { sayHello as say } from './lib'; |
또는 모든 모듈을 한 번에 로드할 수 있다.
1 | import * as lib from './lib'; |
이 포맷은 또한 export할 때 기본값도 지원한다.
1 | // lib.js |
따라서 이렇게 import 할 수 있다.
1 | import sayHello, { sayGoodbye } from './lib'; |
함수 뿐만 아니라 어떤 것이든 export 할 수 있다.
1 | // lib.js |
그러나 기본 모듈 형식은 모든 브라우저를 다 지원하지는 못한다.
우리는 이미 ES6 모듈 포맷을 사용하고 있다. 하지만 우린 우리의 코드를 브라우저에서 실제로 실행시키기 전에 우리의 코드를 AMD 또는 CommonJ와 같은 ES5 모듈 포맷으로 트랜스파일 시키는 Babel과 같은 트랜스파일러가 필요하다.
2 - 5. Module loaders
모듈 로더는 특정한 모듈 포맷으로 쓰여진 모듈을 해석하고 로드한다.
모듈 로더는 런타임에 실행된다:
- 모듈 로더를 브라우저에서 로드한다.
- 모듈에게 어떤 메인 앱 파일을 로드할지 말한다.
- 모듈 로더는 메인 앱 파일을 다운받고 해석한다.
- 모듈 로더는 필요에 따라 파일을 다운로드한다.
브라우저의 개발자 콘솔을 열면 많은 파일이 모듈 로더에 의해 요청시 로드되는 것을 볼 수 있다.
인기있는 모듈 로더들
- RequireJS: AMD 포맷의 모듈을 위한 로더
- SystemJS: AMD, CommonJS, UMD System.register 포맷의 모듈을 위한 로더
2 - 6. Module bundlers
모듈 번들러는 모듈 로더를 대체한다.
하지만 모듈 로더와 대조적으로 모듈 번들러는 빌드 될 때 실행된다.
- 빌드시 모듈 번들러를 실행하여 번들 파일을 생성한다.
- 브라우저에서 번들을 로드할 수 있다.
브라우저의 개발자 콘솔에서 네트워크 탭을 열면, 오직 한 개의 파일이 로드되어있는 것을 볼 수 있다. 브라우저에서는 모듈 로더가 필요 없다. 모든 코드는 번들 안에 포함되어있다.
인기있는 모듈 번들러
- Broserify: CommonJS 모듈을 위한 번들러
- Webpack: AMD, CommonJS, ES6 모듈을 위한 번들러
요약
최신 자바스크립트 개발 환경에서 tooling을 더 잘 이해하려면 모듈, 모듈 포맷, 모듈 로더 및 모듈 번들러의 차이점을 이해해야한다.
모듈은 구현 세부 사항을 캡슐화하고 공개 API를 노출하여 재사용이 가능한 코드로 다른 코드에서 쉽게로드하고 사용할 수 있다.
모듈 형식은 모듈을 정의하는 데 사용하는 구문이다. AMD, CommonJS, UMD 및 System.register와 같은 다양한 모듈 형식이 과거에 등장했으며 이제 ES6부터 기본 모듈 형식을 사용할 수 있다.
모듈 로더는 런타임에 특정 모듈 형식으로 작성된 모듈을 해석하고 로드한다. 인기있는 예는 RequireJS 및 SystemJS이다.
모듈 번들러는 모듈 로더를 대체하고 빌드시 모든 코드 번들을 생성한다. 인기있는 예는 Browserify 및 Webpack이다.
3. Namespace pattern
- reference: http://www.nextree.co.kr/p7650/
네임스페이스 : 구분이 가능하도록 정해놓은 범위나 영역
원하는 결과를 얻기 위해 좀 더 ‘자세한’ 정보가 필요한데 이를 자바스크립트 관점에서 바라보면 네임스페이스가 자세한 정보가 된다.
=> namespacing : 객체나 변수가 겹치지 않는 안전한 소스코드를 만드는 개념.
하지만 자바스크립트에서는 아직까지 네임스페이싱을 위한 기능을 지원하지 않기 때문에
- 자바스크립트의 모든 객체는 프로퍼티를 가진다.
- 그 프로퍼티는 다시 다른 객체를 담을 수 있다.
위의 특성들을 이용해 네임스페이스와 비슷한 효과를 얻을 수 있는데 이러한 기법을 namespace pattern이라고 한다.
3 - 1. obejct literal namespacing(객체 리터럴 네임스페이싱)
- 가장 기본적인 네임스페이싱 방식
- 하나의 전역 객체를 만든 후 모든 함수, 객체, 변수를 추가하여 구현
- 전역 객체 선언 시에 리터럴로 미리 선언 가능
장점 : 코드 내에서 뿐 아니라 같은 페이지에 존재하는 JS 라이브러리나 third-party code와의 이름 충돌도 방지해 주며 체계적
단점 :
- 모든 변수와 함수에 상위 객체 명을 모두 붙여야 하기 때문에 소스 코드량이 늘어남. 결국 그에 따라 다운로드해야 하는 파일의 크기도 늘어남
- 전역 인스턴스가 단 하나 뿐이기 때문에 코드의 어느 한 부분이 수정되어도 전역 인스턴스를 수정. 계속해서 나머지 기능들도 갱신된 상태를 물려받게 됨.
- 매번 객체에 접근하는데다가 이름이 중첩되고 길어지므로 검색이 느려지게 됨.
1 | // 하나의 전역 객체 |
매번 객체에 접근하는 단점을 해결하고, 미래의 유지보수를 위해 this
키워드를 사용하는 방법을 생각할 수도 있지만, namespace로 사용되고 있는 객체를 this를 사용해서 참조해서는 안 된다.
1 | // namespace 사용 |
this
키워드는 함수를 호출하는 방식에 의해 정해지게 되는데, console.log(MYAPP.sayHello());
에서는 dot notation 방법으로 sayHello 함수를 호출하고있기 때문에 MYAPP이 this
가 된다.
하지만 console.log(direct());
에서는 일반 함수 호출 방법으로 direct함수를 호출하고 있기 떄문에 this는 window를 가리키게 되고, window.message는 없기 때문에 undefined를 리턴한다.
따라서 절대 this로 네임스페이스로 사용되고 있는 객체를 참조해서는 안 된다. 이러한 네임스페이스 패턴의 단점을 해결하기 위해서는 샌드박스 패턴(sandbox Pattern)을 사용해야 한다.
3 - 2. 샌드박스 패턴
전역 범위를 더럽히지 않고 맘껏 쓰일 수 있도록 유효 범위를 정해준다.
위에서 다룬 네임스페이스 패턴에서는 단 하나의 전역 객체를 생성했다. 샌드박스 패턴에서는 생성자를 유일한 전역으로 사용한다. 그리고 유일한 전역인 생성자에게 콜백함수를 전달해 모든 기능을 샌드박스 내부 환경으로 격리시키는 방법을 사용한다.
1 | function Sandbox() { |
이렇게 샌드박스 패턴을 사용하면 콜백함수로 감싸진 샌드박스 객체 안에서 마음껏 구현이 가능하다. 또한 위에서 언급한 네임스페이스 패턴의 몇 가지 단점을 극복할 수 있다.
- 단 하나의 전역변수에 의존하는 네임스페이스 패턴의 단점을 여러 개의 샌드박스 객체를 생성함으로 극복할 수 있다.
- 점으로 연결된 긴 이름을 쓸 필요가 없다.
- runtime에 탐색 작업을 거치지 않게 해준다.
3 - 3. 범용 네임스페이스 함수
- 프로그램이 복잡해짐에 따라 코드의 각 부분들이 별개의 파일로 분리되어 선택적으로 문서에 포함되는 경우가 많음.
- 그러다보면 네임스페이스로 사용할 객체를 선언할 때나 그 내부의 프로퍼티를 정의할 때 이미 있는 것을 재정의하는 일도 생길 수 있음.
- 이를 확인하지 못한 채 지나가면 내용을 덮어쓰는 문제가 생김.
1 | // 1번 |
1번 처럼 객체를 선언하는 대신, 2번처럼 미리 선언되어있는지를 확인하고 정의해주어야한다. 3번은 2번과 똑같이 작동하는 short-hand 방식.
하지만 이런식으로 반복하다보면 중복되는 코드가 생겨남. => 재사용 가능한 함수를 만드는 것이 좋다.
1 | // 가장 상단에 위치할 객체는 먼저 선언합니다. |