JavaScript 프로토타입 완벽 이해하기
개요
프로토타입(Prototype)은 JavaScript의 핵심 메커니즘으로, 객체 간 속성과 메서드를 공유하는 방식입니다. 프로토타입을 이해하면 JavaScript가 어떻게 상속을 구현하는지 알 수 있고, 메모리를 효율적으로 사용하는 코드를 작성할 수 있습니다.
배경
왜 필요한가?
JavaScript는 클래스 기반 언어가 아닌 프로토타입 기반 언어입니다. 전통적인 객체지향 언어에서는 클래스를 통해 객체를 생성하고 상속을 구현하지만, JavaScript는 프로토타입을 통해 이를 해결합니다.
프로토타입이 없다면 다음과 같은 문제가 발생합니다:
// 프로토타입 없이 객체를 만들면
function createUser(name, age) {
return {
name: name,
age: age,
greet: function() {
console.log(`안녕하세요, ${this.name}입니다.`);
}
};
}
const user1 = createUser('김철수', 25);
const user2 = createUser('이영희', 30);
// 문제: 각 객체마다 greet 함수의 복사본이 생성됨
console.log(user1.greet === user2.greet); // false - 메모리 낭비!1000명의 사용자를 만들면 1000개의 동일한 greet 함수가 메모리에 생성됩니다. 프로토타입은 이 문제를 해결합니다.
ES6 이전의 방식
ES6 class 문법이 등장하기 전에는 생성자 함수와 프로토타입을 직접 사용했습니다:
function User(name, age) {
this.name = name;
this.age = age;
}
User.prototype.greet = function() {
console.log(`안녕하세요, ${this.name}입니다.`);
};
const user = new User('김철수', 25);
user.greet(); // "안녕하세요, 김철수입니다."동작 원리
프로토타입 체인의 핵심 메커니즘
JavaScript의 모든 객체는 숨겨진 [[Prototype]] 속성을 가지고 있습니다. 이 속성은 다른 객체를 참조하며, 이를 통해 속성과 메서드를 상속받습니다.
1단계: 객체 생성 시 프로토타입 연결
function Dog(name) {
this.name = name;
}
Dog.prototype.bark = function() {
console.log(`${this.name}: 멍멍!`);
};
const myDog = new Dog('바둑이');
// myDog 객체의 [[Prototype]]이 Dog.prototype을 가리킴2단계: 속성 접근 시 프로토타입 체인 탐색
myDog.bark(); // "바둑이: 멍멍!"
// 탐색 과정:
// 1. myDog 객체에서 bark 메서드 찾기 → 없음
// 2. myDog.[[Prototype]] (Dog.prototype)에서 찾기 → 발견!
// 3. 메서드 실행3단계: 체인을 따라 계속 탐색
myDog.toString(); // "[object Object]"
// 탐색 과정:
// 1. myDog에서 toString 찾기 → 없음
// 2. Dog.prototype에서 찾기 → 없음
// 3. Object.prototype에서 찾기 → 발견!시각적 이해
[myDog 객체]
name: "바둑이"
[[Prototype]] ──→ [Dog.prototype]
bark: function
[[Prototype]] ──→ [Object.prototype]
toString: function
hasOwnProperty: function
[[Prototype]] ──→ null코드로 확인하기
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name}이(가) 먹이를 먹습니다.`);
};
const cat = new Animal('나비');
// 프로토타입 확인
console.log(Object.getPrototypeOf(cat) === Animal.prototype); // true
// 속성이 어디에 있는지 확인
console.log(cat.hasOwnProperty('name')); // true - 객체 자체에 있음
console.log(cat.hasOwnProperty('eat')); // false - 프로토타입에 있음
// 프로토타입 체인 확인
console.log(cat.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true - 체인의 끝주요 특징
특징 1: 동적 프로토타입
프로토타입은 실행 중에도 수정할 수 있습니다:
function Car(model) {
this.model = model;
}
const myCar = new Car('소나타');
// 나중에 프로토타입에 메서드 추가
Car.prototype.drive = function() {
console.log(`${this.model}이(가) 달립니다.`);
};
// 이미 생성된 객체도 새 메서드를 사용 가능
myCar.drive(); // "소나타이(가) 달립니다."원리: 객체는 프로토타입을 참조하고 있기 때문에, 프로토타입이 변경되면 모든 객체에 즉시 반영됩니다.
특징 2: 메모리 효율성
프로토타입에 정의된 메서드는 모든 인스턴스가 공유합니다:
function User(name) {
this.name = name;
}
User.prototype.greet = function() {
console.log(`안녕하세요, ${this.name}입니다.`);
};
const user1 = new User('철수');
const user2 = new User('영희');
const user3 = new User('민수');
// 모든 인스턴스가 같은 greet 함수를 공유
console.log(user1.greet === user2.greet); // true
console.log(user2.greet === user3.greet); // true
// 결과: greet 함수는 메모리에 딱 1개만 존재특징 3: 프로토타입 상속
프로토타입을 사용하여 상속 구조를 만들 수 있습니다:
// 부모 생성자
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name}이(가) 먹이를 먹습니다.`);
};
// 자식 생성자
function Dog(name, breed) {
Animal.call(this, name); // 부모 생성자 호출
this.breed = breed;
}
// 프로토타입 상속 설정
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
// 자식 메서드 추가
Dog.prototype.bark = function() {
console.log(`${this.name}: 멍멍!`);
};
const myDog = new Dog('바둑이', '진돗개');
myDog.eat(); // "바둑이이(가) 먹이를 먹습니다." - 상속받은 메서드
myDog.bark(); // "바둑이: 멍멍!" - 자체 메서드실제 사용 사례
사례 1: 내장 객체 확장 (주의 필요)
JavaScript의 내장 객체도 프로토타입을 사용합니다:
// Array.prototype에 새 메서드 추가 (실무에서는 권장하지 않음)
Array.prototype.first = function() {
return this[0];
};
const numbers = [1, 2, 3, 4, 5];
console.log(numbers.first()); // 1
// 모든 배열에서 사용 가능
const fruits = ['사과', '바나나', '오렌지'];
console.log(fruits.first()); // "사과"주의: 내장 객체의 프로토타입을 수정하면 예기치 않은 충돌이 발생할 수 있으므로 실무에서는 피해야 합니다.
사례 2: 플러그인 시스템
프로토타입을 사용하여 확장 가능한 시스템을 만들 수 있습니다:
function Calculator() {
this.result = 0;
}
Calculator.prototype.add = function(x) {
this.result += x;
return this; // 체이닝을 위해
};
Calculator.prototype.multiply = function(x) {
this.result *= x;
return this;
};
// 플러그인으로 새 기능 추가
Calculator.prototype.square = function() {
this.result = this.result * this.result;
return this;
};
const calc = new Calculator();
calc.add(5).multiply(2).square();
console.log(calc.result); // 100 (5 + 0 = 5, 5 * 2 = 10, 10^2 = 100)사례 3: 모던 JavaScript에서의 활용
ES6 클래스 문법도 내부적으로는 프로토타입을 사용합니다:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`안녕하세요, ${this.name}입니다.`);
}
}
// 내부적으로는 프로토타입 기반
const person = new Person('김철수', 25);
console.log(Person.prototype.greet === person.__proto__.greet); // true
// 프로토타입에 메서드 추가 가능
Person.prototype.sayAge = function() {
console.log(`저는 ${this.age}살입니다.`);
};
person.sayAge(); // "저는 25살입니다."장점과 한계
장점
- ✅ 메모리 효율성: 메서드를 모든 인스턴스가 공유하여 메모리 사용량 감소
- ✅ 동적 확장: 런타임에 프로토타입을 수정하여 모든 인스턴스에 새 기능 추가 가능
- ✅ 상속 구현: 프로토타입 체인을 통한 자연스러운 상속 메커니즘
- ✅ 성능: 메서드 호출 시 프로토타입 체인 탐색은 매우 빠름
한계
- ⚠️ 복잡성: 프로토타입 체인 개념이 초보자에게 어려울 수 있음
- ⚠️ 디버깅 어려움: 속성이 어디에서 오는지 추적하기 어려울 수 있음
- ⚠️ 프로퍼티 섀도잉: 프로토타입에 같은 이름의 속성이 있으면 가려질 수 있음
function User(name) {
this.name = name;
}
User.prototype.age = 0;
const user = new User('철수');
console.log(user.age); // 0 - 프로토타입에서
user.age = 25; // 객체 자체에 age 속성 추가
console.log(user.age); // 25 - 자체 속성이 프로토타입을 가림
console.log(User.prototype.age); // 0 - 프로토타입은 여전히 0- ⚠️ 전역 오염 위험: 내장 객체 프로토타입 수정 시 충돌 가능
트레이드오프
언제 사용해야 하는가:
- 많은 수의 인스턴스를 생성할 때
- 모든 인스턴스가 공통 메서드를 공유해야 할 때
- 상속 구조가 필요할 때
언제 피해야 하는가:
- 내장 객체(Array, Object 등)의 프로토타입을 수정하는 경우
- 프로토타입 체인이 너무 깊어지는 경우 (성능 저하)
- 간단한 일회성 객체를 만들 때 (객체 리터럴로 충분)
관련 개념
유사 개념
클래스 기반 상속 (Java, C++)
- JavaScript의 프로토타입 상속과 비슷하지만, 클래스는 컴파일 시점에 고정됨
- 프로토타입은 런타임에 동적으로 변경 가능
ES6 Class
- 프로토타입의 문법적 설탕(Syntactic Sugar)
- 내부적으로는 여전히 프로토타입 사용
- 더 익숙한 클래스 문법 제공
// ES6 Class는 내부적으로 프로토타입 사용
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name}이(가) 먹이를 먹습니다.`);
}
}
// 위 코드는 아래와 동일하게 동작
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name}이(가) 먹이를 먹습니다.`);
};대안
Object.create()
- 프로토타입을 명시적으로 지정하여 객체 생성
- 생성자 함수 없이도 프로토타입 상속 가능
const animalMethods = {
eat() {
console.log(`${this.name}이(가) 먹이를 먹습니다.`);
}
};
const cat = Object.create(animalMethods);
cat.name = '나비';
cat.eat(); // "나비이(가) 먹이를 먹습니다."Composition (조합)
- 상속 대신 객체 조합을 사용하는 패턴
- “상속보다 조합을 선호하라” 원칙
// 상속 대신 조합 사용
function createAnimal(name) {
return {
name,
eat() {
console.log(`${this.name}이(가) 먹이를 먹습니다.`);
}
};
}
function createDog(name, breed) {
return {
...createAnimal(name),
breed,
bark() {
console.log(`${this.name}: 멍멍!`);
}
};
}
const myDog = createDog('바둑이', '진돗개');
myDog.eat(); // "바둑이이(가) 먹이를 먹습니다."
myDog.bark(); // "바둑이: 멍멍!"장단점 비교:
- 프로토타입: 메모리 효율적, 동적 확장 가능, 복잡함
- Object.create(): 명시적, 유연함, 생성자 불필요
- 조합: 유연함, 이해하기 쉬움, 메모리 사용량 많을 수 있음
실용적인 팁
팁 1: hasOwnProperty로 속성 구분하기
function User(name) {
this.name = name;
}
User.prototype.greet = function() {
console.log(`안녕하세요, ${this.name}입니다.`);
};
const user = new User('철수');
// 객체 자체의 속성인지 확인
console.log(user.hasOwnProperty('name')); // true
console.log(user.hasOwnProperty('greet')); // false
// for...in 루프에서 주의
for (let key in user) {
if (user.hasOwnProperty(key)) {
console.log(key, user[key]); // "name 철수"만 출력
}
}팁 2: Object.getPrototypeOf() 사용하기
// ❌ 권장하지 않음: __proto__ 사용
const proto = user.__proto__;
// ✅ 권장: Object.getPrototypeOf() 사용
const proto = Object.getPrototypeOf(user);
console.log(proto === User.prototype); // true팁 3: instanceof로 타입 확인
function Animal(name) {
this.name = name;
}
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
const myDog = new Dog('바둑이', '진돗개');
console.log(myDog instanceof Dog); // true
console.log(myDog instanceof Animal); // true
console.log(myDog instanceof Object); // true더 알아보기
심화 학습:
실습:
관련 자료:
요약
프로토타입은 JavaScript의 핵심 메커니즘으로:
- 모든 객체는
[[Prototype]]속성을 통해 다른 객체와 연결됨 - 프로토타입 체인을 따라 속성과 메서드를 탐색
- 메모리 효율적으로 메서드를 공유
- ES6 class도 내부적으로는 프로토타입 사용
프로토타입을 이해하면 JavaScript의 동작 원리를 깊이 이해할 수 있고, 더 효율적인 코드를 작성할 수 있습니다.