개인의 기록
  • 소개
  • 프로젝트
  • 글
  • 링크

© 2026 newgirok

← 글 목록

프로토타입 — JavaScript 상속의 실제 구조

2025년 8월 28일
JavaScript프로토타입상속객체지향

class Dog extends Animal은 클래스 기반 언어의 상속처럼 보입니다. 그런데 JavaScript 엔진은 실제로 클래스를 사용하지 않습니다. 내부에서는 객체가 다른 객체를 참조하는 체인 구조로 동작하며, class 문법은 이를 감싼 표현일 뿐입니다.

프로토타입이란 무엇인가

JavaScript의 모든 객체는 다른 객체를 가리키는 숨겨진 참조를 하나 갖고 있습니다. 이 참조 대상을 프로토타입이라고 합니다. 객체에서 어떤 속성이나 메서드를 찾지 못하면, 엔진은 이 참조를 따라 프로토타입 객체에서 다시 탐색합니다. 프로토타입에도 없으면 프로토타입의 프로토타입으로 올라갑니다. 이 탐색 경로 전체를 프로토타입 체인이라고 합니다.

프로토타입 체인 탐색 — toString 동작 원리

직접 정의한 적 없는 toString을 모든 객체에서 호출할 수 있는 이유가 바로 체인 탐색입니다.

const obj = { name: "alice" };

console.log(obj.toString()); // "[object Object]"

엔진의 탐색 순서는 다음과 같습니다.

  1. obj 자체에 toString 속성이 있는가? → 없음
  2. obj의 프로토타입(Object.prototype)에 toString이 있는가? → 있음, 실행

Object.prototype은 모든 일반 객체의 프로토타입 체인 끝에 위치하며, toString, hasOwnProperty 등의 공통 메서드가 여기에 정의되어 있습니다.

프로토타입 체인 구조

obj
 │
 └──[[Prototype]]──→ Object.prototype
                          │
                          └──[[Prototype]]──→ null

배열이나 함수의 경우 체인이 더 깁니다.

[]
 │
 └──[[Prototype]]──→ Array.prototype
                          │
                          └──[[Prototype]]──→ Object.prototype
                                                   │
                                                   └──[[Prototype]]──→ null

__proto__ vs prototype

이 두 가지는 이름이 비슷하지만 역할이 다릅니다.

속성주체의미
__proto__모든 객체이 객체의 프로토타입을 가리키는 참조
prototype함수 객체new로 생성한 인스턴스의 __proto__가 될 객체
function Dog(name) {
  this.name = name;
}

Dog.prototype.bark = function () {
  console.log(`${this.name}: 왈!`);
};

const d = new Dog("초코");

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

이 두 검증문은 서로 다른 연결을 확인합니다.

첫 번째 줄(d.__proto__ === Dog.prototype)은 new Dog로 인스턴스를 만들 때 d가 Dog.prototype을 참조하도록 자바스크립트가 자동으로 연결해준다는 사실을 보여줍니다.

두 번째 줄(Dog.prototype.__proto__ === Object.prototype)이 보여주는 것은 Dog.prototype 역시 { bark: fn } 형태의 평범한 객체라는 사실입니다. 객체인 이상 Dog.prototype도 자신만의 __proto__를 가지며, 따로 지정하지 않았다면 이는 Object.prototype을 가리킵니다.

즉 d → Dog.prototype → Object.prototype로 이어지는 체인에서, d.__proto__는 첫 번째 연결을, Dog.prototype.__proto__는 두 번째 연결을 확인하는 것입니다. 구조로 나타내면 다음과 같습니다.

d (인스턴스)
 │
 └──__proto__──→ Dog.prototype  { bark: fn }
                      │
                      └──__proto__──→ Object.prototype

__proto__ 대신 Object.getPrototypeOf

__proto__는 오랫동안 비표준으로 취급되다가 ES6에서 하위 호환을 위해 정식 표준에 포함됐지만, MDN을 비롯한 공식 문서는 여전히 실무 코드에서 직접 사용하지 말 것을 권장합니다. 프로토타입을 읽을 때는 Object.getPrototypeOf를, 설정할 때는 Object.setPrototypeOf를 사용하는 것이 표준적인 방법입니다.

console.log(Object.getPrototypeOf(d) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Object.prototype); // true

Object.getPrototypeOf(x)는 x.__proto__를 읽는 것과 동일한 결과를 반환합니다. 즉 위 두 줄은 앞서 살펴본 d.__proto__ === Dog.prototype, Dog.prototype.__proto__ === Object.prototype 검증을 함수 호출 방식으로 다시 확인한 것입니다.

__proto__를 피해야 하는 이유는 크게 두 가지입니다.

  1. 모든 환경에서 보장되지 않음: __proto__는 Object.prototype에 정의된 접근자(getter/setter)이기 때문에, Object.create(null)처럼 프로토타입 체인이 끊긴 객체에서는 아예 존재하지 않습니다. 반면 Object.getPrototypeOf는 함수이므로 어떤 객체에도 안전하게 사용할 수 있습니다.
  2. 성능: 엔진에 따라 __proto__ 접근자 경유가 Object.getPrototypeOf의 직접 호출보다 느릴 수 있습니다.
const obj = Object.create(null);

console.log(obj.__proto__); // undefined — __proto__ 접근자 자체가 없음
console.log(Object.getPrototypeOf(obj)); // null — 정상적으로 null 반환

정리하면 __proto__는 디버깅 콘솔이나 학습용으로 프로토타입 체인을 빠르게 확인할 때는 유용하지만, 실제 코드에서 프로토타입을 읽거나 쓸 때는 Object.getPrototypeOf / Object.setPrototypeOf를 사용하는 것이 안전합니다.

new 연산자 — 인스턴스 생성 과정

new Dog("초코")를 실행하면 엔진은 다음 4단계를 수행합니다.

// new Dog("초코")의 내부 동작을 직접 표현하면:
function newDog(name) {
  // 1. 빈 객체 생성
  const instance = {};

  // 2. 프로토타입 연결
  Object.setPrototypeOf(instance, Dog.prototype);

  // 3. 생성자 함수 실행 (this = instance)
  Dog.call(instance, name);

  // 4. 완성된 객체 반환
  return instance;
}

생성자 함수가 명시적으로 객체를 반환하면 그 객체가 반환되고, 그렇지 않으면 4단계에서 instance가 반환됩니다.

생성자 함수(Constructor Function): new 키워드와 함께 호출하여 새 객체를 만드는 일반 함수. 관례상 이름을 대문자로 시작합니다.

프로토타입으로 상속 구현 — Animal → Dog

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

Animal.prototype.eat = function () {
  console.log(`${this.name}이(가) 먹습니다.`);
};

function Dog(name, breed) {
  Animal.call(this, name); // Animal의 초기화 코드 실행
  this.breed = breed;
}

// Dog.prototype이 Animal.prototype을 참조하도록 연결
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function () {
  console.log(`${this.name}: 왈!`);
};

const d = new Dog("초코", "말티즈");
d.eat();  // "초코이(가) 먹습니다."
d.bark(); // "초코: 왈!"

Object.create: 인자로 받은 객체를 프로토타입으로 설정한 새 빈 객체를 만듭니다. new 없이 프로토타입 체인을 직접 구성할 때 사용합니다.

체인은 다음과 같이 구성됩니다.

d
 └──__proto__──→ Dog.prototype
                      └──__proto__──→ Animal.prototype
                                           └──__proto__──→ Object.prototype
                                                                └──→ null

class는 문법적 설탕

class 문법은 프로토타입을 감싼 문법적 설탕으로, 위의 코드를 훨씬 간결하게 표현합니다. 엔진이 실행하는 내용은 앞의 프로토타입 코드와 동일합니다.

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

  eat() {
    console.log(`${this.name}이(가) 먹습니다.`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }

  bark() {
    console.log(`${this.name}: 왈!`);
  }
}

const d = new Dog("초코", "말티즈");
console.log(d.__proto__ === Dog.prototype);            // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true

extends는 Object.create로 프로토타입을 연결하고, super는 Animal.call(this, name)에 해당합니다.

문법적 설탕(Syntactic Sugar): 기능은 동일하지만 코드를 더 읽기 쉽게 표현하기 위한 문법. 엔진 내부 동작은 바뀌지 않습니다.

hasOwnProperty와 Object.keys — 자신의 속성과 프로토타입 속성 구분

for...in 루프나 속성 확인 시 체인 전체가 탐색되기 때문에, 직접 정의한 속성인지 상속된 속성인지 구분이 필요할 때가 있습니다.

const d = new Dog("초코", "말티즈");

console.log(d.hasOwnProperty("name"));  // true  — 인스턴스 직접 소유
console.log(d.hasOwnProperty("breed")); // true  — 인스턴스 직접 소유
console.log(d.hasOwnProperty("bark"));  // false — Dog.prototype 소유
console.log(d.hasOwnProperty("eat"));   // false — Animal.prototype 소유

// for...in은 체인 전체를 순회합니다
for (const key in d) {
  if (d.hasOwnProperty(key)) {
    console.log("직접 속성:", key); // name, breed만 출력
  }
}
console.log(Object.keys(d)); // ["name", "breed"] — 인스턴스 직접 소유 속성만 반환

Object.keys는 프로토타입 체인을 타지 않고 인스턴스가 직접 소유한 속성만 반환하므로, for...in과 달리 hasOwnProperty로 걸러낼 필요 없이 안전하게 사용할 수 있습니다.

다만 특정 키 하나의 소유 여부만 확인하고 싶을 때는(obj.hasOwnProperty(key)), 배열을 만들어 includes로 찾는 것보다 hasOwnProperty가 더 직접적입니다.

요약

개념핵심
프로토타입객체가 속성 탐색을 위임하는 참조 대상
프로토타입 체인탐색이 실패할 때마다 거슬러 올라가는 참조 연결
__proto__인스턴스가 자신의 프로토타입을 가리키는 참조
prototype생성자 함수로 만들 인스턴스의 프로토타입이 될 객체
Object.getPrototypeOf__proto__를 대체하는 표준 메서드, 모든 객체에 안전하게 사용 가능
문법적 설탕class는 프로토타입 체인을 감싼 간결한 표현
방식프로토타입 연결
생성자 함수Object.create로 프로토타입 연결을 명시적으로 설정
class extends프로토타입 연결과 정적(static) 멤버 상속까지 자동으로 처리

JavaScript의 상속은 class 문법 뒤에 숨어있지만 실제로는 객체가 객체를 참조하는 체인 구조입니다. 속성 하나를 찾을 때마다 이 체인이 탐색되며, class는 그 과정을 감싼 표현일 뿐 동작 방식은 바뀌지 않습니다.

← 이전 글클로저 — 렉시컬 환경을 기억하는 함수
다음 글 →모듈 — 코드를 나누고 연결하는 방법