Javascript는 프로토타입 기반 언어이다.

huurray·2021년 4월 13일
6
post-thumbnail

들어가기

Java, C++과 같은 클래스 기반 객체지향 프로그래밍 언어와 달리 Javascript는 프로토타입 기반 객체지향 프로그래밍 언어이다.

ES6부터 Class 문법이 추가되었지만 문법이 추가되었다는 것이지 Javascript가 클래스 기반으로 바뀌었다는 것은 아니다. 사실 Class도 프로토타입 기반의 함수일 뿐이다. 그럼 프로토타입이 Javascript에 어떻게 구성되어 있는지 정리해보자.

Javasciprt의 프로토타입은 Evan Moon님의 블로그의 프로토타입 글을 보고 도움을 많이 받았다. 정말 이해가 잘되게 설명해주셔서 그 글을 참고하며 재작성해보려한다.

프로토타입 개념

프로토타입(prototype)이라고 하면 일반적으로 자바스크립트만을 떠올리지만 사실 프로토타입은 자바스크립트에서만 사용되는 것은 아니고 하나의 디자인 패턴이다.

프로토타입 패턴의 특징은 객체를 복제하면서 생성한다는 점이다.

클래스 패턴 언어(Java)와 비교해보며 알아보자. 예를 들어 게임 캐릭터를 하나 생성한다고 생각해보자. 처음 캐릭터가 생성될 때 너무 맨몸이면 유저가 싫어할 것 같으니 기본적인 무기과 갑옷을 가진 상태로 생성하고자 한다.

// Player.java
class Weapon {}
class Armor {}
class BasicSward extends Weapon {}
class BasicArmor extends Armor {}

class Player {
    public Weapon weapon;
    public Armor armor;

    public Player() {
        this.weapon = new BasicSward(); // 기본 무기
        this.armor = new BasicArmor(); // 기본 갑옷
    }
}

Player 객체는 자신이 생성될 때 BasicSward 객체와 BasicArmor 객체까지 함께 생성해야한다. 이런 경우 그냥 Player 객체만 생성하는 상황보다 객체의 생성 비용이 높다고 할 수 있다. 그리고 다른 기본 아이템을 추가할수록 Player의 객체의 생성 비용 더 높아질 것이다.

그럼 이런 비용을 줄이는 좋은 방법이 없을까? 만약 캐릭터가 처음 생성될 때 가지고 있는 아이템이 항상 같다는 전제 조건이 있다면 생성 비용이 높은 Player객체를 딱 한번만 생성하고 그 다음부터는 생성된 객체를 복사해서 사용해도 될 것 같다는 생각이 든다.

이런 관점으로 접근하는 것이 바로 프로토타입 패턴이라고 할 수 있다. 프로토타입, 즉 원본 객체가 존재하고 그 객체를 복제해서 새로운 객체를 생성하는 방법인 것이다. (프로토타입의 사전적인 정의는 "본래의 형태", "시제품" 등이고 여기서는 "본래의 형태"의 의미로 사용된 듯하다.)

자바스크립트의 프로토타입

자바스크립트도 프로토타입 기반 언어이기 때문에 역시 복제를 통해 새로운 객체를 생성한다. 그럼 이제 자바스크립트가 무엇을 복제해서 객체를 생성하고 있는 것인지 알아보자.

function User () {}

const evan = new User();

console.log(11, evan);
console.log(22, typeof evan);
11, User { __proto__: Object }
22, "object"

User함수에 new라는 생성자를 붙여서 객체를 인스턴스화 했다. 그런데 evan의 타입을 찍어보면 함수가 아니고 객체이다. evan은 무엇으로부터 복제된 것일까? 사실 evan은 User함수의 prototype 객체를 복제한 것이다. 자바스크립트의 모든 객체는 prototype 객체를 가지고 있고 그 prototype을 복제하면서 객체를 생성한다.

function User () {}

console.log(11, User.prototype);
console.log(22, typeof User.prototype);
11, { constructor: f User(), __proto__: Object }
22, "object"

처음 User에서 함수를 선언할 때 함수의 프로토타입 객체도 함께 생성된다. 그리고 이 프로토타입 객체는 함수를 사용해서 새로운 객체를 생성할 때 원본 객체 역할을 해줄 객체를 의미한다.

즉, new User()라는 문법을 사용하여 새로운 객체를 만들게 되면 User 함수 자체가 아니라 User 함수가 생성될 때 함께 생성된 User 함수의 프로토타입 객체를 복제해서 새로운 객체를 만든다는 것이다.
(User함수 이전의 프로토타입이 어디서 왔는지는 아래의 프로토타입 체인 파트에서 설명하겠다.)

그럼 프로토타입 내부를 들여다 보자. 프로토타입에 들어있는 constructor__proto__는 뭘까?

constructor 프로퍼티

이 프로퍼티는 단어 그대로 원래의 생성자 함수(자기 자신)을 참조하고 있다.

console.log(User.prototype.constructor === User); // true

자신에게 자심을 참조하는 프로퍼티를 굳이 왜 가지고 있을까 싶지만 자신의 의해 생선된 인스턴스 객체 입장에서 봤을때 그 원형이 무엇인지 알 수 있는 수단이 된다. (앞서 말했듯 인스턴스는 생성자의 prototype을 그대로 복제하므로!)

__proto__ 프로퍼티

새롭게 생성된 객체는 원본 객체와의 연결을 가지고 있다. 이때 이 연결을 프로토타입 링크(Prototype Link)라고 한다. 그리고 이 링크가 담기는 프로퍼티가 __proto__ 프로퍼티이다. 또한 프로토타입 링크는 생략가능하다.

const evan = new User();
console.log(evan.__proto__.constructor === User); // true
console.log(evan.constructor === User); // true

ECMAScript 명세에는 __proto__ 프로퍼티가 아니라 [[prototype]]이라는 명칭으로 정의되어 있다. __proto__는 옛날에 사용한 방식이고 현재는 브라우저들이 이 프로토타입 링크를 구현한 대상일 뿐이라고 한다. 많은 브라우저에서 사용하기 때문에 호환성 유지 차원에서 인정되었지만 ECMAScript 명세에는 Object.getPrototypeOf()를 통해 접근하는 것을 권장한다.

프로토타입 체인

여기서 한가지 의문이 든다. 모든 객체가 이런식으로 프로토타입 기반방식으로 복사되고 생성되는데 그럼 그 끝에는 어떤 prototype이 있는걸까?

결론부터 이야기하면 Object.prototype가 모든 객체들의 조상님이다.

이게 정말인지 확인해보고 싶다면, 아무 객체나 골라잡아서 그 객체의 __proto__ 프로퍼티를 통해 끝까지 타고 올라가보면 된다.

const first = String.__proto__;
const second = first.__proto__;
const third = second.__proto__;

console.log(11, first.constructor.name);
console.log(22, second.constructor.name);
console.log(33, third.constructor.name);
11, Function
22, Object
33, Uncaught TypeError: Cannot read property 'constructor' of null

String 함수는 Function.prototype 객체를 원본으로 가진다는 것을 볼 수 있다. 그리고 Function.prototype은 결국 객체이기 때문에 Object.prototype 객체를 원본으로 가진다. 그런데 Object의 __proto__를 보면 null값을 나타낸다. 즉, Object의 위로는 더 이상 조상이 없는 것이다.

그리고 이렇게 프로토타입으로 이루어진 객체들의 관계를 프로토타입 체인(Prototype Chain)이라고 한다.

그래서 Javascript에서 객체의 프로퍼티나 메서드에 접근하려고 할때 해당 객체에 존재 하지 않는다면 __proto__ 링크를 따라 차례대로 부모 prototype 객체를 검색한다. 그래서 다음과 같은 코드도 가능하다.

Object.prototype.toTest = function () { return 'test1' }
console.log( "abc".toTest() );
// "test1"

function fn () {}
fn.prototype.name = 'test2'
var newFn = new fn();
console.log(newFn.name)
// "test2"
console.log(newFn.toTest());
// "test1"

Object는 자바스크립트의 모든 객체의 조상인데 Object의 프로토타입에 메서드를 바로 추가하면 저렇게 String과 Function으로 만들어지는 인스턴스에도 당연히 복제되어 있는 것이다. (따라서 저렇게 추가하면 이름이 겹치고 코드가 엉망이 될것이니 저런 사용은 지양해야 한다.)

마무리

프로토타입은 결국 단순한 디자인 패턴이라는 개념을 알고 공부한다면 이해하기 어렵지 않은 개념이었다. 그러나 앞으로 프로토타입 체인과 여러가지 상속 기법을 구현할 때 이 프로토타입을 어떻게 활용하는지 공부해봐야할 것이다.

참고

코어 자바스크립트 - 프로토타입
Evans Library - 프로토타입

profile
Frontend Developer.

0개의 댓글