객체지향을 위한 JavaScript의 선택 [JavaScript Prototype]

Dengo·2023년 4월 18일
0

JavaScript

목록 보기
5/6

옛날 JS의 문제점

배경

자바스크립트를 조금이라도 공부한 사람은 아시겠지만, 본래 자바스크립트에서는 Class문법이 없었습니다.
이 문법이 지원되기 시작한 것은 2015년에 나온 ES6 버전부터 입니다.
하지만 이 문법이 나오기 이전부터 자바스크립트는 계속해서 쓰여왔을 것 입니다.
그렇다면 Class문법이 나오기 이전의 자바스크립트에서는 어떻게 해서 객체지향 프로그래밍을 구현해왔을까요?

Latte is... new F()

글을 쓰는 저도 이미 자바스크립트의 최신 문법이 마구 나온뒤에야 공부를 시작했지만...훨씬 이전 부터 웹 개발을 했던 분들이라면 이렇게 말할 수 도 있을 것 같습니다.
"나때는 말이야~ 함수로 객체를 만들었어~ 😤"

저는 자바스크립트를 처음 공부할 때 '클래스 대신에 함수로 객체를 만들 수 있다는 사실을 알고 있지만, 그것은 과거의 경우에나 그랬고 지금은 뭐 Class문법이 있으니까 그거 쓰면 되겠지' 라고 생각을 했었습니다.
하지만 오늘 알아볼 Prototype을 제대로 알기 위해서는 저도 여러분도, 앞으로 쓸일이 없을지 언정 일단은 알아야 할 것 같네요

function a() { }
const obj = new a() // 빈 객체 만들어짐

아주 간략하게 해보자면 위와 같이 객체를 만들 수 있습니다.
new연산자와 함께 함수를 호출하면 객체를 생성할 수 있도록 한 것 입니다.

객체는 만들었는데... 상속은?

함수를 통해서 객체를 찍어낼 수는 있는데 Class처럼 두 함수 간에 상속이 이루어져야 한다면 어떻게 해야할까요?
또, 메서드의 선언은 어떻게 해야할지도 궁금증이 생길 수 있습니다.

function Animal() {
  this.x = 10;
  this.walk = function() { console.log("아장아장"); }
}

function Rabbit() {
  Animal.call(this);
  this.y = 20;
}

new Rabbit() // { x: 10, y: 20, walk: f() }

Class가 없던 시절, 자바스크립트에서는 함수(클래스)간의 상속을 이렇게 구현해야 했을 것 입니다.

하지만 이 방법에는 문제가 있습니다.
Animal 함수안의 x의 경우, new Animal()을 통해서 만들어지는 모든 객체마다 고유의 값을 가져야 합니다.
반면에 walk는 만들어지는 모든 객체마다 동일하게 실행될 함수 입니다.

위와 같이 구현한채로 끝낸다면,
new Animal()을 통해서 객체를 만들때마다 매번 새로운 walk메서드가 굳이만들어지게 됩니다.
이는 비효율적인 메모리 관리로 이어지겠죠.
오늘 다룰 내용은 바로 이 문제를 해결하는 방법에 대해서 입니다.

Prototype 이란?

함수객체의 prototype 프로퍼티

위의 문제를 해결하기 위해 고안한 방법은 다음과 같습니다.

  • 함수(클래스)가 담을 메서드만을 담은 객체를 만든다.
  • 함수(클래스)에 프로퍼티를 하나 할당하고 이것이 메서드만을 담은 객체 를 가리키게 한다.
  • 함수(클래스)를 new연산자를 통해서 객체를 만들었을 때 이 만들어진 객체가 메서드만을 담은 객체를 자동으로 가리키게 한다.

자바스크립트에서는 함수도 객체입니다.
그렇기 때문에 프로퍼티를 할당할 수 있고 위의 두 번째 항목에서 프로퍼티에 메서드만을 담은 객체를 할당할 수 있었던거죠.
이 프로퍼티를 모든 함수에서 무조건 갖게하여 해결하였는데,
이 프로퍼티를 prototype이라고 합니다.

가령 우리가 흔히 자주 사용하는 배열도 Array라는 이름의 함수(클래스)를 통해서 찍어낸 객체입니다.
배열을 통해서 map, filter, reduce와 같은 메서드를 사용할 수 있는 이유는 바로 Array.prototype이 가리키는 객체 안에 이 메서드들이 전부 들어있기 때문입니다.

Array.prototype은 객체이기 때문에 개발자가 임의의 메서드를 집어넣을 수도 있습니다.

Array.prototype.top = function () { 
	return this[this.length - 1]; 
}

[1,2,3,4,5].top() // 5

prototype프로퍼티가 가리키는 모든 객체는 반드시 constuctor라는 프로퍼티를 갖고 있습니다.
그리고 constructor 프로퍼티는 자동으로 함수 자기 자신을 가리킵니다.
따라서 객체가 만들어지면 자신을 만든 함수가 무엇인지 기억할 수 있는것이죠.

function Animal() {
	this.x = 10;
}

Animal.prototype.constructor === Animal; // true
const obj = new Animal();
obj.constructor === Animal; // true

객체의 [[Prototype]] 숨김 프로퍼티

계속해서 배열 객체를 예시로 들어보겠습니다.

  • Array 함수(클래스)를 통해서 배열 객체 하나가 생성된다.
  • 이 배열 객체는 Array.prototype을 가리키게 된다.
  • 따라서 Array의 메서드에 접근할 수 있게된다.

이렇게 만들어진 객체가 Array.prototype에 접근할 수 있는 방법은 어떻게 될까요?
마찬가지로 객체 내부에 이 프로토타입 객체를 참조할 프로퍼티를 무조건 두게 하는 것 입니다.
이것을 [[Prototype]] 프로퍼티라고 합니다.
이 프로퍼티는 무조건 단 하나의 객체를 가지고 있어야 합니다.

자바스크립트의 모든 객체는 [[Prototype]] 숨겨진 프로퍼티를 가지고 있습니다.
이러한 숨겨진 프로퍼티에 접근하게 하기 위한 getter, setter함수가 있는데 이것을 __proto__ 라고 합니다.

const arr = [1,2,3];
arr.__proto__ === Array.prototype // true

여기서 arr에 map함수를 사용하려하는 상황을 가정해보겠습니다.

  • arr객체 안에서 map을 찾음, 근데 없음
  • [[Prototype]] 숨김 프로퍼티에 접근, 여기서 찾아서 사용.

이와 같은 자바스크립트 내부 동작을 Prototype Chaining이라고 부릅니다.

Prototype Chaining과 상속

다시 Animal과 Rabbit을 볼까요?

function Animal() {
  this.x = 10;
}

Animal.prototype.walk = functiono() {
	console.log("아장아장");
}

function Rabbit() {
  Animal.call(this);
  this.y = 20;
}

const rabbit = new Rabbit() // { x: 10, y: 20 }
rabbit.walk() // 에러! walk를 찾을 수 없습니다.

다시 처음 문제로 돌아와서, Animal의 메서드 walk를 상속받는 문제를 해결해보겠습니다.

우리가 앞서 학습한대로 Animal.prototype에 walk를 만들어서 new Animal()을 통해서 객체를 만들었을때는 walk메서드를 사용할 수 있게 하였습니다.

하지만 여전히 new Rabbit()을 통해 만들어진 객체에서는 사용하지 못합니다.
여지껏 배운 내용을 총 종합했을때,
메서드의 상속까지 이루어지게 하려면 어떻게 해야할까요?

function Animal() {
  this.x = 10;
}

Animal.prototype.walk = function() {
	console.log("아장아장");
}

function Rabbit() {
  Animal.call(this);
  this.y = 20;
}

// 진짜 메서드 상속!
Rabbit.prototype.__proto__ = Animal.prototype;

const rabbit = new Rabbit() // { x: 10, y: 20 }
rabbit.walk() // 아장아장

기존의 문제를 항목화해서 정리해보면

  • rabbit객체 안에서 walk를 찾음, 하지만 없음
  • rabbit의 [[Prototype]] 프로퍼티를 뒤짐, 하지만 역시나 없음

여기에서

  • [[Prototype]] 프로퍼티가 가리키는 Rabbit.prototype의 [[Prototype]] 프로퍼티가 Animal.prototype을 가리키게 함

을 추가해서 해결을 한 것 입니다.

다시 이야기 하면 Prototype Chaining이란,
객체안에서 프로퍼티를 찾는데 없으면 이 객체의 [[Prototype]] 프로퍼티를 뒤지는 행위라고 말을 했었죠.

이 말은 Rabbit.prototype역시 객체이기 때문에 [[Prototype]] 프로퍼티를 갖고 있을거고 Rabbit.prototype에서 없으면 또 이어서 [[Prototype]] 프로퍼티 안을 뒤질 것을 뜻합니다.

이처럼 Prototype Chaining을 통해서 Class문법 이전 세대에 상속을 구현할 수 있었습니다.

Prototype Chaining 그 끝은?

만약에 위 코드에서 객체 rabbit안에 끝내 못찾을 함수 eat을 찾는과정은 어떻게 될까요?

  • rabbit객체 안에서 eat찾음 , 없음
  • rabbit의 [[Prototype]] 프로퍼티를 뒤짐 (Rabbit.prototype을 뒤짐), 하지만 없음
  • Rabbit.prototype의 [[Prototype]] 프로퍼티를 뒤짐 (Animal.prototype을 뒤짐), 하지만 없음
  • Animal.prototype의 [[Prototype]] 프로퍼티를 뒤짐 (Object.prototype을 뒤짐), 역시 없음
  • Object.prototype의 [[Prototype]] 프로퍼티를 뒤짐 하지만 Object.prototype.__proto__는 null을 반환한다. 따라서 끝내 못찾음

앞서 모든 [[Prototype]] 프로퍼티는 반드시 단 하나의 객체만을 가져야 한다고 말을 했는데 이는 끝내 Object.prototype으로 Prototype Chaining이 이루어지기 위해서 일 것 입니다.

Object를 상속받은 Array

Array는 Object를 상속받은 함수(클래스)입니다.
다시 표현하면 아래와 같은 관계를 갖습니다.

Array.prototype.__proto__ === Object.prototype // true

그러니까 임의의 배열 객체에서 hasOwnProperty와 같은 Object의 프로토타입 메서드를 Prototype Chaining을 통해서 사용할 수 있는 것이죠.

하지만 toString과 같이 Array의 프로토타입 메서드로도 가지고 있고 Object의 프로토타입 메서드로도 가지고 있는 경우가 있습니다.

이것을 객체지향 용어로 Method Overriding(메서드 오버라이딩)이라고 부릅니다.
Object의 메서드 toString을 Array에서 다시 정의 한 것 이죠.

만약에 배열 객체에서 toString을 사용하려 한다면 Prototype Chaining에 의해서 Array.prototype에서 먼저 찾게 될 것 이고 그렇기 때문에 Object의 toString이 아닌 Array의 toString을 사용할 수 있을 것 입니다.

ES6, Class문법

class Animal {
  constructor() {
    this.x = 10;
  }
  walk() {
    console.log("아장아장");
  }
}

class Rabbit extends Animal {
  constructor() {
    super();
    this.y = 20;
  }
}

ES6가 2015년에 나온 이후로 오늘 살펴본 예제는 다음과 같이 표현할 수 있게 되었습니다.

이제는 이렇게 쓸 수 있는데 Prototype을 뭐하러 알아야 함?ㅋ 🤷

이라고 생각할 수 있는데 Class문법은 결국 모던 웹 개발에서 더 친화적인 객체지향을 구현할 수 있게 했을 뿐 오늘 우리가 살펴본 예제의 Syntactic Sugar에 불과합니다.

그러니까 겉모습만 사용하기 쉽게 한거고 내부적인 구현은 Prototype을 통해서 이루어져 있기 때문에 자바스크립트를 사용하는 개발자라면 반드시 알고 넘어가야하는 개념이 된 것 이죠 🙂

오늘 학습한 내용을 완벽히 짚고 넘어가면

  • 프로토타입 메서드란 무엇인지
  • 프로토타입이 굳이 왜 필요한지
  • 클래스의 메서드를 화살표 함수로 작성하게 되면 어떤 문제를 야기할지

와 같은 내용들을 자연스럽게 답할 수 있을 것 같습니다.

읽어주셔서 감사합니다!

참고

https://ko.javascript.info/function-prototype
을 비롯한 모던 JavaScript 튜토리얼의 모든 프로토타입 아티클
👍

profile
Software Engineer (전산쟁이)

0개의 댓글