Frontend/JAVASCRIPT

클래스 없는 상속의 마법, 자바스크립트 프로토타입 완벽 이해하기

미니임 2026. 3. 2. 12:13

 

현대 자바스크립트는 class 문법을 지원하며 우리에게 익숙한 객체지향 프로그래밍의 외형을 갖췄습니다. 하지만 그 이면을 들여다보면, 자바스크립트는 여전히 프로토타입(Prototype) 기반의 언어라는 본질을 유지하고 있습니다. Java나 C++ 같은 클래스 기반 언어에 익숙한 개발자들이 자바스크립트의 상속을 다룰 때 가장 흔히 겪는 혼란은 바로 이 '뿌리'를 오해하는 데서 시작됩니다.


1. 붕어빵 틀이 아닌, '유전자 지도'로서의 프로토타입

일반적인 클래스 기반 언어에서 상속이 '설계도(Class)로부터 객체(Instance)를 복사하는 과정'이라면, 자바스크립트의 상속은 **'연결(Link)'**의 개념입니다.

깊이 있는 비유: 가문 레시피와 요리사

어느 유명한 식당의 요리사(객체)가 있다고 가정해 봅시다. 이 요리사는 자신만의 특별한 소스 레시피를 가지고 있지 않지만, 손님이 주문하면 당황하지 않습니다. 요리사의 공책에는 **"레시피가 없으면 스승님(Prototype)의 공책을 확인하라"**는 메모가 적혀 있기 때문입니다.

만약 스승님의 공책에도 없다면? 다시 그 스승의 스승님 공책을 확인합니다. 이것이 바로 자바스크립트의 **프로토타입 체인(Prototype Chain)**입니다. 객체는 속성을 직접 소유하기보다, 상위 객체에 "빌려 쓰러 가는 길"을 알고 있는 셈입니다.


2. 작동 원리: __proto__와 prototype의 차이

이 개념을 명확히 하려면 두 가지 핵심 속성을 구분해야 합니다.

  • prototype 속성: 함수(특히 생성자 함수)가 생성될 때 가지는 속성입니다. "내가 만든 자식들에게 물려줄 유산 목록"이라고 보시면 됩니다.
  • [[Prototype]] (내부 슬롯, __proto__로 접근): 모든 객체가 가지는 속성으로, "나의 부모(상위 프로토타입)가 누구인가"를 가리키는 이정표입니다.

3. 실전 예제: 이커머스 장바구니 시스템 구현

단순한 예제 대신, 실제 서비스에서 공통 기능을 확장하는 구조를 설계해 보겠습니다. 모든 상품의 공통 기능을 담은 Product를 정의하고, 이를 상속받는 Electronics 객체를 만들어 봅니다.

JavaScript
 
// 1. 기초가 되는 생성자 함수 정의
function Product(name, price) {
  this.name = name;
  this.price = price;
}

// 2. 모든 상품이 공유할 메서드를 프로토타입에 정의 (메모리 효율성 극대화)
Product.prototype.getDetails = function() {
  return `${this.name}의 가격은 ${this.price.toLocaleString()}원입니다.`;
};

// 3. 자식 생성자 함수 정의
function Electronics(name, price, brand) {
  // Product의 속성을 빌려옴 (Call-site binding)
  Product.call(this, name, price);
  this.brand = brand;
}

// 4. 프로토타입 체인 연결 (상속의 핵심)
// Object.create는 첫 번째 인자를 부모로 하는 새로운 객체를 생성합니다.
Electronics.prototype = Object.create(Product.prototype);

// 5. 파괴된 constructor 복구 (상속 시 필수 작업)
Electronics.prototype.constructor = Electronics;

// 6. 자식만의 고유 메서드 추가
Electronics.prototype.powerOn = function() {
  return `${this.brand} ${this.name}의 전원을 켭니다.`;
};

// 실무 적용
const myLaptop = new Electronics("MacBook Pro", 3500000, "Apple");

console.log(myLaptop.getDetails()); // 부모의 메서드 호출 성공
console.log(myLaptop.powerOn());    // 자신의 메서드 호출 성공

코드 분석 및 트러블슈팅

  • Product.call(this, ...)를 사용하는 이유: Object.create는 프로토타입만 연결할 뿐, 부모 생성자 함수 내부의 로직(속성 할당 등)을 실행해주지 않습니다. 이를 누락하면 name과 price가 undefined로 남게 됩니다.
  • constructor 복구의 중요성: Object.create로 프로토타입을 덮어쓰면 Electronics.prototype.constructor가 Product를 가리키게 됩니다. 이를 방치하면 인스턴스의 타입을 판별하거나 동적 생성을 할 때 예기치 못한 버그가 발생할 수 있습니다.

4. 트레이드오프(Trade-offs): 프로토타입의 양날의 검

장점

  • 메모리 효율성: 수만 개의 객체를 생성하더라도 메서드는 프로토타입 영역에 단 하나만 존재합니다.
  • 런타임 확장: 이미 생성된 객체라도 부모 프로토타입에 메서드를 추가하면 즉시 모든 자식 객체에서 사용할 수 있는 유연함을 가집니다.

단점 및 주의사항

  • 성능 저하: 프로토타입 체인이 너무 길어지면(Deep Hierarchy), 속성을 찾기 위해 상위로 거슬러 올라가는 과정에서 성능 오버헤드가 발생합니다. 존재하지 않는 속성에 접근할 때 체인의 끝인 null까지 확인해야 하므로 특히 주의해야 합니다.
  • 참조 타입 공유 문제: 프로토타입에 객체나 배열 같은 참조 타입을 직접 정의하면, 모든 인스턴스가 해당 데이터를 공유하게 되어 의도치 않은 상태 변이가 일어날 수 있습니다.

5. 결론: 왜 여전히 프로토타입인가?

ES6의 class는 프로토타입을 감싸는 **문법적 설탕(Syntactic Sugar)**일 뿐입니다. class 내부에서도 결국 위에서 설명한 prototype 연결 작업이 자동으로 수행됩니다. 따라서 이 원리를 이해하는 것은 자바스크립트의 실행 컨텍스트와 메모리 구조를 이해하는 것과 같습니다.

반응형