자바스크립트는 프로토타입 기반 언어입니다.
우리와 친숙한 클래스 기반 언어에서는 '상속'이라는 개념을 사용하지만 프로토타입 기반 언어에서는 어떤 객체를 원형(prototype)으로 삼고 이를 참조함으로써 상속과 비슷한 기능을 수행합니다.
유명한 프로그래밍 언어의 상당수가 클래스 기반인 것에 비교하면 프로토타입은 꽤나 독특한 개념이라 할 수 있습니다.
클래스 기반 언어에 익숙한 개발자라면 자바스크립트를 배척하는 이유로 프로토타입이 복잡하고 어렵다는 점을 들지만, 오히려 자바스크립트는 프로토타입이라는 개념을 제대로 이해하는 것만으로도 이미 숙련자 레벨에 도달할 수 있는 시야를 확보할 수 있는 셈이라고 합니다.
본 포스팅에서는 프로토타입에 대한 이해를 중점으로 글을 작성하고자 합니다.
프로토타입의 개념 이해
constructor, prototype, instance
다음은 프로토타입을 가장 잘 나타내는 도식입니다.
이 도식만 이해하면 프로토타입은 끝입니다. 이 그림으로부터 전체 구조를 파악하고, 반대로 전체 구조로부터 이 그림을 도출해낼 수 있으면 됩니다.
위 그림을 예제 코드로 바꿔보면 다음과 같습니다.
var instance = new Constructor();
코드를 보면서 흐름을 기술해보겠습니다.
- 어떤 생성자 함수(Constructor)를 new 연산자와 함께 호출하면
- Constructor에서 정의된 내용을 바탕으로 새로운 인스턴스(instance)가 생성됩니다.
- 이때, instance에는 __proto__라는 프로퍼티가 자동으로 생성되는데,
- 이 프로퍼티는 Constructor의 prototype이라는 프로퍼티를 참조합니다.
prototype이라는 프로퍼티와 __proto__라는 프로퍼티가 등장했는데, 이 둘의 관계가 프로토타입 개념의 핵심입니다.
prototype은 객체이고, 이를 참조하는 __proto__ 역시 당연히 객체입니다.
prototype 객체 내부에는 인스턴스가 사용할 메서드를 정의하고, 그러면 인스턴스에서 숨겨진 프로퍼티인 __proto__를 통해 이 메서드에 접근할 수 있습니다.
기술한 내용을 천천히 읽어도 이해가 되지 않아도 앞으로 나올 예제코드를 통해 이해하면 됩니다.
<코어 자바스크립트> 서적에서는 편의를 위해 __proto__로 표기하지만 이는 학습 목적으로 이해하고
실무자라면 __proto__가 아닌 Object.getPrototypeOf() 또는 Object.create() 등을 이용하는게 좋습니다.
다음은 Person이라는 생성자 함수의 prototype에 getName이라는 메서드를 저장한 예제 코드입니다.
const Person = function(name) {
this._name = name;
};
// Person 프로토타입에 getName이라는 메서드 정의
Person.prototype.getName = function() {
return this._name;
};
이렇게 코드를 작성하면 Person의 인스턴스는 __proto__ 프로퍼티를 통해 getName을 호출할 수 있습니다.
const doctor = new Person("의사");
doctor.__proto__.getName(); // undefined
위의 코드에서 예상되는 결과 값은 "의사"입니다. 하지만 undefined를 출력하고 있습니다.
디버깅을 하지 않고 출력된 결과만 봤을 때, 다음을 유추할 수 있어야 합니다.
- "의사"라는 값이 나오지 않았고, TypeError가 발생하지 않았다.
- undefined가 출력되었다는 것은, 이를 호출한 함수는 실행 가능한 함수에 해당합니다.
- 에러가 아닌 undefined가 나왔으니, getName이라는 메서드는 호출 가능한 함수이다.
그럼 이제 this._name의 this에 원래의 의도와는 다른 값이 할당된 것이 아닐까, 라는 의심을 할 수 있습니다.
개발자 콘솔에서 디버깅을 해보면 우리가 원하는 값과는 다른 값이 this에 바인딩 된 것을 확인할 수 있습니다.
어떤 함수를 메서드로서 호출할 때는 메서드명 바로 앞의 객체가 곧 this가 됩니다.
즉, doctor.__proto__.getName()에서 getName 함수 내부에서의 this는 doctor가 아니라 doctor.__proto__라는 객체를 바인딩 하고 있는 것입니다.
이 객체(doctor.__proto__) 내부에는 name이라는 프로퍼티가 없으므로 '찾고자 하는 식별자가 정의돼 있지 않을 때는 Error 대신 undefined를 반환한다'라는 자바스크립트 규약에 의해 undefined가 반환된 것입니다.
이는 다음 코드로 확인할 수 있습니다.
const doctor = new Person("의사");
doctor.__proto__._name = '의사, __proto__';
doctor.__proto_.getName(); // 의사 __proto__
하지만 __proto__를 사용하는건 여간 귀찮은 일이고 가독성도 떨어집니다. 그럼 __proto__ 없이 인스턴스에서 곧바로 메서드를 쓸 수 있는 방법은 없을까, 라는 생각을 할 수 있습니다.
다음은 예제 코드입니다.
const doctor = new Person('의사');
doctor.getName() // 의사
__proto__를 쓰지 않으면 this는 instnace를 가르키는게 맞지만, 이대로 메서드가 호출되고 심지어 원하는 값이 나오는 건 좀 이상하다고 생각할 수 있습니다. 개발을 하다가 에러를 만나면 '이게 왜 안되지?'라던가, 에러 없이 잘 실행되면 '이게 왜 되지?'라는 그런 익숙하면서도 묘한 느낌을 받은 적이 있을겁니다.
하지만 위의 코드는 정상입니다. 그 이유는 __proto__가 원래부터 생략 가능한 프로퍼티로 정해졌기 때문입니다.
이 정의를 바탕으로 자바스크립트의 전체 구조가 구성됐다고 해도 과언이 아니라고 하네요.
'생략 가능한 프로퍼티'라는 개념은 언어를 창시하고 전체 구조를 설꼐한 '브랜든 아이크'의 머리에서 나온 아이디어로, 이해의 영역이 아니므로 '그냥 그런가보다'하는 수밖에 없습니다.
여기서는 __proto__가 생략 가능하다는 점만 기억하면 됩니다.
앞서 봤던 프로토타입 도식입니다. 지금까지의 내용을 바탕으로 프로토타입 도식을 글로 풀어쓰면 다음과 같습니다.
new 연산자로 Constructor를 호출하면 instance가 만들어지는데,
이 instnace의 생략 가능한 프로퍼티인 __proto는 Constructor의 prototype을 참조한다.
조금 더 상세히 설명하면 다음과 같습니다.
자바스크립트는 함수에 자동으로 객체인 prototype 프로퍼티를 생성하는데, 해당 함수를 생성자 함수로서 사용할 경우, 즉 new 연산자와 함께 함수를 호출할 경우, 그로부터 생성된 인스턴스에는 숨겨진 프로퍼티인 __proto__가 자동으로 생성되며, 이 프로퍼티는 생성자 함수의 prototype 프로퍼티를 참조합니다. __proto__ 프로퍼티는 생략 가능하도록 구현돼 있기 때문에 생성자 함수의 prototype에 어떤 메서드나 프로퍼티가 있다면 인스턴스에서도 마치 자신의 것처럼 해당 메서드나 프로퍼티에 접근할 수 있게 됩니다.
다시 또 예제 코드를 실행하고 크롬 개발자 도구의 콘솔을 활용해서 출력 결과를 보겠습니다.
const Constructor = function(name) {
this.name = name;
};
Constructor.prototype.method = function() {};
Constructor.prototype.propertyStr = 'Constructor prototype property';
const instance = new Constructor('Instance');
console.dir(Constructor); // A
console.dir(instance); // B
예제 코드 (A)에서는 Constructor의 디렉터리 구조를 출력하라고 했습니다.
출력 결과의 첫 줄에는 함수라는 의미의 f와 함수 이름은 Constructor, 인자 name이 있습니다.
그 내부에는 옅은 색의 arguments, caller, length, name, prototype, __proto__ 등의 프로퍼티들이 있습니다.
다시 prototype을 열어보면 코드에서 추가한 method와 propertyStr 등의 값은 짙은 색으로 보이고, constructor, __proto__ 등은 옅은 색으로 보입니다.
이런 색상의 차이는 { enumerable: false } 속성이 부여된 프로퍼티인지 여부에 따릅니다.
짙은색은 enumerable, 즉 열거 가능한 프로퍼티임을 의미하고, 옅은색은 innumerable, 즉 열거할 수 없는 프로퍼티임을 의미합니다.
for in 등으로 객체의 프로퍼티 전체에 접근하고자 할 때 접근 가능 여부를 색상으로 구분지어 표기하는 것입니다.
프로토타입 체인
메서드 오버라이딩
prototype 객체를 참조하는 __proto__를 생략하면 인스턴스는 prototype에 정의된 프로퍼티나 메서드를 마치 자신의 것처럼 사용할 수 있다고 앞서 살펴봤습니다.
그런데 만약 인스턴스가 동일한 이름의 프로퍼티 또는 메서드를 가지고 있으면 어떤 출력 결과가 나올까, 라는 생각을 할 수 있습니다.
다음은 우리에게 익숙한 오버라이딩 코드입니다.
const Person = function(name) {
this.name = name;
};
Person.prototype.getName = function() {
return this.name;
};
const doctor = new Person("의사");
// 여기서 getName 오버라이딩
doctor.getName = function() {
return this.name + "뇽뇽";
};
cosole.log(doctor.getName()); // 의사뇽뇽
doctor.__proto__.getName이 아닌 doctor 객체에 있는 getName 메서드가 호출됐습니다.
오버라이딩 개념이 생소한 분들은 혼란스러울 수 있습니다.
오버라이딩은 메서드 위에 메서드를 덮어씌웠다고 생각하면 쉽습니다.
원본 메서드를 제거하고 다른 대상으로 교체하는 것이 아니라 원본이 그대로 있는 상태에서 다른 대상을 그 위에 얹는 이미지를 떠올리면 됩니다.
자바스크립트 엔진이 getName이라는 메서드를 찾는 방식은 가장 가까운 대상인 자신의 프로퍼티로 검색하고, 없으면 그다음으로 가까운 대상인 __proto__를 검색하는 순서로 진행됩니다.
그러니까 __proto__에 있는 메서드는 자신에게 있는 메서드보다 검색 순서에서 밀려 호출되지 않는 것입니다.
정리
어떤 생성자 함수를 new 연산자와 함께 호출하면 Constructor에서 정의된 내용을 바탕으로 새로운 인스턴스가 생성되는데, 이 인스턴스에는 __proto__라는, Constructor의 prototype 프로퍼티를 참조하는 프로퍼티가 자동으로 부여됩니다.
__proto__는 생략 가능한 속상이라서, 인스턴스는 Constructor.prototype의 메서드를 마치 자신의 메서드인 것처럼 호출할 수 있습니다.
코어 자바스크립트: 핵심 개념과 동작 원리로 이해하는 자바스크립트 프로그래밍을
공부하며 이해한 내용을 기반으로 정리한 게시글입니다.
'코드 > JavaScript' 카테고리의 다른 글
let, var, const 기초 (0) | 2024.06.18 |
---|---|
자바스크립트 클래스 (0) | 2024.05.29 |
클로저 (0) | 2024.05.22 |
콜백함수 (Callback) (0) | 2024.05.17 |
undefined와 null의 차이 (0) | 2024.05.17 |