Dandy Now!
  • [JavaScript] 프로토타입 이해하기
    2025년 06월 10일 21시 55분 42초에 업로드 된 글입니다.
    작성자: DandyNow
    728x90
    반응형

    JavaScript의 프로토타입 이해하기

    JavaScript에서 프로토타입(Prototype)은 객체 지향 프로그래밍의 핵심 개념 중 하나이다. 이는 객체가 다른 객체의 속성과 메서드를 상속받을 수 있도록 하는 메커니즘을 제공한다. 클래스 기반 상속과는 다른, 프로토타입 기반 상속이라는 독특한 방식을 사용한다.

    1. 프로토타입의 개념

    JavaScript의 모든 객체는 [[Prototype]]이라는 내부 슬롯을 가지며, 이는 다른 객체를 참조한다. 이 참조는 해당 객체의 프로토타입이라고 불린다. 객체에서 특정 속성이나 메서드를 찾을 때, 먼저 자신의 속성을 탐색하고 없으면 [[Prototype]]이 가리키는 객체에서 찾는다. 이 과정이 계속 연결되어 프로토타입 체인(Prototype Chain)을 형성한다.

    • __proto__ 속성: 대부분의 JavaScript 환경에서 객체의 [[Prototype]] 내부 슬롯은 __proto__라는 비표준 속성을 통해 접근할 수 있었다. 하지만 이는 비표준이므로, Object.getPrototypeOf() 또는 Object.setPrototypeOf() 같은 표준 메서드를 사용하는 것이 권장된다.
    • prototype 속성: 함수 객체는 특별히 prototype이라는 속성을 가진다. 이 prototype 속성은 함수가 생성자(constructor)로 사용될 때, 해당 함수를 통해 생성된 모든 인스턴스 객체의 [[Prototype]]이 될 객체를 가리킨다.
    • 예시:
    // 생성자 함수
    // 특징
    // 1) `new` 키워드와 함께 호출: 일반 함수와 달리 `new` 연산자와 함께 호출될 때 특별하게 동작한다.
    // 2) `this` 바인딩: 호출 시 `this`는 새로 생성될 객체를 가리킨다.
    // 3) 객체 반환:** 명시적으로 `return` 문을 사용하지 않아도, `this`가 가리키는 새로운 객체를 자동으로 반환한다.
    // 4) 프로토타입 연결: 생성자 함수의 `prototype` 속성에 정의된 메서드나 속성이, 이 생성자 함수로 생성된 모든 인스턴스의 프로토타입 체인에 연결되어 공유된다.
    function Person(name) {
      this.name = name;
    }
    
    Person.prototype.sayHello = function() {
      console.log(`안녕하세요, 저는 ${this.name}입니다.`);
    };
    
    const john = new Person('John');
    john.sayHello(); // 안녕하세요, 저는 John입니다.
    
    console.log(john.__proto__ === Person.prototype); // true
    console.log(Person.prototype.constructor === Person); // true

    위 예시에서 Person 함수는 생성자로 사용되었고, Person.prototype 객체에 sayHello 메서드를 추가했다. new Person('John')으로 생성된 john 객체는 Person.prototype을 자신의 프로토타입으로 가지게 되며, 따라서 sayHello 메서드를 직접 정의하지 않아도 호출할 수 있다.

    2. 프로토타입을 사용하는 이유

    프로토타입은 JavaScript에서 여러 중요한 역할을 수행하며, 다음과 같은 이유로 사용된다.

    • 객체 간 상속 및 재사용: 프로토타입을 통해 객체들은 다른 객체의 속성과 메서드를 상속받을 수 있다. 이는 코드의 재사용성을 높이고, DRY(Don't Repeat Yourself) 원칙을 따르는 데 도움을 준다(여러 개의 Person 객체를 생성할 때, sayHello 메서드를 각 객체마다 복사하는 대신 Person.prototype에 한 번만 정의하여 모든 인스턴스가 공유 가능).
    • 메모리 효율성: 공통된 속성이나 메서드를 각 인스턴스마다 복사하는 대신, 프로토타입 객체에 한 번만 저장하고 여러 인스턴스가 이를 참조하게 함으로써 메모리를 절약할 수 있다. 수많은 객체를 생성할 때 이점은 더욱 커진다.
    • 동적인 확장성: 런타임에 프로토타입 객체에 속성이나 메서드를 추가하면, 해당 프로토타입을 상속받는 모든 객체가 즉시 해당 속성이나 메서드를 사용할 수 있게 된다. 이는 객체의 기능을 동적으로 확장하는 데 유용하다.
    • 클래스 문법의 기반: ES6에서 도입된 class 키워드는 사실 문법적 설탕(syntactic sugar)에 불과하며, 내부적으로는 여전히 프로토타입 기반 상속을 사용한다. class를 이해하기 위해서는 프로토타입에 대한 이해가 필수적이다.

    프로토타입은 JavaScript의 객체 지향 특성을 이해하고 활용하는 데 있어 근본적인 개념이다. 이를 통해 유연하고 효율적인 객체 간의 관계를 설정할 수 있다.


    3. Object.create()를 이용한 프로토타입 정의 방법

    Object.create(proto, [propertiesObject]) 메서드는 새로운 객체를 생성하고, 이 객체의 프로토타입을 첫 번째 인자로 전달된 proto 객체로 설정한다. 선택적으로 두 번째 인자로 propertiesObject를 전달하여 새로운 객체의 속성들을 정의할 수도 있다.

    이 방법은 기존의 생성자 함수 방식과 약간 다르게, 특정 객체를 직접적인 프로토타입으로 삼아 새로운 객체를 만들 때 유용하다.

    • 기본 사용법 예시:
    // 1. 프로토타입이 될 객체를 정의한다.
    const animalPrototype = {
      makeSound: function() {
        console.log("동물이 소리를 낸다.");
      },
      eat: function() {
        console.log("동물이 음식을 먹는다.");
      }
    };
    
    // 2. animalPrototype을 프로토타입으로 하는 새로운 객체를 생성한다.
    const dog = Object.create(animalPrototype);
    
    // 3. 새로운 객체에 고유한 속성을 추가한다.
    dog.name = "바둑이";
    dog.makeSound = function() { // makeSound 메서드를 오버라이드할 수도 있다.
      console.log("멍멍!");
    };
    
    dog.makeSound(); // 멍멍!
    dog.eat();       // 동물이 음식을 먹는다.
    console.log(dog.name); // 바둑이
    
    console.log(Object.getPrototypeOf(dog) === animalPrototype); // true

    이 예시에서 dog 객체는 animalPrototype을 자신의 프로토타입으로 가지므로, animalPrototype에 정의된 eat 메서드를 상속받아 사용할 수 있다. makeSound처럼 동일한 이름의 메서드를 dog 객체 자체에 정의하면, 프로토타입 체인 탐색 규칙에 따라 dog 객체 자신의 makeSound가 호출된다.

    • 속성 정의와 함께 사용하는 예시:
    const personPrototype = {
      greet: function() {
        console.log(`Hello, my name is ${this.name}.`);
      }
    };
    
    const user1 = Object.create(personPrototype, {
      name: {
        value: "Alice",
        writable: true,
        enumerable: true,
        configurable: true
      },
      age: {
        value: 30,
        writable: true,
        enumerable: true,
        configurable: true
      }
    });
    
    user1.greet(); // Hello, my name is Alice.
    console.log(user1.age); // 30

    여기서 propertiesObjectObject.defineProperties()에서 사용하는 속성 디스크립터(property descriptor) 형식과 동일하다.

    4. 왜 Object.create()를 사용할까?

    • 클래스/생성자 함수 없이 상속 구현: Object.create()는 클래스나 생성자 함수를 사용하지 않고도 특정 객체를 프로토타입으로 설정하여 상속 관계를 직접적으로 만들 수 있게 해준다. 이는 "클래스가 없는" 프로토타입 기반 상속의 본질에 더 가깝다고 볼 수 있다.

    • 순수한 객체 생성 (null 프로토타입): Object.create(null)을 사용하면 프로토타입 체인에 Object.prototype이 없는, 완전히 순수한 객체를 생성할 수 있다. 이는 특히 해시맵(hash map)처럼 순수하게 키-값 쌍으로만 이루어진 데이터를 다룰 때 유용하며, 예기치 않은 프로토타입 속성(예: toString, hasOwnProperty)과의 충돌을 피할 수 있다.

    • 복잡한 상속 구조: 특정 객체의 프로토타입을 동적으로 변경하거나, 다단계 상속 구조를 명시적으로 설정할 때 유연성을 제공한다.

    Object.create()는 객체의 프로토타입 체인을 명시적으로 제어하고자 할 때 강력한 도구가 된다. 하지만 일반적으로는 ES6 class 문법이나 생성자 함수 방식이 더 흔하게 사용되며, Object.create()는 특정 상황이나 고급 패턴에서 활용도가 높다.

    728x90
    반응형
    댓글