Javascript Prototype methods vs Object methods

우리는 Javascript 객체를 다루기 위해 종종 생성자 함수를 사용하곤 합니다. 그러던 중에 생성자 함수 내부에 직접 메소드를 정의해주는 것과 prototype을 사용해서 메소드를 정의해주는 것의 차이점이 무엇일까에 대한 의문이 시작되었고, 여러가지 자료를 통해 알아본 결과는 충격적이었습니다.

javascript-prototype-methods-vs-object-methods라는 포스트를 보고 번역을 한 글이며 조금의 추가와 변경, 예제를 바꾸어서 쓴 글입니다.


Object에서 생성 시점에 Method를 선언하는 방법 ?

객체를 정의할 때, 객체 내부에 메소드를 추가할 수 있는 방법은 두가지가 있습니다.

  1. 객체 생성자 내부에서 this.func = function(){}와 같이 선언하는 방법
  2. Obj.prototype.func = function(){}과 같이 prototype을 이용하는 것입니다.

두가지 방법 모두 모든 인스턴스에서 func() 함수에 접근할 수 있는데, 우리는 어떤 방법을 사용해야 효율적일까요 ?

이 질문에 대한 대답을 미리 하자면, 대부분의 경우에서 prototype을 사용하는 2번 방법이 효율적입니다. 만약에 생성자 함수에 추가하는 메소드의 용도가 객체 인스터스들이 공통으로 사용되는 함수를 정의하는 것이라면, prototype을 사용해야 합니다.

그 이유에 대해서 알아봅시다.

1. Prototype을 사용하면 공용으로 사용되는 메소드의 변경이 쉽다.

prototype을 사용해서 객체에 공용 메소드를 추가하게 된다면 객체 인스턴스가 생성된 후에 해당 메소드에 대한 로직을 간편하게 수정이 가능합니다. 또한, 기존의 인스턴스와 새로 만들어지는 인스턴스 모두 갱신된 코드를 사용하게 됩니다.

하지만 prototype을 사용했을 때이고, 객체 생성자 함수 내부에서 메소드를 정의한 경우에는 불가능합니다.

아래의 예제를 보시죠.

function Animal(animal) {
  this.animal = animal;

  // 객체 생성자 내에서 모든 객체 인스턴스에서 공용으로 쓰이는 메소드를 추가합니다.
  this.bark = function(cry) {
    console.log(this.animal + " : " + cry);
  };
}

// dog와 cat 인스턴스를 만들고 울음소리를 넣어서 짖어봅시다.
var dog = new Animal("dog");
var cat = new Animal("cat");
dog.bark("woof"); // dog : woof
cat.bark("mew"); // cat : mew

// 동물들이 똑똑해져 우리의 뜻대로 행동하지 않습니다. 'kkk...'를 출력으로 바꿔보고 싶습니다.

Animal.bark = function() {
  console.log(`${this.animal} : kkk...`);
};
// 이렇게 해선 변화가 없습니다 ...

// 변화가 없는지 확인해봅시다.
dog.bark(); // dog : undefined
cat.bark(); // cat : undefined
// 똑같군요 ...

var cow = new Animal("cow");
cow.bark(); // cow : undefined
// 심지어 새로만든 인스턴스도 영향이 없군요 ㅠㅠ

// 객체 생성자내에서 직접 메소드를 추가해준 경우, 각각의 인스턴스에서 `bark`메소드를 오버라이드 해야합니다.

dog.bark = function() {
  console.log("dog : kkk...");
};
cat.bark = function() {
  console.log("cat : kkk...");
};
dog.bark(); // dog : kkk...
cat.bark(); // cat : kkk...

위와 같이 공통으로 사용되는 로직을 수정하기 위해서 공통 메소드를 변경하려고 했으나, 이미 인스턴스는 생성되었기 때문에 쉽게 공용 메소드를 변경하지 못하며, 일일히 인스턴스를 변경해주어야합니다.

같은 로직이지만 prototype을 사용한 것을 봅시다.

function Animal(animal) {
  this.animal = animal;
}

// 객체 prototype에서 모든 객체 인스턴스에서 공용으로 쓰이는 메소드를 추가합니다.
Animal.prototype.bark = function(cry) {
  console.log(this.animal + " : " + cry);
};

// dog와 cat 인스턴스를 만들고 울음소리를 넣어서 짖어봅시다.
var dog = new Animal("dog");
var cat = new Animal("cat");
dog.bark("woof"); // dog : woof
cat.bark("mew"); // cat : mew

// 동물들이 똑똑해져 우리의 뜻대로 행동하지 않습니다. 'kkk...'를 출력으로 바꿔보고 싶습니다.

// prototype을 이용해서 간단히 ! 변경해봅시다.
Animal.prototype.bark = function() {
  console.log(`${this.animal} : kkk...`);
};

// 변화가 있는지 확인해볼까요 ?
dog.bark(); // dog : kkk...
cat.bark(); // cat : kkk...
// 올바르게 동작하는군요 !!

var cow = new Animal("cow");
cow.bark(); // cow : kkk...
// 심지어 새로만든 인스턴스도 우리의 의도대로 정상적으로 작동합니다 !

우리의 의도대로 prototype을 사용했을 때 객체 공통 메소드에 대한 변화가 객체의 모든 인스턴스들에 정상적으로 반영되는 것을 볼 수 있었습니다.

2. Prototype을 사용하면 빠르고 메모리가 효율적입니다.

객체의 생성자 함수에서 메소드를 추가하게 되면 각 인스턴스를 생성하는 시점에 각 메소들의 사본을 가지게 됩니다. 이는 시스템의 처리 시간과 메모리에 비효율적입니다.

허나, 객체의 prototype을 통해서 메소드를 추가하면 오직 단 하나의 버전의 메소드가 존재하게 됩니다. 이는 생성자 함수 내에서 메소드를 추가하는 것보다 처리 시간과 메모리 모두 이점을 가질 수 있습니다.

아래의 예시를 봅시다.

function Animal(animal) {
  this.animal = animal;
  this.bark = function(cry) {
    console.log(this.animal + " : " + cry);
  };
}

console.time("Object Methods");
var exampleArray = [];
for (var i = 0; i <= 10000000; i++) {
  exampleArray.push(new Animal("cow"));
}
console.timeEnd("Object Methods"); //8959.079ms

Windows 10의 크롬에서 위 코드를 실행시켰을 때, console.time메소드를 통해서 실행 시간을 구했을 때 8940.622ms이라는 값을 얻을 수 있습니다. 이제 prototype을 이용해 추가하도록 아래와 같이 수정해보도록 하겠습니다.

function Animal(animal) {
  this.animal = animal;
}

Animal.prototype.bark = function(cry) {
  console.log(this.animal + " : " + cry);
};

console.time("Prototype Methods");
var exampleArray = [];
for (var i = 0; i <= 10000000; i++) {
  exampleArray.push(new Animal("cow"));
}
console.timeEnd("Prototype Methods"); //8959.079ms

이번에는 console.time메소드를 통해 시간을 계산했을 때 고작 6694.637ms밖에 걸리지 않음을 볼 수 있었고 비교적 많은 시간을 줄일 수 있었습니다. 고작 prototype을 통해 메소드를 새로 추가해준다는 차이점만으로 약 2.2초의 시간을 아낄 수 있었습니다.

자바스크립트 객체들 간에 공유되는 공통 로직에서 메소드를 추가할 때에 prototype를 사용해야하는 이유에 대해서 충분히 설명이 되었으면 좋겠습니다.

포스트 읽어주셔서 감사합니다.