메인 콘텐츠로 바로가기

JavaScript 프로토타입 완벽 이해하기

개요

프로토타입(Prototype)은 JavaScript의 핵심 메커니즘으로, 객체 간 속성과 메서드를 공유하는 방식입니다. 프로토타입을 이해하면 JavaScript가 어떻게 상속을 구현하는지 알 수 있고, 메모리를 효율적으로 사용하는 코드를 작성할 수 있습니다.

배경

왜 필요한가?

JavaScript는 클래스 기반 언어가 아닌 프로토타입 기반 언어입니다. 전통적인 객체지향 언어에서는 클래스를 통해 객체를 생성하고 상속을 구현하지만, JavaScript는 프로토타입을 통해 이를 해결합니다.

프로토타입이 없다면 다음과 같은 문제가 발생합니다:

// 프로토타입 없이 객체를 만들면 function createUser(name, age) { return { name: name, age: age, greet: function() { console.log(`안녕하세요, ${this.name}입니다.`); } }; } const user1 = createUser('김철수', 25); const user2 = createUser('이영희', 30); // 문제: 각 객체마다 greet 함수의 복사본이 생성됨 console.log(user1.greet === user2.greet); // false - 메모리 낭비!

1000명의 사용자를 만들면 1000개의 동일한 greet 함수가 메모리에 생성됩니다. 프로토타입은 이 문제를 해결합니다.

ES6 이전의 방식

ES6 class 문법이 등장하기 전에는 생성자 함수와 프로토타입을 직접 사용했습니다:

function User(name, age) { this.name = name; this.age = age; } User.prototype.greet = function() { console.log(`안녕하세요, ${this.name}입니다.`); }; const user = new User('김철수', 25); user.greet(); // "안녕하세요, 김철수입니다."

동작 원리

프로토타입 체인의 핵심 메커니즘

JavaScript의 모든 객체는 숨겨진 [[Prototype]] 속성을 가지고 있습니다. 이 속성은 다른 객체를 참조하며, 이를 통해 속성과 메서드를 상속받습니다.

1단계: 객체 생성 시 프로토타입 연결

function Dog(name) { this.name = name; } Dog.prototype.bark = function() { console.log(`${this.name}: 멍멍!`); }; const myDog = new Dog('바둑이'); // myDog 객체의 [[Prototype]]이 Dog.prototype을 가리킴

2단계: 속성 접근 시 프로토타입 체인 탐색

myDog.bark(); // "바둑이: 멍멍!" // 탐색 과정: // 1. myDog 객체에서 bark 메서드 찾기 → 없음 // 2. myDog.[[Prototype]] (Dog.prototype)에서 찾기 → 발견! // 3. 메서드 실행

3단계: 체인을 따라 계속 탐색

myDog.toString(); // "[object Object]" // 탐색 과정: // 1. myDog에서 toString 찾기 → 없음 // 2. Dog.prototype에서 찾기 → 없음 // 3. Object.prototype에서 찾기 → 발견!

시각적 이해

[myDog 객체] name: "바둑이" [[Prototype]] ──→ [Dog.prototype] bark: function [[Prototype]] ──→ [Object.prototype] toString: function hasOwnProperty: function [[Prototype]] ──→ null

코드로 확인하기

function Animal(name) { this.name = name; } Animal.prototype.eat = function() { console.log(`${this.name}이(가) 먹이를 먹습니다.`); }; const cat = new Animal('나비'); // 프로토타입 확인 console.log(Object.getPrototypeOf(cat) === Animal.prototype); // true // 속성이 어디에 있는지 확인 console.log(cat.hasOwnProperty('name')); // true - 객체 자체에 있음 console.log(cat.hasOwnProperty('eat')); // false - 프로토타입에 있음 // 프로토타입 체인 확인 console.log(cat.__proto__ === Animal.prototype); // true console.log(Animal.prototype.__proto__ === Object.prototype); // true console.log(Object.prototype.__proto__ === null); // true - 체인의 끝

주요 특징

특징 1: 동적 프로토타입

프로토타입은 실행 중에도 수정할 수 있습니다:

function Car(model) { this.model = model; } const myCar = new Car('소나타'); // 나중에 프로토타입에 메서드 추가 Car.prototype.drive = function() { console.log(`${this.model}이(가) 달립니다.`); }; // 이미 생성된 객체도 새 메서드를 사용 가능 myCar.drive(); // "소나타이(가) 달립니다."

원리: 객체는 프로토타입을 참조하고 있기 때문에, 프로토타입이 변경되면 모든 객체에 즉시 반영됩니다.

특징 2: 메모리 효율성

프로토타입에 정의된 메서드는 모든 인스턴스가 공유합니다:

function User(name) { this.name = name; } User.prototype.greet = function() { console.log(`안녕하세요, ${this.name}입니다.`); }; const user1 = new User('철수'); const user2 = new User('영희'); const user3 = new User('민수'); // 모든 인스턴스가 같은 greet 함수를 공유 console.log(user1.greet === user2.greet); // true console.log(user2.greet === user3.greet); // true // 결과: greet 함수는 메모리에 딱 1개만 존재

특징 3: 프로토타입 상속

프로토타입을 사용하여 상속 구조를 만들 수 있습니다:

// 부모 생성자 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(`${this.name}: 멍멍!`); }; const myDog = new Dog('바둑이', '진돗개'); myDog.eat(); // "바둑이이(가) 먹이를 먹습니다." - 상속받은 메서드 myDog.bark(); // "바둑이: 멍멍!" - 자체 메서드

실제 사용 사례

사례 1: 내장 객체 확장 (주의 필요)

JavaScript의 내장 객체도 프로토타입을 사용합니다:

// Array.prototype에 새 메서드 추가 (실무에서는 권장하지 않음) Array.prototype.first = function() { return this[0]; }; const numbers = [1, 2, 3, 4, 5]; console.log(numbers.first()); // 1 // 모든 배열에서 사용 가능 const fruits = ['사과', '바나나', '오렌지']; console.log(fruits.first()); // "사과"

주의: 내장 객체의 프로토타입을 수정하면 예기치 않은 충돌이 발생할 수 있으므로 실무에서는 피해야 합니다.

사례 2: 플러그인 시스템

프로토타입을 사용하여 확장 가능한 시스템을 만들 수 있습니다:

function Calculator() { this.result = 0; } Calculator.prototype.add = function(x) { this.result += x; return this; // 체이닝을 위해 }; Calculator.prototype.multiply = function(x) { this.result *= x; return this; }; // 플러그인으로 새 기능 추가 Calculator.prototype.square = function() { this.result = this.result * this.result; return this; }; const calc = new Calculator(); calc.add(5).multiply(2).square(); console.log(calc.result); // 100 (5 + 0 = 5, 5 * 2 = 10, 10^2 = 100)

사례 3: 모던 JavaScript에서의 활용

ES6 클래스 문법도 내부적으로는 프로토타입을 사용합니다:

class Person { constructor(name, age) { this.name = name; this.age = age; } greet() { console.log(`안녕하세요, ${this.name}입니다.`); } } // 내부적으로는 프로토타입 기반 const person = new Person('김철수', 25); console.log(Person.prototype.greet === person.__proto__.greet); // true // 프로토타입에 메서드 추가 가능 Person.prototype.sayAge = function() { console.log(`저는 ${this.age}살입니다.`); }; person.sayAge(); // "저는 25살입니다."

장점과 한계

장점

  • 메모리 효율성: 메서드를 모든 인스턴스가 공유하여 메모리 사용량 감소
  • 동적 확장: 런타임에 프로토타입을 수정하여 모든 인스턴스에 새 기능 추가 가능
  • 상속 구현: 프로토타입 체인을 통한 자연스러운 상속 메커니즘
  • 성능: 메서드 호출 시 프로토타입 체인 탐색은 매우 빠름

한계

  • ⚠️ 복잡성: 프로토타입 체인 개념이 초보자에게 어려울 수 있음
  • ⚠️ 디버깅 어려움: 속성이 어디에서 오는지 추적하기 어려울 수 있음
  • ⚠️ 프로퍼티 섀도잉: 프로토타입에 같은 이름의 속성이 있으면 가려질 수 있음
function User(name) { this.name = name; } User.prototype.age = 0; const user = new User('철수'); console.log(user.age); // 0 - 프로토타입에서 user.age = 25; // 객체 자체에 age 속성 추가 console.log(user.age); // 25 - 자체 속성이 프로토타입을 가림 console.log(User.prototype.age); // 0 - 프로토타입은 여전히 0
  • ⚠️ 전역 오염 위험: 내장 객체 프로토타입 수정 시 충돌 가능

트레이드오프

언제 사용해야 하는가:

  • 많은 수의 인스턴스를 생성할 때
  • 모든 인스턴스가 공통 메서드를 공유해야 할 때
  • 상속 구조가 필요할 때

언제 피해야 하는가:

  • 내장 객체(Array, Object 등)의 프로토타입을 수정하는 경우
  • 프로토타입 체인이 너무 깊어지는 경우 (성능 저하)
  • 간단한 일회성 객체를 만들 때 (객체 리터럴로 충분)

관련 개념

유사 개념

클래스 기반 상속 (Java, C++)

  • JavaScript의 프로토타입 상속과 비슷하지만, 클래스는 컴파일 시점에 고정됨
  • 프로토타입은 런타임에 동적으로 변경 가능

ES6 Class

  • 프로토타입의 문법적 설탕(Syntactic Sugar)
  • 내부적으로는 여전히 프로토타입 사용
  • 더 익숙한 클래스 문법 제공
// ES6 Class는 내부적으로 프로토타입 사용 class Animal { constructor(name) { this.name = name; } eat() { console.log(`${this.name}이(가) 먹이를 먹습니다.`); } } // 위 코드는 아래와 동일하게 동작 function Animal(name) { this.name = name; } Animal.prototype.eat = function() { console.log(`${this.name}이(가) 먹이를 먹습니다.`); };

대안

Object.create()

  • 프로토타입을 명시적으로 지정하여 객체 생성
  • 생성자 함수 없이도 프로토타입 상속 가능
const animalMethods = { eat() { console.log(`${this.name}이(가) 먹이를 먹습니다.`); } }; const cat = Object.create(animalMethods); cat.name = '나비'; cat.eat(); // "나비이(가) 먹이를 먹습니다."

Composition (조합)

  • 상속 대신 객체 조합을 사용하는 패턴
  • “상속보다 조합을 선호하라” 원칙
// 상속 대신 조합 사용 function createAnimal(name) { return { name, eat() { console.log(`${this.name}이(가) 먹이를 먹습니다.`); } }; } function createDog(name, breed) { return { ...createAnimal(name), breed, bark() { console.log(`${this.name}: 멍멍!`); } }; } const myDog = createDog('바둑이', '진돗개'); myDog.eat(); // "바둑이이(가) 먹이를 먹습니다." myDog.bark(); // "바둑이: 멍멍!"

장단점 비교:

  • 프로토타입: 메모리 효율적, 동적 확장 가능, 복잡함
  • Object.create(): 명시적, 유연함, 생성자 불필요
  • 조합: 유연함, 이해하기 쉬움, 메모리 사용량 많을 수 있음

실용적인 팁

팁 1: hasOwnProperty로 속성 구분하기

function User(name) { this.name = name; } User.prototype.greet = function() { console.log(`안녕하세요, ${this.name}입니다.`); }; const user = new User('철수'); // 객체 자체의 속성인지 확인 console.log(user.hasOwnProperty('name')); // true console.log(user.hasOwnProperty('greet')); // false // for...in 루프에서 주의 for (let key in user) { if (user.hasOwnProperty(key)) { console.log(key, user[key]); // "name 철수"만 출력 } }

팁 2: Object.getPrototypeOf() 사용하기

// ❌ 권장하지 않음: __proto__ 사용 const proto = user.__proto__; // ✅ 권장: Object.getPrototypeOf() 사용 const proto = Object.getPrototypeOf(user); console.log(proto === User.prototype); // true

팁 3: instanceof로 타입 확인

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(myDog instanceof Dog); // true console.log(myDog instanceof Animal); // true console.log(myDog instanceof Object); // true

더 알아보기

심화 학습:

실습:

관련 자료:

요약

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

  • 모든 객체는 [[Prototype]] 속성을 통해 다른 객체와 연결됨
  • 프로토타입 체인을 따라 속성과 메서드를 탐색
  • 메모리 효율적으로 메서드를 공유
  • ES6 class도 내부적으로는 프로토타입 사용

프로토타입을 이해하면 JavaScript의 동작 원리를 깊이 이해할 수 있고, 더 효율적인 코드를 작성할 수 있습니다.

댓글

developjik
All content is licensed under CC BY-NC-SA 4.0 unless otherwise noted.