전체 목록
JavaScriptHard#04

프로토타입 체인(Prototype Chain)이란 무엇이며 상속과 어떤 관련이 있나요?

#JS#프로토타입#상속#OOP
힌트

__proto__와 prototype 속성의 차이를 생각해보세요.

정답 및 해설

프로토타입 체인(Prototype Chain)이란 무엇이며 상속과 어떤 관련이 있나요?

JavaScript는 클래스 기반 언어(Java, C++)와 달리 프로토타입 기반(Prototype-based) 언어입니다. 객체가 다른 객체를 직접 참조하는 방식으로 속성과 메서드를 공유하고 상속합니다. ES6에서 도입된 class 문법도 내부적으로는 프로토타입 메커니즘을 기반으로 작동하는 문법적 설탕(syntactic sugar)입니다.

[[Prototype]] 내부 슬롯

모든 JavaScript 객체는 [[Prototype]]이라는 내부 슬롯을 가집니다. 이 슬롯은 다른 객체를 참조하거나 null을 가리킵니다. 이 참조를 통해 연결된 객체들의 사슬이 프로토타입 체인입니다.

const obj = { name: 'Alice' };

// __proto__는 [[Prototype]]에 접근하는 접근자 프로퍼티 (비표준이지만 널리 지원됨)
console.log(obj.__proto__ === Object.prototype); // true

// 표준 방법
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true

프로퍼티 탐색 과정

객체에서 프로퍼티나 메서드를 찾을 때 JavaScript 엔진은 다음 순서로 탐색합니다.

  1. 현재 객체에서 찾는다.
  2. 없으면 [[Prototype]]이 가리키는 객체에서 찾는다.
  3. 없으면 그 객체의 [[Prototype]]에서 찾는다.
  4. null에 도달할 때까지 반복한다.
  5. null에 도달하면 undefined를 반환한다.
const animal = {
  breathe() {
    return '숨을 쉽니다';
  }
};

const dog = {
  bark() {
    return '멍멍!';
  }
};

// dog의 [[Prototype]]을 animal로 설정
Object.setPrototypeOf(dog, animal);

console.log(dog.bark());    // '멍멍!' — dog 자신의 메서드
console.log(dog.breathe()); // '숨을 쉽니다' — [[Prototype]]인 animal에서 찾음
console.log(dog.fly);       // undefined — 어디서도 찾지 못함

생성자 함수와 prototype 프로퍼티

함수는 prototype 프로퍼티를 가집니다. new 키워드로 객체를 생성하면, 생성된 객체의 [[Prototype]]이 생성자 함수의 prototype 객체를 가리킵니다.

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

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

const alice = new Person('Alice', 25);
const bob = new Person('Bob', 30);

console.log(alice.greet()); // '안녕하세요, 저는 Alice입니다.'
console.log(bob.greet());   // '안녕하세요, 저는 Bob입니다.'

// alice와 bob은 같은 greet 함수를 공유 (메모리 절약)
console.log(alice.greet === bob.greet); // true

// 프로토타입 체인 확인
console.log(Object.getPrototypeOf(alice) === Person.prototype); // true
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype); // true

메모리 관점에서의 장점

// 나쁜 예 — 각 인스턴스마다 함수가 별도로 생성됨
function PersonBad(name) {
  this.name = name;
  this.greet = function() { return `Hi, ${this.name}`; }; // 매번 새 함수 생성
}

// 좋은 예 — 모든 인스턴스가 프로토타입의 함수를 공유
function PersonGood(name) {
  this.name = name;
}
PersonGood.prototype.greet = function() { return `Hi, ${this.name}`; };

프로토타입 체인을 통한 상속

생성자 함수를 이용한 상속

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

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

function Dog(name, breed) {
  Animal.call(this, name); // 부모 생성자 호출
  this.breed = breed;
}

// Dog의 prototype을 Animal의 인스턴스로 설정 (프로토타입 체인 연결)
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // constructor 복원

Dog.prototype.bark = function () {
  return `${this.name}: 멍멍!`;
};

const rex = new Dog('Rex', '진돗개');
console.log(rex.bark());  // 'Rex: 멍멍!' — Dog.prototype에서
console.log(rex.speak()); // 'Rex이(가) 소리를 냅니다.' — Animal.prototype에서
console.log(rex instanceof Dog);    // true
console.log(rex instanceof Animal); // true

// 프로토타입 체인: rex → Dog.prototype → Animal.prototype → Object.prototype → null

ES6 class 문법 (내부적으로 동일)

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

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

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // Animal.call(this, name)과 동일
    this.breed = breed;
  }

  bark() {
    return `${this.name}: 멍멍!`;
  }
}

const rex = new Dog('Rex', '진돗개');
console.log(rex.bark());  // 'Rex: 멍멍!'
console.log(rex.speak()); // 'Rex이(가) 소리를 냅니다.'

// class도 내부적으로 프로토타입을 사용
console.log(Object.getPrototypeOf(Dog) === Animal); // true (정적 상속)
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true

Object.create()로 직접 프로토타입 설정

const vehicleProto = {
  start() {
    return `${this.type} 시동을 겁니다.`;
  },
  stop() {
    return `${this.type} 시동을 끕니다.`;
  }
};

const car = Object.create(vehicleProto);
car.type = '자동차';

const bike = Object.create(vehicleProto);
bike.type = '자전거';

console.log(car.start());  // '자동차 시동을 겁니다.'
console.log(bike.start()); // '자전거 시동을 겁니다.'
console.log(Object.getPrototypeOf(car) === vehicleProto); // true

프로퍼티 쉐도잉 (Property Shadowing)

인스턴스에 프로토타입과 같은 이름의 프로퍼티를 추가하면, 인스턴스 자신의 프로퍼티가 프로토타입의 것을 가립니다.

function Shape(color) {
  this.color = color;
}
Shape.prototype.describe = function () {
  return `색상: ${this.color}`;
};

const circle = new Shape('빨강');
// 인스턴스에 같은 이름의 메서드 추가 (쉐도잉)
circle.describe = function () {
  return `원 — ${this.color}`;
};

console.log(circle.describe()); // '원 — 빨강' (쉐도잉된 메서드)

const square = new Shape('파랑');
console.log(square.describe()); // '색상: 파랑' (프로토타입 메서드)

hasOwnProperty와 in 연산자

const person = { name: 'Alice' };
Object.prototype.species = 'human'; // 모든 객체의 프로토타입에 추가

console.log('name' in person);    // true (자신 + 프로토타입 체인 전체 탐색)
console.log('species' in person); // true (프로토타입에서 찾음)

console.log(person.hasOwnProperty('name'));    // true (자신만 탐색)
console.log(person.hasOwnProperty('species')); // false (자신에게 없음)

// for...in은 프로토타입 체인까지 열거하므로 주의 필요
for (const key in person) {
  if (person.hasOwnProperty(key)) {
    console.log(key); // 'name'만 출력
  }
}

정리

개념설명
[[Prototype]]모든 객체가 가진 내부 슬롯, 다른 객체를 참조
프로토타입 체인[[Prototype]] 참조가 연결된 사슬
prototype 프로퍼티함수만 가지는 프로퍼티, new로 생성된 객체의 [[Prototype]]이 됨
Object.create()특정 객체를 프로토타입으로 하는 새 객체 생성
class프로토타입 기반 상속의 문법적 설탕
프로퍼티 쉐도잉인스턴스 프로퍼티가 프로토타입 프로퍼티를 가리는 현상

프로토타입 체인은 JavaScript의 가장 핵심적인 특성 중 하나입니다. class 문법을 주로 사용하더라도, 내부 동작 원리를 이해하면 상속 관련 버그를 진단하고 성능을 최적화하는 데 큰 도움이 됩니다.