🧬 프로토타입이란?
📖 정의
**프로토타입(Prototype)**은 JavaScript 객체가 다른 객체로부터 속성과 메서드를 상속받을 수 있게 해주는 메커니즘입니다. 모든 JavaScript 객체는 다른 객체를 참조하는 내부 링크를 가지고 있으며, 이를 통해 속성과 메서드를 공유합니다.
🎯 비유로 이해하기
유전자 비유
프로토타입을 유전자에 비유하면:
부모 (Prototype)
├─ 눈 색깔
├─ 키
└─ 혈액형
자식 (Object)
├─ 부모의 특징 상속
├─ 자신만의 특징 추가
└─ 부모 특징 덮어쓰기 가능
과정:
1. 자식은 부모의 유전자를 물려받음
2. 자식에게 없는 특징은 부모에게 물어봄
3. 부모에게도 없으면 할아버지에게 물어봄
4. 최상위 조상까지 거슬러 올라감
JavaScript도 동일:
const child = Object.create(parent);
child.name // 자식에게 없으면 parent 확인
도서관 시스템 비유
중앙 도서관 (Object.prototype)
├─ 모든 책의 기본 규칙
├─ 대출 시스템
└─ 반납 시스템
지역 도서관 (Array.prototype)
├─ 중앙 도서관 규칙 상속
├─ 배열 전용 기능 추가
└─ push, pop, map 등
나의 책장 (배열 인스턴스)
├─ 지역 도서관 기능 사용
├─ 나만의 책 보관
└─ 필요한 기능은 상위에서 빌려옴
예시:
const myBooks = ['책1', '책2'];
myBooks.push('책3'); // Array.prototype에서
myBooks.toString(); // Object.prototype에서
myBooks.myMethod(); // 본인 것
프로토타입 체인:
myBooks → Array.prototype → Object.prototype → null
회사 조직도 비유
CEO (Object.prototype)
├─ 기본 규칙: toString, valueOf 등
└─ 모든 직원이 사용 가능
부서장 (특정 Prototype)
├─ CEO 규칙 + 부서별 규칙
├─ Array: 배열 메서드
├─ Function: 함수 메서드
└─ Date: 날짜 메서드
직원 (인스턴스)
├─ 부서 규칙 사용
├─ 개인 업무 수행
└─ 필요시 상급자에게 질의
예시:
직원: "이 일 어떻게 하죠?"
└─> 부서장: "우리 매뉴얼에 있어"
└─> CEO: "그럼 회사 규정 확인해봐"
└─> null: "최상위까지 없으면 undefined"
코드로:
employee.task()
→ 직원에게 없으면
→ 부서장 확인
→ CEO 확인
→ 없으면 undefined
⚙️ 작동 원리
1. 프로토타입 기본 개념
// 모든 객체는 프로토타입을 가짐
// 객체 생성
const person = {
name: '철수',
age: 25
};
// person의 프로토타입 확인
console.log(Object.getPrototypeOf(person));
// Object.prototype
// person은 Object.prototype의 메서드 사용 가능
console.log(person.toString()); // Object.prototype에서
console.log(person.hasOwnProperty('name')); // true
// 프로토타입 체인
console.log(person.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null (최상위)
// 체인:
// person → Object.prototype → null
2. 생성자 함수와 프로토타입
// 생성자 함수
function Person(name, age) {
// 인스턴스 속성
this.name = name;
this.age = age;
}
// 프로토타입에 메서드 추가
Person.prototype.greet = function() {
console.log(`안녕, 나는 ${this.name}이야!`);
};
Person.prototype.getAge = function() {
return this.age;
};
// 인스턴스 생성
const person1 = new Person('철수', 25);
const person2 = new Person('영희', 23);
// 메서드 호출
person1.greet(); // 안녕, 나는 철수야!
person2.greet(); // 안녕, 나는 영희야!
// 메서드는 프로토타입에 한 번만 존재
console.log(person1.greet === person2.greet); // true (같은 함수)
// ❌ 메서드를 생성자에 넣으면?
function BadPerson(name) {
this.name = name;
this.greet = function() { // 매번 새로 생성!
console.log(`안녕, ${this.name}`);
};
}
const bad1 = new BadPerson('철수');
const bad2 = new BadPerson('영희');
console.log(bad1.greet === bad2.greet); // false (다른 함수)
// 메모리 낭비! 인스턴스마다 함수 복사
// ✅ 프로토타입 사용 (메모리 효율)
// 모든 인스턴스가 하나의 메서드 공유
3. 프로토타입 체인
// 프로토타입 체인: 상속 구조
// 1단계: Animal (부모)
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name}이(가) 먹고 있어요`);
};
Animal.prototype.sleep = function() {
console.log(`${this.name}이(가) 자고 있어요`);
};
// 2단계: Dog (자식)
function Dog(name, breed) {
Animal.call(this, name); // 부모 생성자 호출
this.breed = breed;
}
// Dog의 프로토타입을 Animal의 인스턴스로 설정
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // constructor 복원
// Dog만의 메서드 추가
Dog.prototype.bark = function() {
console.log(`${this.name}: 멍멍!`);
};
// 인스턴스 생성
const myDog = new Dog('바둑이', '진돗개');
// 프로토타입 체인 탐색
myDog.bark(); // Dog.prototype에서
myDog.eat(); // Animal.prototype에서
myDog.toString(); // Object.prototype에서
// 체인 구조:
// myDog → Dog.prototype → Animal.prototype → Object.prototype → null
console.log(myDog instanceof Dog); // true
console.log(myDog instanceof Animal); // true
console.log(myDog instanceof Object); // true
// 프로토타입 확인
console.log(Object.getPrototypeOf(myDog) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
4. Object.create()
// Object.create(): 프로토타입을 지정하여 객체 생성
// 프로토타입 객체
const personPrototype = {
greet() {
console.log(`안녕, 나는 ${this.name}이야`);
},
introduce() {
console.log(`나이는 ${this.age}살이야`);
}
};
// personPrototype을 프로토타입으로 하는 객체 생성
const person1 = Object.create(personPrototype);
person1.name = '철수';
person1.age = 25;
const person2 = Object.create(personPrototype);
person2.name = '영희';
person2.age = 23;
person1.greet(); // 안녕, 나는 철수야
person2.introduce(); // 나이는 23살이야
// 프로토타입 체인 확인
console.log(Object.getPrototypeOf(person1) === personPrototype); // true
// null 프로토타입 객체 (순수 객체)
const pureObject = Object.create(null);
console.log(pureObject.toString); // undefined
console.log(Object.getPrototypeOf(pureObject)); // null
// 활용: 진짜 딕셔너리
const dictionary = Object.create(null);
dictionary.toString = '문자열'; // 충돌 없음!
dictionary.hasOwnProperty = '속성'; // 안전!
5. 프로토타입 vs Class
// ES6 Class는 프로토타입의 문법적 설탕(Syntactic Sugar)
// 프로토타입 방식
function PersonProto(name, age) {
this.name = name;
this.age = age;
}
PersonProto.prototype.greet = function() {
console.log(`안녕, ${this.name}야`);
};
// Class 방식 (내부적으로는 프로토타입)
class PersonClass {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`안녕, ${this.name}야`);
}
}
// 두 방식은 동일하게 작동
const proto = new PersonProto('철수', 25);
const classInstance = new PersonClass('영희', 23);
proto.greet();
classInstance.greet();
// Class도 프로토타입 사용
console.log(typeof PersonClass); // function
console.log(PersonClass.prototype.greet); // function
// 차이점:
// 1. Class는 호이스팅 안 됨
// 2. Class는 strict mode
// 3. Class 문법이 더 깔끔
// 4. 상속이 더 쉬움
// 상속 비교
// ❌ 프로토타입 방식 (복잡)
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log('먹는 중');
};
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('멍멍');
};
// ✅ Class 방식 (간단)
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log('먹는 중');
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
bark() {
console.log('멍멍');
}
}
const dog = new Dog('바둑이', '진돗개');
dog.eat(); // Animal에서 상속
dog.bark(); // Dog의 메서드
💡 실제 예시
프로토타입 상속 구현
// 실전 프로토타입 상속 패턴
// 1. 기본 클래스
function Shape(x, y) {
this.x = x;
this.y = y;
}
Shape.prototype.move = function(dx, dy) {
this.x += dx;
this.y += dy;
console.log(`위치: (${this.x}, ${this.y})`);
};
Shape.prototype.getPosition = function() {
return { x: this.x, y: this.y };
};
// 2. Circle 상속
function Circle(x, y, radius) {
Shape.call(this, x, y); // 부모 생성자 호출
this.radius = radius;
}
// 프로토타입 체인 설정
Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;
// Circle 메서드 추가
Circle.prototype.getArea = function() {
return Math.PI * this.radius * this.radius;
};
Circle.prototype.getCircumference = function() {
return 2 * Math.PI * this.radius;
};
// 3. Rectangle 상속
function Rectangle(x, y, width, height) {
Shape.call(this, x, y);
this.width = width;
this.height = height;
}
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
Rectangle.prototype.getArea = function() {
return this.width * this.height;
};
Rectangle.prototype.getPerimeter = function() {
return 2 * (this.width + this.height);
};
// 사용
const circle = new Circle(0, 0, 10);
console.log(circle.getArea()); // 314.159...
circle.move(5, 5); // 위치: (5, 5)
const rect = new Rectangle(0, 0, 10, 20);
console.log(rect.getArea()); // 200
rect.move(3, 3); // 위치: (3, 3)
// instanceof 확인
console.log(circle instanceof Circle); // true
console.log(circle instanceof Shape); // true
console.log(circle instanceof Rectangle); // false
// 프로토타입 체인
console.log(Object.getPrototypeOf(circle) === Circle.prototype); // true
console.log(Object.getPrototypeOf(Circle.prototype) === Shape.prototype); // true
프로토타입 메서드 확장
// 내장 객체의 프로토타입 확장 (주의해서 사용!)
// ⚠️ 주의: 내장 프로토타입 수정은 권장하지 않음
// 하지만 학습용으로 이해하기
// Array 프로토타입 확장
Array.prototype.first = function() {
return this[0];
};
Array.prototype.last = function() {
return this[this.length - 1];
};
Array.prototype.sum = function() {
return this.reduce((acc, val) => acc + val, 0);
};
Array.prototype.average = function() {
return this.sum() / this.length;
};
// 사용
const numbers = [1, 2, 3, 4, 5];
console.log(numbers.first()); // 1
console.log(numbers.last()); // 5
console.log(numbers.sum()); // 15
console.log(numbers.average()); // 3
// String 프로토타입 확장
String.prototype.capitalize = function() {
return this.charAt(0).toUpperCase() + this.slice(1);
};
String.prototype.reverse = function() {
return this.split('').reverse().join('');
};
// 사용
console.log('hello'.capitalize()); // Hello
console.log('hello'.reverse()); // olleh
// ✅ 더 안전한 방법: 별도 유틸리티
const ArrayUtils = {
first: (arr) => arr[0],
last: (arr) => arr[arr.length - 1],
sum: (arr) => arr.reduce((a, b) => a + b, 0),
average: (arr) => ArrayUtils.sum(arr) / arr.length
};
console.log(ArrayUtils.first(numbers)); // 1
console.log(ArrayUtils.average(numbers)); // 3
// 왜 내장 프로토타입 수정을 피해야 하나?
// 1. 다른 라이브러리와 충돌 가능
// 2. 미래의 JavaScript 표준과 충돌 가능
// 3. 예기치 않은 동작 발생 가능
프로토타입 패턴 (디자인 패턴)
// 프로토타입 패턴: 객체를 복제하여 새 객체 생성
// 1. 얕은 복사
const carPrototype = {
wheels: 4,
engine: 'V6',
clone() {
return Object.create(Object.getPrototypeOf(this),
Object.getOwnPropertyDescriptors(this)
);
},
start() {
console.log('엔진 시동');
}
};
const car1 = Object.create(carPrototype);
car1.color = 'red';
car1.brand = 'Tesla';
const car2 = car1.clone();
car2.color = 'blue';
console.log(car1.color); // red
console.log(car2.color); // blue
console.log(car1.wheels); // 4 (프로토타입에서)
console.log(car2.wheels); // 4 (프로토타입에서)
// 2. 복잡한 객체 복제
class Enemy {
constructor(name, health, attack) {
this.name = name;
this.health = health;
this.attack = attack;
this.skills = [];
}
addSkill(skill) {
this.skills.push(skill);
}
clone() {
const cloned = Object.create(Object.getPrototypeOf(this));
cloned.name = this.name;
cloned.health = this.health;
cloned.attack = this.attack;
cloned.skills = [...this.skills]; // 배열 복 사
return cloned;
}
attackPlayer() {
console.log(`${this.name}이(가) ${this.attack} 데미지로 공격!`);
}
}
// 기본 적 생성
const basicEnemy = new Enemy('Goblin', 100, 10);
basicEnemy.addSkill('돌진');
// 복제하여 여러 적 생성
const enemy1 = basicEnemy.clone();
enemy1.name = 'Goblin 1';
const enemy2 = basicEnemy.clone();
enemy2.name = 'Goblin 2';
enemy2.attack = 15;
enemy1.attackPlayer(); // Goblin 1이(가) 10 데미지로 공격!
enemy2.attackPlayer(); // Goblin 2이(가) 15 데미지로 공격!
// 3. 프로토타입 레지스트리
class PrototypeRegistry {
constructor() {
this.prototypes = {};
}
register(key, prototype) {
this.prototypes[key] = prototype;
}
create(key) {
const prototype = this.prototypes[key];
return prototype ? prototype.clone() : null;
}
}
// 사용
const registry = new PrototypeRegistry();
// 프로토타입 등록
registry.register('weakEnemy', new Enemy('Weak', 50, 5));
registry.register('strongEnemy', new Enemy('Strong', 200, 30));
registry.register('bossEnemy', new Enemy('Boss', 1000, 100));
// 필요할 때 생성
const enemy3 = registry.create('weakEnemy');
const enemy4 = registry.create('strongEnemy');
const boss = registry.create('bossEnemy');
console.log(enemy3.name); // Weak
console.log(boss.health); // 1000
프로토타입 체인 탐색
// 프로토타입 체인을 따라 속성 찾기
const grandParent = {
familyName: '김',
getFamilyName() {
return this.familyName;
}
};
const parent = Object.create(grandParent);
parent.job = '의사';
parent.getJob = function() {
return this.job;
};
const child = Object.create(parent);
child.name = '철수';
child.age = 10;
child.getName = function() {
return this.name;
};
// 프로토타입 체인 탐색
console.log(child.name); // child에서 찾음
console.log(child.job); // parent에서 찾음
console.log(child.familyName); // grandParent에서 찾음
console.log(child.toString()); // Object.prototype에서 찾음
// hasOwnProperty: 자신의 속성인지 확인
console.log(child.hasOwnProperty('name')); // true
console.log(child.hasOwnProperty('job')); // false
console.log(child.hasOwnProperty('familyName')); // false
// in 연산자: 체인 전체에서 확인
console.log('name' in child); // true
console.log('job' in child); // true
console.log('familyName' in child); // true
// 프로토타입 체인 출력 함수
function printPrototypeChain(obj) {
let current = obj;
let level = 0;
while (current !== null) {
console.log(`레벨 ${level}:`, current);
const props = Object.getOwnPropertyNames(current);
console.log(' 속성들:', props);
current = Object.getPrototypeOf(current);
level++;
}
}
console.log('\n=== 프로토타입 체인 ===');
printPrototypeChain(child);
// 출력:
// 레벨 0: { name: '철수', age: 10, getName: [Function] }
// 속성들: [ 'name', 'age', 'getName' ]
// 레벨 1: { job: '의사', getJob: [Function] }
// 속성들: [ 'job', 'getJob' ]
// 레벨 2: { familyName: '김', getFamilyName: [Function] }
// 속성들: [ 'familyName', 'getFamilyName' ]
// 레벨 3: [Object: null prototype] {}
// 속성들: [ ... Object.prototype 메서드들 ... ]
믹스인 패턴
// 믹스인: 여러 소스에서 속성을 합쳐 객체 생성
// 믹스인 함수
function mixin(target, ...sources) {
Object.assign(target, ...sources);
return target;
}
// 또는
function mixinProto(target, ...sources) {
sources.forEach(source => {
Object.getOwnPropertyNames(source).forEach(name => {
if (name !== 'constructor') {
Object.defineProperty(
target,
name,
Object.getOwnPropertyDescriptor(source, name)
);
}
});
});
return target;
}
// 재사용 가능한 기능들
const canEat = {
eat(food) {
console.log(`${this.name}이(가) ${food}을(를) 먹어요`);
}
};
const canWalk = {
walk() {
console.log(`${this.name}이(가) 걸어요`);
}
};
const canSwim = {
swim() {
console.log(`${this.name}이(가) 수영해요`);
}
};
const canFly = {
fly() {
console.log(`${this.name}이(가) 날아요`);
}
};
// 사람 클래스
class Person {
constructor(name) {
this.name = name;
}
}
// 믹스인 적용
mixin(Person.prototype, canEat, canWalk);
const person = new Person('철수');
person.eat('밥'); // 철수이(가) 밥을(를) 먹어요
person.walk(); // 철수이(가) 걸어요
// 물고기 클래스
class Fish {
constructor(name) {
this.name = name;
}
}
mixin(Fish.prototype, canEat, canSwim);
const fish = new Fish('금붕어');
fish.eat('모이'); // 금붕어이(가) 모이을(를) 먹어요
fish.swim(); // 금붕어이(가) 수영해요
// 오리 클래스 (다중 능력)
class Duck {
constructor(name) {
this.name = name;
}
}
mixin(Duck.prototype, canEat, canWalk, canSwim, canFly);
const duck = new Duck('도널드');
duck.eat('빵'); // 도널드이(가) 빵을(를) 먹어요
duck.walk(); // 도널드이(가) 걸어요
duck.swim(); // 도널드이(가) 수영해요
duck.fly(); // 도널드이(가) 날아요
// 고급 믹스인: 메서드 체이닝
const Chainable = {
setName(name) {
this.name = name;
return this;
},
setAge(age) {
this.age = age;
return this;
},
setEmail(email) {
this.email = email;
return this;
},
build() {
return { ...this };
}
};
class User {}
mixin(User.prototype, Chainable);
const user = new User()
.setName('철수')
.setAge(25)
.setEmail('kim@example.com')
.build();
console.log(user); // { name: '철수', age: 25, email: 'kim@example.com' }
프로토타입 기반 게임 엔티티
// 게임 엔티티 시스템
// 기본 GameObject
function GameObject(x, y) {
this.x = x;
this.y = y;
this.active = true;
}
GameObject.prototype.update = function(deltaTime) {
// 기본 업데이트 로직
};
GameObject.prototype.render = function(ctx) {
// 기본 렌더링 로직
};
GameObject.prototype.destroy = function() {
this.active = false;
};
// Sprite (이미지가 있는 객체)
function Sprite(x, y, image) {
GameObject.call(this, x, y);
this.image = image;
this.width = 32;
this.height = 32;
}
Sprite.prototype = Object.create(GameObject.prototype);
Sprite.prototype.constructor = Sprite;
Sprite.prototype.render = function(ctx) {
if (this.active) {
ctx.drawImage(this.image, this.x, this.y, this.width, this.height);
}
};
// AnimatedSprite (애니메이션)
function AnimatedSprite(x, y, spriteSheet) {
Sprite.call(this, x, y, spriteSheet);
this.frameIndex = 0;
this.frameCount = 4;
this.frameDelay = 100;
this.lastFrameTime = 0;
}
AnimatedSprite.prototype = Object.create(Sprite.prototype);
AnimatedSprite.prototype.constructor = AnimatedSprite;
AnimatedSprite.prototype.update = function(deltaTime) {
this.lastFrameTime += deltaTime;
if (this.lastFrameTime >= this.frameDelay) {
this.frameIndex = (this.frameIndex + 1) % this.frameCount;
this.lastFrameTime = 0;
}
};
// Character (이동 가능한 캐릭터)
function Character(x, y, image, speed) {
AnimatedSprite.call(this, x, y, image);
this.speed = speed;
this.vx = 0;
this.vy = 0;
this.health = 100;
}
Character.prototype = Object.create(AnimatedSprite.prototype);
Character.prototype.constructor = Character;
Character.prototype.move = function(dx, dy) {
this.vx = dx * this.speed;
this.vy = dy * this.speed;
};
Character.prototype.update = function(deltaTime) {
// 부모 업데이트 호출
AnimatedSprite.prototype.update.call(this, deltaTime);
// 위치 업데이트
this.x += this.vx * deltaTime;
this.y += this.vy * deltaTime;
};
Character.prototype.takeDamage = function(damage) {
this.health -= damage;
if (this.health <= 0) {
this.destroy();
}
};
// Player (플레이어 캐릭터)
function Player(x, y, image) {
Character.call(this, x, y, image, 5);
this.score = 0;
}
Player.prototype = Object.create(Character.prototype);
Player.prototype.constructor = Player;
Player.prototype.addScore = function(points) {
this.score += points;
};
// Enemy (적 캐릭터)
function Enemy(x, y, image, aiType) {
Character.call(this, x, y, image, 3);
this.aiType = aiType;
this.target = null;
}
Enemy.prototype = Object.create(Character.prototype);
Enemy.prototype.constructor = Enemy;
Enemy.prototype.setTarget = function(target) {
this.target = target;
};
Enemy.prototype.update = function(deltaTime) {
Character.prototype.update.call(this, deltaTime);
// AI 로직
if (this.target && this.active) {
const dx = this.target.x - this.x;
const dy = this.target.y - this.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
this.move(dx / distance, dy / distance);
}
}
};
// 사용 예시
const player = new Player(100, 100, 'player.png');
const enemy1 = new Enemy(200, 200, 'enemy.png', 'chase');
const enemy2 = new Enemy(300, 100, 'enemy.png', 'patrol');
enemy1.setTarget(player);
enemy2.setTarget(player);
// 게임 루프
function gameLoop(deltaTime) {
// 업데이트
player.update(deltaTime);
enemy1.update(deltaTime);
enemy2.update(deltaTime);
// 렌더링
// player.render(ctx);
// enemy1.render(ctx);
// enemy2.render(ctx);
}
// 프로토타입 체인 확인
console.log(player instanceof Player); // true
console.log(player instanceof Character); // true
console.log(player instanceof AnimatedSprite); // true
console.log(player instanceof Sprite); // true
console.log(player instanceof GameObject); // true
🤔 자주 묻는 질문
Q1. __proto__와 prototype의 차이는?
A: 둘은 다른 개념입니다:
// prototype: 함수의 속성
// __proto__: 객체의 내부 링크
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log(`안녕, ${this.name}`);
};
const person = new Person('철수');
// prototype: 생성자 함수의 속성
console.log(Person.prototype); // { greet: [Function], constructor: [Function] }
// __proto__: 객체의 프로토타입 링크
console.log(person.__proto__); // Person.prototype과 같음
console.log(person.__proto__ === Person.prototype); // true
// 관계:
// Person (함수)
// ├─ prototype: 객체 { greet, constructor }
// └─ 인스턴스를 만들 때 사용
//
// person (객체)
// └─ __proto__: Person.prototype을 가리킴
// ⚠️ __proto__는 deprecated
// ✅ 대신 사용:
Object.getPrototypeOf(person); // __proto__ 읽기
Object.setPrototypeOf(person, newProto); // __proto__ 설정
// 예시: 프로토타입 체인
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log('먹는 중');
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
const dog = new Dog('바둑이', '진돗개');
// 체인:
console.log(dog.__proto__ === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null
// 정리:
const summary = {
prototype: {
설명: '함수의 속성',
용도: '인스턴스가 상속받을 메서드와 속성',
접근: 'Constructor.prototype',
예시: 'Person.prototype.greet'
},
__proto__: {
설명: '객체의 내부 링크',
용도: '프로토타입 체인 연결',
접근: 'Object.getPrototypeOf(obj)',
예시: 'Object.getPrototypeOf(person)'
}
};
Q2. 프로토타입 vs Class, 어떤 것을 사용해야 하나요?
A: 현대 JavaScript에서는 Class를 권장합니다:
// ✅ Class 사용 (권장)
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name}이(가) 먹어요`);
}
sleep() {
console.log(`${this.name}이(가) 자요`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
bark() {
console.log('멍멍!');
}
}
const dog = new Dog('바둑이', '진돗개');
dog.eat(); // 바둑이이(가) 먹어요
dog.bark(); // 멍멍!
// ❌ 프로토타입 직접 사용 (복잡)
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('멍멍!');
};
// Class의 장점:
const classAdvantages = {
가독성: '더 직관적이고 이해하기 쉬움',
유지보수: '코드 구조가 명확',
상속: 'extends와 super로 간단',
정적메서드: 'static 키워드 지원',
private필드: '#privateField 지원 (최신)',
표준: 'ES6+ 표준, 모던 JavaScript'
};
// Class 고급 기능
class User {
// Private 필드 (ES2022+)
#password;
// Public 필드
name;
email;
// Static 필드
static userCount = 0;
constructor(name, email, password) {
this.name = name;
this.email = email;
this.#password = password;
User.userCount++;
}
// Private 메서드
#hashPassword(password) {
return `hashed_${password}`;
}
// Public 메서드
checkPassword(password) {
return this.#password === this.#hashPassword(password);
}
// Getter
get userInfo() {
return `${this.name} (${this.email})`;
}
// Setter
set password(newPassword) {
this.#password = this.#hashPassword(newPassword);
}
// Static 메서드
static getUserCount() {
return User.userCount;
}
}
const user = new User('철수', 'kim@example.com', '1234');
console.log(user.userInfo); // 철수 (kim@example.com)
// console.log(user.#password); // ❌ 에러! Private
console.log(User.getUserCount()); // 1
// 언제 프로토타입을 직접 사용할까?
const usePrototype = {
레거시코드: '기존 프로토타입 코드 유지보수',
라이브러리: '폴리필이나 유틸리티 라이브러리',
성능: '극한의 성능 최적화 (매우 드물)',
학습: 'JavaScript 내부 동작 이해'
};
// 결론: Class 사용하되, 프로토타입 이해 필수!
Q3. 프로토타입 체인은 성능에 영향을 주나요?
A: 네, 깊은 체인은 성능에 영향을 줄 수 있습니다:
// 프로토타입 체인과 성능
// ❌ 깊은 체인 (느림)
const level5 = {};
const level4 = Object.create(level5);
const level3 = Object.create(level4);
const level2 = Object.create(level3);
const level1 = Object.create(level2);
level5.value = 42;
console.time('deep chain');
for (let i = 0; i < 1000000; i++) {
const v = level1.value; // 5단계 탐색
}
console.timeEnd('deep chain'); // ~50ms
// ✅ 얕은 체인 (빠름)
const shallow = { value: 42 };
console.time('direct access');
for (let i = 0; i < 1000000; i++) {
const v = shallow.value; // 직접 접근
}
console.timeEnd('direct access'); // ~5ms
// 최적화 팁
// 1. 자주 사용하는 속성은 캐싱
class OptimizedClass {
constructor() {
// 프로토타입에서 가져와서 저장
this._cachedMethod = this.expensiveMethod.bind(this);
}
expensiveMethod() {
// 복잡한 로직
}
callMethod() {
this._cachedMethod(); // 캐시된 메서드 사용
}
}
// 2. hasOwnProperty로 체인 탐색 방지
const obj = Object.create({ inherited: 'value' });
obj.own = 'value';
console.time('with chain');
for (let i = 0; i < 1000000; i++) {
'inherited' in obj; // 체인 탐색
}
console.timeEnd('with chain');
console.time('own only');
for (let i = 0; i < 1000000; i++) {
obj.hasOwnProperty('own'); // 자신만 확인
}
console.timeEnd('own only');
// 3. Object.create(null) 사용
// 프로토타입 체인 없음 = 빠른 접근
const fastMap = Object.create(null);
fastMap.key1 = 'value1';
fastMap.key2 = 'value2';
// Object.prototype 메서드 없음
console.log(fastMap.toString); // undefined
// 4. 모노모픽 최적화 (V8)
// 같은 "모양"의 객체는 최적화됨
class Point {
constructor(x, y) {
this.x = x; // 항상 같은 순서
this.y = y;
}
}
// ✅ 좋은 예: 모두 같은 모양
const points = [];
for (let i = 0; i < 10000; i++) {
points.push(new Point(i, i));
}
// ❌ 나쁜 예: 다른 모양들
const mixed = [];
for (let i = 0; i < 10000; i++) {
if (i % 2 === 0) {
mixed.push({ x: i, y: i });
} else {
mixed.push({ a: i, b: i, c: i }); // 다른 모양!
}
}
// 5. 프로토타입 메서드 vs 인스턴스 메서드
// ✅ 프로토타입 메서드 (메모리 효율)
class Proto {
method() {}
}
const protoInstances = [];
for (let i = 0; i < 10000; i++) {
protoInstances.push(new Proto());
}
// method는 메모리에 1개만
// ❌ 인스턴스 메서드 (메모리 비효율)
class Instance {
constructor() {
this.method = function() {}; // 매번 생성!
}
}
const instanceInstances = [];
for (let i = 0; i < 10000; i++) {
instanceInstances.push(new Instance());
}
// method가 메모리에 10000개!
// 성능 측정
if (performance.memory) {
console.log('Proto 메모리:',
performance.memory.usedJSHeapSize / 1024 / 1024, 'MB');
}
Q4. 프로토타입 오염(Prototype Pollution)이란?
A: 프로토타입을 조작하여 보안 문제를 일으키는 공격입니다:
// 프로토타입 오염 (Prototype Pollution)
// ❌ 취약한 코드
function merge(target, source) {
for (let key in source) {
target[key] = source[key];
}
return target;
}
// 공격
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
const user = {};
merge(user, malicious);
// 모든 객체가 영향받음!
const anyObject = {};
console.log(anyObject.isAdmin); // true (위험!)
// ✅ 안전한 merge
function safeMerge(target, source) {
for (let key in source) {
// __proto__, constructor, prototype 제외
if (key === '__proto__' ||
key === 'constructor' ||
key === 'prototype') {
continue;
}
if (source.hasOwnProperty(key)) {
target[key] = source[key];
}
}
return target;
}
// ✅ Object.create(null) 사용
const safeObject = Object.create(null);
safeObject.data = 'safe';
// __proto__가 없음!
// ✅ Object.assign 사용 (안전)
const target = {};
const source = { a: 1, __proto__: { b: 2 } };
Object.assign(target, source);
console.log(target.b); // undefined (안전)
// ✅ Map 사용 (더 안전)
const safeMap = new Map();
safeMap.set('__proto__', 'value');
safeMap.set('constructor', 'value');
// 프로토타입과 분리됨!
// 실전 방어 코드
function deepMerge(target, source) {
const isObject = (obj) =>
obj && typeof obj === 'object' && !Array.isArray(obj);
if (!isObject(target) || !isObject(source)) {
return source;
}
Object.keys(source).forEach(key => {
// 위험한 키 차단
if (['__proto__', 'constructor', 'prototype'].includes(key)) {
return;
}
const targetValue = target[key];
const sourceValue = source[key];
if (isObject(targetValue) && isObject(sourceValue)) {
target[key] = deepMerge(
Object.assign({}, targetValue),
sourceValue
);
} else {
target[key] = sourceValue;
}
});
return target;
}
// 보안 체크리스트
const securityChecklist = {
입력검증: '사용자 입력을 항상 검증',
안전한API: 'Object.assign, spread 사용',
키필터링: '__proto__ 등 위험 키 차단',
타입체크: '객체 타입 확인',
라이브러리: '최신 버전 사용',
CSP: 'Content Security Policy 설정'
};