Javascript Prototype Chain 이해하기

Posted by negabaro kim on Friday, March 8, 2019 Tags: js   15 minute read

프로토타입 체인

객체와 객체의 연결을 통한 단방향 공유 관계를 칭함

통상적인 상속 개념과 다른 javascript에서의 상속

var objectMadeByLiteral = {};
var objectMadeByConstructor = new Object();

통상적으로 위 코드는 Ojbect객체의 인스턴스를 만든것에 불과하니 상속받았다고 표현하긴 힘들다. 하지만 자바스크립트에서는 조금 다른 개념으로 생각해야 한다

지금 만들어진 객체가 Object 타입의 인스턴스 객체인 것도 맞지만 프로토타입을 이용한 상속을 지원하는 자바스크립트에서는 Object 생성자의 프로토타입을 상속받은 객체라고 표현하는 게 더 정확한 표현이다.

사실 상속이라는 표현도 OOP의 관점에서 사용하는 단어로 표현하고자 한 것일 뿐 실제로는 링크드리스트 형태의 참조를 통한 객체끼리의 연결에 가깝고 클래스 메커니즘처럼 정적이지 않고 매우 동적이다.

이런 동적인 연결이 좋다는 뜻은 아니다. 상속 구조의 변경이 언어나 엔진 차원에서 아무 제약이 없다 보니 약속된 컨벤션 규칙 혹은 안티 패턴에 대한 이해가 없어 제대로 사용하지 않았을 때는 헬게이트가 열리게 되는 단점이기도 하다.

__proto__를 이용한 상속

자바스크립트에서는 프로토타입을 이용해 객체와 객체를 연결하고 한쪽 방향으로 상속을 받는 형태를 만들 수가 있다.

자바스크립트에서 상속을 받는다는것은 다른말로 객체를 연결해 멤버 함수나 멤버 변수를 공유한다는 뜻이다.

이런점을 이용해 자바스크립트에서는 상속과 비슷한 효과를 얻는다.

__proto__

__proto__ 속성은 ECMAScript의 스펙 [[Prototype]] 이 자바스크립트로 노출된 것인데 예전 스펙이 legacy 처럼 남아있는 것이다. 모던 브라우저 개발자 도구에서도 디버깅 편의상 노출하고 있지만 개발 코드에서 직접적으로 접근하는 것은 피해야 한다. 여기서도 프로토타입의 이해를 돕기 위해 사용한다. __proto__ 이 참조하는 객체를 확인해야 하는 상황(예를 들면 프레임웍 개발)이라면 __proto__ 속성을 직접 사용하지 말고 Object.getPrototypeOf() 를 이용해서 참조하면 된다

예제)

var sana = {
    skill: "샤샤샤"
}

var chang = {
}

위 예제를 보자. chang객체는 sana객체의 멤버변수인skill에 접근할 방법이 없다. 이때__proto__라는 특수한 속성을 이용한다.

var sana = {
    skill: "샤샤샤"
}

var chang = {
}

chang.__proto__ = sana;
chang.skill // '샤샤샤'

코드해설

위 예제에서는 chang 의 __proto__ 속성이 sana 객체를 가리키고(참조)있다.

이 말은 sana객체의 멤버 변수나 메서드를 몇 가지 제약은 있지만 마치 chang객체가 소유한 것처럼 사용할 수 있다는 것이다.

이 과정에서 상속과 비슷한 효과를 얻을 수 있게 된다.

다른 클래스를 통한 상속의 경우 클래스의 상속 정보를 이용해 상속 구조의 모습을 가진 새로운 객체를 찍어내는 반면 프로토타입을 통한 상속 구조는 존재하는 객체와 존재하는 객체의 동적인 연결로 풀어낸다.

그렇다 보니 이미 객체가 만들어진 상태라도 상속된 내용이 변경되거나 혹은 추가되기도 하고 아예 상속 구조를 바꿀 수도 있게 된다.

물론 대부분 안티 패턴이다

Javascript에서의 상속=공유다.

예제)

var sana = {
    skill: "사또떨"
}

var chang = {
}

chang.__proto__ = sana;
chang.skill // '사또떨'

sana.skill = '사다닥'; //상속받은 객체의 내용 변경

chang.skill // '사다닥'


sana.buzzword = "치즈김밥?"
chang.buzzword // '치즈김밥?'

delete sana.buzzword // 상속받은 객체의 내용 삭제
chang.buzzword // undefined

chang은 멤버변수가 하나도 없었는데 부모객체(sana)에서 정의한 내용을 그대로 참조가능하다.

일반적인 클래스개념에서는 상상도 할 수 없는 일이다.

이런 방식이 좋다 나쁘다를 떠나서 자바스크립트에서는 이렇게밖에 못한다.

사실 이런 성질을 이용해 클래스 메커니즘을 흉내 내는 것은 비교적 쉬운 반면 클래스 기반의 언어가 프로토타입 메커니즘을 흉내내기는 매우 어려울 것이다.

아무튼 객체와 객체의 연결을 통한 단방향 공유 관계를 프로토타입 체인이라고 한다.

자바스크립트의 프로토타입 상속은 사실 이 내용만 제대로 이해하면 90% 이상 이해했다고 봐도 된다.

이런 연결을 직접적으로 코드에서 __proto__ 에 접근하지 않고 만들어야 하는데 그 방법은 몇 가지가 있다.

하지만 그 전에 프로토타입을 통해 식별자를 찾는 과정을 조금 더 살펴보자.

프로토타입 룩업

프로토타입 룩업이라는 과정은 클래스의 상속과 비교되는 특징 중 하나이다.

프로토타입 식별자 룩업 방법

식별자 룩업방법에는 이하 2가지가 있다.

  1. 프로토타입 룩업
  2. 스코프 룩업

여기서는 프로토타입 룩업을 통해 식별자를 찾아가는 방법에 대해 살펴보자.

프로토타입 룩업

프로토타입 룩업이라는 과정은 클래스의 상속과 비교되는 특징 중 하나이다.

쉽게 이야기하면 클래스 상속은 객체를 만든 시점에 이미 이 객체가 상속구조를 통해 어떤 멤버들을 보유하고 있는지 결정된 반면

프로토타입 체인을 통한 상속의 경우 실행을 해봐야 객체가 해당 멤버를 가지고 있는지 알 수 있다.

물론 개발자는 알고 있겠지만 자바스크립트 엔진의 관점에서는 메서드를 실행할 때 동적으로 해당 메서드를 찾아서 실행한다는 의미다. 그래서 이미 만들어진 객체의 상속된 내용이 변경될 수 있는 것이다. 객체가 만들어 질 때가 아닌 실행할 때의 내용이 중요하니까 말이다. 이렇게 프로토타입 체인을 통해 객체의 메서스나 속성을 찾아가는 과정을 프로토타입 룩업이라고 한다.

var jyp = {
    skill1: '공기반소리반'
};

var twice = {
    __proto__: jyp,
    skill2: '에너지'
};

var itzy = {
    __proto__: twice,
    skill3: '카리스마'
};

itzy.skill1 // '공기반소리반'

위 코드에서는 객체3개를 만들고 각 객체의 __proto__속성을 이용해 itzy->twice->jyp로 연결했다. itzy객체에 없는 멤버변수인 skill1이라는 속성에 접근하려하면 자바스크립트 엔진은 아래와 같은 작업을 수행한다. (엔진의 최적화 설정에 따라 단계를 축소될 수 있다.)

  1. itzy객체 내부에서 skill1 속성을 찾는다 -> 없다.
  2. itzy객체에 __proto__ 속성이 존재하는지 확인한다 -> 있다.
  3. itzy객체의 __proto__ 속성이 참조하는 객체로 이동한다 -> twice객체로 이동
  4. twice객체 내부에 skill1 속성을 찾는다 -> 없다.
  5. twice객체에 __proto__ 속성이 존재하는지 확인한다 -> 있다.
  6. jyp객체 내부에 __proto__ 속성이 참조하는 객체로 이동한다 -> jyp객체로 이동
  7. jyp객체 내부에서 skill1 속성을 찾는다 -> 있다!!
  8. 찾은 속성의 값을 리턴한다.

단순히 말하면 __proto__의 연결을 따라 링크드리스트를 탐색하듯 탐색해서 원하는 키값을 찾는것이다.

어떤 객체에도 존재하지 않는 속성인 shortcomings을 찾게되면?

어떤 객체에도 존재하지 않는 shortcomings이라는 속성을 찾게되면 7번부터 다른 과정을 거치게 된다.

(7번부터)

  1. jyp객체 내부에서 skill1 속성을 찾는다 -> 없다..!
  2. jyp객체에 __proto__ 속성이 존재하는지 확인한다. -> 있다.
  3. jyp객체의 __proto__ 속성이 참조하는 객체로 이동한다. -> Object.prototype 로 이동
  4. Object.prototype 에서 shortcomings 속성을 찾는다. -> 없다.
  5. Object.prototype 에서 __proto__ 속성을 찾는다. -> 없다.
  6. undefined리턴

모든 프로토타입 체인의 끝은 항상 Object.prototype 이다 그래서 Object.prototype은 종점과 같기 때문에 __proto__ 속성이 없다.

shortcomings이라는 속성은 프로토타입의 마지막 단계인 Object.prototype 에 존재하지 않고 Object.prototype 에는 __proto__ 속성이 존재하지 않으니 탐색을 종료하고 undefined를 리턴한다.

크롬과 Node.js의 자바스크립트 엔진인 V8은 이 과정을 최적화해 탐색 비용을 줄여 퍼포먼스를 향상시켰다. 단일 링크드리스트 형태로 한쪽 방향의 연결이다 보니 상속이란 개념을 얼추 적용할 수 있다. itzy에서 jyp의 속성은 접근할 수 있지만 jyp에서 itzy의 속성은 접근할 수 없다.

메소드 오버라이드

itzy에서 jyp의 속성은 접근할 수 있지만 jyp에서 itzy의 속성은 접근할 수 없다.

var jyp = {
    skill1: '공기반소리반'
};

var twice = {
    __proto__: jyp,
    skill2: '에너지'
};

var itzy = {
    __proto__: twice,
    skill3: '카리스마'
};

jyp.skill2 // undefined  --> twice의skill2에 접근할 수 없다!
jyp.skill3 // undefined  --> itzy의skill3에 접근할 수 없다!

이런 점을 이용해 메서드 오버라이드를 구현할 수 있다.

var jyp = {
    skill: '공기반소리반'
};

var twice = {
    __proto__: jyp,
    skill: '에너지'
};

var itzy = {
    __proto__: twice
};

jyp.skill //'공기반소리반'
itzy.skill //'에너지'

itzy 의 skill속성을 실행하면 프로토타입 룩업을 통해 itzy->twice로 이동하고 twice에 이미 해당 속성이 있기 때문에 jyp까지 안올라가고 twice의 skill속성이 실행된다

자바스크립트에서는 이런 상황을 jyp의 skill메서드를 twcie가 오버라이드 했다라고 말한다.

생성자를 통한 프로토타입 체인의 문제점

생성자를 이용해 객체를 생성하면 생성된 객체는 생성자의 프로토타입 객체와 프로토타입 체인으로 연결된다.

function Jyp(skill) {
    this.skill = skill;
}

Jyp.prototype.getSkill = function() { return this.skill; };
var twice = new Jyp('공기반소리반');

매우 간단한 생성자 예제이다. Jyp 가 만들어낸 객체twice 는 skill 이라는 속성을 가지고 있고

getSkill 이라는 프로토타입 메서드를 사용할 수 있다.

twice 객체가 Jyp 의 프로토타입 메서드에 접근할 수 있는 이유는 twice 객체의 __proto__ 속성이 Jyp.prototype 을 참조하고 있기 때문이다.

이 과정은 생성자를 new 키워드와 함께 사용할 때 엔진 내부에서 연결해준다.

실제로 엔진은 아래와 같은 행동을 한다. (이해를 돕기 위한 코드이다)

var twice = new Jyp('공기반소리반');

// 엔진 내부에서 하는 일
twice = {}; // 새로운 객체를 만들고
Jyp.call(twice, '공기반소리반'); // call이용해 Jyp함수의 this를 p로 대신해서 실행해주고
twice.__proto__ = Jyp.prototype; // 프로토타입을 연결한다.


twice.getSkill(); // '공기반소리반'

위 코드는 생성자가 만들어 내는 객체가 어떻게 생성자의 =prototype=과 연결되는지 보여주고 있다.

위 과정을 통해서 프로토타입 룩업시 twice -> Jyp.prototype 의 탐색 과정을 만들어 낼 수 있다.

다만 문제가 있다.

현재의 코드에선 Object 타입을 제외한다면 의도적으로 구현된 상속이란 개념은 아직 들어가 있지 않다.

의도적으로 구현된 상속이라함은 통상적인 프로그램에서의 상속개념을 얘기한다 부모,자식간에 어떤 속성을 공유하지 않고 독자적인 인스턴스를 새로 생성할 수 있는것 말이다.

twice.__proto__ = Jyp.prototype 이부분이 문제다. twice객체가 Jyp객체의 prototype을 참조,즉 공유하고 있으므로

twice.prototype.getSkill = function() { return "없어요.." };

위 코드와 같이 twice인스턴스의 prototype을 수정하면 Jyp객체에서 getSkill해도 없어요가 리턴되버린다..

jyp는 skill이 있는데 불구하고 prototype을 공유한거 때문에 둘다 skill이 없어요 가 되버리는것.

생성자를 통한 프로토타입 체인의 문제점을 해결하기 위한 꼼수

좋지 않다. 위에서 설명한 문제를 해결하려면 어떻게 해야할까?

그 옛날 선조 자바스크립트 개발자분들은 이런생각을 했다.

__proto__에 직접 대입만 안하면 되는거지??

중간다리용 객체를 만들어서 중간다리의 prototype에 부모의 prototype을 대입해버리면 어떨까?(__proto__에 대입하면 참조,공유 해버린다)

성공적인 발상이고 멋진 꼼수였다.

var jyp = {
    skill: '공기반소리반'
};

function Sixteen() {} //중간다리용 객체
Sixteen.prototype = jyp;

var twice = new Sixteen();
twice.skill = '에너지';

console.log(jyp.skill);    //'공기반소리반'
console.log(twice.skill);  //'에너지'

위 예제와 같이 Sixteen 이라는 중간다리용 객체 (대리 혹은 임시)를 만들고 사용자가 직접 __proto__를 건드리지 않고 중간다리에 Sixteen.prototype = jyp 함으로서 참조,공유가 아닌 대입,교체을 하게되어 새로운 인스턴스를 만들 수 있게 된 것

이걸로 자바스크립트에서 통상적으로 프로그래밍에서 말하는 상속이 실현가능 하게 되었다.

__proto__ 를 직접 건드리는건 왜 문제일까?

정확히는 __proto__ 가 참조하는 prototype안에 name이라는 속성이 문제다. 이 속성은 굉장히 특별해서 Twice의 프로토타입으로 그대로 받아서 사용하게 되면 name이라는 속성을 Twice의 인스턴스(twice)들이 그대로 공유해서 사용하게 되는데 이 부분때문에 참조,공유를 해버리는 문제가 발생한다.

그래서 이 name속성을 받지않게 하기위해 Sixteen같은 임시 생성자를 이용해 순수하게 프로토타입 체인만 연결된 빈 객체를 만들어서 Twice프로토타입에 대입시킨것이다.

Object.create();

위에서 설명한 꼼수는 중간다리용 객체를 만들어야 되는등 코드가 굉장히 장황하다

그래서 이 꼼수를 착안해서 표준API가 등장했는데 그것이 Object.create()이다.

var jyp = {
    skill: '공기반소리반'
};

var twice = Object.create(jyp);

twice.skill = '에너지';

console.log(jyp.skill);    //'공기반소리반'
console.log(twice.skill);  //'에너지'

위 예제와 같이 번거로운 임시 생성자를 만들 필요 없이Object.create의 첫번째 인자에 상속받을 생성자를 대입해주면 끝이다.

ES6에서 class등장

자바스크립트는 상속이 이루어지는 개념은 간단한데 그것을 구현하는 코드가 상당히 장황하다. Object.create() 을 사용해도 장황한 건 마찬가진데

이를 보완하기 위해 ECMAScript6에서 class 스펙이 추가되었다.

클래스라고는 하지만 새로운 개념이 아니고 상속의 구현 원리는 기존과 동일한 내용으로 장황했던 코드를 간결하게 하는 숏컷이 추가됐다고 생각하면 된다.

class를 이용하면 이하와 같이 쓸 수 있다.

class Jyp {
    constructor(name) {
        this.name = name;
    }

    getSkill() {
        return this.name;
    }
}

class Twice extends Jyp {
    constructor(name) {
        super(name); // 생성자 빌려쓰기 대신....super 함수를 이용 한다.
        this.energy = 100;
    }

    getEnergy() {
        return this.energy;
    }

}

코드가 간결해지고 이해하기 쉽게 바뀌었다.

코드는 달라졌다 하더라도 이를 통해 만들어진 객체의 프로토타입 체인 연결 구조는 기존과 동일하고 또한 동일한 방식의 프로토타입 룩업으로 식별자를 찾아간다.

정리

  1. Javascript에서는 __proto__를 이용해서 상속 비슷한것을 구현해낸다
  2. __proto__를 통해서 상위 객체를 찾아가는 행위(상속 비슷한것을 하는)를 프로토타입 체인이라 칭한다.
  3. __proto__에 직접 대입시 부모 프로토타입의 속성을 공유,참조 한다.
  4. 공유,참조 하기 싫을때는 중간 생성자를 통해서 부모 프로토타입을 대입받는 형식으로 꼼수를 사용해서 부모 속성을 참조,공유 하지 않는 독자적인 객체를 구현했다.
  5. 꼼수를 사용하면 코드가 장황해져서 Object.create()가 등장했다.
  6. Object.create()도 장황해서 ES6부터 class가 등장했다.
https://meetup.toast.com/posts/104
https://qiita.com/howdy39/items/35729490b024ca295d6c
http://maeharin.hatenablog.com/entry/20130215/javascript_prototype_chain