icon
6장 : 자바스크립트 심화 I

프로토타입과 상속

대부분의 객체 지향 프로그래밍(OOP) 언어는 '클래스(Class)'를 기반으로 상속을 구현합니다. 즉, 부모 클래스의 특성을 자식 클래스가 물려받는 방식이죠. 하지만 자바스크립트는 ES6에서 class 키워드를 도입하기 전까지는 클래스라는 개념이 없었으며, 대신 프로토타입(Prototype) 이라는 독특한 메커니즘을 통해 상속을 구현했습니다. 이를 프로토타입 기반 상속(Prototype-based Inheritance) 이라고 합니다.

class 키워드가 도입된 지금도, 자바스크립트의 객체 지향은 여전히 내부적으로는 프로토타입을 기반으로 동작합니다. 따라서 자바스크립트 개발자라면 class 문법 뒤에 숨겨진 프로토타입의 원리를 정확히 이해하는 것이 필수적입니다. 이는 여러분이 작성하는 코드의 성능과 효율성을 높이고, 라이브러리나 프레임워크의 동작 방식을 더 깊이 이해하는 데 큰 도움이 될 것입니다.

이번 장에서는 프로토타입의 개념부터 시작하여, 어떻게 상속이 이루어지는지, 그리고 class 키워드는 프로토타입과 어떤 관계에 있는지 깊이 있게 알아보겠습니다.


프로토타입 (Prototype): 모든 객체의 부모

자바스크립트에서 모든 객체는 자신의 부모 역할을 하는 또 다른 객체와 연결되어 있습니다. 이 부모 객체를 프로토타입(Prototype) 이라고 부릅니다. 객체는 자신의 프로토타입으로부터 속성과 메서드를 상속받을 수 있습니다.

객체에서 어떤 속성이나 메서드를 찾을 때, 자바스크립트 엔진은 먼저 그 객체 자신에게 해당 속성이 있는지 확인합니다. 만약 없다면, 그 객체의 프로토타입으로 이동하여 찾습니다. 프로토타입에도 없다면, 다시 그 프로토타입의 프로토타입으로 이동하여 찾는 과정을 반복합니다. 이 연쇄적인 연결을 프로토타입 체인(Prototype Chain) 이라고 합니다.

내부 슬롯과 접근자 프로퍼티

모든 객체는 [[Prototype]]이라는 숨겨진 내부 슬롯(internal slot)을 가집니다. 이 [[Prototype]] 슬롯은 해당 객체의 프로토타입 객체를 가리킵니다. 개발자는 __proto__ 접근자 프로퍼티(getter/setter)를 통해 이 [[Prototype]] 슬롯에 간접적으로 접근할 수 있습니다. (표준적인 방법은 아니지만, 많은 브라우저에서 제공하며 이해를 돕기 위해 사용됩니다.)

const user = {
    name: "김민준",
    age: 28
};

// user 객체의 프로토타입을 확인합니다.
// 브라우저 환경에서는 일반적으로 Object.prototype이 출력됩니다.
console.log(user.__proto__);
// 또는 더 표준적인 방법: Object.getPrototypeOf(user)
console.log(Object.getPrototypeOf(user));

// user는 Object.prototype의 toString 메서드를 상속받아 사용합니다.
console.log(user.toString()); // 결과: [object Object]
// user 객체 자체에는 toString 메서드가 없지만, 프로토타입 체인을 통해 Object.prototype.toString을 찾아서 사용합니다.

생성자 함수와 prototype 프로퍼티

함수는 객체이지만, 일반 객체와는 다르게 prototype이라는 특별한 프로퍼티를 가집니다. 이 prototype 프로퍼티는 해당 함수가 생성자 함수(Constructor Function) 로 사용될 때, 이 함수가 생성할 인스턴스(Instance) 객체들의 프로토타입이 될 객체를 가리킵니다.

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

// Person 함수의 prototype 객체에 sayHello 메서드를 추가합니다.
Person.prototype.sayHello = function() {
    console.log(`안녕하세요, 제 이름은 ${this.name}입니다.`);
};

const person1 = new Person("이수민", 25);
const person2 = new Person("박지훈", 30);

// person1과 person2는 Person의 인스턴스이며,
// 이들의 [[Prototype]]은 Person.prototype을 가리킵니다.
console.log(person1.__proto__ === Person.prototype); // 결과: true
console.log(person2.__proto__ === Person.prototype); // 결과: true

// 인스턴스들은 자신에게 sayHello 메서드가 없지만, 프로토타입 체인을 통해 접근합니다.
person1.sayHello(); // 결과: 안녕하세요, 제 이름은 이수민입니다.
person2.sayHello(); // 결과: 안녕하세요, 제 이름은 박지훈입니다.

// 각 인스턴스는 sayHello 메서드를 복사한 것이 아니라,
// 프로토타입 객체의 동일한 메서드를 참조합니다.
console.log(person1.sayHello === person2.sayHello); // 결과: true

이러한 방식으로 메서드를 프로토타입에 정의하면, 모든 인스턴스가 동일한 메서드를 공유하게 되어 메모리를 효율적으로 사용할 수 있습니다. 만약 this.sayHello = function() { ... }와 같이 생성자 함수 내부에 메서드를 정의하면, 인스턴스가 생성될 때마다 새로운 함수가 생성되어 메모리 낭비가 발생합니다.

constructor 프로퍼티

모든 프로토타입 객체는 constructor라는 프로퍼티를 가집니다. 이 constructor 프로퍼티는 자신을 생성한 생성자 함수를 가리킵니다.

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

console.log(Animal.prototype.constructor === Animal); // 결과: true

const dog = new Animal("바둑이");
console.log(dog.constructor === Animal); // 결과: true

이를 통해 어떤 객체가 어떤 생성자 함수에 의해 생성되었는지 알 수 있습니다.


상속: 프로토타입 체인의 활용

자바스크립트의 상속은 이 프로토타입 체인을 통해 이루어집니다. 한 객체가 다른 객체의 프로토타입이 되고, 그 객체는 또 다른 객체의 프로토타입이 되는 식으로 연결되어, 마치 사슬처럼 이어지는 구조입니다.

프로토타입 체인을 이용한 속성/메서드 검색

객체에서 속성이나 메서드에 접근하려고 할 때, 다음과 같은 순서로 검색이 진행됩니다.

  1. 객체 자신에서 검색: 가장 먼저 해당 속성이나 메서드가 객체 자체에 직접 정의되어 있는지 확인합니다.
  2. 프로토타입 체인 상위로 이동: 만약 객체 자신에게 없으면, [[Prototype]]이 가리키는 부모 객체(프로토타입)로 이동하여 거기서 다시 해당 속성을 찾습니다.
  3. 반복: 이 과정을 프로토타입 체인의 끝(일반적으로 Object.prototypenull 프로토타입)에 도달할 때까지 반복합니다.
  4. undefined 반환: 체인의 끝까지 도달했는데도 찾지 못하면, undefined를 반환합니다.
const parent = {
    parentProp: "Parent Value",
    parentMethod: function() {
        console.log("Parent Method Called!");
    }
};

const child = {
    childProp: "Child Value"
};

// child의 프로토타입을 parent로 설정합니다.
// Object.setPrototypeOf()는 객체의 [[Prototype]]을 설정하는 표준적인 방법입니다.
Object.setPrototypeOf(child, parent);

console.log(child.childProp);   // 결과: Child Value (child 객체 자신의 속성)
console.log(child.parentProp);  // 결과: Parent Value (프로토타입 체인을 통해 parent에서 찾음)
child.parentMethod();           // 결과: Parent Method Called! (프로토타입 체인을 통해 parent에서 찾음)

console.log(child.nonExistent); // 결과: undefined (체인 끝까지 찾았으나 없음)

생성자 함수를 이용한 상속 (고전적인 방식)

ES6 class 문법이 도입되기 전에는 주로 생성자 함수와 프로토타입을 조합하여 상속을 구현했습니다.

// 부모 생성자 함수
function Animal(name) {
    this.name = name;
    this.species = "동물";
}

Animal.prototype.sound = function() {
    console.log("동물 소리!");
};

// 자식 생성자 함수
function Dog(name, breed) {
    // Animal 생성자를 호출하여 name과 species를 상속받습니다.
    // call() 메서드를 사용하여 this를 Dog의 인스턴스로 바인딩합니다.
    Animal.call(this, name); // 상위 생성자의 속성 상속
    this.breed = breed;
}

// Dog.prototype을 Animal.prototype의 새로운 객체로 설정하여 프로토타입 체인을 연결합니다.
// Object.create()는 지정된 프로토타입을 가진 새로운 객체를 만듭니다.
Dog.prototype = Object.create(Animal.prototype);
// constructor 프로퍼티를 다시 Dog로 설정해줍니다. (필수)
Dog.prototype.constructor = Dog;

// Dog.prototype에 Dog만의 메서드를 추가합니다.
Dog.prototype.bark = function() {
    console.log("멍멍!");
};

const myDog = new Dog("댕댕이", "시바견");

console.log(myDog.name);     // 결과: 댕댕이 (Animal에서 상속받은 속성)
console.log(myDog.species);  // 결과: 동물 (Animal에서 상속받은 속성)
console.log(myDog.breed);    // 결과: 시바견 (Dog 자신의 속성)

myDog.sound(); // 결과: 동물 소리! (Animal.prototype에서 상속받은 메서드)
myDog.bark();  // 결과: 멍멍! (Dog.prototype 자신의 메서드)

console.log(myDog.__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);          // true (체인의 끝)

이러한 고전적인 상속 방식은 call()Object.create()를 조합하여 복잡하게 보일 수 있습니다.


class 키워드: 문법적 설탕

ES6(ECMAScript 2015)부터 자바스크립트에 class 키워드가 도입되었습니다. 이는 기존의 프로토타입 기반 상속을 더 익숙한 객체 지향 언어의 클래스 문법처럼 보이도록 하는 "문법적 설탕(Syntactic Sugar)" 입니다. 즉, class 키워드를 사용해도 내부적으로는 여전히 프로토타입 기반으로 동작합니다.

class 선언 및 인스턴스 생성

// 부모 클래스
class Animal {
    constructor(name) {
        this.name = name;
        this.species = "동물";
    }

    sound() { // 이 메서드는 Animal.prototype에 추가됩니다.
        console.log("동물 소리!");
    }
}

// 자식 클래스 (extends 키워드를 사용하여 상속)
class Dog extends Animal {
    constructor(name, breed) {
        super(name); // 부모 클래스의 constructor를 호출합니다. (필수)
        this.breed = breed;
    }

    bark() { // 이 메서드는 Dog.prototype에 추가됩니다.
        console.log("멍멍!");
    }

    // 부모 메서드 오버라이딩 (재정의)
    sound() {
        console.log(`${this.name}는 멍멍 짖습니다!`);
    }
}

const myCat = new Animal("나비");
myCat.sound(); // 결과: 동물 소리!

const myNewDog = new Dog("초코", "푸들");
console.log(myNewDog.name);     // 결과: 초코
console.log(myNewDog.species);  // 결과: 동물
console.log(myNewDog.breed);    // 결과: 푸들

myNewDog.sound(); // 결과: 초코는 멍멍 짖습니다! (오버라이딩된 메서드)
myNewDog.bark();  // 결과: 멍멍!

// 내부적으로 여전히 프로토타입 체인으로 연결되어 있습니다.
console.log(myNewDog.__proto__ === Dog.prototype);        // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true
  • class 클래스명 { ... }: 클래스를 선언합니다.
  • constructor(매개변수): 인스턴스가 생성될 때 호출되는 특별한 메서드입니다. 여기서 인스턴스의 속성을 초기화합니다.
  • 메서드: constructor 외에 정의된 함수들은 해당 클래스의 prototype 객체에 추가됩니다.
  • extends 부모클래스: 다른 클래스를 상속받을 때 사용합니다.
  • super(): 자식 클래스의 constructor 내부에서 부모 클래스의 constructor를 호출할 때 사용합니다. super()를 호출하기 전에는 this를 사용할 수 없습니다. 부모 클래스의 속성들을 초기화하는 역할을 합니다.

class 문법은 이전의 생성자 함수 방식보다 훨씬 직관적이고 가독성이 좋습니다. 하지만 기억해야 할 중요한 점은 이것이 새로운 상속 모델을 제공하는 것이 아니라, 기존의 프로토타입 기반 상속을 더 쉽게 작성할 수 있도록 돕는 문법적 편의성이라는 것입니다.


마무리하며

이번 장에서는 자바스크립트의 객체 지향 프로그래밍을 이해하는 데 가장 핵심적인 개념인 프로토타입(Prototype) 과 이를 통한 상속(Inheritance) 에 대해 깊이 있게 학습했습니다.

여러분은 모든 자바스크립트 객체가 [[Prototype]]이라는 내부 슬롯을 통해 부모 객체(프로토타입)와 연결되어 있다는 것을 배웠습니다. 그리고 이 연결을 통해 속성과 메서드를 찾고 공유하는 프로토타입 체인의 원리를 이해했습니다. 또한, 생성자 함수의 prototype 프로퍼티가 인스턴스의 프로토타입이 되는 방식, 그리고 constructor 프로퍼티의 역할도 살펴보았습니다.

마지막으로, ES6에서 도입된 class 키워드가 실제로는 프로토타입 기반 상속의 복잡한 구현을 감춰주는 문법적 설탕이라는 점을 명확히 인지했습니다. class 문법을 사용하면 더욱 직관적이고 간결하게 객체 지향 코드를 작성할 수 있지만, 그 밑바탕에는 여전히 프로토타입 체인이 존재한다는 사실을 잊지 않는 것이 중요합니다.

프로토타입과 상속은 자바스크립트의 깊이를 이해하는 데 필수적인 개념이며, 이를 통해 여러분은 더욱 효율적이고 견고한 객체 지향 코드를 작성할 수 있게 될 것입니다. 다양한 객체와 생성자 함수, 그리고 클래스를 직접 만들어보고 프로토타입 체인이 어떻게 연결되는지 탐색해보는 연습을 꾸준히 해보시길 바랍니다. 다음 장에서는 자바스크립트의 비동기 처리와 콜백, Promise, async/await에 대해 다루겠습니다.