안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

자바스크립트 (JavaScript)

[JavaScript] 프로토타입 (Prototype)

피그브라더 2020. 7. 28. 00:59

1. 개요 - JavaScript는 프로토타입 기반의 언어

JavaScript는 객체 지향 언어임에도 불구하고 다른 객체 지향 언어(Java, Python 등)와 달리 클래스(Class)의 개념이 존재하지 않는다. 그래서 다른 객체 지향 언어를 공부하다가 새롭게 JavaScript를 공부하기 시작하는 사람들은 주로 이 부분에서 많은 혼란을 겪는다. JavaScript는 클래스 대신 프로토타입(Prototype)이라는 개념을 통해 객체 지향 프로그래밍을 지원하는데, 실제로 JavaScript를 사용하는 상당수의 사람들은 프로토타입의 개념을 정확히 이해하지 못하고 사용하는 경우가 많다. 하지만 JavaScript는 프로토타입 기반의 언어라 불릴 만큼 프로토타입에서 시작하여 프로토타입으로 끝나기 때문에, JavaScript를 사용하는 사람이라면 프로토타입의 개념을 정확히 이해하고 있을 필요가 있다. 프로토타입의 개념을 이해하는 순간 JavaScript를 바라보는 깊이가 한층 달라질 것이다.

 

물론 ES6(= ES2015) 이후 버전의 JavaScript에는 클래스 문법이 추가되었지만, 이는 추가적인 문법적 양념일 뿐 JavaScript가 프로토타입 기반의 언어라는 사실이 달라진 것은 아니다. 여전히 프로토타입은 꼭 공부해둬야 하는 주제인 것이다.

 

2. 프로토타입 객체 (Prototype Object), 프로토타입 링크 (Prototype Link)

JavaScript에서 원시 타입(Number, String, Boolean, Null, Undefined)의 값을 제외한 나머지 것들은 전부 다 객체(Object)이다. 심지어 함수 자체도 객체이기 때문에 함수가 정의되는 순간 이에 해당하는 객체가 메모리에 할당된다. 물론 여기서 말하는 객체란 다음과 같이 키(Key)와 값(Value)의 쌍들을 갖는 데이터를 말하는 것이다.

var obj1 = {};
var ob2 = {
    name: '홍길동',
    age: 27
};

 

지금부터 중요하다. 모든 객체들은 __proto__ 프로퍼티를 가지고, 이는 자신을 만들어낼 때 사용한 원형, 즉 프로토타입 객체(Prototype Object)를 가리킨다. 그리고 이때 __proto__ 프로퍼티를 프로토타입 링크(Prototype Link)라고 부른다. 자신을 만들어낼 때 사용한 프로토타입 객체를 가리킨다는 의미를 가지기 때문이다. 여기서 말하는 프로토타입 객체란 도대체 무엇을 의미하는 것일까?

 

프로토타입 객체란 말 그대로 어떠한 객체를 만들 때 원형으로 사용하는 객체를 의미한다. 만약 A라는 객체를 원형으로 사용하여 B와 C라는 새로운 객체를 만든다면, B와 C는 __proto__ 프로퍼티(= 프로토타입 링크)가 A를 가리키게 되고, 이로 인해 B와 C는 A가 가지고 있는 모든 프로퍼티들을 참조할 수 있게 된다. 이는 마치 Java나 Python과 같은 다른 객체 지향 언어들에서 '클래스(→ A에 대응)'를 원형으로 사용하여 '인스턴스(→ B에 대응)'들을 생성하는 것과 비슷하다고 할 수 있다. 특정 클래스를 기반으로 여러 인스턴스들을 생성하면 그 인스턴스들은 모두 해당 클래스의 공통된 속성을 공유하기 때문이다. 마치 붕어빵 틀을 이용하여 여러 붕어빵을 찍어내는 것과 유사하다.

 

3. 객체의 생성 - 함수를 이용한다

JavaScript의 모든 객체들은 특정 함수와 new 연산자를 이용하여 생성한다. 이게 갑자기 무슨 소린가 싶을 수 있다. 일반적으로 객체를 생성할 때는 중괄호 문법을 사용하여 정의하면 그만이기 때문이다. 예를 들면 다음과 같다.

var obj = {
    name: '홍길동',
    age: 27
};

 

그런데 놀랍게도 사실 이 코드는 다음 코드와 동일하다. JavaScript가 기본적으로 제공하는 내장 함수인 Object와 new 연산자를 이용하여 객체를 생성하는 것이다. 위 코드는 단순히 아래 코드를 쉽게 쓸 수 있도록 JavaScript에서 제공하는 '리터럴 문법'에 지나지 않는다.

var obj = new Object();
obj.name = '홍길동';
obj.age = 27;

 

머리가 갑자기 복잡해진다. 여기서 말하는 함수가 내가 알던 함수가 맞나 싶을 수 있다. 그러나 당황하지 말고 천천히 따라가 보자. Object는 함수이고, new 연산자는 Object 함수를 생성자(constructor)로서 활용하여 새로운 객체를 생성하는 연산자일 뿐이다. 그렇다면 JavaScript에서 정의하는 모든 함수들은 특정 객체를 생성하기 위한 생성자의 역할을 수행할 수 있다는 것일까? 그렇다. 그러면 어떻게 그것이 가능한지 하나하나 따져보도록 하자.

 

우선, 앞서 말했던 것처럼 JavaScript의 모든 객체들은 __proto__ 프로퍼티를 가지고 있으며, 이는 자신을 만들어낼 때 사용한 프로토타입 객체를 가리키고 있어야 한다. 즉 모든 객체들은 자신을 만들어낸 프로토타입 객체가 반드시 하나 있어야 한다는 말이다. (물론 최상위 프로토타입 객체는 자신을 만들어낸 프로토타입 객체가 없으므로 __proto__ 프로퍼티의 값이 null이다. 뒤에서 알아보겠지만 Object 프로토타입 객체가 이에 해당한다. 지금은 일단 넘어가자.) 그렇다면 위 코드에서 obj는 어떠한 프로토타입 객체를 원형으로 사용하여 만들어진 것일까? 그것을 결정해주는 게 바로 Object 함수의 정의와 관련이 있다. 이를 이해하기 위해서는, JavaScript에서 특정 함수를 정의하는 순간 내부적으로 일어나는 일들을 먼저 이해해야 한다. 다음을 읽어보자.

 

JavaScript에서 특정 함수를 정의하는 순간, 메모리에는 두 종류의 객체가 생성된다. 하나는 그 함수에 해당하는 객체이고(함수 자체도 객체라고 앞서 설명하였음), 나머지 하나는 그 함수를 통해 생성할 객체들의 원형이 될 프로토타입 객체이다. 이때 함수 객체는 해당 프로토타입 객체를 prototype 프로퍼티로 가리키게 되고, 프로토타입 객체는 해당 함수 객체를 constructor 프로퍼티로 가리키게 된다. 예를 들어 Person 함수를 정의하는 순간 메모리에 생성되는 두 객체의 연결 구조는 다음과 같다. Person 함수 객체(= Person)와 Person 프로토타입 객체(= Person.prototype)가 생성되고, 이 둘은 서로 일대일 관계로 연결된다.

 

 

여기서 Person 프로토타입 객체가 바로 "new Person()"을 통해 생성되는 객체의 원형이 될 프로토타입 객체에 해당하는 것이다. 즉 새로 생성되는 객체의 __proto__ 프로퍼티가 가리키는 객체라는 말이다. 따라서 Person 함수를 정의한 직후 Person 프로토타입 객체에 특정 프로퍼티들을 추가하면, 생성된 객체도 해당 프로퍼티들을 참조할 수 있게 된다. 다음 예시 코드를 살펴보자.

/* 함수의 정의 : 함수 객체와 프로토타입 객체가 생성됨. */
function Person() {};

/* 프로토타입 객체에 프로퍼티 추가 */
Person.prototype.sharedProperty = '공유 속성';

/* 객체 생성 */
var person = new Person();

/* 객체 자체에 프로퍼티 추가 */
person.name = '홍길동';
person.age = 27;

console.log(person.__proto__);
console.log(person);

 

이를 그림으로 나타내면 다음과 같다. 구체적인 프로퍼티들은 생략하였고, 개략적인 연결 구조만 나타내었다.

 

 

참고로 위 코드는 아래와 같이 작성할 수도 있다.

/* 함수의 정의 : 함수 객체와 프로토타입 객체가 생성됨. */
function Person(name, age) {
    this.name = name;
    this.age = age;
};

/* 프로토타입 객체에 프로퍼티 추가 */
Person.prototype.sharedProperty = '공유 속성';

/* 객체 생성 */
var person = new Person('홍길동', 27);

 

그럼 이제 이쯤에서 멈추고 아까 했던 질문을 다시 떠올려보자. 다음 예시 코드에서 obj의 원형이 될 프로토타입 객체는 무엇일까?

var obj = new Object();
obj.name = '홍길동';
obj.age = 27;

 

지금까지 잘 따라왔다면, "Object 프로토타입 객체입니다"라고 답할 수 있을 것이다. 즉, Object 함수를 정의하는 순간 생성된 Object 프로토타입 객체가 obj를 생성할 때 원형으로 사용된 것이다. 물론 Object는 JavaScript의 기본 내장 함수이기 때문에 Object 프로토타입 객체도 이미 메모리에 생성되어 있을 것이다. 참고로 JavaScript에서 리터럴 문법으로 작성하는 모든 객체들은 이와 같이 Object 함수를 이용하여 생성이 되며, 따라서 Object 프로토타입 객체가 가지고 있는 모든 프로퍼티들을 참조할 수 있다. 이 객체들은 __proto__ 프로퍼티가 Object 프로토타입 객체를 가리키기 때문이다. 예를 들어 Object 프로토타입 객체의 프로퍼티 중 하나인 toString 함수를 호출할 수 있다.

 

정리해보자! JavaScript의 모든 객체들은 특정 함수를 이용하여 생성된다. 함수를 정의하는 순간, 그 함수를 이용해서 생성할 객체의 원형이 될 프로토타입 객체도 함께 생성된다. 생성되는 객체는 __proto__ 프로퍼티(= 프로토타입 링크)로서 해당 프로토타입 객체를 가리키게 되며, 따라서 해당 프로토타입 객체의 모든 프로퍼티들을 참조할 수 있게 된다.

 

4. 프로토타입 체인 (Prototype Chain)

지금까지의 내용을 잘 따라왔다면, 프로토타입 체인을 이해하는 것도 크게 무리가 없을 것이다. 프로토타입 체인(Prototype Chain)은 객체 자신을 만들어낼 때 원형으로 사용된 프로토타입 객체도 마찬가지로 자신을 만들어낼 때 원형으로 사용된 프로토타입 객체가 있음을 의미한다. 이러한 체인은 Object 프로토타입 객체에 도달할 때까지 이어진다. Object 프로토타입 객체는 프로토타입 체인의 최상위에 위치해 있기 때문에 __proto__ 프로퍼티의 값이 null이다. 이러한 맥락에서 Object 프로토타입 객체는 체인의 종점이라고 볼 수 있다.

 

리터럴 문법에 의해 작성되는 JavaScript 객체들은 기본적으로 Object 함수에 의해 생성이 된다. 따라서 __proto__ 프로퍼티가 Object 프로토타입 객체를 가리키며 그곳이 곧 체인의 종점이다. 그런데 다른 함수에 의해 생성된 객체는 상황이 조금 다르다. 예를 들어 아까 예로 들었던 Person 함수에 의해 생성되는 객체를 생각해보자. 이 객체는 __proto__ 프로퍼티가 Person 프로토타입 객체를 가리킨다. 그렇다면 Person 프로토타입 객체는 __proto__ 프로퍼티가 무엇을 가리킬까? 특별히 설정해주지 않았다면, 이 또한 일반적인 JavaScript 객체와 다를 것이 없기 때문에 __proto__ 프로퍼티가 Object 프로토타입 객체를 가리킨다. 그리고 이곳이 곧 체인의 종점이 된다.

 

이러한 프로토타입 체인의 개념은 크게 두 가지 맥락에서 중요하다.

 

첫째, 객체의 특정 프로퍼티를 탐색하는 것은 프로토타입 체인을 따라 이뤄지기 때문이다. 특정 프로퍼티를 참조하고자 하는 경우 우선 자기 자신이 가지고 있는 프로퍼티들을 탐색한다. 만약 여기서 발견되지 않으면 __proto__ 프로퍼티가 가리키는 프로토타입 객체를 찾아가서 같은 과정을 반복한다. 이 과정을 Object 프로토타입 객체에 도달할 때까지 반복하며, 끝까지 탐색에 실패하는 경우 undefined가 반환이 된다. 이 때문에 JavaScript의 모든 객체들은 Object 프로토타입 객체가 가지고 있는 프로퍼티들을 참조할 수 있다. 이러한 확장의 원리는 JavaScript가 다른 객체 지향 언어들이 가지고 있는 클래스와 상속의 개념을 유사하게 모방하기 위한 수단이 된다.

 

둘째, 함수든 배열이든 결국 다 똑같이 '함수에 의해 생성되는' 객체라는 점을 이해하기 위해서이다.

 

앞서 계속 말했지만 함수 자체도 결국 하나의 객체이다. 따라서 함수 객체를 생성하기 위한 함수도 반드시 존재한다. 그것이 바로 Function 함수이다. 즉 Object, Person과 같은 모든 함수 객체들은 Function 함수에 의해 생성되며, __proto__ 프로퍼티가 Function 프로토타입 객체를 가리키게 된다. 이로 인해 JavaScript의 모든 함수들은 Function 프로토타입 객체의 모든 프로퍼티들(ex. bind 함수)을 참조할 수 있게 된다. 그리고 Function 프로토타입 객체의 __proto__ 프로퍼티는 Object 프로토타입 객체를 가리키며, 이곳에서 체인의 종점을 맞이한다.

다음으로, 배열도 결국에는 Array라는 함수에 의해 생성되는 객체라는 것을 이해할 수 있어야 한다. "[1, 2, 3]"과 같은 리터럴 문법으로 작성되는 모든 배열 객체들은 Array 함수에 의해 생성이 되며, __proto__ 프로퍼티가 Array 프로토타입 객체를 가리키게 된다. 따라서 JavaScript의 모든 배열들은 Array 프로토타입 객체의 모든 프로퍼티들(ex. forEach 함수)을 참조할 수 있게 된다. 마찬가지로, Array 프로토타입 객체의 __proto__ 프로퍼티는 Object 프로토타입 객체를 가리키며, 이곳에서 체인의 종점을 맞이한다.

 

지금까지 설명한 내용을 도식으로 총 정리하면 다음과 같다. 하나하나 곱씹으며 천천히 이해해보기 바란다.