다시 ES5

최근의 자바스크립트는 ECMAScript 6버전 이후로 언어 차원에서의 발전이 급속도로 진행되고 있습니다. 기존 OOP언어에서 제공하던 class, import등등의 키워들이 사용 가능해졌고 interface, abstract과 같은 키워드들도 등록 될 것으로 보여집니다.
또한, 웹개발에 있어 자바스크립트만을 사용하던 시대와 다르게 coffeescript, typescript, kotlinjs등 타언어로 개발하고 이를 자바스크립트로 변환할 수 있게 되었습니다. 이러한 시점에서 ES5를 다시 한번 들여다 보는게 무의미할 수도 있지만, 현재 왜 웹 어플리케이션의 개발방법론이 자바스크립트 일변도에서 다양화 되고 있는지 집어보는 차원에서 다시 한번 ES5를 들여다 보겠습니다.
주로, ES5의 객체 지향 프로그래밍과 디자인 패턴에 대해 다루려고 합니다. 첫번째 시간으로 자바스크립트의 OOP에 대해 간략히 설명하겠습니다.


ES5는 class키워드로 클래스를 생성할 수 없습니다. 대신 함수를 이용하여 클래스를 생성할 수 있습니다.
자바스크립트에서 함수는 1급 객체입니다. 1급 객체라는 말은 함수형 프로그래밍에서도 중요하게 다뤄집니다. 객체는 자신만의 속성과 메소드를 가지고 있습니다. 또한 객체이기 때문에 함수의 인자로 전달 될 수도 있으며, 함수의 반환값으로 함수를 가질 수도 있습니다.
자바스크립트의 함수도 객체이므로 자신만의 속성과 메소드를 가지고 있습니다. 바로 porototype과 apply, call입니다. porototype은 클래스를 만드는데 핵심 키워드입니다. apply, call은 함수 안에서 사용된 컨텍스트(this)를 유지, 강제할 수 있는 기능을 가지고 있다.

클래스

ES5에서 클래스는 다음과 같이 만들 수 있습니다.

var Animal = function () {

};

Animal.prototype.run = function () {

};

Animal.prototype.eat = function () {

};

var dog = new Animal();
dog.run();
dog.eat();



다음과 같이도 만들 수 있습니다.

var Animal = function () {

};

Animal.prototype = {
    run: function () {

    },
    eat: function () {

    }
};

타입스크립트로 작성된 클래스를 변환하면 다음과 같은 ES5형태의 클래스로 변환됩니다.

var Animal = /** @class */ (function () {
    function AClass() {
    }
    Animal.prototype.run = function () {
        console.log('run!!');
    };
    return Animal;
}());


상속

ES5는 porototype기반 상속을 지원합니다. prototype기반 상속은 두가지를 기억하면 도움이 됩니다.

  1. 자바스크립트의 모든 함수는 prototype를 가지고 있다.
  2. 자바스트립트의 모든 객체는 proto 라는 속성을 가지고 있으며, 이는 자신의 생성자 함수의 prototype을 레퍼런스한다.

고로, 셍성자 함수의 prototype와 그 인스턴스의 __proto__는 같은 것이다.

var A = function () {

};

var a = new A();
console.log(a.__proto__ === A.prototype); //true

아래는 상속의 기본 패턴입니다.

var Animal = function () {};
Animal.prototype.run = function () {};
Animal.prototype.eat = function () {};

var Dog = function () {
    this.name = '왈왈';
};
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;
Dog.prototype.getName = function () {};

var dog = new Dog(); 같이 객체를 생성했다고 가정 합니다. 그럼 이제 두번째 규칙을 기억하면 됩니다. 자바스크립트의 모든 객체는 __proto__를 가지고 있습니다. 그리고 이는 자신의 생성자 함수의 prototype를 가르킵니다. 객체가 어떤 프로퍼티를 읽으려 할때 순서는 다음과 같습니다.

  1. 먼저 자기 자신에게서 해당 프로퍼티가 있는지 보고 있으면 이를 반환한다.
  2. 없으먄, 자신의 __proto__로 이동해서 해당 프로퍼티가 있는지 보고 이를 반환한다.
  3. 없으면, 다시 자신의 __proto__로 이동한다.
  4. 최종 찾지 못하면 undefinded를 반환한다.

여기서는 만약 dog.run을 읽으려 한다면, this 자기 자신에게서는 없으므로 proto__로 이동합니다. 이는 곧 자신의 생성자 함수인 Dog.prototype이니까 여기서 찾아봅니다. 하지만 없습니다. prototype 객체는 다시 자신의 __proto 로 이동합니다. 이는 자신의 생성자 함수 곧 Animal.prototype입니다. 여기에 있으니 이를 반환합니다.

인터페이스

인터페이스는 규약입니다. 인터페이스는 인터페이스를 구헌혀는 클래스에서 인터페이스에서 정의된 추상 메소드들을 반드시 구현해야 합니다. 구현하지 않으면 에러를 냅니다. 에러가 난다는게 핵심입니다.
인터페이스 기반의 프로그래밍을 해라, 라는 말을 한번쯤은 들어봤을 것입니다. 특정 프로그램의 소스 코드를 볼때, 개발자는 해당 소스의 인터페이스만 보고 해당 프로그래밍이 어떤 요소들을 가지고 있는지 대략적인 파악이 가능합니다. 그것은 해당 인터페이스의 요소가 반드시 구현되어 있을 것임을 보장하기 때문입니다.
요약하먄, 인터페이스는 인터페이스를 상속받은 클래스에서 반드시 구현해야하며 구현하지 않으면 에러를 냅니다.
인터페이스는 OOP에서 중요한 역할을 수행합니다. 자바 등의 OOP언어에서 인터페이스는 구약 뿐 아니라, 다형성과도 깊은 연관이 있습니다.

Dustin Diaz의 ‘JavaScript 디자인 패턴‘책을 보면 자바스크립트에서의 인터페이스가 다음과 같이 구현되어 있습니다.

var Interface = function(name, methods) {
    if(arguments.length != 2) {
        throw new Error("Interface constructor called with " + arguments.length
          + "arguments, but expected exactly 2.");
    }
    
    this.name = name;
    this.methods = [];
    for(var i = 0, len = methods.length; i < len; i++) {
        if(typeof methods[i] !== 'string') {
            throw new Error("Interface constructor expects method names to be " 
              + "passed in as a string.");
        }
        this.methods.push(methods[i]);        
    }    
};    

아래와 같이 인터페이스를 생성합니다.

var IAnimal = new Interface('IAnimal', ['run', 'eat']);

클래스가 인터페이스를 구현하고 있는지를 검사하는 정정 메소드를 정의 합니다.

Interface.ensureImplements = function(object) {
    if(arguments.length < 2) {
        throw new Error("Function Interface.ensureImplements called with " + 
          arguments.length  + "arguments, but expected at least 2.");
    }

    for(var i = 1, len = arguments.length; i < len; i++) {
        var interface = arguments[i];
        if(interface.constructor !== Interface) {
            throw new Error("Function Interface.ensureImplements expects arguments "   
              + "two and above to be instances of Interface.");
        }
        
        for(var j = 0, methodsLen = interface.methods.length; j < methodsLen; j++) {
            var method = interface.methods[j];
            if(!object[method] || typeof object[method] !== 'function') {
                throw new Error("Function Interface.ensureImplements: object " 
                  + "does not implement the " + interface.name 
                  + " interface. Method " + method + " was not found.");
            }
        }
    } 
};

인터페이스를 생성했으니 이를 클래스에 상속시켜 구현해야 합니다. 여기서 다시한번, 인터페이스의 역할을 설명하자면, 인터페이스는 인터페이스를 구현하는 클래스에서 인터페이스에서 지정한 추상 메소드를 반드시 구현해야 합니다. 구현하지 않으면, 에러를 냅니다.

구현하지 않으면, 에러를 냅니다.

이 말에 주목해서 사용법을 확인해 봅시다.

var Animal = (function () {
    function AClass() {
    }
    Animal.prototype.run = function () {
        console.log('run!!');
    };
    Animal.prototype.eat = function () {
        console.log('eat!!');
    };
    return Animal;
}());

var animalInstance = new Animal();
Interface.ensureImplements(animalInstance, IAnimal);

여러개의 인터페이스를 지정하려면 다음과 같이 합니다.

Interface.ensureImplements(animalInstance, IAnimal, ISomting1, ISomting2);