객체 모델링

JavaScript의 프로토타입 체이닝에 대해서 알아보고, 게시판 흉내내기, 상자 안에서 튕기는 공들을 모델링합니다.
JavaScript, prototype, 의존성 분리, 모델링


프로토타입 체이닝

OOP 파트에서 다뤘던 JavaScript의 class 키워드는 ES6에서 부터 등장합니다. 웹 브라우저간 호환성을 위해서.또 OOP에 대한 새로운 관점을 위해서 ES6 이전 버전의 JavaScript의 class 구현에 대해서 알아보겠습니다. class의 핵심은 생성자 함수, 인스턴스 속성/메소드, 클래스 속성/메소드로 볼 수 있습니다. ES5와 그 이전의 JavaScript에서는 OOP를 구현하기 위해서 프로토타입 체이닝(Prototype Chaining)이라는 간단한 방식을 제공합니다. 이 외에 상속(extends 키워드)과 같은 문법은 제공되지 않습니다.

생성자 함수와 인스턴스

var Person = function(name) {
   this.name = name; // 인스턴스 속성
   this.speakName = function(number){ // 인스턴스간 공유되지 않는 메소드
      for(var i=0; i < number; i++) {
        console.log("my name is " + this.name);
      }
   }
};

var kim = new Person("kim");
kim.speakName(10);

인스턴스의 모든 속성은 생성자 함수안에서 정의가 가능합니다. 하지만 위와 같은 코드는 인스턴스들이 공유하는 속성이 없이 제 각각 속성, 함수를 가지게 됩니다. 이때 생성자 함수의 prototype 객체를 통해 인스턴스들이 공유하는 속성을 정의 할 수 있습니다.

prototype 객체와 인스턴스 메소드

var Person = function(name) {
   this.name = name; // 인스턴스 속성
};

Person.prototype; // 모든 함수(생성자가 아니여도)에는 prototype Object 객체가 자동으로 붙어 있습니다.

Person.prototype.speakName = function(number){ // 인스턴스간 공유되는 메소드
  for(var i=0; i < number; i++) {
    console.log("my name is " + this.name);
  }
}

var kim = new Person("kim");
kim.speakName(10);

kim 객체에서 kim.speakName을 찾으면 우선 kim.speakName을 찾습니다. 없으면 kim.__proto__.speakName을 찾습니다. kim.__proto__.speakName이 없으면kim.__proto__.__proto__ 에서 speakName을 찾습니다.

이때 __proto__라는 속성은 인스턴스를 생성 할 때 내부적으로 설정되는 참조로써 객체의 생성자 함수의 prototype 객체를 가리킵니다. 즉 kim.__proto__Person.prototype (= kim.constructor.prototype) 이됩니다.

이 때 kim.__proto__는 일반적인 Object 객체이기 때문에 kim.__proto__.__proto__Object.prototype이 됩니다. 때문에 Object.prototype의 속성들은 JavaScript의 모든 객체가 공유하는 속성이됩니다. Object.prototype.speakName도 없으면 kim.speakName은 그제서야 undefined가 됩니다.

즉 체인을 정리하면

  • kim.speakName
  • kim.constructor.prototype.speakName (kim.__proto__.speakName)
  • kim.constructor.prototype.constructor.prototype.speakName (kim.__proto__.__proto__.speakName)

다시 정리하면

  • kim.speakName
  • Person.prototype.speakName
  • Object.prototype.speakName

프로토타입 체이닝프로토타입 체이닝

이런 방식의 OOP 구현을 프로토타입 체이닝(Prototype Chaining)이라고 합니다. class로 구현하는 OOP와 비슷하면서도 좀 더 확장성이 있는 듯하고, 캡슐화가 덜된 것 같기도 합니다. A 클래스에서 B 클래스를 상속하려면 B.prototype의 값들을 하나 하나 A.prototype에 복사하면 되겠죠? 만약 생성자 함수를 같이 쓰거나 메소드를 오버라이딩 하자면 또 다른 길을 찾아봐야 하겠습니다.

prototype 객체와 인스턴스 속성

var Person = function(name) {
    if (name)
        this.name = name; // 인스턴스 속성
};

Person.prototype.name = "WHOIS"; // 물론 메소드가 아닌 값도 공유 할 수 있습니다.

var noname = new Person();
console.log(noname.name); // WHOIS

var kim = new Person("KIM");
console.log(kim.name); // KIM

객체의 속성을 참조 할 때의 우선 순위 역시 프로토타입 체인을 따릅니다.

  1. 인스턴스에 직접 붙은 속성
  2. 생성자 함수의 prototype에 붙은 속성
  3. Object.prototype에 붙은 속성

클래스 속성/메소드

var Person = function(name) {
    this.name = name;

    Person.list.push(this); // 또는 this.constructor.list.push(this);
};

Person.list = []; // 클래스 속성
Person.printAllNames = function(){ // 클래스 메소드
    Person.list.forEach(function(person){
        console.log(person.name);
    });
}

var son = new Person("SON");
var kim = new Person("KIM");
Person.printAllNames();

인스턴스 없이 참조 할 속성이나 메소드가 필요하다면, 위와 같이 생성자 함수자체에 직접 속성을 붙혀서 클래스 속성/메소드를 구현 할 수 있습니다. 클래스 메소드니 static 메소드니 문법 자체에 메이기 보다는, JS의 유연함을 이용해서 필요한 기능을 작성하시면 됩니다.

생성자 함수와 prototype을 이용해 클래스 정의

개를 추상화해서 클래스를 만들어봅니다.

  • 인스턴스 속성/메소드
    • name (String)
    • age (Number)
    • color (String)
    • speak (Function): console에 인스턴스의 name, age 및 color 속성을 출력
  • 클래스 속성/메소드
    • totalDogs (Number): 지금까지 생성된 Dog 인스턴스의 총 수
    • printTotalDogs (Function): console에 totalDogs를 출력
var Dog = function(name, age, color) {
    this.name = name;
    this.age = age;
    this.color = color;
    this.constructor.totalDogs++;
};

Dog.prototype.speak = function(){
    console.log("안녕 내 이름은", this.name, "이고 내 나이는", this.age, "이고 내 색깔은", this.color, "이야 반가워!");
};

Dog.totalDogs = 0;
Dog.printTotalDogs = function(){
    console.log("모두 ", Dog.totalDogs, "마리의 멍멍이");
};

var mong = new Dog("mong", 6, "black and white");
var mung = new Dog("mung", 3, "white");
mung.speak();
Dog.printTotalDogs();

모델링

일시적으로 원하는 결과를 만들어 내는 것 이상으로 코드를 설계 할 수 있는 능력이 필요합니다. 모델링과 뷰와 데이터의 분리라는 개념에 대해서 알아보겠습니다.

색이 바뀌는 공

공을 생성하고 색상을 바꾸는 잘 작동하는 코드입니다. 다만 추상화되지 않아서 코드에 의미를 부여하기 힘듭니다.
OOP를 도입해서 컨테이너를 모델링하고, 색이 바뀌는 공에서 나아가서 컨테이너 안에서 공이 움직이면서 벽을 만나면 튕기도록 구현하겠습니다. 객체를 모델링(의인화)해서 생각하면 다른 객체들과의 관계를 유기적으로 구성할 수 있고 이해하기 쉬운 코드를 만들 수 있습니다.

  • 공이 컨테이너의 오른쪽, 왼쪽 벽에 부딪치면(x좌표가 경계를 넘어서면) x축 방향의 속도가 반대가 됩니다.
  • 공이 컨테이너의 위, 아래 벽에 부딪치면(y좌표가 경계를 넘어서면) y축 방향의 속도가 반대가 됩니다.

모델링, 튕기는 공

의존성 분리

공과 컨테이너 객체의 의존성 분리

결합 (Coupling)결합 (Coupling)

공 객체는 독립적으로 쓰일 수 있습니다. 컨테이너 객체 역시 독립적으로 쓰일 수 있으며, 공 객체 뿐만 아니라 move, collideX, collideY, getX, getY, getView 메소드를 (올바르게) 지닌 어떤 객체라도 담고 움직이게 할 수 있습니다. 컨테이너의 의존 사항처럼 특정 객체에 대한 직접적인 의존이 아닌, 두리뭉실한 형태 대한 의존을 명시하는 것을 OOP에서는 Interface라고 합니다.

뷰와 데이터의 분리

뷰가 보여지기 이전에 데이터가 먼저 존재하고 여러 계산이 이루어집니다. 가변적인 값들은 뷰에 직접 속하기 보다는, 객체에서 따로 관리하며 뷰에서 그 데이터를 이용하는 형태가 좋습니다. 뷰와 데이터를 분리해서 관리하면 코드를 구조화하기에 용이하고 추가적인 기능 구현이나 유지 보수 과정이 깔끔해집니다. 이런 코드는 확장성이 좋다고 합니다.

뷰와 데이터가 긴밀하게 결합되어있으면(Coupled) 새로운 속성이나 기능을 만들 때마다 뷰에 접근하여 뷰의 속성에서 데이터를 추출하고, 다시 어떤 처리를 한 후 또다시 뷰에 반영해야 합니다. 즉, 가능은 하지만 코드를 읽어 보았을 때 의미적으로 말이 안되는 코드입니다.

뷰와 데이터를 잘 분리하면 프로그램은 다음과 같은 구조를 같습니다.

  • 데이터의 설계 코드 (Model)
  • 데이터를 생성 및 조작하는 코드 (Controller)
  • 데이터를 화면에 반영(render)하는 코드 (View)

MVC 패턴MVC 패턴

플랫폼에 상관 없이, 응용프로그램의 대표적인 구조로 MVC 라고 불리는 디자인 패턴이 있습니다. 각 글자는 Model, View, Controller를 의미합니다. 지금 튕기는 공을 만들면서 Model을 설계했고, View가 모델에 종속되긴 했지만 View의 로직도 어느 정도 분리를 해보았습니다. 사용자와의 상호작용은 없었기 때문에 초기화하는 코드 외에 Controller라고 부를 코드는 없었습니다. 앞으로 계속 될 실습과 추후 웹서비스 파트에서는 항상 모델과 뷰, 컨트롤러를 분리하려고 애쓰도록 하겠습니다. 먼 길로 돌아가는 것 같지만 이런 원리를 지킴으로써 더욱 빠르고 탄탄한 개발이 가능 할 것입니다.

모델링, 튕기는 공, 다시

공뿐만 아니라 어떤 오브젝트라도 컨테이너 안에서 튕길 수 있도록 코드를 개선해봅니다.

이 강의를 포함한 커리큘럼
저자

김동욱

개발 경력 약 10년, 전문 분야는 웹 및 각종 응용 소프트웨어 플랫폼입니다. Codeflow를 운영하고 있습니다.

2018년 04월 10일 업데이트

지원되지 않는 웹 브라우저거나 예기치 않은 오류가 발생했습니다.