Skip to main content

🧬 프로토타입이란?

📖 정의

**프로토타입(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 설정'
};

Q5. 프로토타입으로 다중 상속이 가능한가요?

A: 직접적인 다중 상속은 안 되지만, 믹스인으로 구현 가능합니다:

// JavaScript는 단일 프로토타입 체인

// ❌ 다중 상속 불가
class Animal {}
class Flyable {}
class Swimmable {}

// class Duck extends Animal, Flyable, Swimmable {} // 에러!

// ✅ 해결 1: 믹스인 패턴
const Flyable = {
fly() {
console.log(`${this.name}이(가) 날아요`);
}
};

const Swimmable = {
swim() {
console.log(`${this.name}이(가) 수영해요`);
}
};

class Animal {
constructor(name) {
this.name = name;
}

eat() {
console.log(`${this.name}이(가) 먹어요`);
}
}

class Duck extends Animal {
constructor(name) {
super(name);
}
}

// 믹스인 적용
Object.assign(Duck.prototype, Flyable, Swimmable);

const duck = new Duck('도널드');
duck.eat(); // Animal에서
duck.fly(); // Flyable에서
duck.swim(); // Swimmable에서

// ✅ 해결 2: 믹스인 헬퍼 함수
function applyMixins(derivedCtor, ...baseCtors) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
if (name !== 'constructor') {
Object.defineProperty(
derivedCtor.prototype,
name,
Object.getOwnPropertyDescriptor(baseCtor.prototype, name)
);
}
});
});
}

class FlyableMixin {
fly() {
console.log('날아요');
}
}

class SwimmableMixin {
swim() {
console.log('수영해요');
}
}

class Penguin extends Animal {
constructor(name) {
super(name);
}
}

applyMixins(Penguin, SwimmableMixin); // 수영만 가능

const penguin = new Penguin('펭수');
penguin.swim(); // 수영해요
// penguin.fly(); // 에러 (날지 못함)

// ✅ 해결 3: 고급 믹스인 (충돌 해결)
function createMixin(...mixins) {
return function(Base) {
return mixins.reduce((acc, mixin) => {
return mixin(acc);
}, Base);
};
}

const Flyable2 = (Base) => class extends Base {
fly() {
console.log(`${this.name} 날기`);
}
};

const Swimmable2 = (Base) => class extends Base {
swim() {
console.log(`${this.name} 수영`);
}
};

const Walkable = (Base) => class extends Base {
walk() {
console.log(`${this.name} 걷기`);
}
};

// 조합
class Duck2 extends createMixin(Flyable2, Swimmable2, Walkable)(Animal) {
constructor(name) {
super(name);
}
}

const duck2 = new Duck2('도널드');
duck2.fly(); // 도널드 날기
duck2.swim(); // 도널드 수영
duck2.walk(); // 도널드 걷기
duck2.eat(); // 도널드이(가) 먹어요

// Proxy로 동적 믹스인
function createDynamicMixin(...mixins) {
const combined = {};

mixins.forEach(mixin => {
Object.assign(combined, mixin);
});

return new Proxy({}, {
get(target, prop) {
if (prop in combined) {
return combined[prop];
}
return target[prop];
},

set(target, prop, value) {
target[prop] = value;
return true;
}
});
}

const abilities = createDynamicMixin(Flyable, Swimmable);
abilities.name = '새';
abilities.fly(); // 새이(가) 날아요
abilities.swim(); // 새이(가) 수영해요

🎓 다음 단계

프로토타입을 이해했다면, 다음을 학습해보세요:

  1. 클로저란? - 프로토타입과 함께 사용되는 클로저
  2. 얕은 복사 vs 깊은 복사 - 객체 복사 이해하기
  3. 이벤트 루프 - JavaScript 실행 모델 이해하기

🎬 마무리

프로토타입은 JavaScript의 핵심 메커니즘입니다:

  • 개념: 객체가 다른 객체를 참조하여 속성 상속
  • 체인: 프로토타입을 따라 속성 검색
  • 상속: 코드 재사용과 메모리 효율
  • Class: 프로토타입의 문법적 설탕

프로토타입을 이해하면 JavaScript의 객체지향을 완전히 마스터할 수 있습니다!