JAVASCRIPT CORE #3 — 프로토타입(Prototype)과 상속(Inheritance)

한영재
13 min readJun 19, 2018

--

코딩을 시작한지 어느덧 5개월 이라는 시간이 지났다. 쉴 새없이 생경한 개념과 지식을 머리 속에 주입하며 달려왔다. 시간이 흐를수록 아는 것이 전보다 많아져도 무언가 공허했다. CS지식이 다소 부족한 비전공자 출신 개발자라는 배경은 차치하고, <React>, <Redux>, <Solidity> 등 새로운 무언가를 적용하고 응용하는데만 급급한 나머지 기초 공부에는 점점 소홀해 졌기 때문일까?

결국 찝찝함은 나를 움직이게 만들었다. 가장 기본으로 돌아가 처음부터 찬찬히 정리하며 나아가려 한다.

그리고 그 중 처음 공부 할 때 몇번이고 생활코딩과 MDN을 들여다보게 했던, 개인적으로 난이도가 높게 느껴졌던 세가지 개념에 대해 정리를 해보려한다.

1. 클로저(closure)

2. 디스(this)

3. 프로토타입(prototype)과 상속(Inheritance)

자바스크립트는 클래스라는 개념이 없다. 대신 프로토타입이라는 개념이 클래스를 대체한다. 자바스크립트와 같은 프로토타입 기반 언어는 이 프로토타입을 이용하여 새로운 객체를 만든다. 나아가 이렇게 생성된 객체 역시 또 다른 객체의 원형이 될 수 있는데, 이를 활용해 객체 지향적인 프로그래밍을 할 수 있게 해주는 것이다.

오승환님의 글 ‘[Javascript ] 프로토타입 이해하기’ 을 통해 정말 많은 관련 개념들을 잘 정리할 수 있었고 이를 바탕으로 내 미디엄 공부노트에 다시 정리한다. 프로토타입은 크게 두 가지를 알고 있으면 된다. 첫번째는 프로토타입 객체를 참조하는 prototype object, 두번째는 Prototype Link 이다. 그리고 이 둘을 통틀어 Prototype 이라 일컫는다. 이 둘을 잘 이해하기 위해서는 자바스크립트 함수와 객체의 내부적인 구조를 이해해야한다. 이번 글에서는 자바스크립트 함수와 객체의 내부적 구조를 훑어보고 나아가 프로토타입과 상속에 대해 정리해보려한다.

prototype object

일단 들어가기 앞서 자바스크립트는 함수를 기반으로한 언어이다. 아래를 보면 모든 객체의 선언은 사실상 함수를 통해서 하게 되는 것을 알 수 있고, new 생성자 함수는 객체를 뱉어내기 때문에 obj 뿐만 아니라 arrfunc 또한모두 객체 라는 것을 알아야 한다.

function Test() {} // => 함수var testObj = new Test(); // => 함수로 객체를 생성

testObj 객체는 Test이라는 함수로부터 파생된 객체이다. 즉 객체는 함수로부터 시작한다. 우리가 평소 리터럴로 생성하던 객체, 배열, 함수도 마찬가지이다.

var obj = {}; 는 아래와 같다.
var obj = new Object();
var arr = []; 는 아래와 같다.
var arr = new Array();
var func = function(a,b){return a + b}; 는 아래와 같다.
var func = new Function('a', 'b', 'return a + b');

그렇다면 이것이 프로토타입을 이해하는데 가장 핵심 중 하나인 Prototype Object랑 무슨 상관인지 함수가 정의될 때 발생하는 2가지 일을 살펴보자.

1.해당 함수에 Constructor(생성자) 자격 부여

Constructor 자격을 부여받아야만 new를 통해 객체를 만들 수 있다. 이것이 오로지 함수만 new 키워드를 사용할 수 있는 이유이다.

test는 constructor가 아닙니다!

2.해당 함수의 Prototype Object 생성 및 연결

함수를 정의하면 함수만 생성되는 것이 아니라 Prototype Object도 같이 생성된다.

함수 정의 시 아키텍쳐

그리고 생성된 함수는 위에서 살펴봤듯이 생성자 함수를 통해 생성된 객체이기 때문에 prototype 이라는 속성을 가지게 되는데 이는 Prototype Object를 가리킨다. Prototype Object는 일반적인 객체와 같으며 디폴트 속성으로 constructor__proto__를 가지고 있다.

Test 함수의 prototype 속성이 Prototype Object를 참조하고 있는 모습

constructor는 생성자 함수 자체를 가리키고 있다.
__proto__는 모든 객체가 빠짐없이 가지고 있는 속성으로 Prototype Link의 핵심 이기에 Prototype Link를 다룰 때 자세히 정리한다.

크롬 콘솔에서 아래와 같이 프로토타입을 활용하는 코드를 작성해보았다.

prototype 활용 예제 코드

위를 보면 Prototype Object는 일반적인 객체로서, 속성을 마음대로 추가 할 수 있다는 것을 알 수 있다. Car 함수를 통해 생성된 bmw와 audi 객체는 wheel이라는 속성을 별도로 직접 설정해주지 않았는데도 4라는 값을 나타내고 있다. 이게 과연 어떻게 가능한 것일까?

바로 Car.prototype을 참조하고 있기에 console.log(bmw.wheel)이 4를 나타내는 것이다. Car.prototype을 살펴보니 함수 정의 시, 기본적으로 생성되던 constructor__proto__ 외에 handlewheel 속성이 추가되어 있다.

Car.prototype 객체에 handle과 wheel 속성이 추가된 모습

그렇다면 과연 어떻게 생성자 함수를 사용해 만든 bmw, audi 객체가 Car.prototype 객체를 참조할 수 있는 것일까?

Prototype Link

Prototype Link의 핵심인 __proto__는 객체가 생성될 때 조상이었던 함수의 Prototype Object를 참조한다. 아래 예제를 살펴보면 bmw객체는 Car 생성자 함수로부터 생성되었으니 Car 함수의 Prototype Object를 가리키고 있다.

Car 함수의 Prototype Object를 참조하고 있는 __proto__

bmw객체가 wheel을 직접 가지고 있지 않기 때문에 wheel 속성을 찾을 때 까지 객체가 생성될 때 조상이었던 함수의 Prototype Object를 탐색한다. 최상위인 Object의 Prototype Object까지 도달했는데도 못찾았을 경우 undefined를 리턴한다. 이렇게 __proto__속성을 통해 상위 프로토타입과 연결되어있는 형태를 프로토타입 체인(Chain)이라고 한다.

생성된 bmw 객체(인스턴스)가 Prototype Object를 참조하는 모습

이때까지 정리한 것을 바탕으로 ECMAScript에서 정의한 프로토타입 개념을 다시 살펴보자.
“ 다른 객체들과 공유된 속성을 제공하는 객체 ”

이 정의를 다시 보자마자 든 생각은, ‘ 아 공유(참조)를 통해 상속한다는 개념이 이제는 더 선명하게 느껴진다’ 이었다. 더 선명한 이해를 바탕으로 프로토타입 체인을 현실세계에 빗대어 설명해보려한다. 우선 나 — 부모 — 조부모 가 있다고 가정하자. 조부모의 어떠한 행위(생성자 함수 실행)를 통해 부모님이 만들어졌고, 마찬가지로 부모님의 어떠한 행위를통해 내가 만들어졌다. 즉 나의 시작점을 찾기위해 프로토타입 체이닝을 타고 거슬러 올라가다보면 그 끝에서 시조를 알 수 있다.

이 때 인간의 수명은 무한하다고 가정해보자, 어느날 내가 100만원이 필요하다고 가정해보자. 우선 나의(나라는 객체의) 지갑(속성)에 100만원이 있는지 확인할 것이다. 있다면 내 돈을 쓰고, 없다면 부모님의 지갑을 참조한다. 부모님의 지갑에 100만원 있다면 부모님에게 받아쓰고 없다면, 다시 조부모님의 지갑을 참조한다. 이런식으로 거슬러 올라가다 최초의 조상 시조님의 지갑에 조차 100만원이 없다면 없다 즉, undefined 를 나타낼것이다. 즉 가족이라는 경제 공동체 아래에서 윗사람의 지갑을 후손들이 기꺼이 공유(참조) 할 수 있는 것이다. 내 기준으로 보면 조상들의 지갑을 가지고 있지는 않더라도 필요하다면 참조할 수 있는 것이다. 다소 억지스럽지만 비약하여 프로토타입 체인을 통한 상속을 나타내어봤다.

이런 프로토타입 체인 방식을 통한 상속이라는 개념에 대해 감이 잡혔다면, 아래에서는 실제 코딩을 할 때, new와 Object.create를 사용하는 두가지 상속 구현 방법에 대해 살펴보겠다. Object.create 방법이 후에 나왔다는 것을 보았을 때, new 생성자 부분에서 무언가 개선할 점 이 있었다는 것을 유추 할 수 있을 것이다. 바로 두 번째는 표준 API를 이용 여부이다. 표준 API가 있는데 왜 애초에 new 생성자를 이용하는가 궁금할 수 있는데 표준 API가 나온 시점이 늦은편이고 늦었다는건 브라우저의 지원이 좋지 않았기 때문이다(IE9이상부터 지원).

  1. new 생성자
  2. Object.create 생성자
  3. new 와 Object.create 의 결합

new 생성자

new 생성자를 이용해 상속을 구현한 아래 예시 코드를 살펴보자.

new 생성자를 사용한 잘못된 상속의 예

그냥 지나칠 수 있겠지만 instanceof = “내 후손이니?” 라고 물을 때 족보를 중요시 여기는 종가집에서는 네가 호모 사피엔스의 후손이기만 하고 아버지의 후손은 아니냐고 되 물을 수 있다. 이를 해결하기 위해 나온 방법이 prototype에 new로 새로운 객체를 만들어 Youngjae.prototype 으로 설정하는 것이다. 아래를 살펴보자.

new 생성자를 사용한 일반적인 상속

이렇게 Person 함수를 이용해서 새로운 객체를 생성한 다음 다른 함수의 prototype으로 설정하게 되면 instanceof Youngjae와 instanceof Person은 모두 true가 된다. 이는 바로 앞의 상속 방법과 비교하여 객체지향을 선호하는 사람들이 만족할만한 결과일 것이다. 하지만 겉으로는 문제가 없어보이나, 속은 그렇지가 못하다. 위에서 테스트했던 것처럼 instanceof Youngjae와 instanceof Person이 모두 다 true로 문제가 없는 듯 보이지만 또 다른 문제가 있다. 바로 constuctor의 연결이 깨지는 현상이다. 아래를 살펴보자

콘솔에서 youngjae 의 constructor 즉 생성자가 누군지 물어보았다.

보다시피 실제로 youngjae를 생성한 f Youngjae() 가 아닌 f Person() 이 나온다. 이는 Youngjae.prototype에 new Person() 으로 생성된 객체로 덮어져서 내부적인 constructor의 연결이 깨진 것이다.

Object.create 생성자

Object.create의 내부 형태는 아래와 같다.

Object.create = function(obj) {
function Ghost() {}
Ghost.prototype = obj;
return new Ghost();
};

위의 내부 형태를 보면 이 Ghost의 prototype이 인자로 받는 객체(obj)를 참조 하도록 한 후 new 생성자로 새로운 객체를 생성하여 리턴하게 된다. 여기서 유의할 점은 바로 Object.create() 함수의 인자(obj)를 상속을 할 객체 자체가 아닌, 상속할 객체의 prototype에 설정하는 점이다. 사용법은 아래와 같다.

function Person(name) {
this.name = name;
}
Person.prototype = {
whoIam: function() {
console.log("I'm " + this.name);
}
}
var youngjae = Object.create(Person.prototype); // #1
youngjae.name = "youngjae"; // #2
youngjae.whoIam(); // I'm youngjae

#1에서 Object.create()의 인자를 보면 Person이 아니라, Object.create(Person.prototype)으로 Person.prototype을 새롭게 생성되는 객체가 참조하도록 하고 있는 것을 볼 수 있다. 이렇게 Person를 직접 넘겨주지 않고 prototype을 넘겨주는 것은 자식 프로토타입과 부모 프로토타입의 연결(체이닝)을 위함이다.

다시 찬찬히 정리하면 인자로 넘어온 obj(Person.prototype)를 그대로 기본함수 Ghost의 prototype으로 설정해주고 있는 것이다. 그리고 new와 함수의 prototype을 설정하던 초창기 상속 방식과 비교를 해보면 #2에서 youngjae.name을 Person 함수의 생성자에 할당하는 것이 아니라 직접 설정하는 것을 볼 수 있다. 이러한 것이 어떻게 보면 개발자의 입장에서 다소 귀찮은 일이 될지도 모르지만 Object.create의 2번째 인자로 이러한 초기화 작업을 별도로 진행할 수 있다. 여기서는 생성자가 호출이 되지 않았고 오로지 prototype만을 넘겨줬기 때문에 별도로 초기화를 해주는 부분을 추가한 것이다.

하지만 Object.create를 사용하게 되면 new와는 비슷하지만 위에서 #2 youngjae.name을 직접 설정 했던 것과 같은 귀찮은 과정을 겪어야한다. 이러한 생성자를 그대로 이용하고 싶다면, 다음과 같이 생성자 호출을 직접할 수도 있다.

var youngjae = Object.create(Person.prototype);
Person.call(youngjae, "youngjae");
youngjae.whoIam(); // I'm youngjae

new 와 Object.create 생성자의 결합

기능적으로는 전혀문제가 없기 때문에, 단순히 내부적인 문제만으로 인해new 대신 Object.create를 사용하는 것이 다소 귀찮은 일 일지도 모른다. 이를 위해 방법이 하나 있다. prototype으로 설정하는 객체를 Object.create로 설정하면서 constructor를 다시 설정해주면 된다. 이는 내부적인 constructor 깨짐 문제를 바로 잡아주면서, 기존에 사용하던 new 생성자를 사용할 수 있는 방법이다.

Youngjae.prototype = Object.create(Person.prototype); /prototype 상속 
Youngjae.prototype.constructor = Youngjae; / 깨진 constructor 링크 수정
var youngjae = new Youngjae();

결언

‘프로토타입과 상속’ 파면 팔수록 미궁 속으로 빠지는 느낌이다. 한마디로 무간지옥 ㅜㅜ 하지만 언젠간 정복할 것이다!

--

--

한영재
한영재

Responses (1)