[JavaScript] 프로토타입(Prototype) 완벽 가이드: 객체 지향의 핵심 원리 이해하기([JavaScript] Prototype Complete Guide: Understanding the Core Principle of Object-Oriented Programming)

안녕하세요! JavaScript는 흔히 '프로토타입 기반 객체 지향 언어'라고 불립니다. C++이나 Java 같은 클래스 기반 언어와는 다른 방식으로 객체 간의 상속과 재사용을 구현하는데, 이 핵심 원리가 바로 프로토타입(Prototype)입니다. 이번 포스팅에서는 JavaScript의 프로토타입이 무엇인지, 어떻게 동작하는지, 그리고 실제 개발에서 어떻게 활용되는지 깊이 있게 다뤄보겠습니다.

1. 프로토타입(Prototype)이란 무엇인가?

JavaScript의 모든 객체는 자신을 생성한 객체(생성자 함수)의 프로토타입 객체를 참조합니다. 그리고 이 프로토타입 객체는 또 다른 프로토타입 객체를 참조할 수 있는데, 이를 프로토타입 체인(Prototype Chain)이라고 부릅니다. JavaScript는 이 체인을 따라 객체의 속성이나 메서드를 찾아갑니다.

간단히 말해, 프로토타입은 객체 간의 상속을 구현하기 위해 사용되는 공유 가능한 속성 및 메서드의 집합이라고 할 수 있습니다.

__proto__prototype의 차이

이 두 용어는 개발자들이 가장 혼동하기 쉬운 부분입니다. 정확히 이해하는 것이 중요합니다.

  • __proto__ (던더 프로토): 모든 객체가 가지는 내부 슬롯입니다. 객체가 생성될 때 자신을 생성한 생성자 함수의 prototype 객체를 가리킵니다. 이를 통해 해당 객체는 프로토타입 체인을 타고 상위 프로토타입의 속성/메서드에 접근할 수 있습니다. 객체가 자신의 프로토타입에 접근하기 위한 링크입니다. (Object.getPrototypeOf()를 사용하는 것이 권장됩니다.)
  • prototype 속성: 함수 객체(특히 생성자 함수)만 가지는 속성입니다. 이 속성은 해당 함수가 생성할 객체들의 '청사진' 역할을 하는 객체를 가리킵니다. 즉, 이 함수로 생성될 객체들이 상속받을 속성들과 메서드들을 정의하는 공간입니다.
function Person(name) {
  this.name = name;
}

// Person 함수의 prototype 객체에 sayHello 메서드 추가
Person.prototype.sayHello = function() {
  console.log(`안녕하세요, 저는 ${this.name}입니다.`);
};

const john = new Person('John');
const jane = new Person('Jane');

john.sayHello(); // "안녕하세요, 저는 John입니다."
jane.sayHello(); // "안녕하세요, 저는 Jane입니다."

console.log(john.__proto__ === Person.prototype); // true: john 객체의 프로토타입은 Person.prototype
console.log(Person.prototype.__proto__ === Object.prototype); // true: Person.prototype의 프로토타입은 Object.prototype
console.log(Object.prototype.__proto__); // null: 프로토타입 체인의 끝

위 코드에서 johnjane은 각자의 name 속성을 가지지만, sayHello 메서드는 Person.prototype에서 공유하여 사용합니다. 이는 메모리를 효율적으로 사용하게 하며, 상속을 구현하는 기본 방식이 됩니다.

2. 프로토타입 체인 (Prototype Chain)

JavaScript는 객체의 속성이나 메서드를 찾을 때, 먼저 해당 객체 자신에서 찾고, 없으면 그 객체의 __proto__가 가리키는 프로토타입 객체에서 찾습니다. 거기에도 없으면 그 프로토타입 객체의 __proto__가 가리키는 다음 프로토타입 객체에서 찾습니다. 이 과정은 체인의 끝(null)에 도달할 때까지 계속됩니다.

const animal = {
  eats: true
};

const rabbit = {
  jumps: true,
  __proto__: animal // rabbit의 프로토타입은 animal
};

const longEar = {
  earLength: 10,
  __proto__: rabbit // longEar의 프로토타입은 rabbit
};

console.log(longEar.eats);      // true (longEar -> rabbit -> animal 체인을 따라 찾음)
console.log(longEar.jumps);     // true (longEar -> rabbit 체인을 따라 찾음)
console.log(longEar.earLength); // 10 (longEar 자신에게 있음)

// 객체에 없는 속성에 접근 시 undefined
console.log(longEar.sleeps); // undefined

이러한 프로토타입 체인 덕분에 여러 객체가 공통된 속성이나 메서드를 효율적으로 공유할 수 있습니다.

3. 프로토타입의 실제 활용 사례

3.1. 메서드 공유 및 메모리 절약

생성자 함수로 여러 개의 객체를 만들 때, 각 객체에 동일한 메서드를 복사하는 대신 프로토타입에 메서드를 정의하여 메모리를 절약하고 효율성을 높일 수 있습니다.

function Car(model) {
  this.model = model;
  // this.drive = function() { console.log(`${this.model}이(가) 운전 중입니다.`); }; // 각 객체마다 생성되어 비효율적
}

// 프로토타입에 drive 메서드 정의
Car.prototype.drive = function() {
  console.log(`${this.model}이(가) 운전 중입니다.`);
};

const myCar = new Car('Sonata');
const yourCar = new Car('K5');

myCar.drive();  // "Sonata이(가) 운전 중입니다."
yourCar.drive(); // "K5이(가) 운전 중입니다."

console.log(myCar.drive === yourCar.drive); // true (동일한 함수 참조)

3.2. 내장 객체 확장 (권장하지 않음)

이론적으로는 JavaScript의 내장 객체(예: Array, String)의 프로토타입을 확장하여 새로운 메서드를 추가할 수 있습니다. 하지만 이는 예기치 않은 부작용을 일으킬 수 있어 매우 신중하게 사용해야 하며, 일반적으로는 권장되지 않습니다.

// String.prototype 확장 (권장하지 않음!)
// String.prototype.reverse = function() {
//   return this.split('').reverse().join('');
// };
// console.log("hello".reverse()); // olleh

// 대신, 유틸리티 함수를 사용하는 것이 좋습니다.
function reverseString(str) {
  return str.split('').reverse().join('');
}
console.log(reverseString("hello"));

이러한 방식은 코드의 예측 불가능성을 높이고, 다른 라이브러리나 프레임워크와 충돌할 가능성이 있기 때문에 피하는 것이 좋습니다.

4. ES6 클래스와 프로토타입

ES6에서 도입된 `class` 문법은 JavaScript에서 객체 지향 프로그래밍을 더 쉽고 익숙하게 작성할 수 있도록 합니다. 하지만 `class`는 새로운 객체 지향 모델을 도입한 것이 아니라, 기존의 프로토타입 기반 상속을 더욱 명확하고 간결한 문법으로 추상화한 '문법적 설탕(Syntactic Sugar)'에 불과합니다.

class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    console.log(`${this.name}이(가) 소리를 냅니다.`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 부모 클래스의 constructor 호출
    this.breed = breed;
  }
  bark() {
    console.log(`${this.name} (${this.breed})이(가) 멍멍 짖습니다.`);
  }
}

const myDog = new Dog('바둑이', '진돗개');
myDog.speak(); // "바둑이이(가) 소리를 냅니다." (Animal.prototype에서 상속)
myDog.bark();  // "바둑이 (진돗개)이(가) 멍멍 짖습니다."

console.log(myDog.__proto__ === Dog.prototype);    // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true

위 코드에서 extends 키워드는 내부적으로 프로토타입 체인을 설정하여 Dog.prototypeAnimal.prototype을 상속받도록 만듭니다. 결국 ES6 클래스도 그 아래에서는 여전히 프로토타입 기반으로 동작합니다.

결론: 프로토타입은 JavaScript의 정수

프로토타입은 JavaScript가 객체 지향 프로그래밍을 구현하는 독특하고 강력한 방식입니다. __proto__prototype의 차이를 명확히 이해하고, 프로토타입 체인이 어떻게 동작하는지 파악하는 것은 JavaScript의 깊이를 이해하는 데 매우 중요합니다. ES6 클래스는 프로토타입 기반 상속을 더 쉽게 사용할 수 있도록 돕지만, 그 본질적인 동작 방식은 여전히 프로토타입에 기반하고 있음을 기억해야 합니다.

  • 모든 객체는 프로토타입을 가집니다.
  • 함수 객체만 prototype 속성을 가집니다.
  • __proto__는 객체가 자신의 프로토타입을 참조하는 링크입니다.
  • 프로토타입 체인을 통해 상속이 구현됩니다.
  • ES6 클래스는 프로토타입의 '문법적 설탕'입니다.

이 포스팅이 JavaScript 프로토타입 개념을 완벽하게 이해하는 데 도움이 되었기를 바랍니다. 궁금한 점이나 추가하고 싶은 내용이 있다면 언제든지 댓글로 남겨주세요!

댓글 없음:

댓글 쓰기

댓글 폭탄 해결! 자바스크립트 댓글 접기/펼치기로 가독성 200% 높이는 법(Solve Comment Chaos: Elevate Readability 200% with JS Comment Folding/Unfolding)

내 웹사이트에 적용! 초간단 자바스크립트 댓글 펼치기/숨기기 튜토리얼 내 웹사이트에 적용! 초간단 자바스크립트 댓글 펼치기/숨기기 튜토...