티스토리 뷰

TIL(Today I Learn)

[JS] 프로토타입이란?

MinsoftK 2021. 12. 6. 21:03
728x90
반응형

 

프로토타입을 공부하면서 굉장히 헷갈리는 부분이 많았다. 내용을 다시 정리해보면서 내가 헷갈렸던 부분을 공유하고 제대로 공부해보고자 한다. 멘토님이 이런 개념들을 공부하고 나라면 어떻게 사용할 것인가? 에 대한 고민이 많이 필요하다고 했다. 공부를 하면서 해당 개념에 대한 나의 생각을 정리해놓는 것이 당연하다고 생각했다. 하지만 생각을 정리하기 위해선 개념들을 확실히 알아야 했다. 아직은 모든 개념들이 연결이 되지는 않지만, 반복적 학습을 통해서 개념들을 연결시키려고 노력하려 한다.

 

1. 프로토타입이란?

자바스크립트에서는 프로토타입을 기반으로 상속을 구현한다. 여기서 프로토타입이란 어떤 객체의 상위 객체의 역할을 하는 객체로서 다른 객체에게 공유 프로퍼티를 제공한다.

이를 이해하기 위해선 내부 슬롯, 내부 메서드를 이해해야 했다. 책에선 다음과 같이 설명한다. 내부 슬롯, 내부 메서드란 자바스크립트 엔진의 구현 알고리즘을 설명하기 위해 ECMAScript 사양에서 사용하는 의사 프로퍼티, 의사 메서드다. 굉장히 이해하기 어려웠는데 여러 번 읽어본 결과 내가 이해한 바는 이렇다.

우리가 의사 코드에 대해서 한 번쯤은 들어봤는데, 동작은 하지 않지만 논리적인 흐름을 알기 위해 쓴 코드를 의사 코드라고 한다. 이처럼 ECMAScript의 사양을 설명하기 위해 내부 슬롯과 내부 메서드를 의사 코드로 사용한다는 의미로 해석했다. 그래서 이런 내부 슬롯과 내부 메서드를 가지게 되는데, 이를 프로퍼티 어트리뷰트라고 하고, 이는 JS엔진이 관리하는 프로퍼티의 내부 상태 값이다. 이는 표기를  [[ ... ]] 같이 한다.

말이 어려워 이해가 어려웠지만, 다음과 같이 이해했다. 결국 자바스크립트 엔진이 우리가 객체의 프로퍼티를 생성해주면 자동으로 프로퍼티 어트리뷰트를 생성해주게 된다. 즉, 프로퍼티가 생성이 되면 자바스크립트 엔진이 [[Value]], [[Writable]] .. 와 같은 내부 슬롯을 생성해주게 된다. 이는 자바스크립트의 내부 로직이기 때문에 개발자가 직접 접근하지는 못하고 __proto__라는 접근자 프로퍼티를 사용해 [[prototype]]을 간접적으로 접근한다.

var person = {
	name:'minsoftk',
}

console.log(Object.getOwnPropertyDescriptor(person,'name'));
// { value : "Lee", writable: true, enumerable: true, configurable: true }

위와 같은 getOwnPropertyDescriptor 메서드를 이용해서 name의 프로퍼티 어트리뷰트를 받아올 수도 있다. 즉, 프로퍼티 어트리뷰트란 'name'이라는 프로퍼티에 내부 슬롯이라는 상태 값을 가지고 있는 것으로 이해했다. 

 

2. 프로토타입을 왜 사용해야 할까?

생성자 함수 이야기를 먼저 해보면, 생성자 함수를 쓰는 이유는 객체 리터럴로만 객체를 생성하기엔 비효율적이기 때문이다. 따라서 생성자 함수를 이용해서 인스턴스를 생성하는 방식으로 객체 리터럴로 객체를 생성하는 방식의 단점을 보완할 수 있다. 만약 아래와 같은 객체를 객체 리터럴로 10개를 생성해야 한다면? 굉장히 코드가 길어지고 비효율적이다. 생성자 함수를 통해서는 new 연산자를 이용해서 인스턴스를 간편하게 생성할 수 있으므로 효율적으로 객체를 생성할 수 있다.

// 객체 리터럴로 객체 생성
const person = {
  name: 'minsoftk',
  age: '11111',
  weight: '65',
  height: '176',

  getName() {
    return this.name;
  },
  getAge() {
    return this.age;
  },
  getWeight() {
    return this.weight;
  },
  getHeight() {
    return this.height;
  },
};

// 생성자 함수로 객체 생성
function Person(name, age, weight, height) {
  this.name = name;
  this.age = age;
  this.weight = weight;
  this.height = height;

  this.getName = function () {
    return this.name;
  };
  this.getAge = function () {
    return this.age;
  };
  this.getWeight = function () {
    return this.weight;
  };
  this.getHeight = function () {
    return this.height;
  };
}

const person = new Person('minsoftk', 30, 65, 176);

 

마찬가지로 프로토타입도 코드 재사용과 효율성을 위해서다. 만약 생성자 함수로 새로 생성되는 인스턴스에 만약 공통된 메서드가 포함된 경우를 생각해보면, 만약 데이터 프로퍼티가 다른 값들로 여러 개의 인스턴스들이 생성이 될때, 똑같은 메서드를 가진다고 생각하면 모든 인스턴스들이 메서드를 중복 생성하고 있다. 모든 인스턴스가 동일한 메서드를 중복 소유하는 것은 메모리를 불필요하게 낭비한다고 한다. 그래서 이를 위해 prototype에 해당 메서드를 등록해서 인스턴스들이 공유해서 사용할 수 있게 만들어준다. 그래서 자바스크립트는 프로토타입을 기반으로 상속을 구현한다고 한다. 

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

  this.getName = function () {
    return this.name;
  };
  this.getAge = function () {
    return this.age;
  };
  this.getWeight = function () {
    return this.weight;
  };
  this.getHeight = function () {
    return this.height;
  };
}

const minsoftk = new Person('minsoftk', 30, 65, 176);
const nklcb = new Person('nklcb', 35,180, 55);
console.log(minsoftk.getName());
console.log(nklcb.getName());

 

위의 코드를 아래와 같이 바꿔주게 된다면 Person라는 생성자 함수의 prototype에 getName, getAge 등등 메서드들이 등록이 된다. 따라서 인스턴스들은 prototype의 메서드를 참조하게 된다. 이 예제를 통해 프로토타입에 대한 이해를 할 수 있었다. 그러면 인스턴스를 생성할 때마다 해당 인스턴스들이 메서드를 prototype에서 참조를 하기 때문에 메모리 절약을 할 수가 있다. 어느 정도로 절약이 되는 건지는 감이 오지는 않으나 상속받아서 메서드를 사용한다는 점은 굉장히 효율적이라는 생각이 든다. 또 코드를 보다 보면 Person 생성자 함수와 프로토타입 메서드들을 묶어주고 싶다는 생각이 강하게 든다. 이럴 땐 전체를 Person이라는 즉시 실행 함수로 묶어줘서 생성자 함수를 반환하게 만들어 줄 수도 있다. 처음엔 굳이 이렇게까지 묶어야 하나 생각했지만, 코드가 굉장히 길어질 경우를 생각해보면 응집도가 높을수록 가독성이 좋기에 묶어 주는 것이 좋다고 생각했다. 

 

// 프로토타입 메서드 생성
// 즉시실행함수로 응집성 높여줌

const Person = (function () {
  function Person(name, age, weight, height) {
    this.name = name;
    this.age = age;
    this.weight = weight;
    this.height = height;
  }
  Person.prototype.getName = function () {
    return this.name;
  };
  Person.prototype.getAge = function () {
    return this.age;
  };
  Person.prototype.getWeight = function () {
    return this.weight;
  };
  Person.prototype.getHeight = function () {
    return this.height;
  };
  return Person;
})();
const minsoftk = new Person('minsoftk', 30, 65, 176);
const nklcb = new Person('nklcb', 35, 180, 55);
console.log(Person.prototype.getName === minsoftk.getName);
console.log(minsoftk.getName());
console.log(nklcb.getName());

 

 

3. 헷갈린 부분

결국 프로토타입은 객체 간 상속을 구현하기 위해 사용한다. 중복되는 메서드를 상위 객체인 prototype에 넣어주게 된다면 인스턴스를 생성해도 하나의 메서드를 공유해서 쓸 수 있다. 만약 100개의 인스턴스를 생성해도 하나의 메서드를 공유해서 사용할 수 있다. 하지만 생성자 함수 안에 메서드를 넣게 된다면, 모든 인스턴스들이 각각 메서드를 가지고 있기 때문에 재사용성 측면이나 메모리를 낭비하게 된다. 그런데 내가 헷갈렸던 부분은 프로토타입 객체에 대한 설명 부분이었다.

책의 내용에서는 "모든 객체는 하나의 프로토타입을 갖는다. 그리고 모든 프로토타입은 생성자 함수와 연결되어 있다. 즉, 객체와 프로토타입과 생성자 함수는 서로 연결되어있다." 이를 해석해서 작성하면 "모든 객체는 하나의 [[prototype]]을 갖는다. 그리고 모든 프로토타입(객체의 프로토타입)은 생성자 함수와 연결되어 있다."인데 같은 프로토타입으로 용어가 쓰이다 보니 이해하는데 굉장히 어려웠다.  이를 정확하게 이해하기 위해선 내부 슬롯인 [[prototype]]에 대해서 정확하게 이해를 하고 있어야 했다.

모든 객체는 프로토타입이라는 내부슬롯 [[prototype]]을 가지고 있다. 객체 생성 방식에 따라 프로토타입이 결정되고 [[prototype]]에 저장된다. 그리고 모든 객체는 __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입을 간접적으로 접근할 수 있다. 즉, 자기 프로퍼티의 [[prototype]]에는 상속을 받아오는 상위 객체인 프로토타입이 저장되어 있다. 이런 개념을 이해해야 각각의 프로토타입을 제대로 해석하고 이해할 수 있었다. 

 

4. 결론

생성자 함수를 공부하면서 this에 대한 내용도 공부가 필요했다. 함수를 호출하는 방식에 따라 this가 달라지기 때문이다. 아직은 this에 대한 이해가 많이 부족하기 때문에 this에 대한 내용도 프로토타입과 연관 지어서 따로 정리해볼 생각이다. 사소한 개념들이 다 연결이 되는데 이를 머릿속에서 정리하기란 굉장히 어려운 것 같다. 이해는 가지만 정리가 안되는 느낌이 강하다. 아직 정확하게 아는 것이 아니라는 의미인데, 유난히 프로토타입이 나에겐 어렵다. 용어적으로도 이해하기 어려워 그런 것 같다. 그럴 때마다 계속 반복해서 읽으면서 이해하려고 노력하는 방법밖에 없는 것 같다.

 

혹시 틀리거나 잘못된 내용이 있다면 댓글로 알려주세요. :)

 

📕 Reference: 

모던 자바스크립트 Deep Dive 19장 프로토타입

https://poiemaweb.com/js-prototype

728x90
반응형

'TIL(Today I Learn)' 카테고리의 다른 글

[JS] CORS 깊게 이해하기  (0) 2021.12.28
[JS] this에 대해서  (0) 2021.12.11
Pollyfill이란? Babel이란?  (2) 2021.11.07
HTML, CSS 기본기 다지기(CSS)(6)  (0) 2021.10.05
Git 기본기 다지기  (0) 2021.10.01
댓글