우리는 Javascript 객체를 다루기 위해 종종 생성자 함수를 사용하곤 합니다. 그러던 중에 생성자 함수 내부에 직접 메소드를 정의해주는 것과 prototype
을 사용해서 메소드를 정의해주는 것의 차이점이 무엇일까에 대한 의문이 시작되었고, 여러가지 자료를 통해 알아본 결과는 충격적이었습니다.
javascript-prototype-methods-vs-object-methods라는 포스트를 보고 번역을 한 글이며 조금의 추가와 변경, 예제를 바꾸어서 쓴 글입니다.
객체를 정의할 때, 객체 내부에 메소드를 추가할 수 있는 방법은 두가지가 있습니다.
this.func = function(){}
와 같이 선언하는 방법Obj.prototype.func = function(){}
과 같이 prototype
을 이용하는 것입니다.두가지 방법 모두 모든 인스턴스에서 func()
함수에 접근할 수 있는데, 우리는 어떤 방법을 사용해야 효율적일까요 ?
이 질문에 대한 대답을 미리 하자면, 대부분의 경우에서 prototype
을 사용하는 2번 방법이 효율적입니다. 만약에 생성자 함수에 추가하는 메소드의 용도가 객체 인스터스들이 공통으로 사용되는 함수를 정의하는 것이라면, 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
을 사용했을 때 객체 공통 메소드에 대한 변화가 객체의 모든 인스턴스들에 정상적으로 반영되는 것을 볼 수 있었습니다.
객체의 생성자 함수에서 메소드를 추가하게 되면 각 인스턴스를 생성하는 시점에 각 메소들의 사본을 가지게 됩니다. 이는 시스템의 처리 시간과 메모리에 비효율적입니다.
허나, 객체의 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"); //6694.637ms
이번에는 console.time
메소드를 통해 시간을 계산했을 때 고작 6694.637ms밖에 걸리지 않음을 볼 수 있었고 비교적 많은 시간을 줄일 수 있었습니다. 고작 prototype
을 통해 메소드를 새로 추가해준다는 차이점만으로 약 2.2초의 시간을 아낄 수 있었습니다.
자바스크립트 객체들 간에 공유되는 공통 로직에서 메소드를 추가할 때에 prototype
를 사용해야하는 이유에 대해서 충분히 설명이 되었으면 좋겠습니다.
포스트 읽어주셔서 감사합니다.
많이 배우고 갑니다~!