본문으로 건너뛰기
JavaScript 프로토타입 체인 다이어그램

# JavaScript의 프로토타입 상속: 객체 지향 프로그래밍의 핵심

Table of Contents

프로토타입 상속이란?

프로토타입 상속은 JavaScript의 핵심 메커니즘 중 하나로, 객체가 다른 객체로부터 속성과 메서드를 상속받을 수 있게 해줍니다. Java나 C++과 같은 클래스 기반 언어와 달리, JavaScript는 프로토타입 기반 언어입니다. 이는 객체가 직접 다른 객체로부터 상속받는다는 의미입니다.

:::important JavaScript의 모든 객체는 내부적으로 [[Prototype]]이라는 숨겨진 속성을 가지고 있습니다. 이 속성은 다른 객체를 참조하거나 null을 가질 수 있습니다. 이것이 바로 프로토타입 체인의 기반입니다. :::

기본 프로토타입 상속

가장 간단한 프로토타입 상속 예제부터 시작해 보겠습니다.

const parent = {
  greet() {
    console.log('Hello from parent!');
  },
  name: 'Parent Object'
};

// parent를 프로토타입으로 가지는 child 객체 생성
const child = Object.create(parent);

child.greet(); // "Hello from parent!"
console.log(child.name); // "Parent Object"

// child 객체 자체에는 greet 메서드가 없습니다
console.log(child.hasOwnProperty('greet')); // false
console.log(child.hasOwnProperty('name')); // false

// 하지만 프로토타입 체인을 통해 접근할 수 있습니다
console.log('greet' in child); // true

프로토타입 체인의 동작 원리

JavaScript에서 객체의 속성이나 메서드에 접근하려고 할 때, 엔진은 다음과 같은 순서로 검색합니다.

  1. 객체 자체에서 속성을 찾습니다.
  2. 없으면 객체의 프로토타입에서 찾습니다.
  3. 프로토타입의 프로토타입에서 찾습니다.
  4. 프로토타입 체인의 끝(null)에 도달할 때까지 계속됩니다.
const grandparent = {
  familyName: 'Kim',
  heritage() {
    return `우리 집안은 ${this.familyName}입니다.`;
  }
};

const parent = Object.create(grandparent);
parent.occupation = 'Engineer';

const child = Object.create(parent);
child.age = 10;

console.log(child.age); // 10 (자신의 속성)
console.log(child.occupation); // "Engineer" (부모로부터)
console.log(child.familyName); // "Kim" (조부모로부터)
console.log(child.heritage()); // "우리 집안은 Kim입니다."

// 프로토타입 체인 확인
console.log(Object.getPrototypeOf(child) === parent); // true
console.log(Object.getPrototypeOf(parent) === grandparent); // true
console.log(Object.getPrototypeOf(grandparent) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null

Object.create()를 사용한 상속

Object.create()는 지정된 프로토타입 객체를 가진 새 객체를 생성하는 가장 명확한 방법입니다.

// 기본 사용법
const animal = {
  type: 'Animal',
  describe() {
    return `This is a ${this.type}`;
  }
};

const dog = Object.create(animal);
dog.type = 'Dog';
console.log(dog.describe()); // "This is a Dog"

// 속성 디스크립터와 함께 사용
const cat = Object.create(animal, {
  type: {
    value: 'Cat',
    writable: true,
    enumerable: true,
    configurable: true
  },
  meow: {
    value: function() {
      return '야옹!';
    },
    enumerable: true
  }
});

console.log(cat.describe()); // "This is a Cat"
console.log(cat.meow()); // "야옹!"

생성자 함수와 프로토타입

ES6 이전에는 생성자 함수와 프로토타입을 사용하여 객체를 생성했습니다.

function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 프로토타입에 메서드 추가
Person.prototype.introduce = function() {
  return `안녕하세요, 저는 ${this.name}이고 ${this.age}살입니다.`;
};

Person.prototype.greet = function() {
  return `안녕하세요!`;
};

const person1 = new Person('홍길동', 30);
const person2 = new Person('김철수', 25);

console.log(person1.introduce()); // "안녕하세요, 저는 홍길동이고 30살입니다."
console.log(person2.introduce()); // "안녕하세요, 저는 김철수이고 25살입니다."

// 모든 인스턴스가 같은 프로토타입을 공유합니다
console.log(person1.introduce === person2.introduce); // true

// prototype 속성 확인
console.log(Object.getPrototypeOf(person1) === Person.prototype); // true

:::tip 프로토타입에 메서드를 정의하면 모든 인스턴스가 같은 함수를 공유하므로 메모리를 절약할 수 있습니다. 만약 생성자 함수 내부에서 메서드를 정의하면, 각 인스턴스마다 새로운 함수가 생성됩니다. :::

프로토타입 상속 구현하기

생성자 함수를 사용하여 상속을 구현하는 방법입니다.

// 부모 생성자
function Vehicle(brand) {
  this.brand = brand;
  this.engineStarted = false;
}

Vehicle.prototype.start = function() {
  this.engineStarted = true;
  return `${this.brand} 엔진이 시작되었습니다.`;
};

Vehicle.prototype.stop = function() {
  this.engineStarted = false;
  return `${this.brand} 엔진이 정지되었습니다.`;
};

// 자식 생성자
function Car(brand, model) {
  // 부모 생성자 호출
  Vehicle.call(this, brand);
  this.model = model;
  this.wheels = 4;
}

// 프로토타입 체인 설정
Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.constructor = Car;

// Car만의 메서드 추가
Car.prototype.drive = function() {
  if (!this.engineStarted) {
    return '먼저 엔진을 시작하세요!';
  }
  return `${this.brand} ${this.model}이(가) 주행 중입니다.`;
};

const myCar = new Car('현대', '소나타');
console.log(myCar.start()); // "현대 엔진이 시작되었습니다."
console.log(myCar.drive()); // "현대 소나타이(가) 주행 중입니다."
console.log(myCar.stop()); // "현대 엔진이 정지되었습니다."

// 인스턴스 체크
console.log(myCar instanceof Car); // true
console.log(myCar instanceof Vehicle); // true
console.log(myCar instanceof Object); // true

:::caution 프로토타입 체인을 설정할 때 반드시 Car.prototype.constructor = Car를 설정해야 합니다. 그렇지 않으면 constructorVehicle을 가리키게 됩니다. :::

ES6 Class와 프로토타입

ES6의 class 문법은 프로토타입 상속의 문법적 설탕(syntactic sugar)입니다. 훨씬 더 직관적입니다.

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    return `${this.name}이(가) 소리를 냅니다.`;
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 부모 클래스의 constructor 호출
    this.breed = breed;
  }

  speak() {
    return `${this.name}이(가) 멍멍 짖습니다!`;
  }

  fetch() {
    return `${this.breed} ${this.name}이(가) 공을 가져옵니다.`;
  }
}

const dog = new Dog('바둑이', '진돗개');
console.log(dog.speak()); // "바둑이이(가) 멍멍 짖습니다!"
console.log(dog.fetch()); // "진돗개 바둑이이(가) 공을 가져옵니다."

// 내부적으로는 여전히 프로토타입 기반입니다
console.log(typeof Dog); // "function"
console.log(Object.getPrototypeOf(dog) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true

프로토타입 관련 주요 메서드

Object.getPrototypeOf()와 Object.setPrototypeOf()

const parent = { x: 1 };
const child = Object.create(parent);

console.log(Object.getPrototypeOf(child) === parent); // true

// 프로토타입 변경 (권장하지 않음)
const newParent = { y: 2 };
Object.setPrototypeOf(child, newParent);
console.log(child.y); // 2
console.log(child.x); // undefined

:::warning Object.setPrototypeOf()는 성능에 큰 영향을 미칠 수 있으므로 사용을 피해야 합니다. 대신 Object.create()를 사용하여 처음부터 올바른 프로토타입으로 객체를 생성하세요. :::

hasOwnProperty()와 in 연산자

const parent = { inherited: true };
const child = Object.create(parent);
child.own = true;

// hasOwnProperty: 자신의 속성만 체크
console.log(child.hasOwnProperty('own')); // true
console.log(child.hasOwnProperty('inherited')); // false

// in 연산자: 프로토타입 체인 전체를 체크
console.log('own' in child); // true
console.log('inherited' in child); // true

// Object.keys()는 자신의 열거 가능한 속성만 반환
console.log(Object.keys(child)); // ['own']

// for...in은 프로토타입 체인의 열거 가능한 속성도 포함
for (let key in child) {
  console.log(key); // 'own', 'inherited'
}

결론

JavaScript의 프로토타입 상속은 강력하고 유연한 메커니즘입니다.

  • 객체는 프로토타입 체인을 통해 다른 객체의 기능을 상속받습니다.
  • ES6 Class는 프로토타입 상속을 더 쉽게 사용하기 위한 문법입니다.
  • 프로토타입을 이해하면 JavaScript의 동작 원리를 더 깊이 이해하고 효율적인 코드를 작성할 수 있습니다.
이 글 공유하기:
My avatar

글을 마치며

이 글이 도움이 되었기를 바랍니다. 궁금한 점이나 의견이 있다면 댓글로 남겨주세요.

더 많은 기술 인사이트와 개발 경험을 공유하고 있으니, 다른 포스트도 확인해보세요.

유럽살며 여행하며 코딩하는 노마드의 여정을 함께 나누며, 함께 성장하는 개발자 커뮤니티를 만들어가요! 🚀


관련 포스트