해당 시리즈는 Leonardomso의 33 Concepts Every JavaScript Developer Should Know 를 보고 공부, 정리한 시리즈이며, 자세한 내용은 링크를 확인하길 바란다.
즉시 실행 함수 IIFE, 모듈, 네임스페이스에 대해서 이번에 알아보자.
추가적으로 전역 변수의 사용을 줄이는데 있어서 위 개념의 활용 방식에 대해서도 얘기해보자.
Immediately Invoked Function Expression
즉시 실행 함수는 말 그대로, 선언된 즉시 실행되는 함수를 의미한다.
(function (){
let people = ['park', 'choi', 'kim']
console.log(people)
}())
즉시 실행 함수는 위와 같이 그룹 연산자 안에 감싸진 형태로 대개 작성된다.
즉시 실행 함수를 생성할 때, 이름이 없는 익명 함수를 사용하는 것이 일반적이지만, 이름이 있는 기명 함수를 사용하여도 상관없다.
(function getNames(){
let people = ['park', 'choi', 'kim']
console.log(people)
}())
getNames() // ReferenceError: 'getNames' is not defined
하지만, 즉시 실행 함수는 한번만 호출되기에 외부에서 다시 호출할 수 없다.
이는 그룹 연산자 내에 있는 함수가 함수 선언문이 아닌 함수 리터럴이기 때문이다.
왜 그럴까?
그것은 그룹 연산자 때문이다!
그룹 연산자는 피연산자를 값으로 평가한다.
함수 선언문은 값으로 평가될 수 없는 표현식이 아닌 문(Statement)
이다.
그렇기에 그룹 연산자 내에 있는 기명 함수는 함수 선언문이 아닌 값으로 평가될 수 있는 함수 리터럴로 평가되어 함수 객체로 인식된다.
즉시 실행 함수는 그룹 연산자와 같이 연산자를 활용해 함수 선언문이 아닌 함수 리터럴로 만들어서 값으로 사용하기 위한 방법임으로 다양한 연산자와 사용할 수 있다.
(function (){
}())
(function (){
})()
!function (){
}()
+function (){
}()
가장 일반적인 방법은 첫번째와 두번째 방식 같이 그룹 연산자를 사용하는 방식이니 그룹 연산자 내 함수를 선언하는 표기로 알아둬도 괜찮다.
모듈은 애플리케이션을 구성하는 요소로서 재사용 가능한 코드 조각을 의미한다.
보통 파일 단위로 모듈을 관리하며, 개별적인 존재로 애플리케이션과 별개의 존재로 관리된다.
모듈이 되기 위해서는 자신만의 파일 스코프 즉, 모듈 스코프를 가질 수 있어야 한다.
모듈은 모듈 스코프를 갖고 있다.
모듈은 기본적으로 모듈 스코프 내에 존재하는 모든 자산(변수, 함수 등등)은 비공개한다.
모듈은 export 를 통해 자신이 공개하고픈 자산들만 공개할 수 있으며,
모듈이 공개한 자산을 사용하고자 하는 사용자는 import 를 통해 사용할 수 있다.
모듈을 통해 기능별로 코드를 파일 단위로 나누어
재사용성을 높여 개발 효율성과 유지보수성을 높이는데 목적을 갖는다.
자바스크립트는 웹페이지의 단순한 보조 기능의 역할을 하기 위해 탄생한 언어이다.
그렇기에, 클라이언트 사이드 자바스크립트 즉, 웹 브라우저 상 자바스크립트에서는 모듈 시스템을 지원하지 않았다.
script
태그를 사용하여 JS 파일을 로드해도 각 파일이 독립적인 스코프를 갖는 것이 아니라 모두 같은 전역 스코프를 공유했다.
즉, 여러개의 JS 파일을 로드해도 결국은 하나의 JS 파일인 것처럼 동작했던 것이었다.
이처럼 브라우저에서 모듈 시스템은 따로 사용할 수 없다가 ES6 때부터 모듈 시스템을 사용할 수 있도록 기능이 추가되었다.
(Node.js (자바스크립트 런타임 환경) 에서는 CommonJS 를 활용하여 모듈 시스템을 구축할 수 있었다.)
ES6 부터 사용할 수 있어서 ESM 이라고도 부른다.
ES6 부터 IE 를 제외한 거의 대부분의 브라우저에서 모듈 기능을 지원하기 시작했다.
사용 방법은 간단한다.
script
태크에 type="module"
어트리뷰트를 추가하기만 하면 된다.
<script type="module" src="app.js" />
앞서 얘기한 모듈이 갖는 특징을 하나씩 살펴보자.
모듈은 독자적인 모듈 스코프를 갖는다.
아래와 같이 ES6 이전처럼 type="module"
를 선언 안했을 시에는 개별적인 스코프가 아닌 모두 같은 전역 스코프를 공유했다.
<!DOCTYPE html>
<html>
<body>
<script src="abc.js"></script>
<script src="efg.js"></script>
</body>
</html>
// abc.js
var x = 'abc'
console.log(window.x) // 'abc'
// efg.js
console.log(window.x) // abc
var x = 'efg'
console.log(window.x) // efg
위 abc.js
와 efg.js
파일 내부에서 출력된 결과를 보면, 파일 내부에서 선언된 x
라는 변수가 파일 내부 스코프에만 국한된 것이 아닌 전역 변수(window.x
) 로 취급되는 것을 볼 수 있다.
ES6 이후 모듈 선언을 하면(type="module"
) 각각의 모듈들이 개별적인 스코프를 갖기에 전역 스코프에는 아무런 영향을 주지 않는다.
<!DOCTYPE html>
<html>
<body>
<script type="module" src="abc.js"></script>
<script type="module" src="efg.js"></script>
</body>
</html>
// abc.js
var x = 'abc'
console.log(x) // abc
console.log(window.x) // undefined
// efg.js
var x = 'efg'
console.log(x) // efg
console.log(window.x) // undefined
위 abc.js
와 efg.js
파일 내부에서 전역 변수(window.x
) 출력한 결과를 undefined
인 것으로 보아 전역 스코프에는 아무런 영향을 미치지 않고 개별적인 스코프를 갖는 것을 볼 수 있다.
외부에 공개하고 싶은 자산(변수, 함수 등) 앞에 export
를 붙이면 된다.
// 변수
export const number = 10;
// 함수
export function add(a, b) {
return a + b;
}
// 클래스
export class Person {
constructor(name) {
this.name = name;
}
}
위와 같이 export
를 매번 붙이지 않고 하나의 객체로 묶어서 한번에 내보낼 수도 있다.
export {number, add, Person};
다른 모듈에서 공개한 자산을 재사용하고 싶으면 import
해서 사용하면 된다.
import { number, add, Person } from './lib.mjs';
console.log(number); // 10
console.log(add(number, 7)); // 17
console.log(new Person('Park')); // Person { name: 'Park' }
위 코드 처럼 export 한 자산의 이름을 그대로 import 해도 되고
as
를 활용하여 이름을 바꿔도 되며,
import { number as N, add as sum, Person as P } from './lib.mjs';
console.log(N); // 10
console.log(sum(5, 7)); // 12
console.log(new P('Park')); // Person { name: 'Park' }
* as
를 활용하여 하나의 객체로 묶은 뒤에도 활용해도 된다.
import * as lib from './lib.mjs';
console.log(lib.number); // 10
console.log(lib.add(number, 7)); // 17
console.log(new lib.Person('Park')); // Person { name: 'Park' }
default
키워드는 export 할 때 사용하는 키워드이다.
default
키워드를 사용하여 export 를 진행하면 그 대상은 import 될 때 임의의 이름으로 import 할 수 있다.
// twice.js
export default x => x + x
// app.js
import twice from './twice.js'
console.log(twice(3)) // 6
위처럼 twice
라는 임의의 이름으로 import 하여 정상적으로 사용한 것을 볼 수 있다.
다만, default export 한 모듈을 import 할 때 임의의 이름으로 설정하여 헷갈리는 것을 예방하기 위해 되도록이면 파일명과 같은 이름을 사용해서 import 하자! (twice
== twice.js
)
단, default 를 사용할 때 2가지를 유의해서 사용하자!
default 는 한번만 사용할 수 있다.
var
, let
, const
와 같은 키워드를 사용할 수 없다.
위 키워드를 사용하여 default export 시 에러가 발생한다.
export default const name = 'park'
// SyntaxError: Unexpected token
Namespace 이름 공간에 대한 정의를 찾아보면, 위키백과에서는 다음과 같이 설명한다.
개체를 구분할 수 있는 범위
즉, 네임스페이스는 특정 변수나 함수의 이름을 구분할 수 있는 공간이다.
예를 들어보자,
A 대학교에는 여러명의 홍길동
이라는 동명이인들이 존재한다.
이 때, 홍길동
이라는 이름만을 갖고 A 대학교에서 찾는 것은 매우 어렵다.
하지만, 만약에 B 단과대의 C 학과 소속의 홍길동
을 찾는다면, 이는 훨씬 쉽다.
이처럼 B 단과대와 C 학과라는 좀 더 자세한 정보들이 있어야 용이하게 원하는 사람을 찾을 수 있고, 이와 같은 자세한 정보 가 자바스크립트에서 Namespace 를 의미한다.
네임스페이싱은 네임스페이스가 겹치지 않게 변수나 함수를 보관하는 코드를 짜는 방법이다.
C++ 이나 Java 에서는 각각 namespace
나 package
와 같은 키워드를 사용하여 네임스페이스를 구축할 수 있다.
하지만, 자바스크립트에서는 따로 그러한 기능을 제공해주지 않기에, 객체를 활용하여 네임스페이스를 구축한다.
var myApp = {}
myApp.id = 0;
myApp.next = function() {
return myApp.id++;
}
myApp.reset = function() {
myApp.id = 0;
}
console.log(myApp.next(), myApp.id, myApp.reset(), myApp.id) //0, 1, undefined, 0
위처럼 myApp
라는 네임스페이스 객체를 통해 객체의 프로퍼티로 변수나 함수 등을 보관하는 방식으로 네임스페이싱을 구현할 수 있다.
다만, 기존에 사용하고 있는 객체와 중첩되지 않게 아래와 같이 중첩 여부를 확인하고 네임스페이싱을 진행해야 한다.
var myApp = myApp || {}
위 3가지 개념은 모두 전역 변수의 사용을 줄이고 대체하는 역할을 수행하는 공통점이 있다.
자바스크립트에서 전역 변수는 다양한 문제를 야기시킬 수 있다.
자바스크립트에서는 코드 어디서든 전역 변수를 참조하고 변경할 수 있는 암묵적 결합(Implicit coupling) 을 허용한다.
애초에 전역 변수를 선언했다는 것이 코드 어디서든 해당 변수를 사용하겠다는 의미이기 때문에 암묵적 결합을 감수하고 사용한다고 볼 수 있다.
하지만, 이러한 특징으로 인해, 예상치 못하게 변수의 상태를 변경시키는 위험이 커진다.
전역 스코프는 생명 주기가 길기 때문에 메모리 공간도 오랫동안 차지하며, 변수 이름이 중복되거나 의도치 않게 재할당한 우려가 있다.
스코프 체인을 따라 변수를 찾아가는 과정에서 전역 스코프는 종점, 즉 가장 마지막에 바라보게 되는 스코프이다.
그렇기에 전역 변수의 검색 속도가 가장 느리다는 문제점을 갖는다.
(자세한 내용은 스코프 관련 게시글을 살펴보자!)
자바스크립트는 여러 파일로 나누어 관리해도 하나의 전역 스코프를 공유한다.
그렇기에 서로 다른 파일에서 동일안 이름의 전역 변수나 함수가 선언되어 예상치 못한 혼란과 결과를 야기시킬 수 있다.
앞에서 즉시 실행 함수는 단 한 번만 호출할 수 있다고 했다.
이런 특징을 살려, 모든 코드를 즉시 실행 함수로 감싸면, 모든 변수는 즉시 실행 함수의 지역 변수이기에 한 번만 호출되고 사라진다.
(function getNames(){
let people = ['park', 'choi', 'kim']
console.log(people)
}())
people // ReferenceError: people is not defined
즉시 실행 함수는 이처럼 전역 변수를 생성하지 않기에 라이브러리에서 자주 활용되는 방법이다.
앞에서 네임스페이스를 설명하면서 소개한 네임스페이스 객체 즉, 전역에서 네임스페이스 역할을 담당할 객체를 따로 생성하여 관리하는 방법이다.
var MYGLOBAL = {} // 전역 네임스페이스 객체
MYGLOBAL.name = 'park'
console.log(MYGLOBAL.name) // 'park'
다만, 전역 네임스페이스 객체도 결국은 전역에 선언되는 것이기에 스코프 체인의 종점에 존재하거나 긴 생명주기를 갖는 것과 같은 전역 변수의 문제점을 갖고 있는 한계를 갖는다.
앞서 말했던 것처럼 ES6 모듈은 개별적인 스코프인 모듈 스코프를 갖기에 전역 변수로 선언되지 않는다.
다만, ES6 모듈 IE 와 같은 구형 브라우저에서는 동작하지 않기에 이 경우 Webpack 등의 모듈 번들러를 사용해야 한다.
IIFE 즉시 실행 함수는 함수를 그룹 연산자와 같은 연산자의 피연산자로 호출되는 함수로, 단 한 번만 호출할 수 있고 다시 호출하거나 참조할 수가 없는 특징을 갖는다.
모듈은 파일 단위의 재사용 가능한 코드 조각으로, 개발 효율성과 유지보수성을 높이는 장점을 갖는다.
모듈은 모듈 스코프를 갖는다.
모듈 내 자산들은 export 를 통해서 공개할 수 있으며, 사용자는 import 를 통해서 공개된 자산을 사용할 수 있다.
Namespace 는 특정 변수나 함수의 이름을 구분할 수 있는 공간으로, 이름을 찾을 수 있는 자세한 정보와 같다.
IIFE, 모듈, 네임스페이스는 모두 각각의 목적이있지만, 공통적으로 전역 변수의 사용을 줄이는데 사용한다는 공통점이 있다.
Essential JavaScript: Mastering Immediately-invoked Function Expressions