들어가며

Java를 배운 개발자가 JavaScript를 처음 접하면 혼란스러워합니다.

// Java의 클래스
class Animal {
    void makeSound() {
        System.out.println("Some sound");
    }
}
 
// JavaScript의... 프로토타입?
function Animal() {}
Animal.prototype.makeSound = function() {
    console.log("Some sound");
};

왜 JavaScript는 클래스 대신 프로토타입을 선택했을까요?

JavaScript 탄생의 배경 (1995년)

브렌던 아이크의 10일

  • 1995년 5월, Netscape의 Brendan Eich는 단 10일 만에 JavaScript의 첫 버전을 만들었습니다
  • 초기 이름은 MochaLiveScriptJavaScript
  • Java의 인기에 편승한 마케팅 전략으로 이름만 JavaScript

설계 요구사항

  1. 간단해야 한다: 비프로그래머도 사용할 수 있어야 함
  2. 빨리 만들어야 한다: 10일의 시간 제약
  3. Java와 다르면서도 비슷해야 한다: 마케팅 전략
  4. 브라우저에서 실행: 가벼워야 함

프로토타입 선택의 이유

1. Self 언어의 영향

Brendan Eich는 Self 언어에서 큰 영감을 받았습니다.

Self는:

  • Sun Microsystems에서 개발한 프로토타입 기반 언어
  • 클래스 없이도 객체 지향 프로그래밍 가능
  • 더 간단하고 유연한 객체 생성 방식
// Self 스타일의 객체 생성 (JavaScript로 표현)
const animal = {
  sound: 'generic',
  makeSound() {
    console.log(this.sound);
  }
};
 
// 프로토타입 체인으로 상속
const dog = Object.create(animal);
dog.sound = 'woof';
dog.makeSound();  // 'woof'

2. 클래스의 복잡성 회피

당시 Java의 클래스 시스템은:

  • 컴파일 필요
  • 타입 시스템 복잡
  • 초보자에게 어려움
  • 무거운 실행 환경

프로토타입의 장점:

  • 컴파일 불필요
  • 동적으로 객체 생성
  • 학습 곡선이 낮음 (처음에는…)
  • 가벼운 실행

3. 동적 언어의 철학

JavaScript는 동적 타입 언어로 설계되었습니다.

// 클래스는 정적 구조를 강제
class Dog {
    bark() {}  // 미리 정의해야 함
}
 
// 프로토타입은 동적 확장 가능
function Dog() {}
Dog.prototype.bark = function() {};
 
// 런타임에 메서드 추가 가능
Dog.prototype.wagTail = function() {};  // 자유롭게 확장!

4. 메모리 효율성

프로토타입 체인은 메모리를 절약합니다.

// 프로토타입: 메서드는 한 곳에만 존재
function Dog(name) {
  this.name = name;
}
 
Dog.prototype.bark = function() {
  console.log(`${this.name} barks!`);
};
 
const dog1 = new Dog('Max');
const dog2 = new Dog('Buddy');
 
// bark 메서드는 prototype에 하나만 존재
// dog1과 dog2는 이를 공유
console.log(dog1.bark === dog2.bark);  // true
 
// 만약 각 인스턴스마다 메서드를 복사한다면?
function BadDog(name) {
  this.name = name;
  this.bark = function() {  // 메모리 낭비!
    console.log(`${this.name} barks!`);
  };
}
 
const bad1 = new BadDog('Max');
const bad2 = new BadDog('Buddy');
console.log(bad1.bark === bad2.bark);  // false (별도 인스턴스)

프로토타입의 작동 원리

프로토타입 체인

function Animal(name) {
  this.name = name;
}
 
Animal.prototype.eat = function() {
  console.log(`${this.name} is eating`);
};
 
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} barks!`);
};
 
const myDog = new Dog('Max', 'Labrador');
 
// 프로토타입 체인:
// myDog → Dog.prototype → Animal.prototype → Object.prototype → null
 
myDog.bark();  // Dog.prototype에서 찾음
myDog.eat();   // Animal.prototype에서 찾음 (위로 올라감)

Prototype vs prototype

헷갈리는 두 개념:

function Dog(name) {
  this.name = name;
}
 
Dog.prototype.bark = function() {};
 
const myDog = new Dog('Max');
 
// Dog.prototype: 생성자 함수의 프로퍼티 (템플릿)
// myDog.[[Prototype]]: 실제 인스턴스의 내부 링크 (__proto__로 접근 가능)
 
console.log(Dog.prototype);          // 객체 (템플릿)
console.log(myDog.__proto__);        // Dog.prototype과 같음
console.log(myDog.__proto__ === Dog.prototype);  // true

프로토타입 vs 클래스 (ES6+)

ES6 Class는 문법 설탕

// ES6 Class
class Dog {
  constructor(name) {
    this.name = name;
  }
 
  bark() {
    console.log(`${this.name} barks!`);
  }
}
 
// 위 코드는 본질적으로 이것과 동일:
function Dog(name) {
  this.name = name;
}
 
Dog.prototype.bark = function() {
  console.log(`${this.name} barks!`);
};
 
// 증명
console.log(typeof Dog);  // 'function'
console.log(Dog.prototype.bark);  // function

왜 ES6에서 Class를 추가했을까?

  1. 가독성: 다른 언어 사용자에게 친숙
  2. 편의성: 상속이 더 간단
  3. 표준화: 일관된 객체 지향 패턴
// ES6 Class로 상속
class Animal {
  constructor(name) {
    this.name = name;
  }
 
  eat() {
    console.log(`${this.name} is eating`);
  }
}
 
class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }
 
  bark() {
    console.log(`${this.name} barks!`);
  }
}
 
// 훨씬 간결하고 명확!

프로토타입의 장점과 단점

장점

유연성: 런타임에 동적으로 확장 가능

Dog.prototype.newMethod = function() {};  // 이미 생성된 인스턴스에도 적용!

메모리 효율: 메서드 공유 ✅ 간단한 구조: (처음에는) 이해하기 쉬움 ✅ Duck Typing: 타입보다는 행동에 집중

단점

혼란스러운 문법: prototype, __proto__, [[Prototype]]상속 복잡성: 프로토타입 체인 설정이 번거로움 ❌ 디버깅 어려움: 체인을 따라가며 추적 ❌ 성능: 프로토타입 체인을 거슬러 올라가는 비용

현대 JavaScript에서의 선택

언제 프로토타입을 직접 사용할까?

// 라이브러리/프레임워크 개발
// 네이티브 객체 확장 (권장하지 않지만)
Array.prototype.customMethod = function() {
  // 모든 배열에서 사용 가능
};
 
// 성능이 중요한 경우
// 메모리 사용을 최소화해야 하는 경우

대부분의 경우 Class 사용 권장

// 현대적인 코드
class UserService {
  constructor(api) {
    this.api = api;
  }
 
  async getUser(id) {
    return await this.api.get(`/users/${id}`);
  }
}
 
// 명확하고, 읽기 쉽고, 유지보수 용이

결론

왜 프로토타입을 선택했나?

  1. 역사적 맥락: 10일의 시간 제약, Self 언어의 영향
  2. 설계 철학: 간단함, 유연성, 동적 특성
  3. 기술적 요구: 가벼움, 메모리 효율

현재의 선택

  • ES6+ Class 사용 권장: 가독성과 유지보수성
  • 프로토타입 이해 필수: JavaScript의 근본 원리
  • 두 가지 모두 같은 메커니즘: Class도 내부적으로는 프로토타입

JavaScript의 프로토타입은 단점도 있지만, 언어의 유연성과 동적 특성의 근간입니다. 이를 이해하는 것은 JavaScript를 깊이 있게 사용하는 첫 걸음입니다.


참고 자료