JavaScript
개체 지향 기술을 이용한 고급 웹 응용 프로그램 만들기
Ray Djajadinata
이 기사에서 다루는 내용:- 프로토타입 기반 언어로서의 JavaScript
- JavaScript를 사용한 개체 지향 프로그래밍
- JavaScript에서의 코딩 트릭
- JavaScript의 미래
| 이 기사에서 사용하는 기술: JavaScript
|
목차
최근에 필자는 웹 응용 프로그래밍 개발 부문에 5년 경력을 가진 소프트웨어 개발자와 인터뷰를 했습니다. 그녀는 4년 6개월 동안 JavaScript를 사용했으며 스스로의 JavaScript 숙련도를 매우 높게 평가하고 있었습니다. 나중에 알게 되었지만 이 개발자는 JavaScript에 대해서 그리 많은 것을 알고 있지는 못했습니다. 이 개발자를 탓하고자 하는 것은 아닙니다. JavaScript에는 이렇듯 상당히 재미있는 면이 있습니다. 많은 개발자들이 C/C++/C#에 대해 알고 있거나 프로그래밍 경험이 있다는 이유만으로 자신이 JavaScript에 대해 많은 것을 알고 있다고 착각하곤 합니다. 필자 역시 최근까지 그러했습니다.
이러한 가정이 전혀 근거가 없는 것은 아닙니다. JavaScript를 사용하여 간단한 작업을 수행하기는 상당히 쉬우며 시작하기도 어렵지 않습니다. 이 언어는 또한 까다롭지 않으며 코딩을 시작하기 전에 많은 것을 배우도록 요구하지도 않습니다. 프로그래머가 아니더라도 몇 시간만 작업하면 홈페이지를 위한 유용한 스크립트를 작성할 수 있을 정도입니다.
사실 최근까지도 필자는 JavaScript에 대한 짧은 지식과 MSDN® DHTML 참조, 그리고 C++/C# 경험을 활용하여 필요한 작업을 수행할 수 있었습니다. 필자의 JavaScript 실력이 부족하다는 것을 느끼게 된 것은 현실적인 AJAX 응용 프로그램을 작성하기 시작하면서부터였습니다. 이 신세대 웹 응용 프로그램의 복잡성과 상호 작용성은 JavaScript 코드를 작성하는 데 있어 완전히 다른 방식을 요구하고 있습니다. 이것은 본격적인 JavaScript 응용 프로그램이며 지금까지 스크립트를 작성하던 느슨한 방법으로는 이러한 응용 프로그램을 작성할 수 없습니다.
개체 지향 프로그래밍(OOP)은 여러 JavaScript 라이브러리에서 코드베이스를 더욱 유지 관리하기 쉽게 하기 위해 널리 사용되는 방법입니다. JavaScript는 OOP를 지원하지만 널리 사용되는 Microsoft® .NET Framework 지원 언어인 C++, C# 또는 Visual Basic®에서 지원하는 방법과는 상당히 다른 방법으로 제공합니다. 따라서 이러한 언어를 오랫동안 사용한 개발자라면 JavaScript에서 OOP를 사용하는 것이 낯설고 처음에는 사용하기 까다롭게 느껴질 수 있습니다. 이 기사에서는 JavaScript 언어에서 어떻게 개체 지향 프로그래밍을 지원하는지 자세히 살펴보고 이러한 지원을 사용하여 JavaScript에서 효과적으로 개체 지향 개발을 수행하는 방법을 알아보겠습니다. 우선 무엇보다 중요한 개체에 대해서 살펴보겠습니다.
JavaScript 개체는 사전입니다.
C++나 C#에서의 개체는 클래스나 구조체의 인스턴스를 의미합니다. 개체는 어떤 템플릿(클래스)에서 인스턴스화했는지에 따라 다른 속성 및 메서드를 가지지만 JavaScript 개체는 이와 다릅니다. JavaScript의 개체는 단순히 이름/값 쌍의 컬렉션입니다. JavaScript 개체는 문자열 키가 있는 사전으로 생각할 수 있습니다. 익숙한 "."(점) 연산자나 일반적으로 사전을 처리할 때 사용되는 "[]" 연산자를 사용하여 개체 속성을 얻거나 설정할 수 있습니다. 다음 코드 조각을 살펴보십시오.
var userObject = new Object();
userObject.lastLoginTime = new Date();
alert(userObject.lastLoginTime);
위 코드는 다음 코드와 정확히 같은 작업을 수행합니다.
var userObject = {}; // equivalent to new Object()
userObject[“lastLoginTime”] = new Date();
alert(userObject[“lastLoginTime”]);
다음과 같이 userObject 정의 내에서 직접 lastLoginTime 속성을 정의할 수도 있습니다.
var userObject = { “lastLoginTime”: new Date() };
alert(userObject.lastLoginTime);
C# 3.0 개체 이니셜라이저와 비슷하다는 것을 알 수 있을 것입니다. 또한 Python에 익숙한 개발자라면 두 번째와 세 번째 코드 조각에서 userObject를 인스턴스화하는 방식이 Python에서 사전을 지정하는 방식과 동일하다는 것도 알 수 있습니다. 유일한 차이는 JavaScript 개체/사전은 문자열 키만 받지만 Python 사전에는 hashable과 같은 개체를 사용할 수 있다는 것입니다.
이러한 예는 JavaScript 개체가 C++ 또는 C# 개체에 비해 얼마나 유연한가를 보여 줍니다. lastLoginTime을 앞서 선언할 필요는 없으며 userObject에 해당하는 이름의 속성이 없는 경우에는 간단하게 userObject에 속성이 추가됩니다. JavaScript 개체가 사전이라는 사실을 기억한다면 이것은 그리 놀라운 일이 아닙니다. 사전에는 항상 새로운 키와 해당하는 값을 추가할 수 있습니다.
개체 속성은 앞서 설명한 것과 같습니다. 그렇다면 개체 메서드는 어떨까요? 이번에도 역시 JavaScript는 C++/C#과는 다릅니다. 개체 메서드를 이해하기 위해서는 먼저 JavaScript 함수를 살펴볼 필요가 있습니다.
JavaScript에서는 함수가 가장 중요합니다.
함수와 개체를 서로 다른 것으로 취급하는 프로그래밍 언어가 많습니다. JavaScript에서는 이 차이가 모호합니다. JavaScript 함수는 실행 가능한 코드와 연결된 개체입니다. 다음과 같은 일반적인 함수를 예로 들어 보겠습니다.
function func(x) {
alert(x);
}
func(“blah”);
이 코드는 JavaScript에서 일반적으로 함수를 정의하는 방법을 보여 줍니다. 그러나 다음과 같이 익명의 함수 개체를 만들고 변수 func에 할당하는 방법으로도 동일한 함수를 정의할 수 있습니다.
var func = function(x) {
alert(x);
};
func(“blah2”);
또는 다음과 같이 Function 생성자를 사용할 수도 있습니다.
var func = new Function(“x”, “alert(x);”);
func(“blah3”);
이를 통해서 함수는 함수 호출 작업을 지원하는 개체라는 것을 알 수 있습니다. Function 생성자를 사용하여 함수를 정의하는 마지막 방법은 자주 사용되지는 않지만 흥미로운 가능성을 열어 줍니다. 그 이유는 함수 본문이 Function 생성자에 대한 String 매개 변수이기 때문입니다. 이것은 런타임에 임의의 함수를 작성할 수 있음을 의미합니다.
함수가 개체라는 것을 확인시켜 주는 예로서 다른 JavaScript 개체를 대상으로 작업할 때와 마찬가지로 함수에 대해 속성을 설정하거나 추가할 수 있습니다.
function sayHi(x) {
alert(“Hi, “ + x + “!”);
}
sayHi.text = “Hello World!”;
sayHi[“text2”] = “Hello World... again.”;
alert(sayHi[“text”]); // displays “Hello World!”
alert(sayHi.text2); // displays “Hello World... again.”
함수는 개체이므로 변수에 할당하고 다른 함수에 인수로 전달하며 다른 함수에서 값으로 반환하는 것은 물론, 개체의 속성이나 배열의 요소로 저장하는 등의 작업이 가능합니다. 그림 1에는 이에 대한 예가 나와 있습니다.
Figure 1 함수는 JavaScript의 가장 중요한 부분임
// assign an anonymous function to a variable
var greet = function(x) {
alert(“Hello, “ + x);
};
greet(“MSDN readers”);
// passing a function as an argument to another
function square(x) {
return x * x;
}
function operateOn(num, func) {
return func(num);
}
// displays 256
alert(operateOn(16, square));
// functions as return values
function makeIncrementer() {
return function(x) { return x + 1; };
}
var inc = makeIncrementer();
// displays 8
alert(inc(7));
// functions stored as array elements
var arr = [];
arr[0] = function(x) { return x * x; };
arr[1] = arr[0](2);
arr[2] = arr[0](arr[1]);
arr[3] = arr[0](arr[2]);
// displays 256
alert(arr[3]);
// functions as object properties
var obj = { “toString” : function() { return “This is an object.”; } };
// calls obj.toString()
alert(obj);
이를 기억한다면 이름을 선택하고 이 이름에 함수를 할당하는 작업으로 간단하게 개체에 메서드를 추가할 수 있습니다. 여기에서는 익명 함수를 해당 메서드 이름에 할당하여 개체에 3개의 메서드를 정의했습니다.
var myDog = {
“name” : “Spot”,
“bark” : function() { alert(“Woof!”); },
“displayFullName” : function() {
alert(this.name + “ The Alpha Dog”);
},
“chaseMrPostman” : function() {
// implementation beyond the scope of this article
}
};
myDog.displayFullName();
myDog.bark(); // Woof!
C++/C# 개발자에게는 displayFullName 함수 내에 사용된 "this" 키워드가 친근하게 느껴질 것입니다. 이 키워드는 메서드를 호출한 개체를 참조합니다. Visual Basic 개발자에게도 역시 생소하지는 않을 것입니다. Visual Basic에서는 이를 "Me"라고 합니다. 따라서 위 예에서 displayFullName의 "this" 값은 myDog 개체입니다. 그러나 "this"의 값은 정적이지 않습니다. 그림 2에서 보여 주고 있는 것처럼 다른 개체를 통해 호출된 경우에는 "this" 값이 해당 개체를 가리키도록 변경됩니다.
Figure 2 개체가 달라짐에 따라 “this”도 달라짐
function displayQuote() {
// the value of “this” will change; depends on
// which object it is called through
alert(this.memorableQuote);
}
var williamShakespeare = {
“memorableQuote”: “It is a wise father that knows his own child.”,
“sayIt” : displayQuote
};
var markTwain = {
“memorableQuote”: “Golf is a good walk spoiled.”,
“sayIt” : displayQuote
};
var oscarWilde = {
“memorableQuote”: “True friends stab you in the front.”
// we can call the function displayQuote
// as a method of oscarWilde without assigning it
// as oscarWilde’s method.
//”sayIt” : displayQuote
};
williamShakespeare.sayIt(); // true, true
markTwain.sayIt(); // he didn’t know where to play golf
// watch this, each function has a method call()
// that allows the function to be called as a
// method of the object passed to call() as an
// argument.
// this line below is equivalent to assigning
// displayQuote to sayIt, and calling oscarWilde.sayIt().
displayQuote.call(oscarWilde); // ouch!
그림 2의 마지막 줄에서는 개체의 메서드로 함수를 호출하는 대안을 보여 줍니다. JavaScript에서 함수는 개체라는 사실을 기억하십시오. 모든 함수 개체에는 함수를 첫 번째 인수의 메서드로 호출하는 메서드 이름 호출이 있습니다. 즉, 호출에 전달하는 개체의 첫 번째 인수가 이 함수 호출에서 "this" 값이 됩니다. 뒤에 살펴보겠지만 이것은 기본 클래스 생성자를 호출하는 유용한 기술입니다.
한 가지 기억해야 할 것은 개체를 소유하지 않은 상태에서 "this"가 포함된 함수를 호출해서는 안 된다는 것입니다. 이러한 호출에서 "this"는 Global 개체를 참조하므로 전역 네임스페이스를 어지럽히게 되고 이것은 응용 프로그램에 심각한 재앙이 될 수 있습니다. 예를 들어 아래의 스크립트는 JavaScript의 전역 함수 isNaN의 동작을 변경하며 이러한 일은 절대 피해야 합니다.
alert(“NaN is NaN: “ + isNaN(NaN));
function x() {
this.isNaN = function() {
return “not anymore!”;
};
}
// alert!!! trampling the Global object!!!
x();
alert(“NaN is NaN: “ + isNaN(NaN));
지금까지 개체를 만들고 속성과 메서드로 완성하는 방법을 살펴보았습니다. 그러나 위의 코드 조각을 자세히 살펴보면 속성과 메서드가 개체 정의 자체 내에 코드로 포함되어 있다는 것을 알 수 있습니다. 개체 생성에 대한 더 본격적인 제어가 필요한 경우에는 어떻게 해야 할까요? 예를 들어 몇 가지 매개 변수의 값에 따라 개체 속성의 값을 계산해야 할 수 있습니다. 또는 런타임에만 얻을 수 있는 값으로 개체 속성을 초기화해야 할 수 있습니다. 이외에도 개체의 인스턴스를 두 개 이상 만들어야 할 수 있는데 이것은 매우 일반적인 요구 사항입니다.
C#에서는 개체 인스턴스를 인스턴스화하는 데 클래스를 사용하지만 JavaScript에는 클래스가 없으므로 상황이 다릅니다. 다음 섹션에서 살펴보겠지만 JavaScript에서는 "new" 연산자와 함께 사용하는 경우 함수가 생성자처럼 동작한다는 사실을 활용하게 됩니다.
생성자 함수는 있지만 클래스는 없습니다.
JavaScript OOP의 특이한 점은 앞서 언급했듯이 JavaScript에는 C# 또는 C++와 같은 클래스가 없다는 것입니다. C#에서의 작업 방법은 다음과 같습니다.
위 코드를 수행하면 클래스 Dog의 인스턴스인 개체를 얻을 수 있습니다. 그러나 JavaScript에서는 클래스가 없습니다. 클래스와 가장 비슷한 효과를 얻는 방법은 다음과 같이 생성자 함수를 정의하는 것입니다.
function DogConstructor(name) {
this.name = name;
this.respondTo = function(name) {
if(this.name == name) {
alert(“Woof”);
}
};
}
var spot = new DogConstructor(“Spot”);
spot.respondTo(“Rover”); // nope
spot.respondTo(“Spot”); // yeah!
여기에서는 무슨 일이 일어나고 있을까요? DogConstructor 함수 정의에 대해서는 잠시 무시하고 다음 줄을 살펴보십시오.
var spot = new DogConstructor(“Spot”);
"new" 연산자가 수행하는 일은 간단합니다. 우선 이 연산자는 비어 있는 새 개체를 만듭니다. 그런 다음 비어 있는 새 개체를 함수 내의 "this" 값으로 설정하고 이어지는 함수를 호출합니다. 다른 말로 하면 위의 코드에서 "new" 연산자는 아래의 두 줄과 비슷하다고 생각할 수 있습니다.
// create an empty object
var spot = {};
// call the function as a method of the empty object
DogConstructor.call(spot, “Spot”);
DogConstructor의 본문에서 볼 수 있는 것처럼 이 함수를 호출하면 해당 호출 시 키워드 "this"가 참조하는 대상으로 개체를 초기화합니다. 이러한 방법으로 개체의 템플릿을 만드는 것이 가능합니다. 비슷한 개체를 만들어야 할 때마다 생성자 함수로 "new"를 호출하면 완전하게 초기화된 개체를 결과로 얻을 수 있습니다. 클래스와 비슷하게 느껴지지 않습니까? 실제로 JavaScript에서 일반적으로 생성자 함수의 이름은 시뮬레이트하는 클래스의 이름이며 따라서 위 예에서는 생성자 함수 이름을 Dog로 지정할 수 있습니다.
// Think of this as class Dog
function Dog(name) {
// instance variable
this.name = name;
// instance method? Hmmm...
this.respondTo = function(name) {
if(this.name == name) {
alert(“Woof”);
}
};
}
var spot = new Dog(“Spot”);
위의 Dog 정의에서 필자는 name이라는 인스턴스 변수를 정의했습니다. Dog를 생성자 함수로 사용하여 생성한 모든 개체는 자체 인스턴스 변수 이름의 복사본을 가지게 됩니다. 이것은 앞서 언급했듯이 개체 사전에 대한 항목입니다. 예상할 수 있듯이 결국 모든 개체는 자체 상태를 저장하기 위한 자체 인스턴스 변수 복사본이 필요합니다. 그러나 다음 줄을 보면 Dog의 모든 인스턴스가 respondTo 메서드의 자체 복사본을 가지고 있음을 알 수 있습니다. 이것은 낭비이며 respondTo의 한 인스턴스를 Dog 인스턴스 간에 공유하면 충분합니다. 다음과 같이 respondTo 정의를 Dog 외부에 배치함으로써 이 문제를 해결할 수 있습니다.
function respondTo() {
// respondTo definition
}
function Dog(name) {
this.name = name;
// attached this function as a method of the object
this.respondTo = respondTo;
}
이 방법을 사용하면 Dog의 모든 인스턴스(즉, 생성자 함수 Dog를 사용하여 만든 모든 인스턴스)가 respondTo의 한 인스턴스를 공유할 수 있습니다. 그러나 메서드의 멤버가 늘어나면 이 방법은 점차 관리하기 어렵게 됩니다. 코드베이스 내에 많은 전역 함수가 생기게 되며 "클래스"의 수가 많아질수록, 해당 메서드의 이름이 비슷할수록 상황은 더욱 악화됩니다. 프로토타입 개체를 사용하는 더 좋은 방법이 있으며 자세한 내용은 다음 섹션에서 살펴보겠습니다.
프로토타입
프로토타입 개체는 JavaScript의 개체 지향 프로그램에서 핵심적인 개념입니다. 프로토타입이라는 이름은 JavaScript에서 기존 예(말하자면 프로토타입) 개체의 복사본에서 개체를 만들기 때문에 붙여진 것입니다. 이 프로토타입 개체의 모든 속성 및 메서드는 이 프로토타입의 생성자로 만드는 모든 개체의 속성 및 메서드로 나타나게 됩니다. 이러한 개체가 해당 프로토타입에서 속성 및 메서드를 상속받는다고 할 수 있습니다. 다음과 같이 새 Dog 개체를 만든다고 가정해 보겠습니다.
var buddy = new Dog(“Buddy“);
프로토타입을 가져온 단 한 라인에서는 명백하게 보이지 않을 수 있지만 buddy에서 참조한 개체는 해당 프로토타입에서 속성 및 메서드를 상속하게 됩니다. 개체 buddy의 프로토타입은 생성자 함수(이 경우에는 Dog 함수)의 속성에서 가져옵니다.
JavaScript의 모든 함수에는 프로토타입 개체를 참조하는 "prototype"이라는 속성이 있습니다. 이 프로토타입 개체에는 함수 자체를 참조하는 "constructor"라는 이름의 속성이 있습니다. 이것은 일종의 순환 참조라고 할 수 있는데 그림 3에서는 이러한 순환 관계를 더 잘 보여 주고 있습니다.
그림 3 모든 함수 프로토타입에는 생성자 속성이 있음
이제 "new" 연산자로 개체를 만들기 위해 함수(이 예에서는 Dog)를 사용하면 결과 개체는 Dog.prototype의 속성을 상속하게 됩니다. 그림 3을 보면 Dog.prototype 개체에 다시 Dog 함수를 가리키는 생성자 속성이 있음을 알 수 있습니다. 따라서 모든 Dog 개체(Dog.prototype에서 상속한) 또한 Dog 함수를 가리키는 생성자 속성을 가지는 것처럼 보입니다. 그림 4의 코드에서 이를 확인할 수 있습니다. 생성자 함수, 프로토타입 개체 및 이들 사용하여 만든 개체 간의 관계는 그림 5에 표시되어 있습니다.
Figure 4 해당 프로토타입의 속성을 가진 것으로 나타나는 개체
var spot = new Dog(“Spot”);
// Dog.prototype is the prototype of spot
alert(Dog.prototype.isPrototypeOf(spot));
// spot inherits the constructor property
// from Dog.prototype
alert(spot.constructor == Dog.prototype.constructor);
alert(spot.constructor == Dog);
// But constructor property doesn’t belong
// to spot. The line below displays “false”
alert(spot.hasOwnProperty(“constructor”));
// The constructor property belongs to Dog.prototype
// The line below displays “true”
alert(Dog.prototype.hasOwnProperty(“constructor”));
그림 5 해당 프로토타입에서 상속된 인스턴스
그림 4에서 hasOwnProperty 및 isPrototypeOf 메서드에 대한 호출을 볼 수 있을 것입니다. 이러한 메서드는 어디에서 온 것일까요? Dog.prototype에서 온 것은 아닙니다. 실제로 Dog.prototype 및 Dog의 인스턴스에서 호출할 수 있는 toString, toLocaleString 및 valueOf와 같은 다른 메서드도 있으며 이들 역시 Dog.prototype에서 온 것은 아닙니다. .NET Framework에도 모든 클래스에 대한 궁극적인 기본 클래스 역할을 하는 System.Object가 있는 것처럼 JavaScript에도 모든 프로토타입에 대한 궁극적인 기본 프로토타입인 Object.prototype이 있습니다. Object.prototype의 프로토타입은 Null입니다.
이 예에서는 Dog.prototype이 개체라는 것을 기억하십시오. 보이지는 않지만 이 개체는 Object 생성자 함수를 호출함으로써 생성되었습니다.
Dog.prototype = new Object();
따라서 Dog의 인스턴스가 Dog.prototype에서 상속하는 것처럼 Dog.prototype은 Object.prototype에서 상속합니다. 결국 Dog의 모든 인스턴스는 Object.prototype의 메서드 및 속성 또한 상속하게 됩니다.
모든 JavaScript 개체는 Object.prototype으로 끝나는 프로토타입의 체인을 따라 상속합니다. 지금까지 살펴본 이 상속은 라이브 개체 간의 상속입니다. 선언되는 시점에 클래스 간에 일어나는 상속의 일반적인 개념과는 차이가 있습니다. 결과적으로 JavaScript 상속은 훨씬 동적이며 다음과 같은 간단한 알고리즘을 사용합니다. 개체의 속성/메서드에 액세스하려고 시도하면 JavaScript는 해당 속성/메서드가 개체에 정의되어 있는지 확인합니다. 개체에 정의되어 있지 않은 경우에는 개체의 프로토타입을 확인합니다. 여기에도 정의되어 있지 않으면 Object.prototype에 이를 때까지 개체 프로토타입의 프로토타입을 계속 확인합니다. 그림 6에서는 이러한 확인 프로세스를 보여 줍니다.
그림 6 프로토타입 체인에서 toString() 메서드 확인 (더 크게 보려면 이미지를 클릭하십시오.)
JavaScript가 속성 액세스와 메서드 호출을 동적으로 확인함에 따르는 몇 가지 효과가 있습니다.
- 프로토타입 개체에 대한 변경은 개체가 생성된 이후에도 개체에 영향을 줍니다.
- 개체에 속성/메서드 X를 정의하여 동일한 이름의 속성/메서드는 해당 개체의 프로토타입에서 숨겨집니다. 예를 들어 Dog.prototype에서 toString 메서드를 정의하여 Object.prototype의 toString 메서드를 다시 정의할 수 있습니다.
- 변경은 프로토타입에서 해당 파생 개체로 한 방향으로만 진행하며 반대로는 진행하지 않습니다.
그림 7에서는 이러한 결과를 보여 줍니다. 그럼 7에서는 또한 앞서 발생했었던 불필요한 메서드 인스턴스 문제를 해결하는 방법도 보여 줍니다. 모든 개체가 별도의 함수 개체를 가지도록 하는 것이 아니라 메서드를 프로토타입 내에 배치함으로써 개체가 메서드를 공유하도록 할 수 있습니다. 이 예에서 getBreed 메서드는 spot에서 toString 메서드를 다시 정의하기 전까지 rover 및 spot에 의해 공유됩니다. 이후에 spot은 자신의 getBreed 메서드 버전을 가지지만 rover 개체 및 새 GreatDane으로 만든 이후 개체는 GreatDane.prototype 개체에 정의된 getBreed 메서드의 인스턴스를 공유하게 됩니다.
Figure 7 프로토타입에서 상속
function GreatDane() { }
var rover = new GreatDane();
var spot = new GreatDane();
GreatDane.prototype.getBreed = function() {
return “Great Dane”;
};
// Works, even though at this point
// rover and spot are already created.
alert(rover.getBreed());
// this hides getBreed() in GreatDane.prototype
spot.getBreed = function() {
return “Little Great Dane”;
};
alert(spot.getBreed());
// but of course, the change to getBreed
// doesn’t propagate back to GreatDane.prototype
// and other objects inheriting from it,
// it only happens in the spot object
alert(rover.getBreed());
정적 속성 및 메서드
인스턴스가 아닌 클래스에 연결된 속성이나 메서드, 말하자면 정적 속성 및 메서드가 필요가 경우가 있습니다. JavaScript에서 함수는 원하는 대로 속성과 메서드를 설정할 수 있는 개체이므로 이러한 경우에 대처하기가 수월합니다. JavaScript에서는 생성자 함수가 클래스를 대신하므로 다음과 같이 생성자 함수에 정적 메서드와 속성을 설정함으로써 클래스에 추가하는 효과를 얻을 수 있습니다.
function DateTime() { }
// set static method now()
DateTime.now = function() {
return new Date();
};
alert(DateTime.now());
JavaScript에서 정적 메서드를 호출하기 위한 구문은 C#에서의 구문과 거의 동일합니다. 생성자 함수의 이름은 사실상 클래스의 이름이므로 이는 놀라운 일이 아닙니다. 이제 클래스와 공용 속성/메서드, 그리고 정적 속성/메서드가 있습니다. 이외에 어떤 것이 필요할까요? 우선 전용 멤버가 있습니다. 그러나 JavaScript에는 전용 멤버에 대한 기본 지원이 없습니다. 적어도 보호되는 것은 없으며 모두가 개체의 모든 속성과 메서드에 액세스할 수 있습니다. 클래스에 전용 멤버를 추가하는 방법이 있지만 이를 위해서는 먼저 차단에 대해서 이해해야 합니다.
차단
필자는 스스로 원해서 JavaScript를 배운 것은 아니었으며 JavaScript를 사용하지 않고 현실적인 AJAX 응용 프로그램을 개발한다는 것이 불가능하다는 것을 깨달은 후에야 배우기 시작했습니다. 처음에는 필자의 프로그래머 수준이 몇 단계가 떨어지는 것이 아닌가 하는 생각도 들었습니다. (JavaScript라니! C++를 사용하는 친구들이 뭐라고 말할까?) 그러나 처음 가지고 있던 거부감을 떨쳐내자 JavaScript가 상당히 강력하고 인상적이며 콤팩트한 언어라는 사실을 깨달았습니다. 필자는 또한 널리 사용되는 다른 언어에서 이제 막 지원하기 시작한 기능에 대해서 자랑하기도 했습니다.
JavaScript의 고급 기능 중 하나로 C# 2.0에서는 익명 메서드를 통해 지원되는 차단에 대한 지원이 있습니다. 차단은 안쪽 함수(C#에서는 안쪽 익명 메서드)가 바깥쪽 함수의 로컬 변수와 바인딩되었을 때 일어나는 런타임 현상입니다. 분명한 것은 이 안쪽 함수가 바깥쪽 함수 외부에서 액세스 가능하지 않다면 이것이 의미가 없게 된다는 것입니다. 한 가지 예를 살펴보겠습니다.
예를 들어 100보다 큰 수만 통과할 수 있고 나머지는 필터링되는 간단한 조건으로 일련의 수를 필터링해야 한다고 가정해 보겠습니다. 그림 8와 같이 함수를 작성할 수 있습니다.
Figure 8 조건자에 기반을 두는 필터링 요소
function filter(pred, arr) {
var len = arr.length;
var filtered = []; // shorter version of new Array();
// iterate through every element in the array...
for(var i = 0; i < len; i++) {
var val = arr[i];
// if the element satisfies the predicate let it through
if(pred(val)) {
filtered.push(val);
}
}
return filtered;
}
var someRandomNumbers = [12, 32, 1, 3, 2, 2, 234, 236, 632,7, 8];
var numbersGreaterThan100 = filter(
function(x) { return (x > 100) ? true : false; },
someRandomNumbers);
// displays 234, 236, 632
alert(numbersGreaterThan100);
그런데 이제는 다른 필터링 조건, 예를 들어 300보다 큰 수만 통과하도록 다른 필터링 조건을 만들어야 한다고 가정해 보겠습니다. 이제 다음과 같은 코드를 사용할 수 있습니다.
var greaterThan300 = filter(
function(x) { return (x > 300) ? true : false; },
someRandomNumbers);
그리고 50, 25, 10, 600 등의 수보다 큰 수를 필터링하게 할 수 있습니다. 영리한 독자라면 이 필터가 모두 "보다 큰"이라는 동일한 조건자를 사용하고 있으며 수만 다르다는 것을 간파할 수 있을 것입니다. 따라서 함수에서 수를 인수로 만들어 다음과 같이 작성할 수 있습니다.
function makeGreaterThanPredicate(lowerBound) {
return function(numberToCheck) {
return (numberToCheck > lowerBound) ? true : false;
};
}
이제 다음과 같이 작업할 수 있습니다.
var greaterThan10 = makeGreaterThanPredicate(10);
var greaterThan100 = makeGreaterThanPredicate(100);
alert(filter(greaterThan10, someRandomNumbers));
alert(filter(greaterThan100, someRandomNumbers));
makeGreaterThanPredicate 함수에서 반환된 안쪽 익명 함수를 눈여겨보십시오. 이 익명 안쪽 함수는 makeGreaterThanPredicate로 전달된 인수인 lowerBound를 사용합니다. 일반적인 범위 지정의 규칙에 따르면 makeGreaterThanPredicate가 존재할 때는 lowerBound가 범위를 벗어나게 됩니다. 그러나 이 경우에 안쪽 익명 함수는 makeGreaterThanPredicate가 존재하는 한참 후에도 lowerBound를 가지고 있습니다. 이를 차단이라고 하는 것은 안쪽 함수가 정의된 위치에서 환경에 대해(바깥쪽 함수의 인수 및 지역 변수) 차단을 수행하기 때문입니다.
차단은 처음에는 사소한 기능처럼 보일 수도 있습니다. 그러나 제대로만 활용한다면 개발자의 아이디어를 코드로 구현하는 새롭고 흥미로운 가능성을 열어 줄 수 있습니다. JavaScript에서 가장 흥미로운 차단의 활용 예는 클래스의 전용 변수를 시뮬레이트하는 것입니다.
전용 속성 시뮬레이션
차단을 사용하여 전용 멤버를 시뮬레이트하는 방법을 살펴보겠습니다. 함수의 지역 변수는 일반적으로 함수 외부에서는 액세스할 수 없습니다. 함수가 더 이상 존재하지 않게 되면 지역 변수의 모든 실질적인 용도는 완전히 사라집니다. 그러나 지역 변수가 안쪽 함수의 차단에 의해 캡처되면 계속 존재하게 됩니다. 이것이 바로 JavaScript 전용 속성을 시뮬레이션하기 위한 핵심입니다. 다음 Person 클래스를 살펴보십시오.
function Person(name, age) {
this.getName = function() { return name; };
this.setName = function(newName) { name = newName; };
this.getAge = function() { return age; };
this.setAge = function(newAge) { age = newAge; };
}
인수 name과 age는 생성자 함수 Person에 대해 로컬입니다. Person이 반환하는 순간 name과 age는 완전히 사라지게 됩니다. 그러나 여기에서 name과 age는 Person 인스턴스의 메서드로 할당된 네 함수에서 캡처되어 계속 유지되며 이러한 네 메서드 내에서만 엄격하게 액세스할 수 있게 됩니다. 즉, 다음과 같은 코드가 가능합니다.
var ray = new Person(“Ray”, 31);
alert(ray.getName());
alert(ray.getAge());
ray.setName(“Younger Ray”);
// Instant rejuvenation!
ray.setAge(22);
alert(ray.getName() + “ is now “ + ray.getAge() +
“ years old.”);
생성자에서 초기화되지 않은 전용 멤버는 다음과 같은 생성자 함수의 지역 변수가 될 수 있습니다.
function Person(name, age) {
var occupation;
this.getOccupation = function() { return occupation; };
this.setOccupation = function(newOcc) { occupation =
newOcc; };
// accessors for name and age
}
이러한 전용 멤버와 C#에서 사용하던 전용 멤버 사이에는 미세한 차이점이 있습니다. C#에서 클래스의 공용 메서드는 클래스의 전용 멤버를 액세스할 수 있었습니다. 그러나 JavaScript에서는 자체 차단 내에 이러한 전용 멤버를 가지고 있는 메서드를 통해서만 이러한 전용 멤버를 액세스할 수 있습니다. 이러한 메서드는 일반적인 공용 메서드와는 다르므로 보통 권한 있는 메서드라고 불립니다. 따라서 Person 공용 메서드 내에서도 전용 메서드에 접근하려면 권한 있는 접근자 메서드를 통해야 합니다.
Person.prototype.somePublicMethod = function() {
// doesn’t work!
// alert(this.name);
// this one below works
alert(this.getName());
};
Douglas Crockford는 차단을 사용하여 전용 멤버를 시뮬레이트하는 기술을 발견(또는 발표)한 첫 번째 인물로 널리 알려져 있습니다. 그의 웹사이트(
javascript.crockford.com)에는 JavaScript에 대한 많은 정보가 있습니다. JavaScript에 관심이 있는 개발자라면 반드시 들러 보아야 할 곳입니다.
클래스로부터 상속
지금까지 생성자 함수가 작동되는 방법과 프로토타입 개체를 사용하여 JavaScript에서 클래스를 시뮬레이트하는 방법을 살펴보았으며 프로토타입 체인을 통해 모든 개체가 Object.prototype의 메서드를 공통적으로 가진다는 것을 확인했습니다. 차단을 사용하여 클래스의 전용 멤버를 시뮬레이트하는 방법도 확인했습니다. 그러나 여기에는 무엇인가가 빠져 있습니다. C#에서는 일상적인 작업이라고 할 수 있는 클래스에서 파생시키는 방법을 아직 설명하지 않았습니다. 아쉽게도 JavaScript에서 클래스를 상속하는 것은 C#에서 콜론을 입력하는 것처럼 단순하지 않으며 그 이상의 작업이 필요합니다. 반면에 JavaScript는 매우 유연하여 클래스에서 상속하는 다양한 방법을 사용할 수 있습니다.
그림 9에서 보여 주는 것처럼 Pet이라는 기본 클래스가 있고 여기에서 파생된 Dog 클래스가 있다고 가정해 보겠습니다. JavaScript에서는 이를 어떻게 구현해야 할까요? Pet 클래스는 간단하며 지금까지 배운 내용으로 구현할 수 있습니다.
그림 9 클래스
// class Pet
function Pet(name) {
this.getName = function() { return name; };
this.setName = function(newName) { name = newName; };
}
Pet.prototype.toString = function() {
return “This pet’s name is: “ + this.getName();
};
// end of class Pet
var parrotty = new Pet(“Parrotty the Parrot”);
alert(parrotty);
그렇다면 이제 Pet에서 파생된 Dog 클래스를 만들고자 한다면 어떻게 해야 할까요? 그림 9에서 볼 수 있는 것처럼 Dog에는 breed라는 추가 속성이 있으며 Pet의 toString 메서드를 다시 정의하였습니다. C#에서는 메서드와 속성의 이름에 파스칼식 대/소문자가 권장되지만 JavaScript에서는 카멜식 대/소문자를 사용하는 것이 관례입니다. 그림 10에서는 이러한 작동 방식을 보여 줍니다.
Figure 10 Pet 클래스에서 파생
// class Dog : Pet
// public Dog(string name, string breed)
function Dog(name, breed) {
// think Dog : base(name)
Pet.call(this, name);
this.getBreed = function() { return breed; };
// Breed doesn’t change, obviously! It’s read only.
// this.setBreed = function(newBreed) { name = newName; };
}
// this makes Dog.prototype inherits
// from Pet.prototype
Dog.prototype = new Pet();
// remember that Pet.prototype.constructor
// points to Pet. We want our Dog instances’
// constructor to point to Dog.
Dog.prototype.constructor = Dog;
// Now we override Pet.prototype.toString
Dog.prototype.toString = function() {
return “This dog’s name is: “ + this.getName() +
“, and its breed is: “ + this.getBreed();
};
// end of class Dog
var dog = new Dog(“Buddy”, “Great Dane”);
// test the new toString()
alert(dog);
// Testing instanceof (similar to the is operator)
// (dog is Dog)? yes
alert(dog instanceof Dog);
// (dog is Pet)? yes
alert(dog instanceof Pet);
// (dog is Object)? yes
alert(dog instanceof Object);
여기서 사용된 프로토타입 대체 트릭은 프로토타입 체인을 올바르게 설정하므로 C#에서와 마찬가지로 instanceof 테스트도 작동합니다. 또한 권한 있는 메서드 역시 예상대로 작동합니다.
네임스페이스 시뮬레이션
C++와 C#에서 네임스페이스는 이름 충돌의 우려를 최소화하기 위해 사용됩니다. .NET Framework에서 네임스페이스는 예를 들어 Microsoft.Build.Task.Message 클래스와 System.Messaging.Message 클래스를 구별하는 데 사용됩니다. JavaScript에는 네임스페이스 지원과 관련된 세부적인 언어 지원은 없지만 개체를 사용하여 손쉽게 네임스페이스를 시뮬레이트할 수 있습니다. JavaScript 라이브러리를 만들고자 한다고 가정해 보겠습니다. 함수 및 클래스를 전역으로 정의하는 대신 다음과 같이 네임스페이스 내에 래핑할 수 있습니다.
var MSDNMagNS = {};
MSDNMagNS.Pet = function(name) { // code here };
MSDNMagNS.Pet.prototype.toString = function() { // code };
var pet = new MSDNMagNS.Pet(“Yammer”);
한 수준의 네임스페이스는 고유하지 않을 수 있으므로 다음과 같이 중첩된 네임스페이스를 만들 수 있습니다.
var MSDNMagNS = {};
// nested namespace “Examples”
MSDNMagNS.Examples = {};
MSDNMagNS.Examples.Pet = function(name) { // code };
MSDNMagNS.Examples.Pet.prototype.toString = function() { // code };
var pet = new MSDNMagNS.Examples.Pet(“Yammer”);
짐작할 수 있겠지만 긴 중첩 네임스페이스를 입력하는 일은 금방 성가시게 느껴지게 됩니다. 다행스럽게도 라이브러리 사용자는 짧은 이름으로 네임스페이스에 별칭을 붙일 수 있습니다.
// MSDNMagNS.Examples and Pet definition...
// think “using Eg = MSDNMagNS.Examples;”
var Eg = MSDNMagNS.Examples;
var pet = new Eg.Pet(“Yammer”);
alert(pet);
Microsoft AJAX 라이브러리의 소스 코드를 살펴보면 라이브러리의 저자 역시 네임스페이스를 구현하기 위해 비슷한 기술을 사용했음을 알 수 있습니다. 정적 메서드 Type.registerNamespace의 구현을 살펴보십시오. 자세한 내용은 보충 기사 "OOP 및 ASP.NET AJAX"를 참조하십시오.
JavaScript에서 이와 같이 코딩해야 할까요?
지금까지 JavaScript에서도 개체 지향 언어를 문제 없이 지원한다는 것을 확인했습니다. 프로토타입 언어로 설계되었지만 JavaScript는 유연하고 강력하므로 널리 사용되는 다른 언어에서 일반적으로 사용되는 클래스 기반 프로그래밍 스타일을 지원할 수 있습니다. 그러나 문제는 JavaScript에서 이와 같이 코딩해야 하는지 결정하는 것입니다. C# 또는 C++에서 코딩하는 방식대로 JavaScript로 코딩하면서 없는 기능은 시뮬레이트하는 것이 현명한 방법일까요? 각각의 프로그래밍 언어는 서로 다르며 한 언어의 최상의 방법이 다른 언어에서는 최상의 방법이 아닐 수 있습니다.
JavaScript에서는 클래스가 다른 클래스에서 상속하는 것과 달리 개체가 다른 개체에서 상속하는 것을 확인했습니다. 따라서 정적 상속 계층을 사용하여 많은 클래스를 만들기는 가능하지만 이것은 JavaScript에 맞는 방식은 아닙니다. Douglas Crockford가 그의 자료 "
JavaScript의 프로토타입 상속"에서 밝힌 것처럼 JavaScript에 맞는 프로그래밍 방식은 프로토타입 개체를 만들고 해당 원본 개체에서 상속하는 새 개체 아래에 간단한 개체 함수를 만드는 방식일지도 모르겠습니다.
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
JavaScript의 개체는 유연하므로 만든 후에 필요에 따라 새 필드와 새 메서드를 사용하여 손쉽게 개체를 보강할 수 있습니다.
지금까지는 모두 좋지만 전 세계에 있는 개발자 대부분이 클래스 기반 프로그래밍에 익숙하다는 사실은 부인할 수 없습니다. 클래스 기반 프로그래밍은 또한 계속해서 유지될 것입니다. 발표 예정인 버전 4 ECMA-262 규약(ECMA-262는 JavaScript의 공식 사양)에 따르면 JavaScript 2.0에는 진정한 클래스가 추가될 예정입니다. 즉, JavaScript는 클래스 기반 언어로 변화하고 있습니다. 그러나 JavaScript 2.0이 널리 보급되기까지는 몇 년이 걸릴 것입니다. 그 전까지는 현재의 JavaScript로 프로토타입 기반 스타일과 클래스 기반 스타일 코드를 모두 읽고 작성할 수 있다는 사실을 알고 있는 것이 중요합니다.
앞으로의 전망
클라이언트에 크게 의존하는 대화식 AJAX 응용 프로그램이 확산됨에 따라 JavaScript는 빠른 속도로 .NET 개발자를 위한 가장 유용한 도구로 자리를 잡고 있습니다. 그러나 JavaScript의 프로토타입 특성은 C++, C# 또는 Visual Basic과 같은 언어에 익숙해 있던 개발자들에게 다소 생소할 수 있습니다. 필자의 경험을 이야기하자면 과정에 어려움이 없었던 것은 아니지만 JavaScript를 배우는 과정은 충분히 가치가 있었다고 생각합니다. 이 기사를 통해 여러분의 과정을 도울 수 있다면 좋다면 좋겠습니다. 바로 그것이 필자의 목표입니다.