자바스크립트 프로토타입 체이닝 완벽 가이드

자바스크립트 프로토타입 체이닝 완벽 가이드

D
dongAuthor
11 min read

자바스크립트 프로토타입 체이닝의 핵심 개념부터 실전 활용까지! 메모리 효율적인 상속 구조와 prototype, proto 차이점을 코드 예제로 쉽게 설명합니다.

자바스크립트를 다루는 개발자라면 반드시 이해해야 할 개념 중 하나가 바로 프로토타입 체이닝입니다. 이 메커니즘은 자바스크립트의 객체 지향 프로그래밍의 핵심이며, 코드의 재사용성과 메모리 효율성을 크게 향상시킬 수 있어요.

많은 개발자들이 ES6 클래스 문법에 익숙해지면서 프로토타입의 중요성을 간과하는 경우가 있습니다. 하지만 클래스 문법도 내부적으로는 프로토타입을 활용하고 있기 때문에, 프로토타입 체이닝을 제대로 이해하면 자바스크립트의 동작 원리를 더 깊이 있게 파악할 수 있어요.

이 글에서는 프로토타입 체이닝의 기본 개념부터 실제 활용 방법까지, 실무에서 바로 적용할 수 있는 내용들을 다뤄보겠습니다. 코드 예제와 함께 단계별로 설명드릴 테니, 끝까지 따라오시면 프로토타입 체이닝 마스터가 될 수 있을 거예요!

자바스크립트와 객체 지향 프로그래밍의 만남

자바스크립트는 다중 패러다임 언어입니다. 함수형 프로그래밍도 가능하고, 객체 지향 프로그래밍도 지원하죠. 하지만 자바나 C++과 같은 전통적인 객체 지향 언어와는 다른 방식을 사용해요.

전통적인 객체 지향 언어들은 클래스 기반 상속을 사용합니다. 클래스라는 설계도를 만들고, 그 설계도를 바탕으로 객체를 생성하는 방식이죠. 반면 자바스크립트는 프로토타입 기반 상속을 사용합니다.

// 전통적인 클래스 기반 언어의 개념 (실제 자바스크립트 코드는 아님)
class Animal {
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(`${this.name}가 소리를 냅니다.`);
  }
}

// 자바스크립트의 프로토타입 기반 접근
function Animal(name) {
  this.name = name;
}

Animal.prototype.speak = function() {
  console.log(`${this.name}가 소리를 냅니다.`);
};

자바스크립트에서는 모든 객체가 다른 객체로부터 직접 상속받을 수 있습니다. 이것이 바로 프로토타입 기반 프로그래밍의 핵심이에요.

프로토타입 기반 프로그래밍이란?

프로토타입 기반 프로그래밍은 클래스 없이도 객체를 생성하고 상속을 구현할 수 있는 프로그래밍 패러다임입니다. 자바스크립트에서 모든 객체는 다른 객체를 참조할 수 있는 숨겨진 링크를 가지고 있어요.

프로토타입 기반 프로그래밍의 특징

  1. 동적 상속: 실행 시간에 객체의 프로토타입을 변경할 수 있습니다.
  2. 유연성: 클래스 선언 없이도 객체 간 상속 관계를 만들 수 있어요.
  3. 메모리 효율성: 공통 메서드를 프로토타입에서 공유하여 메모리를 절약할 수 있습니다.
// 프로토타입 기반 상속 예제
const animal = {
  type: '동물',
  speak() {
    console.log(`${this.name}${this.type}입니다.`);
  }
};

const dog = Object.create(animal);
dog.name = '멍멍이';
dog.breed = '진돗개';

dog.speak(); // "멍멍이는 동물입니다."

이 방식의 장점은 매우 유연하다는 것입니다. 필요에 따라 객체의 구조를 동적으로 변경할 수 있고, 복잡한 클래스 계층 구조 없이도 상속을 구현할 수 있어요.

함수의 prototype 프로퍼티 깊이 파헤치기

자바스크립트에서 함수는 특별한 객체입니다. 모든 함수는 prototype이라는 프로퍼티를 가지고 있어요. 이 prototype 프로퍼티는 해당 함수가 생성자로 사용될 때 만들어지는 객체들이 상속받을 프로토타입 객체를 가리킵니다.

function Person(name) {
  this.name = name;
}

console.log(Person.prototype); // {constructor: ƒ}
console.log(typeof Person.prototype); // "object"

함수가 생성되면, 자바스크립트 엔진은 자동으로 해당 함수의 prototype 객체를 만듭니다. 이 프로토타입 객체는 기본적으로 constructor 프로퍼티만을 가지고 있어요.

prototype 프로퍼티 활용하기

prototype 프로퍼티를 활용하면 생성자 함수로 만든 모든 인스턴스가 공통으로 사용할 수 있는 메서드와 프로퍼티를 정의할 수 있습니다.

function Car(brand, model) {
  this.brand = brand;
  this.model = model;
}

// 프로토타입에 메서드 추가
Car.prototype.getInfo = function() {
  return `${this.brand} ${this.model}`;
};

Car.prototype.start = function() {
  console.log(`${this.getInfo()}의 시동을 켭니다.`);
};

const myCar = new Car('현대', '소나타');
const yourCar = new Car('기아', 'K5');

myCar.start(); // "현대 소나타의 시동을 켭니다."
yourCar.start(); // "기아 K5의 시동을 켭니다."

// 두 객체가 같은 메서드를 공유하는지 확인
console.log(myCar.start === yourCar.start); // true

이 방식의 큰 장점은 메모리 효율성입니다. 만약 각 인스턴스마다 메서드를 개별적으로 정의한다면, 인스턴스 개수만큼 메서드가 메모리에 생성되겠죠. 하지만 프로토타입을 사용하면 하나의 메서드를 모든 인스턴스가 공유할 수 있어요.

__proto__와 [[Prototype]] 내부 슬롯 이해하기

자바스크립트의 모든 객체는 [[Prototype]]이라는 내부 슬롯을 가지고 있습니다. 이는 해당 객체의 프로토타입을 가리키는 숨겨진 링크예요. 많은 브라우저에서 이 내부 슬롯에 __proto__라는 프로퍼티로 접근할 수 있게 해줍니다.

__proto__와 prototype의 차이점

이 둘을 혼동하는 경우가 많은데, 명확히 구분해야 해요.

function Animal(name) {
  this.name = name;
}

const dog = new Animal('멍멍이');

// prototype: 함수의 프로퍼티 (생성자 함수에만 존재)
console.log(Animal.prototype); // {constructor: ƒ}

// __proto__: 객체의 프로퍼티 (모든 객체에 존재)
console.log(dog.__proto__); // {constructor: ƒ}

// 이 둘은 같은 객체를 가리킵니다
console.log(Animal.prototype === dog.__proto__); // true

실제 프로토타입 체인 확인하기

function Shape(type) {
  this.type = type;
}

Shape.prototype.getType = function() {
  return this.type;
};

function Circle(radius) {
  Shape.call(this, '원');
  this.radius = radius;
}

// Circle이 Shape을 상속받도록 설정
Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;

Circle.prototype.getArea = function() {
  return Math.PI * this.radius * this.radius;
};

const myCircle = new Circle(5);

// 프로토타입 체인 확인
console.log(myCircle.__proto__ === Circle.prototype); // true
console.log(myCircle.__proto__.__proto__ === Shape.prototype); // true
console.log(myCircle.__proto__.__proto__.__proto__ === Object.prototype); // true

프로토타입 체이닝의 동작 원리

프로토타입 체이닝은 자바스크립트가 객체의 프로퍼티나 메서드를 찾는 메커니즘입니다. 해당 객체에 원하는 프로퍼티가 없으면, 프로토타입 체인을 따라 올라가면서 찾아요.

프로퍼티 검색 과정

  1. 먼저 객체 자신의 프로퍼티에서 검색
  2. 없으면 __proto__가 가리키는 프로토타입 객체에서 검색
  3. 여전히 없으면 그 프로토타입의 프로토타입에서 검색
  4. Object.prototype까지 올라가서도 없으면 undefined 반환
function Animal(name) {
  this.name = name;
}

Animal.prototype.speak = 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('멍멍!');
};

const myDog = new Dog('바둑이', '진돗개');

// 프로퍼티 검색 과정 시뮬레이션
console.log(myDog.name); // 1단계: myDog 객체에서 찾음 - "바둑이"
console.log(myDog.breed); // 1단계: myDog 객체에서 찾음 - "진돗개"
myDog.bark(); // 2단계: Dog.prototype에서 찾음 - "멍멍!"
myDog.speak(); // 3단계: Animal.prototype에서 찾음 - "바둑이가 소리를 냅니다."
console.log(myDog.toString()); // 4단계: Object.prototype에서 찾음

동적 프로토타입 변경

프로토타입은 실행 시간에 동적으로 변경할 수 있어요. 이는 매우 강력한 기능이지만, 신중하게 사용해야 합니다.

function User(name) {
  this.name = name;
}

const user1 = new User('김철수');
const user2 = new User('이영희');

// 나중에 프로토타입에 메서드 추가
User.prototype.greet = function() {
  console.log(`안녕하세요, ${this.name}입니다.`);
};

// 이미 생성된 객체들도 새로운 메서드를 사용할 수 있음
user1.greet(); // "안녕하세요, 김철수입니다."
user2.greet(); // "안녕하세요, 이영희입니다."

프로토타입 체인의 제약사항과 주의점

프로토타입 체이닝은 강력하지만, 몇 가지 제약사항과 주의해야 할 점들이 있어요.

순환 참조 방지

자바스크립트는 프로토타입 체인에서 순환 참조를 허용하지 않습니다.

const obj1 = {};
const obj2 = {};

obj1.__proto__ = obj2;
// obj2.__proto__ = obj1; // TypeError: Cyclic __proto__ value

성능 고려사항

프로토타입 체인이 길어질수록 프로퍼티 검색 시간이 늘어납니다. 특히 자주 접근하는 프로퍼티의 경우 성능에 영향을 줄 수 있어요.

// 성능을 위한 캐싱 예제
function ExpensiveOperation() {}

ExpensiveOperation.prototype.calculate = function() {
  if (this._cachedResult) {
    return this._cachedResult;
  }
  
  // 복잡한 계산 수행
  this._cachedResult = Math.random() * 1000;
  return this._cachedResult;
};

const operation = new ExpensiveOperation();
console.log(operation.calculate()); // 첫 번째 호출: 계산 수행
console.log(operation.calculate()); // 두 번째 호출: 캐시된 결과 반환

프로퍼티 수정과 삭제 제한

프로토타입 체인을 통해 접근한 프로퍼티는 읽기만 가능하고, 수정이나 삭제는 해당 객체에 직접적으로 작용합니다.

const parent = { x: 1 };
const child = Object.create(parent);

console.log(child.x); // 1 (parent에서 상속)

child.x = 2; // child 객체에 새로운 x 프로퍼티 생성
console.log(child.x); // 2
console.log(parent.x); // 1 (변경되지 않음)

delete child.x;
console.log(child.x); // 1 (다시 parent에서 상속)

클래스 기반 vs 프로토타입 기반 상속 비교

ES6에서 도입된 클래스 문법과 전통적인 프로토타입 기반 상속을 비교해보겠습니다.

클래스 문법 (ES6+)

class Animal {
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(`${this.name}가 소리를 냅니다.`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }
  
  bark() {
    console.log('멍멍!');
  }
}

const myDog = new Dog('바둑이', '진돗개');

프로토타입 기반 구현

function Animal(name) {
  this.name = name;
}

Animal.prototype.speak = 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('멍멍!');
};

const myDog = new Dog('바둑이', '진돗개');

각 방식의 장단점

클래스 문법의 장점:

  • 문법이 직관적이고 읽기 쉬움
  • 다른 객체 지향 언어와 유사한 구조
  • super 키워드로 부모 클래스 접근이 간편

프로토타입 기반의 장점:

  • 더 유연한 상속 구조
  • 런타임에 동적으로 상속 관계 변경 가능
  • 메모리 효율성을 더 세밀하게 제어 가능

프로토타입 체이닝의 실용적 이점

메모리 절약과 성능 최적화

프로토타입을 활용하면 메서드를 모든 인스턴스가 공유하므로 메모리를 효율적으로 사용할 수 있어요.

// 비효율적인 방법
function User(name) {
  this.name = name;
  this.greet = function() { // 각 인스턴스마다 함수 생성
    console.log(`안녕하세요, ${this.name}입니다.`);
  };
}

// 효율적인 방법
function User(name) {
  this.name = name;
}

User.prototype.greet = function() { // 모든 인스턴스가 공유
  console.log(`안녕하세요, ${this.name}입니다.`);
};

// 메모리 사용량 비교
const users1 = Array(1000).fill().map((_, i) => new User(`사용자${i}`));
// 첫 번째 방법: 1000개의 greet 함수가 메모리에 생성
// 두 번째 방법: 1개의 greet 함수를 모든 인스턴스가 공유

코드 재사용성 향상

프로토타입 체이닝을 활용하면 공통 기능을 쉽게 재사용할 수 있습니다.

// 공통 기능을 가진 기본 클래스
function BaseEntity() {}

BaseEntity.prototype.save = function() {
  console.log(`${this.constructor.name}을(를) 저장합니다.`);
};

BaseEntity.prototype.delete = function() {
  console.log(`${this.constructor.name}을(를) 삭제합니다.`);
};

// 특화된 클래스들
function User(name) {
  this.name = name;
}
User.prototype = Object.create(BaseEntity.prototype);
User.prototype.constructor = User;

function Product(name, price) {
  this.name = name;
  this.price = price;
}
Product.prototype = Object.create(BaseEntity.prototype);
Product.prototype.constructor = Product;

const user = new User('김철수');
const product = new Product('노트북', 1000000);

user.save(); // "User을(를) 저장합니다."
product.delete(); // "Product을(를) 삭제합니다."

프로토타입 관리 메서드들

Object.getPrototypeOf와 Object.setPrototypeOf

이 메서드들을 사용하여 객체의 프로토타입을 안전하게 조회하고 변경할 수 있어요.

const animal = {
  type: '동물',
  speak() {
    console.log(`${this.name}${this.type}입니다.`);
  }
};

const dog = { name: '멍멍이' };

// 프로토타입 설정
Object.setPrototypeOf(dog, animal);

// 프로토타입 조회
console.log(Object.getPrototypeOf(dog) === animal); // true

dog.speak(); // "멍멍이는 동물입니다."

Object.create 활용하기

Object.create는 지정된 프로토타입을 가진 새 객체를 생성하는 가장 깔끔한 방법입니다.

const vehiclePrototype = {
  start() {
    console.log(`${this.brand} ${this.model}의 시동을 켭니다.`);
  },
  
  stop() {
    console.log(`${this.brand} ${this.model}의 시동을 끕니다.`);
  }
};

// 프로토타입을 지정하여 객체 생성
const car = Object.create(vehiclePrototype, {
  brand: { value: '현대', writable: true },
  model: { value: '소나타', writable: true },
  year: { value: 2023, writable: true }
});

car.start(); // "현대 소나타의 시동을 켭니다."

프로토타입 체인 안전하게 탐색하기

function walkPrototypeChain(obj) {
  const chain = [];
  let current = obj;
  
  while (current !== null) {
    chain.push(current.constructor?.name || 'Anonymous');
    current = Object.getPrototypeOf(current);
  }
  
  return chain;
}

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(walkPrototypeChain(myDog)); 
// ['Dog', 'Animal', 'Object']

실전 프로토타입 상속 활용 예제

플러그인 시스템 구현

// 기본 플러그인 클래스
function Plugin(name) {
  this.name = name;
  this.enabled = false;
}

Plugin.prototype.enable = function() {
  this.enabled = true;
  console.log(`${this.name} 플러그인이 활성화되었습니다.`);
};

Plugin.prototype.disable = function() {
  this.enabled = false;
  console.log(`${this.name} 플러그인이 비활성화되었습니다.`);
};

// 특화된 플러그인들
function AuthPlugin() {
  Plugin.call(this, 'Authentication');
}
AuthPlugin.prototype = Object.create(Plugin.prototype);
AuthPlugin.prototype.constructor = AuthPlugin;

AuthPlugin.prototype.authenticate = function(user) {
  if (!this.enabled) {
    throw new Error('인증 플러그인이 비활성화되어 있습니다.');
  }
  console.log(`${user} 사용자를 인증합니다.`);
};

function LoggingPlugin() {
  Plugin.call(this, 'Logging');
}
LoggingPlugin.prototype = Object.create(Plugin.prototype);
LoggingPlugin.prototype.constructor = LoggingPlugin;

LoggingPlugin.prototype.log = function(message) {
  if (!this.enabled) return;
  console.log(`[LOG] ${message}`);
};

// 사용 예제
const authPlugin = new AuthPlugin();
const loggingPlugin = new LoggingPlugin();

authPlugin.enable();
loggingPlugin.enable();

authPlugin.authenticate('김철수');
loggingPlugin.log('사용자가 로그인했습니다.');

믹스인 패턴 구현

// 믹스인 객체들
const Flyable = {
  fly() {
    console.log(`${this.name}이(가) 날아갑니다.`);
  }
};

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

// 믹스인을 적용하는 헬퍼 함수
function mixin(target, ...sources) {
  sources.forEach(source => {
    Object.getOwnPropertyNames(source).forEach(name => {
      if (name !== 'constructor') {
        target.prototype[name] = source[name];
      }
    });
  });
  return target;
}

// 기본 동물 클래스
function Animal(name) {
  this.name = name;
}

// 오리 클래스 (날 수도 있고 수영도 할 수 있음)
function Duck(name) {
  Animal.call(this, name);
}
Duck.prototype = Object.create(Animal.prototype);
Duck.prototype.constructor = Duck;

// 믹스인 적용
mixin(Duck, Flyable, Swimmable);

const donald = new Duck('도널드');
donald.fly(); // "도널드이(가) 날아갑니다."
donald.swim(); // "도널드이(가) 수영합니다."

데코레이터 패턴 구현

// 기본 커피 클래스
function Coffee() {
  this.cost = 1000;
  this.description = '기본 커피';
}

Coffee.prototype.getCost = function() {
  return this.cost;
};

Coffee.prototype.getDescription = function() {
  return this.description;
};

// 데코레이터 기본 클래스
function CoffeeDecorator(coffee) {
  this.coffee = coffee;
}

CoffeeDecorator.prototype.getCost = function() {
  return this.coffee.getCost();
};

CoffeeDecorator.prototype.getDescription = function() {
  return this.coffee.getDescription();
};

// 구체적인 데코레이터들
function MilkDecorator(coffee) {
  CoffeeDecorator.call(this, coffee);
}
MilkDecorator.prototype = Object.create(CoffeeDecorator.prototype);
MilkDecorator.prototype.constructor = MilkDecorator;

MilkDecorator.prototype.getCost = function() {
  return this.coffee.getCost() + 500;
};

MilkDecorator.prototype.getDescription = function() {
  return this.coffee.getDescription() + ', 우유';
};

function SugarDecorator(coffee) {
  CoffeeDecorator.call(this, coffee);
}
SugarDecorator.prototype = Object.create(CoffeeDecorator.prototype);
SugarDecorator.prototype.constructor = SugarDecorator;

SugarDecorator.prototype.getCost = function() {
  return this.coffee.getCost() + 200;
};

SugarDecorator.prototype.getDescription = function() {
  return this.coffee.getDescription() + ', 설탕';
};

// 사용 예제
let myCoffee = new Coffee();
myCoffee = new MilkDecorator(myCoffee);
myCoffee = new SugarDecorator(myCoffee);

console.log(myCoffee.getDescription()); // "기본 커피, 우유, 설탕"
console.log(myCoffee.getCost()); // 1700

프로토타입 체이닝 마스터하기

프로토타입 체이닝은 자바스크립트의 핵심 개념 중 하나입니다. 이를 제대로 이해하면 더 효율적이고 유연한 코드를 작성할 수 있어요.

핵심 포인트를 정리하면 다음과 같습니다:

  1. 프로토타입 기반 상속: 자바스크립트는 클래스 기반이 아닌 프로토타입 기반 상속을 사용합니다.
  2. 메모리 효율성: 프로토타입을 활용하면 메서드를 공유하여 메모리를 절약할 수 있어요.
  3. 동적 특성: 실행 시간에 프로토타입을 변경할 수 있어 매우 유연한 프로그래밍이 가능합니다.
  4. 체인 탐색: 프로퍼티 검색 시 프로토타입 체인을 따라 올라가며 검색하는 메커니즘을 이해해야 해요.
  5. 안전한 사용: Object.getPrototypeOf, Object.setPrototypeOf, Object.create 등의 메서드를 활용하여 안전하게 프로토타입을 관리할 수 있습니다.

프로토타입 체이닝을 마스터하면 ES6 클래스 문법도 더 깊이 있게 이해할 수 있고, 자바스크립트 라이브러리나 프레임워크의 내부 동작 원리도 파악하기 쉬워집니다. 실무에서 마주치는 복잡한 상속 구조나 디자인 패턴도 자신 있게 다룰 수 있을 거예요.

지금부터 여러분의 프로젝트에서 프로토타입 체이닝을 적극 활용해보세요. 더 깔끔하고 효율적인 코드를 작성할 수 있을 것입니다!