티스토리 뷰

▶들어가기 전

앞으로는 자바스크립트 객체의 프로퍼티나 메서드에 접근하는 원리에 대해 알아보고, 상속을 구현해 볼 것입니다.

또한 앞서 언급했던 스코프가 무엇인지, 어떤 규칙으로 결정되는지 구체적으로 살펴보고 파생되는 개념인 모듈, 클로저, 호이스팅에 대해 알아보겠습니다.

 

▶프로토타입

▷프로토타입이란?

프로토타입(prototype)이란 무언가 제품을 만드는 과정에서 시험용으로 미리 만들어보는 물건을 의미하기도 합니다.

즉 일반적으로 원형이라는 뜻을 가지고 있습니다. 다시 말해 자신을 만들어낸 객체의 원형이라고 할 수 있겠습니다.

생성자 함수에 정의한 모든 객체가 공유할 원형(모든 함수에는 프로토타입이 있고, 모든 객체는 Object를 상속받습니다)을 말합니다.

이때 구체적으로 프로토타입 객체(Prototype  Object)는 객체가 생성될 때 생성된 객체의 부모가 되는 객체의 원형을 지칭합니다.

 

자바스크립트에서는 프로토타입을 기반으로 객체 지향의 상속 개념을 구현합니다.

모든 객체는 자신의 부모 역할을 하는 프로토타입 객체(이하 프로토타입으로 표현)의 참조 링크를 가지고 있으며, 이 링크를 통해 프로토타입으로부터 프로퍼티나 메서드를 상속받을 수 있습니다.

 

1) 프로토타입과 프로토타입 체인

객체의 프로토타입은 참조 링크 형태로 [[Prototype]] 내부 프로퍼티에 저장됩니다.

참조 링크 형태로 저장되기 때문에 동일한 프로토타입을 상속받은 객체는 모두 같은 프로퍼티와 메서드를 공유합니다.

 

프로토타입의 상속 구조 예시

위의 객체 me, you, him은 동일한 프로토타입 Person.prototype을 상속받고 있습니다.

각 객체는 Person.prototype의 정보를 [[Prototype]] 프로퍼티에 참조 링크 형태로 저장하고 있습니다.

Person.prototype은 물리적인 객체가 아닌 참조 링크 형태로 저장되기 때문에 Person.prototype의 모든 변경 사항은 me, you, him에 공유됩니다.

  • [[Prototype]]과 __proto__ 프로퍼티

[[Prototype]]은 자바스크립트 엔진 내부에서만 사용하는 숨겨진 프로퍼티이지만 크롬, 파이어폭스와 같은 모던 브라우저에서 __proto__ 프로퍼티에 접근할 수 있습니다.

하지만 __proto__ 프로퍼티는 표준 명세가 아니며 이를 통한 프로토타입의 접근도 공식적인 방법은 아닙니다.

또한 모든 브라우저에서 구현된 프로퍼티가 아니므로 실제 애플리케이션의 코드에서 __proto__ 프로퍼티는 가급적 사용하지 않는 것이 좋습니다.

 

▷프로토타입 체인

아래 예제의 obj 객체에서 toString() 메서드를 호출하면 정상적으로 호출됩니다.

선언된 메서드가 obj 객체 내에 없는데 어떻게 함수 호출이 성공했을까요?

const obj = {
	name: 'javascript'
};

console.log(obj.toString()); // '[Object object]'

이것은 프로토타입 체인이라는 개념 때문에 가능합니다.

프로토타입 체인은 상위 프로토타입과 연쇄적으로 연결된 구조입니다.

그리고 프로퍼티나 메서드에 접근하기 위해 이 연결 구조를 따라 차례대로 검색하는 것을 프로토타입 체이닝이라고 합니다.

예제 코드는 다음과 과정처럼 프로토 타입 체인을 통해 toString() 메서드가 호출된 것입니다.

 

1단계

obj 객체의 toString() 메서드를 호출하기 위해 obj 객체의 프로퍼티나 메서드를 검색합니다.

2단계

1번 과정에서 toString() 메서드를 찾지 못했기 때문에 프로토타입 체인을 통해 상위 프로토타입에서 toString() 메서드를 검색합니다.

3단계

상위 프로토타입에서 toString() 메서드를 찾았기 때문에 이 메서드를 호출합니다.

 

▷최상위 프로토타입

Object.prototype은 프로토타입 체인의 최상위에 있는 프로토타입입니다.

모든 객체가 가진 프로토타입 체인의 종점은 모두 Object.prototype입니다.

모든 객체가 Object.prototype을 프로토타입으로써 공유한다는 의미입니다.

  • 프로토타입의 생성

객체의 부모가 되는 프로토타입은 어떻게 설정되는 것일까요?

객체의 프로토타입은 객체가 생성되는 시점에서 설정됩니다.

Object.prototype을 상속받는 객체들

 

▷다양한 객체의 프로토타입

객체 리터럴로 생성한 객체의 프로토타입 체인에 대해 알아보았습니다.

 

+) 객체 리터럴

리터럴은 사람이 이해할 수 있는 문자나 약속된 기호를 사용해 값을 생성하는 표기법을 말합니다.

자바스크립트에서 객체를 생성하는 가장 일반적인 방법이 객체 리터럴을 사용하는 것입니다.

객체 리터럴은 중괄호({}) 내에 0개 이상의 프로퍼티를 정의합니다.

// object literal
var me = {
  name : 'Kim',
  intro : function() {
    console.log(`My name is ${this.name}`);
  }
};

console.log(typeof me); // object
console.log(me); // { name: 'Kim', intro: [Function: intro] }

객체 리터럴의 중괄호는 코드 블록을 의미하는 것이 아니라 값으로 평가되기 때문에 닫는 괄호 뒤에는 세미콜론(;)을 붙입니다.

 

+) 프로터피와 메소드

프로퍼티(property)는 객체의 상태를 나타내는 값을 의미하고, 메서드(method)는 프로퍼티를 참조하고 조작할 수 있는 동작을 의미합니다.

그럼 배열처럼 내장된 객체의 프로토타입은 어떨까요?

이들 객체는 독특하게 각자 자신의 프로토타입을 따로 정의하고 있습니다.

자바스크립트에는 배열 외에도 랩퍼 객체, 함수, 정규식과 같은 내장 객체들이 있습니다.

이러한 객체들 역시 자신의 고유한 프로토타입을 따로 가지고 있기 때문에 다양한 메서드나 프로퍼티들을 사용할 수 있는 것입니다.

 

2) 프로토타입과 생성자 함수

모든 함수는 prototype이라는 특별한 프로퍼티가 존재합니다.

일반적인 함수에서는 prototype프로퍼티를 사용할 일이 없지만 new 키워드와 함께 생성자 함수로 사용할 경우에는 특별한 역할을 합니다.

여기서 한 가지 중요한 점이 있습니다.

앞에서 이야기한 객체의 프로토타입을 가리키는 참조 링크 [[Prototype]]과 함수의 prototype 프로퍼티를 구분해야 한다는 점입니다.

함수의 prototype 프로퍼티는 특별한 역할을 수행하긴 하지만 일반적인 객체의 프로퍼티이며, 프로토타입을 가리키는 참조 링크가 아닙니다.

그렇다면 생성자 함수에서 prototype 프로퍼티는 어떤 역할을 할까요?

▷객체의 생성과 함수의 property 프로퍼티

생성자 함수로 생성된 객체의 프로토타입은 아래와 같은 규칙을 따라 설정됩니다.

생성자 함수로 생성된 객체'생성자 함수의 prototype 프로퍼티'가 프로토타입([[Prototype]])으로 설정됩니다.

function Vehicle(type) {
	this.type = type;
}

const vehicle = new Vehicle('Car');

console.log(Vehicle.prototype == vehicle.__proto__); // true

위에서 설명한 규칙처럼 Vehicle() 생성자 함수의 prototype 프로퍼티가 생성자 함수를 통해 생성된 객체 vehicle의 프로토타입으로 설정된 것을 볼 수 있습니다.

그리고 이 객체의 프로토타입 상속 구조는 다음처럼 표현할 수 있습니다.

vehicle 객체의 프로토타입 상속 구조

vehicle 객체의 프로토타입은 Vehicle() 생성자 함수의 prototype 프로퍼티인 Vehicle.prototype을 참조 링크로 가리키며, 이 객체는 Object.prototype을 프로토타입으로 가리킵니다.

생성자 함수를 통해 생성된 모든 객체는 이러한 매커니즘으로 상속을 구현합니다.

 

▷함수의 prototype 프로퍼티와 프로토타입의 관계

함수의 prototype 프로퍼티는 constructor 프로퍼티 하나만 가진 객체입니다.

constructor 프로퍼티자신과 연결된 생성자 함수를 가리키며, 이 프로퍼티를 통해 객체가 어떤 생성자 함수를 통해 생성되었는지 알 수 있습니다.

생성자 함수와 생성자 함수의 prototype 프로퍼티는 서로 상호 참조하는 관계입니다.

 

명확한 이해를 위해 위의 설명을 그림과 함께 봅시다.

다음 그림은 앞의 예제 코드에서 보았던 Vehicle() 생성자 함수와 vehicle 객체의 관계를 그림으로 나타낸 것입니다.

 

Vehicle() 생성자 함수와 prototype 프로퍼티의 관계

Vehicle() 생성자 함수의 prototype 프로퍼티이 Vehicle.prototype 객체에 constructor 프로퍼티가 존재하는 것을 볼 수 있습니다.

그리고 이 constructor 프로퍼티는 자신과 연결된 Vehicle() 생성자 함수를 참조하고 있습니다.

즉 Vehicle() 생성자 함수와 Vehicle.prototype은 서로 상호 참조의 관계인 것입니다.

그리고 vehicle 객체는 프로토타입 체인을 통해 Vehicle.prototype의 constructor 프로퍼티로 자신을 생성한 생성자 함수에 접근할 수 있습니다.

 

3) 프로토타입의 확장과 상속

자바스크립트 객체의 프로토타입과 상속을 위한 메커니즘인 프로토타입 체인에 대해 알아 보았습니다.

그렇다면 객체의 부모가되는 프로토타입에 메서드나 프로퍼티를 추가하고 싶다면 어떻게 해야 할까요?

방법은 아주 간단합니다.

프로토타입 역시 자바스크립트 객체이기 때문에 일반 객체처럼 동적으로 프로퍼티나 메서드를 추가하거나 삭제할 수 있습니다.

그리고 이렇게 변경된 프로퍼티는 실시간으로 프로토타입 체인을 통한 검색에 반영됩니다.

function Vehicle(type) {
	this.type = type;
}

Vehicle.prototype.stop = function () {
	console.log('stop!');
}

const vehicle = new Vehicle('Car');

console.log(vehicle.stop()); // 'stop!'

Vehicle.prototype에 stop() 메서드를 추가하였습니다.

그 결과 vehicle 객체에서 프로토타입 체인을 통해 stop() 메서드를 호출할 수 있게 되었습니다.

단, 객체가 생성된 이후에 프로토타입의 프로퍼티를 수정하는 것은 지양해야 합니다.

모든 객체가 프로토타입을 공유하기 때문에 프로토타입의 프로퍼티를 수정하거나 삭제한다면 혼란과 버그를 초래할 수 있습니다.

function Vehicle(type) {
	this.type = type;
}

Vehicle.prototype.stop = function () {
	console.log('stop!');
}

const vehicle = new Vehicle('Car');

console.log(vehicle.stop()); // 'stop!'

Vehicle.prototype.stop = function () {
	throw new Error(`Don't change the prototype methed`);
}

console.log(vehicle.stop()); // Uncaught Error: Don't change the prototype methed

vehicle 객체의 프로토타입 체인

예제 코드에서는 Vehicle.prototype의 stop() 메서드를 동적으로 변경하였습니다.

위 그림에서 볼 수 있듯이 vehicle 객체는 변함없이 Vehicle.prototype에 대한 링크를 유지하고 있기 때문에 stop() 메서드의 변경에 바로 영향을 받게 됩니다.

vehicle 객체에서 변경된 stop() 메서드를 호출하면 기존과 다르게 에러가 발생하여 정상적으로 동작하지 않습니다.

이 문제는 Vehicle() 생성자 함수를 통해 생성한 객체가 많을수록 더욱 심각한 상황을 초래할 것입니다.

▷프로토타입을 사용한 상속 구현

프로토타입을 사용한 상속 구현은 생각보다 까다롭습니다.

생성된 객체와 부모 프로토타입의 링크를 깨뜨리지 않게끔 구현해야 하기 때문입니다.

앞의 예제 코드에서 보았던 Vehicle 클래스를 상속받는 Car라는 클래스를 만들어 상속을 구현해 보겠습니다.

function Vehicle() {
	console.log('initialize Vehicle');
}

Vehicle.prototype.run = function () {
	console.log('run!');
}

Vehicle.prototype.stop = function () {
	console.log('stop!');
}

function Car(type) {
	this.type = type;
}

function inherit(parnet, child) {
	function F() {};
   	F.prototype = parent.prototype;
   	child.prototype = new F();
   	child.prototype.constructor = child;
}

inherit(Vehicle, Car);

console.log(new Car('SUV'));

inherit() 함수를 이용하여 Car 클래스가 Vehicle 클래스를 상속하도록 구현하였습니다.

inherit() 함수의 코드는 간단하지만 이해하기 쉬운 코드는 아닙니다.

코드의 생성자 함수와 prototype 프로퍼티들의 연결 관계를 다음 그림처럼 나타낼 수 있습니다.

car 객체의 프로토타입 상속 구조

F() 생성자 함수의 prototype 프로퍼티로 부모 생성자 함수 Vehicle()의 prototype프로퍼티를 설정하였습니다.

그리고 F() 생성자 함수를 사용하여 빈 객체를 만든 후 자식 생성자 함수 Car()의 prototype 프로퍼티로 설정합니다.

이렇게 되면 Car() 생성자 함수를 통해 생성된 car 객체에서 프로토타입 체인을 통해 Vehicle.prototype에 접근할 수 있게 됩니다.

 

Car() 생성자 함수의 prototype 프로퍼티로 Vehicle 클래스의 객체가 아닌 F() 생성자 함수로 생성한 빈 객체를 둔 이유가 무엇일까요?

만약 Vehicle 클래스의 객체를 Car() 생성자 함수의 prototype 프로퍼티로 설정한다면 다음과 같은 문제가 발생할 수 있습니다.

function Vehicle() {
	console.log('initialize Vehicle');
}

Vehicle.prototype.run = function () {
	console.log('run!');
}

Vehicle.prototype.stop = function () {
	cosole.log('stop!');
}

const vehicle = new Vehicle();

funtion Car(type) {
	this.type = type;
}
Car.prototype = vehicle;
vehicle.myProperty = 'myProperty';
console.log(Car.prototype.myProperty); // myProperty

car 객체와 vehicle 객체의 프로토타입 상속 구조

우리의 목적은 Car 클래스가 Vehicle 클래스를 상속받도록 하는 것입니다.

Car() 생성자 함수의 prototype 프로퍼티의 상위 프로토타입으로써 Vehicle() 생성자 함수의 prototype 프로퍼티를 찾을 수 있어야 합니다.

vehicle 객체를 Car.prototype으로 설장한 경우에도 프로토타입 체이닝에 따라 Vehicle() 생성자 함수의 prototype 프로퍼티를 찾을 수 있습니다.

하지만 이 경우, 문제가 있습니다.

위 코드처럼 vehicle 객체에 vehicle.myProperty와 같은 프로퍼티를 설정한다면 필요 없는 프로퍼티까지 상속받게 됩니다.

우리가 상속 받고 싶은 것은 Vehicle.prototype에 정의된 프로퍼티나 메서드이지 특정 vehicle 객체의 프로퍼티나 메서드가 아닙니다.

이러한 문제를 방지하기 위해 F() 생성자 함수를 사용하여 부모 클래스의 인스턴스나 자식 클래스의 인스턴스를 독립적으로 만들어 사용하는 것입니다.

function inherit(parent, child) {
	function F() {};
   	F.prototype = parent.prototype;
   	child.prototype = new F();
   	child.prototype.constructor = child;
}
  • 생성자 빌려 쓰기

inherit() 함수를 사용하여 정상적으로 Car 클래스가 Vehicle 클래스를 상속한 것을 확인했습니다.

하지만 아직 한 가지 문제가 있습니다.

Car 클래스의 인스턴스를 생성할 때 부모 클래스의 Vehicle() 생성자 함수가 호출되지 않는 것입니다.

이 문제는 Car() 생성자 함수에서 apply() 메서드를 사용하여 해결할 수 있습니다.

function Car(type) {
	Vehicle.apply(this, arguments);
   	this.type = type;
}

apply() 메서드를 사용하여 Vehicle() 생성자 함수의 첫 번째 인자로 Car() 생성자 함수에서 생성된 객체를 전달합니다.

이렇게 하면 새로 생성된 객체로 this 바인딩이 변경되기 때문에 Vehicle() 생성자 함수에서 이 객체를 대상으로 동작을 수행합니다.

이러한 방식으로 자식 클래스의 인스턴스를 생성할 때 부모 클래스의 생성자를 호출하는 것을 생성자 빌려 쓰기라고 합니다.

위의 내용을 모두 합친 최종 코드를 실행해 보면 Car 클래스가 Vehicle 클래스를 올바르게 상속받으며, Vehicle() 생성자 함수도 Car 클래스의 인스턴스 생성 시 호출됩니다.

function Vehicle() {
	console.log('initialize Vehicle');
}

Vehicle.prototype.run = function () {
	console.log('run!');
}

Vehicle.prototype.stop = function () {
	console.log('stop!');
}

function Car(type) {
	Vehicle.apply(this, arguments);
    this.type = type;
}

function inherit(parent, child) {
	function F() {};
   	F.prototype = parent.prototype;
   	child.prototype = new F();
   	child.prototype.constructor = child;
}

inherit(Vehicle, Car);

console.log(new Car('SUV'))

프로토타입을 사용한 상속 구현 코드를 보며 생성자 함수와 프로토타입 그리고 프로토타입 체인의 원리를 이해하는 것이 얼마나 중요한지 조금이나마 느꼈을 것입니다.

프로토타입 기반의 상속이라는 개념 자체가 클래스 기반의 객체 지향 언어와는 달라 생소하고 어려울 수 있지만, 자바스크립트 개발을 위해서는 필수로 알아야 하는 부분입니다.

 

4) class

자바스크립트의 클래스와 상속은 생성자 함수와 프로토타입을 사용하여 구현할 수 있습니다.

하지만 프로토타입을 사용한 구현은 직관적이지 않고 번거로운 면이 있습니다.

이러한 문제를 해결하기 위해 ES2015에서 class 키워드를 이용한 새로운 문법이 등장하였습니다.

이 문법은 문법적 설탕(Syntactic sugar)으로 좀 더 편하고 세련되게 클래스와 상속을 구현할 수 있게 해줍니다.

 

+) 문법적 설탕(Syntactic sugar)

컴퓨터 과학에서 문법적 설탕은 간결한 표현으로 사람이 더 이해하기 쉽도록 고안된 문법을 의미합니다.

자바스크립트에서는 class 문법과 나중에 네트워크에서 다룰 async, await 문법이 이에 해당합니다.

 

class Vehicle {
	constructor() {
   		console.log('initialize Vehicle');
    }
    
  	run() {
  		console.log('run!');
    }
    
  	stop() {
   		console.log('stop!;);
    }
}
console.log(new Vehicle());

클래스를 선언하기 위해서는 클래스의 이름과 함께 class 키워드를 사용해야 합니다.

클래스의 몸체는 중괄호({})로 묶어 정의하며, 몸체에는 생성자 함수의 역할을 하는 constructor() 생성자 메서드나 기존 생성자 함수의 prototype 프로퍼티에 정의했던 확장 프로퍼티나 메서드가 정의됩니다.

 

class 문법을 사용하여 선언한 클래스 역시 함수이며, 내부적으로는 프로토타입을 기반으로 동작합니다.

즉 클래스 생성을 위한 문법만 달라질 뿐 기존과 동일하게 프로토타입 체인을 통해 프로퍼티를 검색하며, prototype 프로퍼티 역시 존재합니다.

▷상속

class 문법을 사용하면 상속도 간단하게 구현할 수 있습니다.

class Vehicle {
	constructor() {
    	console.log('initialize Vehicle');
    }
    
   	run() {
    	console.log('run!');
    }
    
    stop() {
   		console.log('stop!');
    }
}

class Car extends Vehicle {
	constructor(type) {
    		super();
       		this.type = type;
    }
}

console.log(new Car('SUV'));

상속 구현을 위한 inherit() 함수도 부모 생성자 함수 호출을 위한 apply() 메서드 호출도 필요하지 않습니다.

extends 키워드 뒤에 상속받을 부모 클래스만 정의한 후, constructor() 생성자 메서드에서 super()를 호출하면 됩니다.

 

extends 키워드로 특정 클래스를 상속받는 경우에는 constructor() 생성자 메서드에서 반드시 this를 사용하기 전에 super()를 먼저 호출해야 합니다.

부모 클래스의 constructor() 메서드에서 반환한 객체를 자식 클래스에서 사용하기 때문에 반드시 super() 호출이 this를 통한 참조보다 먼저 실행되어야 합니다.

그렇지 않을 경우 ReferenceError가 발생합니다.

 

▷정적 메서드와 private 접근 제한자

class 문법에서는 static 키워드를 사용하여 정적 메서드를 정의할 수도 있습니다.

정적 메서드는 특정 인스턴스에 묶이는 것이 아니기 때문에 this가 아닌 클래스 이름을 사용하여 접근할 수 있습니다.

정적 메서드는 특정한 형태의 인스턴스를 생성하는 팩토리 함수를 정의할 때 많이 사용됩니다.

class Car extends Vehicle() {
	constructor(type) {
    		super();
       		thiis.type = type;
    }
    
    static CreateSUV() {
    	return new Car('SUV');
    }
}
  • private 접근 제한자

클래스의 프로퍼티와 메서드들은 기본적으로 모두 public하기 때문에 외부에서 접근하여 수정합니다.

TC39에서는 이를 보완하기 위해 # prifix를 추가해 private 클래스 필드를 선언하는 명세 작업을 진행하고 있습니다.

class Car extends Vehicle {
	#name;
    
    constructor(type) {
    	super();
       	this.type = type;
       	this.#name = 'myCar';
    }
}

const car = new Car('SUV');

console.log(car.type); //'SUV'
console.log(car.#name); // Uncaught SyntaxError: Private field '#name' must be declared...

예제 코드의 마지막 줄처럼 외부에서 private 필드에 접근하는 경우 SyntaxError가 발생합니다.

private 클래스 필드는 아직 정식 명세는 아니지만 크롬과 같은 일부 모던 브라우저에서 사용할 수 있습니다.

드디어 자바스크립트에서도 간결하고 완벽한 캡슐화르 구현할 날이 가까워진 것 같습니다.

 


여기까지 자바스크립트 프로토타입에 대해 알아보았습니다 :)

728x90
LIST
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/03   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
글 보관함