프로그래밍 언어의 장점을 활용해서 사용하는 것만큼, 단점을 보완하는 것도 중요한 일입니다. 단점을 무시한 채 장점만을 활용한 코드는 효율성과 안정성을 보장하기 힘들기 때문입니다.

전역변수를 기초로 하는 JavaScript의 단점 때문에, 전역 네임스페이스(Global Namespace)의 오염 문제는 많이 신경쓰이는 부분입니다. 여러 스크립트가 한 페이지 안에 함께 있는 소스코드에서는 전역 변수가 많아질 수록 이름이 겹칠 우려가 있습니다. 뿐만 아니라 어느 곳에서든 접근할 수 있으므로 소스코드의 신뢰성을 떨어뜨릴 수 있습니다. 이런 단점을 보완하는 몇 가지 방법을 살펴봅니다. 그리고 그 중에서 네임스페이스 패턴은 자세히 알아보겠습니다.

1. var 사용

JavaScript에는 암묵적 전역(implied globals)이라는 개념이 있습니다. 그렇기 때문에 var를 사용하지 않고 변수를 선언하거나, 선언되지 않은 변수를 사용하면 아무리 지역 함수 내에 있더라도 전역에 속하게 됩니다. 여기서, 변수에 관한 내용임에도, ‘전역 변수’가 되었다고 하지 않고, ‘전역에 속하게 되었다’고 한 이유는 var를 사용하지 않았을 경우에는 전역 변수가 아닌 전역 객체의 프로퍼티(property)로 생성되기 때문입니다. 아래의 예제를 봅시다.

/*
 * a : 함수에 속하지 않고 var를 사용하여 선언된 전역 변수
 * b : 함수에 속하지 않고 암묵적으로 생성된 전역 객체의 프로퍼티
 * c : 함수에 속하고 암묵적으로 생성된 전역 객체의 프로퍼티
 */

var a = 1;
b = 2;
(function () { 
	c = 3; 
}());

delete a; 
delete b; 
delete c;

console.log(typeof a);        // number 출력
console.log(typeof b);        // undefined 출력
console.log(typeof c);        // undefined 출력

[예제1] 암묵적 전역 변수

프로퍼티는 delete연산자로 삭제할 수 있지만 변수는 그렇지 않다는 특성을 통해, 변수 b와 c는 전역 변수가 아닌 전역 객체의 프로퍼티가 됨을 알 수 있습니다.

또 한가지, 단일 var패턴이라는 것이 있습니다. 이 패턴은 함수 상단에서 var선언을 하나만 쓰고 여러 개의 변수를 쉼표로 연결하여 선언합니다. 이는 함수에서 필요로 하는 모든 지역변수를 한군데서 찾을 수 있게 해주고, 변수를 선언하기 전에 사용하면 발생하는 논리적인 오류를 막아준다는 장점이 있습니다. 하지만 복잡한 소스 코드의 구조 안에서 모든 변수를 함수의 위쪽에 선언한 다는 것은 어려운 일이므로, 이 패턴은 모듈 단위 개발에서 효과가 있습니다.

2. 즉시 실행 함수와 즉시 객체 초기화

전역 변수를 줄이기 위해, 즉시 실행 함수(IIFE, Immediately-Invoked Function Expression)를 사용하는 방법이 있습니다. 이 함수는 선언과 동시에 바로 실행되므로 전역 변수를 만들지 않아, 플러그인이나 라이브러리 등을 만들 때 많이 사용됩니다. 또한 즉시 실행 함수는 모듈 패턴의 기반이 되기도 합니다. 이에 대한 자세한 내용은 넥스트리 홈블로그 내의 이재욱 / JavaScript: 함수(function) 다시 보기 글을 참조하세요.

console.log(typeof 
// 함수 선언문
function func() {
	var days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
	var today = new Date();
	var msg = 'Today is ' + days[today.getDay()] + ', ' + today.getDate(); 
	console.log(msg);  
}
);   // function 출력
console.log(typeof 
// 즉시 실행 함수
(function () {
	var days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
	var today = new Date();
	var msg = 'Today is ' + days[today.getDay()] + ', ' + today.getDate(); 
	console.log(msg);  // Today is Mon, 24 출력
}())
);   // undefined 출력

[예제2] 즉시 실행 함수

typeof키워드로 각 함수의 타입을 알아보았습니다. 함수 선언문으로 선언된 위쪽 코드의 함수 타입은 function입니다. 즉, 이대로 전역 범위에 선언하면 전역 객체에 func라는 이름의 함수가 추가된다는 뜻입니다. 반면, 익명의 즉시 실행 함수 표현식으로 선언된 아래쪽 코드의 함수 타입은 undefined입니다. 함수가 즉시 실행되고 난 후엔 전역에 남지 않고 바로 사라지기 때문입니다. (+ 즉시 실행 기명 함수도 실행 결과는 같지만, 한번만 사용하고 다시 호출할 일이 없으므로 기명으로 쓸 필요가 없습니다.)

그렇기 때문에, 재사용할 필요 없이 한번만 호출할 함수들은 즉시 실행 함수 패턴을 사용하면 전역 네임스페이스를 오염시키지 않습니다.

그리고, 즉시 객체 초기화라는 패턴도 있습니다. 괄호로 묶어서 바로 초기화하는 사용 방식과, 전역 변수를 만들지 않는다는 장점이 즉시 실행 함수와 비슷합니다. 단점으로는 대부분의 JavaScript 압축 도구가 즉시 실행 함수에 비해 효과적으로 압축하지 못한다는 것입니다. 다음의 코드는 즉시 객체 초기화 패턴의 예제입니다.

({
	// 속성 정의
	name: "nextree",

	// 객체 메소드 정의
	getName: function () {
		return this.name;
	},

	// 초기화 메소드 정의
	init: function () {
		console.log(this.getName());   // nextree 출력
	}
}).init();

[예제3] 즉시 객체 초기화

또 한가지 고려해야 할 것은, 즉시 실행 함수와 즉시 객체 초기화의 남용으로 인한 메모리 낭비입니다. JavaScript는 이렇게 할당이 없이 정의만 할 경우, 전역 네임스페이스는 건드리지 않더라도, 전역 실행 컨텍스트(EC: Execution Context)의 temp=[]내에 key-value를 추가하게 됩니다. 이 EC.temp영역은 개발자가 접근할 수 없는 영역입니다. 그렇기 때문에 스크립트 내의 다른 영역은 물론 어디에서도 접근할 수 없어, 소스코드의 신뢰성에는 큰 도움이 됩니다. 하지만 같은 이유로 이 패턴을 남용하면, 직접 관리 할 수 없는 공간에 메모리가 계속 쌓이게 됩니다. 그렇기 때문에 소스코드의 신뢰성과 메모리의 문제를 함께 고민해서, 이를 남용하지 않고 적절히 사용해야 합니다.

3. 네임스페이스 패턴(Namespace Pattern)

Namespace Pattern이란?

네임스페이스(namespace)는 구분이 가능하도록 정해놓은 범위나 영역을 뜻합니다. 즉, 말 그대로 이름 공간을 선언하여 다른 공간과 구분하도록 합니다.

한 가지 예를 들어보겠습니다. 만약 대한민국에 사는 홍길동이라는 사람을 찾는다면, 처음에 의도했던 특정한 사람을 찾아내는 것은 거의 불가능합니다. 서울에 사는 홍길동이라고 하면 범위는 조금 좁아지겠지만 여전히 원하던 사람을 찾기 어렵겠죠. 대한민국, 서울시, **구, ##로 123번지에 사는 홍길동 이라는 정보가 있어야만 특정 인물을 찾을 수 있습니다. 이처럼, 원하는 결과를 얻기 위해서는 좀 더 ‘자세한’ 정보가 필요합니다. 이를 JavaScript 언어 관점에서 바라보면, '자세한 정보'들이 바로 네임스페이스입니다. 그래서, 대한민국에서 홍길동을 찾기는 전역 네임스페이스에서 특정 변수를 직접 찾기와 같은 것이죠.

결국 네임스페이싱(namespacing)은 객체나 변수가 겹치지 않는 안전한 소스코드를 만드는 개념입니다. 하지만 JavaScript는 아직까지 네임스페이싱을 위한 기능을 지원하지 않기 때문에 다음의 특성을 통해 네임스페이스와 비슷한 효과를 얻을 수 있습니다.

  • JavaScript의 모든 객체는 프로퍼티를 가집니다.
  • 그 프로퍼티는 다시 다른 객체를 담을 수 있습니다.

이러한 네임스페이싱 코딩 기법들을 *네임스페이스 패턴(namespace pattern)*이라고 합니다.

3.1 객체 리터럴 네임스페이싱(Object Literal Namespacing)

가장 기본적인 네임스페이스 패턴은 객체 리터럴 네임스페이싱 방식 입니다. 하나의 전역 객체(global object)를 만든 후, 모든 함수, 객체, 변수를 여기에 추가하여 구현합니다. 물론 꼭 나중에 추가하지 않아도 전역 객체 선언 시에 리터럴로 미리 선언해 주어도 됩니다.

// 하나의 전역 객체
var MYAPP = {};

MYAPP.Parent = function() { console.log('Parent'); };
MYAPP.Child = function() { console.log('Child'); };

MYAPP.variable = 1;

// 객체 컨테이너
MYAPP.modules = {};

// 객체들을 컨테이너 안에 추가합니다.
MYAPP.modules.module1 = {};
MYAPP.modules.module1.data = {a: 1, b: 2};
MYAPP.modules.module2 = {};

MYAPP.Parent();                               // Parent 출력
console.log(MYAPP.modules.module1.data.a);    // 1 출력
MYAPP.Child();                                // Child 출력

[예제4] 네임스페이스 패턴

이 패턴은 코드 내에서 뿐 아니라 같은 페이지에 존재하는 JS 라이브러리나 서드 파티 코드(third-party code)와의 이름 충돌도 방지해 주며 체계적이라는 장점이 있습니다. 하지만 다음과 같은 단점도 존재합니다.

모든 변수와 함수에, 상위 객체 명을 모두 붙여야 하기 때문에 소스코드량이 늘어납니다. 결국 그에 따라 다운로드해야 하는 파일의 크기도 늘어나게 됩니다.

전역 인스턴스가 단 하나뿐이기 때문에 코드의 어느 한 부분이 수정되어도 전역 인스턴스를 수정하게 됩니다. 계속해서 나머지 기능들도 갱신된 상태를 물려받게 됩니다.

매번 객체에 접근하는데다, 이름이 중첩되고 길어지므로 검색이 느려지게 됩니다.
여기서, 매번 오브젝트에 접근하는 단점을 해결하고, 미래의 유지보수를 위하여 this키워드를 사용하는 것을 생각할 수도 있습니다. this를 사용하면 전역객체에 접근하여 검색할 필요가 없이, 바로 상위 객체만을 검색할 수 있기 때문입니다.

하지만 namespace로 사용되고 있는 객체를 this를 사용하여 참조해서는 안 됩니다. 다음의 예를 살펴보겠습니다.

var MYAPP = {};

MYAPP.message = "Hi";
MYAPP.sayHello = function() {
	// 네임스페이스 명을 사용하여 리턴
	return MYAPP.message;
};

console.log(MYAPP.sayHello());    // Hi 출력
var direct = MYAPP.sayHello;
console.log(direct());            // Hi 출력
var MYAPP = {};

MYAPP.message = "Hi";
MYAPP.sayHello = function() {
	// this를 사용하여 리턴
	return this.message;
};

console.log(MYAPP.sayHello());    // Hi 출력
var direct = MYAPP.sayHello;
console.log(direct());            // undefined 출력

[예제5] this키워드를 사용한 참조

위쪽의 코드는 네임스페이스명을 사용하고, 아래쪽의 코드는 this키워드를 사용하여 같은 객체에 속한 변수를 리턴하고자 했습니다. 위쪽 코드는 네임스페이스명을 사용했을 때나 그를 새로운 전역변수에 대입했을 때나 같은 결과를 출력합니다. 하지만 아래쪽 코드는 네임스페이스명을 사용하여 출력하였을 때는 원하던 결과를 리턴하지만 그를 새로운 전역변수에 대입하자, undefined를 출력합니다. 이는 this키워드의 특성 때문입니다. 함수 영역 안에 있는 this키워드는 부모의 자식으로 불렸을 때에만 그 부모객체를 가리키고, 직접 호출하였을 때는 더 이상 부모객체가 아닌 전역 객체를 가리키기 때문입니다.

그래서 this는 절대 네임스페이스로 사용되고 있는 객체를 참조해서는 안됩니다. 위와 같이 네임스페이스로부터 식별자(identifier)를 가져오는데 혼란이 될 수 있기 때문입니다.

이러한 네임스페이스 패턴의 단점을 해결하기 위해서는 this키워드가 아닌 샌드박스 패턴(Sandbox Pattern)을 사용해야 합니다. 샌드박스 패턴은 뒤에서 살펴보겠습니다.

3.2 범용 네임스페이스 함수

프로그램이 복잡해짐에 따라, 코드의 각 부분들이 별개의 파일로 분리되어 선택적으로 문서에 포함되는 경우가 많습니다. 그러다 보면, 네임스페이스로 사용할 객체를 선언할 때나, 그 내부의 프로퍼티를 정의 할 때, 이미 있는 것을 재정의하는 일도 생길 수 있습니다. 이를 확인하지 못한 채 지나가면, 내용을 덮어쓰는 문제가 생깁니다.

// 1번
var MYAPP = {};

// 2번
if (typeof MYAPP === "undefined") {
	var MYAPP = {};
}

// 3번
var MYAPP = MYAPP || {};

[예제6] 전역 객체의 존재여부를 확인

따라서 1번처럼 객체를 선언하는 대신, 2번처럼 MYAPP이 미리 선언되었는지를 확인하고 정의해주어야 합니다. 3번은 2번과 똑같이 작동하는 short-hand방식입니다. 하지만 이런 식으로 반복해서 확인 작업을 해주면 중복되는 내용의 코드가 상당히 많이 생겨날 수 있습니다. 네임스페이스의 깊이가 깊어질수록 각 단계마다 확인해 주어야 하기 때문입니다. 그래서 아래의 소스코드와 같이 이 작업을 맡아줄 재사용 가능한 함수를 만드는 것이 좋습니다.

// 가장 상단에 위치할 객체는 먼저 선언합니다. 
// (namespace함수를 전역으로 선언하지 않기 위함입니다.)
var MYAPP = MYAPP || {};

MYAPP.nsFunc = function (ns_string) {
	// '.'으로 구분된 네임스페이스 표기를 쪼갭니다
	var sections = ns_string.split('.'),
	    parent = MYAPP,
	    i;

	// 최상단의 MYAPP객체는 이미 선언되었으므로 제거합니다.
	if (sections[0] === "MYAPP") {
		sections = sections.slice(1);
	}

	var s_length = sections.length;
	for (i=0; i<s_length; i+=1) {
		// 프로퍼티가 존재하지 않아야만 생성합니다.
		if (typeof parent[sections[i]] === "undefined") {
			parent[sections[i]] = {};
		}
		parent = parent[sections[i]];
	}
	return parent;
};

[예제7] 전역 객체의 존재 확인 함수

이렇게 만든 namespace 함수를 사용하면 다음과 같은 긴 네임스페이스도 안전하게 만들 수 있습니다. 코드 아래쪽 그림은 크롬의 개발자 도구를 사용하여 확인한 것인데, korea를 최상단으로 일련의 네임스페이스가 생성된 것을 볼 수 있습니다.

MYAPP.nsFunc('korea.seoul.geumcheongu.gasandigital1ro.jeiplatz.name');
console.log(window.MYAPP);
yrkim-140321-namespace-01

3.3 샌드박스 패턴(Sandbox Pattern)

샌드박스는 본래, 말 그대로 어린이가 안에서 노는 모래 놀이 통을 의미합니다. 모래를 담은 상자에는 그 상자 밖으로 모래를 흘리거나 더럽히지 말고 그 안에서는 맘껏 놀라는 의미가 담긴 것입니다. JavaScript 샌드박스 패턴도 본래의 의미와 같이 코드들이 전역 범위를 더럽히지 않고 맘껏 쓰일 수 있도록 유효 범위를 정해줍니다.

위에서 살펴 보았던 네임스페이스 패턴에서는 단 하나의 전역 객체를 생성했습니다. 샌드박스 패턴에서는 생성자를 유일한 전역으로 사용합니다. 그리고 유일한 전역인 생성자에게 콜백 함수(Callback function)를 전달해 모든 기능을 샌드박스 내부 환경으로 격리 시키는 방법을 사용합니다.

다음과 같이 샌드박스 생성자를 구현합니다.

function Sandbox() {
	    // argument를 배열로 바꿉니다.
	var args = Array.prototype.slice.call(arguments),
	    // 마지막 인자는 콜백 함수 
	    callback = args.pop(),
	    // 모듈은 배열로 전달될 수도있고 개별 인자로 전달 될 수도 있습니다.
	    modules = (args[0] && typeof args[0] === "string") ? args : args[0],
	    i;

	// 함수가 생성자로 호출되도록 보장(new를 강제하지 않는 패턴)
	if (!(this instanceof Sandbox)) {
		return new Sandbox(modules, callback);
	}

	// this에 필요한 프로퍼티들을 추가
	this.a = 1;
	this.b = 2;

	// "this객체에 모듈을 추가"
	// 모듈이 없거나 "*"(전부)이면 사용 가능한 모든 모듈을 사용한다는 의미입니다.
	if (!modules || modules === '*' || modules[0] === '*') {
		modules = [];
		for (i in Sandbox.Modules) {
			if (Sandbox.modules.hasOwnProperty(i)) {
				modules.push(i);
			}
		}
	}

	// 필요한 모듈들을 초기화
	var m_length = modules.length;
	for (i=0; i<m_length; i+=1) {
		Sandbox.modules[modules[i]](this);
	}

	// 콜백 함수 호출
	callback(this);
}

// 필요한 프로토타입 프로퍼티들을 추가
Sandbox.prototype = {
	name: "nextree",
	getName: function () {
		return this.name;
	}
};

[예제8] 샌드박스 생성자

그리고 다음과 같이 new키워드를 사용하지 않고 'ajax'와 'dom'이라는 가상의 모듈을 사용하는 객체를 생성 합니다. 이처럼 사용할 모듈들을 앞쪽 인자(argument)로 전달해주고, 마지막 인자로는 콜백 함수를 전달해주면 됩니다.

Sandbox ('ajax', 'dom', function (box) {
	// console.log(box);
});

이렇게 샌드박스 패턴을 사용하면 콜백 함수로 감싸진 샌드박스 객체 안에서 마음껏 구현이 가능합니다. 또한 위에서 언급했던 네임스페이스 패턴의 몇 가지 단점을 극복할 수 있습니다.

  • 단 하나의 전역변수에 의존하는 네임스페이스 패턴의 단점을 여러 개의 샌드박스 객체를 생성함으로 극복할 수 있습니다.
  • 점으로 연결된 긴 이름을 쓸 필요가 없습니다.
  • 런타임(Runtime)에 탐색 작업을 거치지 않게 해줍니다.

맺음말

지금까지 전역 네임스페이스의 오염을 줄일 수 있는 몇 가지 패턴을 살펴보았습니다.

모든 언어에서 기본기를 다지는 것은 중요합니다. 하지만 JavaScript에서는 더욱 그렇다는 생각이 듭니다. JavaScript의 동적인 특성으로 인해 기본을 지키지 않았어도 개발자가 쉽게 알아채기 힘들기 때문입니다. 그렇기 때문에 패턴이라고 하기엔 너무 기본적인 바로 그 패턴부터 지켜나가는 것이 JavaScript의 단점을 보완하며 개발할 수 있는 개발자가 되는 길이라는 생각합니다.

읽어주셔서 감사합니다.

참고 도서 및 사이트