Prototype
프로토타입
JavaScript의 핵심 상속 메커니즘. 객체가 다른 객체의 프로퍼티와 메서드를 프로토타입 체인을 통해 상속받는 방식.
프로토타입
JavaScript의 핵심 상속 메커니즘. 객체가 다른 객체의 프로퍼티와 메서드를 프로토타입 체인을 통해 상속받는 방식.
Prototype(프로토타입)은 JavaScript에서 객체 지향 프로그래밍을 구현하는 핵심 메커니즘입니다. Java나 C++과 같은 클래스 기반 언어와 달리, JavaScript는 프로토타입 기반 상속을 사용합니다. 모든 JavaScript 객체는 내부적으로 다른 객체를 참조하는 [[Prototype]] 슬롯을 가지며, 이 참조된 객체가 바로 프로토타입입니다. 객체에서 특정 프로퍼티나 메서드를 찾을 때, 해당 객체에 없으면 프로토타입 객체에서 검색합니다.
프로토타입 체인(Prototype Chain)은 객체들이 프로토타입으로 연결된 구조를 말합니다. 객체에서 프로퍼티를 조회할 때, JavaScript 엔진은 먼저 해당 객체의 own property를 확인하고, 없으면 [[Prototype]]이 가리키는 객체를 확인합니다. 이 과정은 프로퍼티를 찾거나 체인의 끝(null)에 도달할 때까지 계속됩니다. 예를 들어 배열 객체에서 toString()을 호출하면, Array.prototype을 거쳐 Object.prototype까지 탐색하여 메서드를 찾습니다.
__proto__와 prototype은 혼동하기 쉬운 개념입니다. prototype은 함수 객체만 가지는 프로퍼티로, new 키워드로 생성될 인스턴스의 [[Prototype]]이 될 객체를 가리킵니다. 반면 __proto__는 모든 객체가 가지는 접근자 프로퍼티로, 해당 객체의 실제 [[Prototype]]을 가리킵니다. 즉, 생성자함수.prototype === 인스턴스.__proto__가 성립합니다. 단, __proto__는 레거시 기능이므로 Object.getPrototypeOf()와 Object.setPrototypeOf() 사용을 권장합니다.
ES6 class 문법은 프로토타입 기반 상속을 더 직관적으로 사용할 수 있게 해주는 문법적 설탕(Syntactic Sugar)입니다. class 키워드로 선언해도 내부적으로는 여전히 프로토타입 체인이 동작합니다. class의 메서드는 자동으로 prototype에 추가되고, extends 키워드는 프로토타입 체인을 설정합니다. 따라서 class 문법을 사용하더라도 프로토타입의 개념을 이해하는 것이 JavaScript를 깊이 있게 다루는 데 필수적입니다.
// 생성자 함수를 통한 프로토타입 상속
function Animal(name) {
this.name = name;
}
// prototype에 메서드 추가 (모든 인스턴스가 공유)
Animal.prototype.speak = function() {
console.log(`${this.name}가 소리를 냅니다.`);
};
Animal.prototype.sleep = function() {
console.log(`${this.name}가 잠을 잡니다.`);
};
// 자식 생성자 함수
function Dog(name, breed) {
// 부모 생성자 호출 (super 역할)
Animal.call(this, name);
this.breed = breed;
}
// 프로토타입 체인 설정 (상속)
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
// Dog만의 메서드 추가
Dog.prototype.bark = function() {
console.log(`${this.name}가 멍멍 짖습니다!`);
};
// 메서드 오버라이딩
Dog.prototype.speak = function() {
console.log(`${this.name}(${this.breed})가 왈왈!`);
};
// 인스턴스 생성 및 테스트
const myDog = new Dog('바둑이', '진돗개');
myDog.speak(); // 바둑이(진돗개)가 왈왈!
myDog.sleep(); // 바둑이가 잠을 잡니다. (상속받은 메서드)
myDog.bark(); // 바둑이가 멍멍 짖습니다!
// 프로토타입 체인 확인
console.log(myDog instanceof Dog); // true
console.log(myDog instanceof Animal); // true
console.log(Dog.prototype.isPrototypeOf(myDog)); // true
// Object.create()를 사용한 프로토타입 상속
// 더 명시적이고 유연한 상속 방식
const personProto = {
greet() {
console.log(`안녕하세요, ${this.name}입니다.`);
},
introduce() {
console.log(`저는 ${this.age}살이고, ${this.job}입니다.`);
}
};
// Object.create로 프로토타입 설정
const developer = Object.create(personProto, {
// 프로퍼티 디스크립터로 세밀한 제어 가능
name: {
value: '김개발',
writable: true,
enumerable: true
},
age: {
value: 28,
writable: true,
enumerable: true
},
job: {
value: '프론트엔드 개발자',
writable: true,
enumerable: true
}
});
// 개발자만의 메서드 추가
developer.code = function(language) {
console.log(`${this.name}가 ${language}로 코딩합니다.`);
};
developer.greet(); // 안녕하세요, 김개발입니다.
developer.introduce(); // 저는 28살이고, 프론트엔드 개발자입니다.
developer.code('JavaScript'); // 김개발가 JavaScript로 코딩합니다.
// 프로토타입 확인
console.log(Object.getPrototypeOf(developer) === personProto); // true
// null 프로토타입으로 순수 딕셔너리 객체 생성
const pureDict = Object.create(null);
pureDict.key = 'value';
console.log(pureDict.hasOwnProperty); // undefined (상속 없음)
// __proto__ vs prototype 이해하기
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, I'm ${this.name}`);
};
const john = new Person('John');
// prototype: 함수 객체의 프로퍼티
// 이 함수로 생성될 인스턴스의 [[Prototype]]이 될 객체
console.log(Person.prototype);
// { sayHello: [Function], constructor: Person }
// __proto__: 모든 객체의 [[Prototype]] 접근자
// 해당 객체가 실제로 상속받는 프로토타입 객체
console.log(john.__proto__);
// { sayHello: [Function], constructor: Person }
// 핵심: 둘은 같은 객체를 참조!
console.log(john.__proto__ === Person.prototype); // true
// 권장: __proto__ 대신 Object.getPrototypeOf() 사용
console.log(Object.getPrototypeOf(john) === Person.prototype); // true
// 프로토타입 체인 시각화
console.log(john.__proto__); // Person.prototype
console.log(john.__proto__.__proto__); // Object.prototype
console.log(john.__proto__.__proto__.__proto__); // null (체인의 끝)
// 프로퍼티 검색 과정
// 1. john.name -> john 객체에서 발견 -> "John"
// 2. john.sayHello -> john에 없음 -> Person.prototype에서 발견
// 3. john.toString -> Person.prototype에 없음 -> Object.prototype에서 발견
면접관:
"JavaScript에서 프로토타입 체인이 무엇이고, 어떻게 동작하는지 설명해주세요."
지원자:
"프로토타입 체인은 JavaScript의 상속 메커니즘입니다. 모든 객체는 [[Prototype]]이라는 내부 슬롯을 통해 다른 객체를 참조합니다. 객체에서 프로퍼티를 찾을 때, 먼저 자신의 프로퍼티를 확인하고 없으면 [[Prototype]]이 가리키는 객체를 탐색합니다. 이 과정이 null에 도달할 때까지 연쇄적으로 일어나서 '체인'이라고 부릅니다."
면접관:
"그렇다면 배열에서 forEach 같은 메서드를 호출할 수 있는 이유가 뭔가요?"
지원자:
"배열 인스턴스의 [[Prototype]]이 Array.prototype을 가리키기 때문입니다. forEach, map, filter 같은 배열 메서드들은 Array.prototype에 정의되어 있어서, 모든 배열 인스턴스가 프로토타입 체인을 통해 공유합니다. 마찬가지로 Object.prototype의 toString, hasOwnProperty도 모든 객체에서 사용 가능한 이유입니다."
면접관:
"프로토타입 체인의 성능 측면에서 주의할 점이 있을까요?"
지원자:
"체인이 길어지면 프로퍼티 검색 시간이 증가합니다. 특히 존재하지 않는 프로퍼티를 찾을 때는 전체 체인을 탐색해야 하므로 성능에 영향을 줄 수 있습니다. 또한 런타임에 프로토타입을 변경하면 V8 엔진의 hidden class 최적화가 무효화되어 성능이 저하됩니다. 따라서 프로토타입 변경은 초기화 시점에만 하는 것이 좋습니다."
시니어 개발자:
"이 코드에서 생성자 함수와 prototype으로 상속을 구현했는데, ES6 class 문법으로 리팩토링하면 어떨까요?"
주니어 개발자:
"class가 더 읽기 쉽긴 한데, 혹시 내부 동작이 다른 건가요? class를 사용하면 프로토타입이 아닌 다른 방식으로 상속되나요?"
시니어 개발자:
"아니요, class는 문법적 설탕(syntactic sugar)일 뿐이에요. 내부적으로는 동일하게 프로토타입 체인을 사용합니다. class의 메서드는 자동으로 prototype에 추가되고, extends는 Object.setPrototypeOf를 통해 체인을 설정해요. 단, class는 항상 strict mode이고 호이스팅되지 않는다는 차이가 있죠."
테크리드:
"팀 컨벤션상 class 사용을 권장하고 있으니 리팩토링하는 게 좋겠어요. 다만 프로토타입 개념은 확실히 이해하고 있어야 해요. 디버깅할 때 __proto__ 확인하거나, 라이브러리 코드 분석할 때 필수니까요."
주니어 개발자:
"알겠습니다! class로 변환하되, 원리는 프로토타입이라는 걸 기억하겠습니다. 혹시 class에서 private 필드(#)나 static 메서드도 프로토타입과 관련이 있나요?"
시니어 개발자:
"static 메서드는 prototype이 아니라 생성자 함수 자체의 프로퍼티로 추가됩니다. 그래서 인스턴스에서는 호출할 수 없어요. private 필드(#)는 완전히 새로운 기능으로, 프로토타입과 무관하게 각 인스턴스에 캡슐화됩니다."
__proto__는 레거시 기능으로, 직접 수정하면 성능이 심각하게 저하됩니다. JavaScript 엔진의 hidden class 최적화가 무효화되어 프로퍼티 접근 속도가 급격히 느려집니다. 대신 Object.create()로 객체 생성 시 프로토타입을 설정하거나, Object.setPrototypeOf()를 사용하되 초기화 시점에만 제한적으로 사용하세요.
사용자 입력을 통해 Object.prototype이나 Array.prototype에 프로퍼티가 추가되면 모든 객체에 영향을 미치는 심각한 보안 취약점이 됩니다. 사용자 입력으로 객체를 병합할 때 __proto__, constructor, prototype 키를 필터링하고, Object.create(null)로 프로토타입 없는 객체를 사용하는 것을 권장합니다.
프로토타입 체인이 길어질수록 프로퍼티 검색 시간이 증가합니다. 자주 사용하는 프로퍼티는 인스턴스에 직접 정의하고, hasOwnProperty()로 own property만 확인할 수 있습니다. 런타임에 프로토타입을 변경하는 것은 피하고, 상속 계층을 3-4단계 이내로 유지하는 것이 좋습니다.