1. 설명
JavaScript에서 class 문법을 사용하면 객체지향 프로그래밍을 직관적으로 구현할 수 있다. 하지만 class는 ES6에서 도입된 문법적 설탕(syntactic sugar)일 뿐이고, 내부적으로는 여전히 프로토타입 기반으로 동작한다. 실제 프로덕션 환경에서 레거시 코드를 다루거나, 프레임워크의 내부 구조를 이해하거나, 메모리 최적화가 필요한 상황에서는 프로토타입의 동작 원리를 정확히 알아야 한다. 프로토타입 체인을 제대로 이해하지 못하면 예상치 못한 버그를 만날 수 있고, 특히 상속 관계에서 메서드가 어디에서 호출되는지 추적하기 어렵다.
2. 개념 정의
프로토타입은 JavaScript에서 효율적인 코드 재사용을 위해 존재하는 메커니즘이다. 모든 함수는 prototype
속성을 가지며, new
키워드로 생성된 인스턴스는 생성자 함수의 prototype
을 참조하는 __proto__
링크를 갖는다. 이를 통해 여러 인스턴스가 동일한 메서드를 공유할 수 있어 메모리를 효율적으로 사용할 수 있다.
프로토타입이 없다면 다음과 같은 상황이 발생한다:
function Car(brand) {
this.brand = brand;
this.drive = function() {
console.log('Driving');
};
}
const car1 = new Car('Tesla');
const car2 = new Car('BMW');
console.log(car1.drive === car2.drive); // false - 각각 다른 함수 객체
이 경우 drive
메서드가 인스턴스마다 별도로 생성되어 메모리를 낭비하게 된다. 프로토타입을 사용하면 이 문제를 해결할 수 있다:
function Car(brand) {
this.brand = brand;
}
Car.prototype.drive = function() {
console.log('Driving');
};
const car1 = new Car('Tesla');
const car2 = new Car('BMW');
console.log(car1.drive === car2.drive); // true - 같은 함수 참조
프로토타입 체인은 객체에서 속성이나 메서드를 찾을 때 따라가는 경로다. 인스턴스에서 속성을 찾지 못하면 __proto__
를 따라 올라가며 찾게 되고, 프로토타입 체인의 끝인 Object.prototype
까지 도달해도 찾지 못하면 undefined
를 반환한다.
3. 예제 코드
class 문법으로 작성된 코드를 프로토타입 기반으로 변환하는 과정을 단계별로 살펴보자.
원본 class 코드:
class ConstructorA {
constructor(name) {
this.name = name;
}
method() {
console.log(`Method called by ${this.name}`);
}
static staticMethod() {
console.log('Static method called');
}
}
class ConstructorB extends ConstructorA {
constructor(name, age) {
super(name);
this.age = age;
}
}
프로토타입 기반 변환:
// ConstructorA를 생성자 함수로 변환
function ConstructorA(name) {
this.name = name;
}
// 인스턴스 메서드는 prototype에 추가
ConstructorA.prototype.method = function() {
console.log(`Method called by ${this.name}`);
};
// static 메서드는 생성자 함수 자체에 추가
ConstructorA.staticMethod = function() {
console.log('Static method called');
};
// ConstructorB 생성자 함수
function ConstructorB(name, age) {
// super(name) 역할: 부모 생성자를 현재 this로 호출
ConstructorA.call(this, name);
this.age = age;
}
// 프로토타입 체인 연결
ConstructorB.prototype = Object.create(ConstructorA.prototype);
// 테스트
const a = new ConstructorA('Alice');
a.method(); // "Method called by Alice"
ConstructorA.staticMethod(); // "Static method called"
const b = new ConstructorB('Bob', 30);
b.method(); // "Method called by Bob"
console.log(b.age); // 30
console.log(b.name); // "Bob"
new
키워드가 실행될 때 내부적으로 다음과 같은 과정이 일어난다:
// new ConstructorB('Bob', 30) 실행 시
// 1단계: 빈 객체 생성
const newObj = {};
// 2단계: 프로토타입 연결
newObj.__proto__ = ConstructorB.prototype;
// 3단계: 생성자 함수 실행
ConstructorB.call(newObj, 'Bob', 30);
// 4단계: 객체 반환
return newObj;
4. 트러블슈팅
프로토타입 상속을 구현할 때 자주 발생하는 실수와 해결 방법을 정리했다.
실수 1: 프로토타입을 직접 할당
// 잘못된 방법
ConstructorB.prototype = ConstructorA.prototype;
// 문제: 같은 객체를 참조하게 됨
ConstructorB.prototype.methodB = function() {};
console.log(ConstructorA.prototype.methodB); // 존재함 (오염됨)
이렇게 하면 ConstructorB.prototype
과 ConstructorA.prototype
이 같은 객체를 가리키게 되어, ConstructorB
에 메서드를 추가하면 ConstructorA
에도 영향을 준다. 반드시 Object.create()
를 사용해 새 객체를 만들어야 한다.
실수 2: super() 호출 누락
function ConstructorB(name, age) {
// ConstructorA.call(this, name); 누락
this.age = age;
}
const b = new ConstructorB('Bob', 30);
console.log(b.name); // undefined
부모 생성자를 호출하지 않으면 부모의 속성이 초기화되지 않는다. class의 super()
는 내부적으로 .call()
을 사용해 부모 생성자를 호출한다.
실수 3: __proto__
를 직접 수정
// 비표준 방식
ConstructorB.prototype.__proto__ = ConstructorA.prototype;
__proto__
는 내부 프로퍼티로 직접 접근하는 것은 비표준이며 성능 문제를 일으킬 수 있다. 항상 Object.create()
또는 Object.setPrototypeOf()
를 사용해야 한다.
5. 모범 사례
실무에서 프로토타입을 다룰 때 권장되는 패턴과 주의점은 다음과 같다.
프로토타입 체인을 디버깅할 때는 Object.getPrototypeOf()
를 사용하는 것이 안전하다.
Chrome DevTools의 Console 탭에서 객체를 펼쳐보면 [[Prototype]]
항목으로 프로토타입 체인을 시각적으로 확인할 수 있다.
const b = new ConstructorB('Bob', 30);
console.log(Object.getPrototypeOf(b)); // ConstructorB.prototype
console.log(Object.getPrototypeOf(Object.getPrototypeOf(b))); // ConstructorA.prototype
메서드를 프로토타입에 추가할 때는 화살표 함수 대신 일반 함수를 사용해야 한다.
화살표 함수는 자신만의 this
를 가지지 않아 인스턴스를 참조할 수 없다.
// 잘못된 방법
ConstructorA.prototype.method = () => {
console.log(this.name); // this는 인스턴스가 아님
};
// 올바른 방법
ConstructorA.prototype.method = function() {
console.log(this.name); // this는 인스턴스
};
레거시 코드를 리팩토링할 때는 먼저 프로토타입 구조를 파악한 후 단계적으로 class 문법으로 전환하는 것이 안전하다. 한 번에 모든 코드를 변경하면 예상치 못한 버그가 발생할 수 있다.
6. 결론 요약
JavaScript의 class 문법은 내부적으로 프로토타입 기반으로 동작하며, 프로토타입은 메모리 효율적인 코드 재사용을 위해 설계되었다. 생성자 함수의 prototype
에 메서드를 정의하면 모든 인스턴스가 동일한 메서드를 공유하게 되고, Object.create()
를 사용해 프로토타입 체인을 구성하면 상속을 구현할 수 있다. 프로토타입의 동작 원리를 이해하면 레거시 코드 유지보수, 메모리 최적화, 프레임워크 내부 구조 파악에 큰 도움이 된다.
레퍼런스
- MDN: Inheritance and the prototype chain
- JavaScript.info: Prototypal inheritance
- ECMA-262 명세: OrdinaryCreateFromConstructor
'Programming > JS' 카테고리의 다른 글
자바스크립트 프로토타입: 플라톤과 아리스토텔레스의 싸움 (0) | 2025.10.10 |
---|---|
실행 컨텍스트: 코드 실행의 숨겨진 원리 (0) | 2025.09.16 |
비교연산자 (0) | 2022.01.19 |
JS - Event (0) | 2022.01.19 |
JS 기본 - 함수 (0) | 2022.01.18 |