2025-09-22 21:52

  • 자바스크립트는 프로토타입이라는 독특한 방식으로 객체 상속을 구현하며, 이는 클래스 기반 언어와 근본적인 차이를 만든다.

  • 모든 객체는 자신의 부모 역할을 하는 프로토타입 객체를 가리키는 숨겨진 [[Prototype]] 링크를 가지며, 이 링크들이 연결되어 프로토타입 체인을 형성한다.

  • 프로토타입을 이해하는 것은 자바스크립트의 동작 원리를 깊이 있게 파악하고, 메모리 효율적인 코드를 작성하기 위한 핵심 열쇠이다.


자바스크립트 프로토타입 완벽 정복 핸드북

자바스크립트 개발자라면 ‘프로토타입’이라는 용어를 최소 한 번 이상은 들어봤을 것이다. 면접 단골 질문이기도 하고, 많은 개발 서적에서 중요하게 다루는 개념이다. 하지만 많은 개발자가 프로토타입을 어렴풋이 ‘상속과 관련된 무언가’ 정도로만 이해하고 넘어가는 경우가 많다.

이 핸드북은 자바스크립트의 심장과도 같은 프로토타입의 세계를 깊이 탐험하기 위해 작성되었다. 왜 자바스크립트는 클래스가 아닌 프로토타입을 선택했는지 그 탄생 배경부터, 내부 구조, 실제 사용법, 그리고 현업에서 마주칠 수 있는 심화 내용까지, 프로토타입의 모든 것을 담았다. 이 글을 끝까지 읽고 나면, 당신은 자바스크립트를 훨씬 더 깊이 있게 이해하고 자신감 있게 다룰 수 있게 될 것이다.

1. 프로토타입의 탄생 배경 모든 것은 효율성에서 시작되다

자바스크립트가 처음 세상에 등장한 1995년, 웹은 지금처럼 복잡하지 않았다. 넷스케이프(Netscape)의 개발자 브렌던 아이크(Brendan Eich)는 웹페이지에 간단한 동적 기능을 추가하기 위한 가벼운 스크립트 언어를 만들어야 했다. 당시 유행하던 Java와 같은 객체 지향 언어의 특징을 일부 가져오되, 복잡한 ‘클래스(Class)’ 시스템은 이 언어의 목표와 맞지 않았다.

문제 상황: 메모리의 비효율성

만약 자바스크립트에 프로토타입이 없다면 어떻게 될까? 여러 명의 사용자(User) 객체를 생성하는 시나리오를 생각해보자.

JavaScript

// 프로토타입이 없는 가상의 시나리오
function User(name, age) {
  this.name = name;
  this.age = age;
  this.introduce = function() {
    console.log(`저는 ${this.name}이고, ${this.age}살입니다.`);
  };
}

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

이 코드에서 user1user2는 각자 nameage라는 고유한 데이터를 가진다. 하지만 introduce라는 메서드(기능)는 어떤가? 두 객체의 introduce 메서드는 완전히 동일한 기능을 수행한다. 그럼에도 불구하고, 위 코드에서는 user1user2가 각각 별개의 introduce 함수를 메모리에 할당받는다. 사용자 객체를 1,000개 생성한다면, 동일한 기능을 하는 함수가 1,000개나 메모리를 차지하게 되는 셈이다. 이는 엄청난 메모리 낭비다.

해결책: 프로토타입이라는 공유 창고

브렌던 아이크는 이러한 비효율을 해결하기 위해 ‘프로토타입’이라는 개념을 도입했다. 그의 아이디어는 간단했다.

“객체들이 공통으로 사용할 속성이나 메서드는 어딘가 한 곳에 모아두고, 개별 객체들은 그곳을 참조하게 만들면 어떨까?”

이 ‘어딘가 한 곳’이 바로 **프로토타입 객체(Prototype Object)**다. 마치 도서관에 있는 한 권의 ‘요리 백과사전’과 같다. 수많은 요리사(객체)들이 각자 자신만의 요리법(고유 데이터)을 가지고 있지만, 기본적인 조리법(공통 메서드)이 필요할 때는 모두가 도서관의 ‘요리 백과사전’을 참조하는 것이다.

JavaScript

// 프로토타입을 활용한 효율적인 코드
function User(name, age) {
  this.name = name;
  this.age = age;
}

// User 함수의 prototype이라는 '공유 창고'에 introduce 메서드를 추가한다.
User.prototype.introduce = function() {
  console.log(`저는 ${this.name}이고, ${this.age}살입니다.`);
};

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

user1.introduce(); // "저는 김철수이고, 30살입니다."
user2.introduce(); // "저는 이영희이고, 25살입니다."

// user1과 user2는 introduce 함수를 직접 가지고 있지 않다!
console.log(user1.hasOwnProperty('introduce')); // false
console.log(user2.hasOwnProperty('introduce')); // false

이제 user1user2introduce 함수를 직접 소유하지 않는다. 대신 두 객체 모두 User.prototype이라는 공유 공간에 있는 단 하나의 introduce 함수를 함께 참조하여 사용한다. 이로써 사용자 객체를 1,000개 만들어도 introduce 함수는 메모리에 단 하나만 존재하게 되어 엄청난 메모리 효율을 얻을 수 있다. 이것이 바로 자바스크립트가 프로토타입을 채택한 핵심적인 이유다.

2. 프로토타입의 핵심 구조 연결고리의 비밀

프로토타입이 어떻게 동작하는지 정확히 이해하려면 두 가지 중요한 개념을 구분해야 한다. 바로 prototype 프로퍼티와 [[Prototype]] 내부 슬롯이다. 이 둘은 이름이 비슷해 혼란을 주지만, 역할은 명확히 다르다.

prototype 프로퍼티 vs [[Prototype]] 링크 (__proto__)

구분prototype 프로퍼티[[Prototype]] 링크 (비표준 __proto__)
소유자함수 객체 (정확히는 생성자 함수)모든 객체
역할이 생성자 함수로 객체를 생성할 때, 생성될 객체의 부모가 될 프로토타입 객체를 가리킨다. (설계도)객체 자신의 **부모 객체(프로토타입)**를 가리키는 숨겨진 링크. (연결고리)
비유붕어빵 틀. 이 틀로 만들어진 붕어빵들은 모두 같은 모양을 갖게 된다.갓 만들어진 붕어빵이 어떤 틀에서 나왔는지를 가리키는 꼬리표.
접근 방법FunctionName.prototypeObject.getPrototypeOf(obj) (표준), obj.__proto__ (비표준)

이를 그림으로 표현하면 다음과 같다.

  1. 개발자가 User라는 생성자 함수를 정의하면, 자바스크립트 엔진은 자동으로 두 개의 객체를 생성한다.

    • User 함수 객체

    • User의 프로토타입 객체 (User.prototype)

  2. 이때 User 함수는 prototype 프로퍼티를 통해 User.prototype 객체를 가리킨다.

  3. 반대로 User.prototype 객체는 constructor 프로퍼티를 통해 User 함수를 가리킨다. 둘은 서로를 참조하는 관계다.

  4. new User()를 통해 user1이라는 인스턴스 객체를 생성하면, user1 객체 내부에 [[Prototype]]이라는 숨겨진 링크가 생성되고, 이 링크는 User.prototype 객체를 가리키게 된다.

[[Prototype]] 링크가 바로 상속의 핵심이다.

프로토타입 체인 (Prototype Chain)

이제 user1.introduce()를 호출했을 때 내부적으로 일어나는 일을 순서대로 따라가 보자.

  1. 자바스크립트 엔진은 먼저 user1 객체가 introduce라는 속성을 직접 가지고 있는지 확인한다.

    • user1nameage만 가지고 있으므로, 찾지 못한다.
  2. 엔진은 포기하지 않고, user1 객체의 [[Prototype]] 링크를 따라 부모인 User.prototype 객체로 이동한다.

  3. User.prototype 객체에서 introduce라는 속성을 다시 검색한다.

    • 찾았다! User.prototype에는 우리가 정의한 introduce 함수가 있다.
  4. 엔진은 이 함수를 실행한다. 이때 함수 내부의 this는 최초 호출의 주체였던 user1 객체를 가리키도록 바인딩된다. 따라서 this.name은 ‘김철수’가 된다.

만약 User.prototype에도 해당 속성이 없다면 어떻게 될까? 엔진은 또다시 User.prototype 객체의 [[Prototype]] 링크를 따라 그 부모로 이동한다. User.prototype도 객체이므로, 그 부모는 바로 **Object.prototype**이다. 이처럼 [[Prototype]] 링크를 통해 상위 프로토타입으로 계속해서 탐색해 나아가는 과정을 프로토타입 체인이라고 부른다.

Object.prototype은 자바스크립트의 모든 객체의 조상님 격으로, toString(), hasOwnProperty() 등과 같은 모든 객체가 공통으로 사용하는 필수 메서드들을 가지고 있다. 만약 프로토타입 체인의 최상단인 Object.prototype에서도 속성을 찾지 못하면, 체인의 끝([[Prototype]]null인 지점)에 도달한 것이므로 최종적으로 undefined를 반환한다.

이러한 프로토타입 체인 덕분에 우리는 user1.toString()처럼 우리가 직접 정의하지 않은 메서드도 자연스럽게 사용할 수 있는 것이다.

3. 프로토타입을 다루는 다양한 방법

자바스크립트는 프로토타입을 다룰 수 있는 여러 가지 방법을 제공한다. 각 방법의 특징을 이해하고 상황에 맞게 사용하는 것이 중요하다.

1. 생성자 함수와 prototype 프로퍼티 (고전적인 방식)

가장 전통적이고 기본적인 방식이다. function 키워드로 생성자 함수를 만들고, 그 함수의 prototype 프로퍼티에 공통 메서드를 추가한다.

JavaScript

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

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

function Dog(name, breed) {
  // Animal 생성자 호출하여 name 프로퍼티를 상속
  Animal.call(this, name); 
  this.breed = breed;
}

// Dog의 프로토타입이 Animal의 프로토타입을 상속받도록 연결
Dog.prototype = Object.create(Animal.prototype);
// 상속 관계가 깨지지 않도록 constructor를 다시 Dog으로 설정
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {
  console.log(`${this.name}이(가) 멍멍! 짖습니다.`);
};

const myDog = new Dog('보리', '진돗개');
myDog.speak(); // "보리이(가) 소리를 냅니다." (Animal.prototype에서 상속)
myDog.bark();  // "보리이(가) 멍멍! 짖습니다."

이 방식은 프로토타입의 동작 원리를 가장 명확하게 보여주지만, 상속 관계를 설정하는 코드가 다소 번거롭다는 단점이 있다.

2. Object.create() (명시적인 방식)

Object.create()는 주어진 객체를 [[Prototype]]으로 하는 새로운 객체를 생성하는 메서드다. 생성자 함수 없이도 객체 간의 상속 관계를 명시적으로 설정할 수 있어 더 직관적일 수 있다.

JavaScript

const animalProto = {
  speak: function() {
    console.log(`${this.name}이(가) 소리를 냅니다.`);
  }
};

const dogProto = Object.create(animalProto); // animalProto를 부모로 하는 자식 프로토타입 생성
dogProto.bark = function() {
  console.log(`${this.name}이(가) 멍멍! 짖습니다.`);
};

// dogProto를 프로토타입으로 가지는 인스턴스 생성
const myDog = Object.create(dogProto);
myDog.name = '보리';

myDog.speak(); // "보리이(가) 소리를 냅니다."
myDog.bark();  // "보리이(가) 멍멍! 짖습니다."

3. ES6 class (현대적인 방식)

ES6부터 도입된 class 문법은 프로토타입 기반의 상속을 훨씬 더 깔끔하고 익숙한 구문으로 포장해준다. 많은 개발자들이 이 방식을 선호하지만, 중요한 것은 class가 새로운 상속 모델을 도입한 것이 아니라, 기존의 프로토타입 동작을 더 쉽게 사용할 수 있도록 만든 ‘문법적 설탕(Syntactic Sugar)‘이라는 점이다.

JavaScript

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

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

class Dog extends Animal { // extends 키워드로 상속 관계를 간단히 설정
  constructor(name, breed) {
    super(name); // super()로 부모 클래스의 constructor 호출
    this.breed = breed;
  }

  bark() {
    console.log(`${this.name}이(가) 멍멍! 짖습니다.`);
  }
}

const myDog = new Dog('보리', '진돗개');
myDog.speak();
myDog.bark();

내부적으로 이 class 코드는 앞서 살펴본 생성자 함수와 prototype을 사용하는 방식으로 변환되어 실행된다. class는 결국 프로토타입을 더 우아하게 다루기 위한 도구인 셈이다.

4. 프로토타입 심화 탐구

프로토타입의 기본 원리를 이해했다면, 이제 몇 가지 심화 주제를 통해 이해의 깊이를 더해보자.

constructor 프로퍼티의 함정

모든 프로토타입 객체는 기본적으로 constructor라는 프로퍼티를 가지며, 이는 자신을 만든 생성자 함수를 가리킨다. (User.prototype.constructor === User)

이는 객체의 ‘출신’을 알아내는 데 유용하게 사용될 수 있다. 하지만 프로토타입 객체 자체를 다른 객체로 덮어씌우는 경우 이 연결이 끊어질 수 있어 주의가 필요하다.

JavaScript

function Person() {}

// 나쁜 예: prototype 객체를 통째로 교체
Person.prototype = {
  sayHello: function() { /* ... */ }
  // 이 객체에는 constructor 프로퍼티가 없다!
};

const p = new Person();
console.log(p.constructor === Person); // false
console.log(p.constructor === Object); // true! (프로토타입 체인을 따라 Object의 constructor를 찾음)

이 문제를 해결하려면, 프로토타입을 교체할 때 constructor 프로퍼티를 수동으로 다시 설정해주어야 한다.

JavaScript

// 좋은 예
Person.prototype = {
  constructor: Person, // constructor를 명시적으로 지정
  sayHello: function() { /* ... */ }
};

내장 객체의 프로토타입 확장 (Monkey Patching)

자바스크립트는 Array, String과 같은 내장 객체의 프로토타입에도 접근하여 새로운 메서드를 추가할 수 있다.

JavaScript

// 주의: 이 방식은 매우 신중하게 사용해야 한다.
Array.prototype.first = function() {
  return this[0];
};

const arr = [10, 20, 30];
console.log(arr.first()); // 10

이를 **‘몽키 패치(Monkey Patching)‘**라고 부르는데, 매우 편리해 보이지만 심각한 부작용을 낳을 수 있어 현업에서는 사용을 강력히 비권장한다.

  • 충돌 위험: 다른 라이브러리나 미래의 자바스크립트 표준에서 동일한 이름의 메서드를 추가할 경우 코드가 예기치 않게 동작하거나 깨질 수 있다.

  • 예측 불가능성: 코드를 읽는 다른 개발자가 first()라는 메서드가 어디서 왔는지 알기 어려워 유지보수를 어렵게 만든다.

꼭 필요한 기능이라면, 프로토타입을 직접 건드리기보다는 별도의 유틸리티 함수로 만드는 것이 훨씬 안전하고 바람직한 방법이다.

for...inhasOwnProperty

for...in 루프는 객체의 모든 열거 가능한 속성을 순회하는데, 이때 객체 자신이 가진 속성뿐만 아니라 프로토타입 체인을 따라 상속받은 속성까지 모두 포함한다.

JavaScript

function Person(name) {
  this.name = name;
}
Person.prototype.country = 'Korea';

const john = new Person('John');

for (const key in john) {
  console.log(key); // "name", "country"
}

이처럼 원치 않는 상속 속성까지 순회하는 것을 막기 위해 Object.prototype.hasOwnProperty() 메서드를 사용한다. 이 메서드는 객체가 해당 속성을 프로토타입이 아닌 자기 자신에게 직접 소유하고 있을 때만 true를 반환한다.

JavaScript

for (const key in john) {
  if (john.hasOwnProperty(key)) {
    console.log(key); // "name"
  }
}

따라서 for...in 루프를 사용할 때는 hasOwnProperty()로 필터링하는 습관을 들이는 것이 안전하다.

결론 프로토타입은 자바스크립트의 정수다

프로토타입은 처음 접했을 때 다소 생소하고 복잡하게 느껴질 수 있다. 하지만 그 근본적인 탄생 이유인 ‘메모리 효율성’과 ‘코드 재사용’을 떠올리고, ‘객체들이 공유하는 하나의 원형(Prototype)‘이라는 핵심 아이디어를 붙잡으면 그 구조가 보이기 시작한다.

생성자 함수의 prototype 프로퍼티는 앞으로 태어날 객체들의 ‘설계도’이고, 모든 객체 안에 숨겨진 [[Prototype]] 링크는 그 설계도를 찾아가는 ‘연결고리’다. 이 연결고리들이 엮여 만들어진 프로토타입 체인을 통해 자바스크립트는 유연하고 강력한 상속을 구현한다.

class 문법이 대세가 된 오늘날에도 그 기저에서 묵묵히 동작하고 있는 프로토타입의 원리를 이해하는 것은, 단순히 문법을 아는 것을 넘어 자바스크립트라는 언어의 영혼을 이해하는 것과 같다. 이 핸드북이 여러분의 프로토타입 정복 여정에 훌륭한 나침반이 되기를 바란다.