거창한 부재만큼 자바스크립트를 얘기하면서 프로토타입을 빼놓을 순 없습니다. 그만큼 중요한 개념이고 자바스크립트를 제대로 그리고 재밋게 사용하려면 꼭 알아야 하는 개념중에 하나이기 때문입니다. 이번 포스트에서 prototype
이 무엇인지, 언제 쓰이는지 그리고 프로토타입기반 언어인 자바스크립트에서 프로토타입을 이용한 상속은 어떻게 구현 하는지까지 알아보겠습니다.
프로토타입이 무엇인지 이해하기 위해서는 먼저 prototype object
와 prototype link
에 대한 개념을 알아야 합니다. 프로토타입 객체부터 하나씩 알아보도록 하겠습니다.
프로토타입 객체
prototype object
는 자바스크립트에서 함수 선언시 생성되는 객체로 constructor
와 __proto__
를 기본 속성으로 가지는, 모든 함수가 가지고 있는 객체입니다. 기본 속성은 아래와 같은 속성값을 가집니다.
1 | { |
선언된 함수는 prototype
이라는 속성을 통해 생성된 prototype object
에 접근할 수 있으며 객체에 원하는 멤버를 추가, 삭제할 수 있습니다. 뒤에서 언급하겠지만 prototype object
에 추가된 멤버는 인스턴스 생서시 매번 인스턴스 멤버로 메모리에 올라가는 것이 아니라 프로토타입의 멤버로 하나의 참조값을 공유하는 특징을 가집니다.
프로토타입 링크
prototype link
자바스크립트 내부 속성인 [[prototpye]]
을 참조할 수 있도록 웹 브라우저 벤더사가 뚫어 놓은 __proto__
라는 속성을 통해 접근할 수 있습니다. 함수만 가지는 prototype
속성과 달리 모든 객체가 가지고 있는 속성으로 생성자 함수의 prototype object
를 참조합니다.
__proto__
속성은 표준 스펙이 아니기 때문에 개발시 사용하지 않도록 하며, ECMAScript 2015 를 사용 가능한 환경에서는 Object.getPrototypeOf 로prototype object
를 참조할 수 있습니다.
아래 생성자 함수
, 객체 리터럴
두가지 방식의 객체 생성 코드를 통해 객체의 __proto__
속성이 참조하는 값을 확인해 보겠습니다.
생성자 함수 사용
생성자 함수를 사용하여 생성된 객체의 __proto__
속성값을 확인하기 위한 코드 입니다.
1 | var Human = function(){} |
Human
이라는 생성자 함수로 생성된 객체(jaewon
인스턴스)의 __proto__
속성이 생성자 함수 Human
의 prototype
속성이 참조하는 prototype object
임을 확인할 수 있습니다.
객체 리터럴 사용
리터럴 방식으로 선언된 객체의 __proto__
속성값을 확인하기 위한 코드 입니다.
1 | var jaewon = {}; |
위 코드를 보면 리터럴 방식으로 객체를 선언 했는데 선언된 객체의 __proto__
속성은 생성자 함수 Object
의 prototype object
를 참조하고 있습니다. new Object()
를 사용하여 생성한게 아니라 리터럴 방식으로 선언했는데 어떻게 된 것 일까요?
자바스크립트는 리터럴 방식으로 객체를 선언하더라도 내부적으로 해당 타입에 대응하는 Wrapper Object
(생성자 함수)를 사용하여 아래 코드와 같이 객체를 생성하기 때문에 리터럴 방식으로 생성된 객체의 __proto__
속성도 Object.prototype
객체를 참조하게 되는 것 입니다.
1 | var jaewon = new Object(); |
아래는 Warpper Object
를 사용하는 예제 코드들 입니다.
1 | var str = "blabla"; |
지금까지 prototype object
와 prototype link
에 대해 알아봤습니다. prototype
이 뭔지 감이 좀 오시나요?
이제 언제 사용하는지 알아보면서 prototype
에 대해 좀 더 알아보도록 하겠습니다.
언제 사용하는 걸까요?
자바스크립트는 prototype
기반 언어로 객체의 뼈대가 될 class
가 없는 언어입니다. 대신 앞에서 살펴본 prototype
을 가지며 이것을 사용해 class
를 사용한 것 처럼 객체를 생성할 수 있습니다. 정확히 말하면 prototype
을 가지는 생성자 함수
를 사용하여 class
를 사용한 것 처럼 객체를 생성할 수 있습니다. 즉, prototype
은 효율적으로 객체를 생성할 필요가 있을 때 사용하게 됩니다.
효율적인 객체 생성의 이해를 돕기위해 아래 prototype 을 활용하지 않은 생성자 함수
, prototype 을 활용한 생성자 함수
두개의 코드를 준비하였습니다.
프로토타입을 활용하지 않은 생성자 함수
요구사항을 처리하기 위한 모든 멤버를 생성자 함수에 선언합니다.
1 | var 붕어빵틀 = function( initParam ){ |
프로토타입을 활용한 생성자 함수
인스턴스마다 독립적으로 가져야하는 요구사항은 생성자 함수의 멤버(인스턴스 멤버)로, 공유되어야 하는 부분들은 prototype object
의 멤버로 선언합니다.
1 | var 붕어빵틀 = function( initParam ){ |
인스턴스 생성
prototype
을 사용한 생성자 함수나 사용하지 않은 생성자 함수 모두 아래 코드로 인스턴스를 생성할 수 있습니다.
1 | var 팥붕어빵1 = new 붕어빵틀({ 앙금: "팥" }); |
하지만 인스턴스 생성 시점에서 아래와 같이 두 방식의 차이점이 발생합니다.
1 | // 프로토타입을 활용하지 않은 생성자 함수의 인스턴스 |
prototype 을 활용하지 않은 생성자 함수의 인스턴스
: 생성자 함수의 모든 멤버를 받기 때문에 굳이 필요치 않은 부분까지 메모리에 올라가 인스턴스를 많이 만들수록 부담이 됩니다.prototype 을 활용한 생성자 함수의 인스턴스
:prototype object
에 선언된 멤버들은 매 인스턴스 마다 메모리에 올라가지 않고 동일한 참조값을 가집니다. ( 원시타입의 경우 인스턴스 멤버,prototype
멤버 여부와 관계 없이 메모리에 올라갑니다. )
기타 프로토타입 특징
prototype
멤버의 내용을 동적으로 변경하면 변경 이전에 생성된 객체라도 적용이 됩니다.- 인스턴스에서는
prototype
의 내용을 읽을수는 있지만 쓸수는 없습니다. prototype
체인의 마지막은 AlwaysObject.prototype
입니다.
지금까지 prototype
이 무엇이고 언제 사용하게 되는지 알아보았습니다. 여기까지만 이해하셔도 이전과는 다른 방식으로 코드를 좀 더 재밋게 작성할 수 있으실거라 생각합니다.
하지만 조금 더 재밋게 사용하기 위해 prototype
상속에 대해서도 조금 알아 보겠습니다.
프로토타입을 사용한 상속 구현
자바스크립트 상속은 객체만으로 가능하지만 복잡한 어플리케이션의 요구사항을 만족하기엔 무리가 있습니다. 그래서 클래스와 같은 생성자 함수를 상속함하여 코드를 좀 더 짜임세 있게 설계할 필요가 있습니다. 실제로는 프로토타입 체인을 연결하고 필요에 따라 메소드를 오버라이드 해주는 것이 전부입니다.
지금은 ECMAScript 2015 (ES6) 스펙에 class
, extends
키워드가 추가되어 쉽게 클래스(생서자 함수)와 상속을 사용할 수 있습니다. 하지만 저처럼 아직 class
, extends
키워드를 사용하지 못하는 안타까운 환경에 놓인 개발자들도 있습니다.
저같은 개발자를 위해 old school 방식으로 상속을 구현하는 방법에 대해 알아보겠습니다.
구현하려는 상속은 다음과 같은 요구사항을 가집니다.
- 부모 생성자가 만드는 인스턴스별 고유해야할 속성 참조가능
- 부모 생성자의 prototype 멤버에 접근가능
이해를 돕기위해 제가 좋아하는 자동차로 예제 코드를 작성해 보겠습니다.
폭스바겐/아우디 사에서는 비용절감을 위한 차세대 자동차 플랫폼을 연구 개발하였습니다. 개발된 플랫폼의 이름은 MQB 로 앞으로 폭스바겐, 아우디에서 생산되는 많은 차종에 공통으로 사용될 플랫폼 입니다.
MQB : 폭스바겐, 아우디사의 비용절감 전략 플랫폼으로 Golf, A3 등 여러 차종에 범용적으로 사용되는 자동차 프레임
MQB
폭스바겐, 아우디(생성자 함수)가 상속받을 차세대 자동차 플랫폼 생성자 함수입니다.
플랫폼이 버틸 수 있는 최대 마력수, 사용 가능한 구동타입, 휠 베이스 등의 정보를 가지고 있습니다.
1 | var MQB = function(){ |
FF: 엔진이 차체 앞에 위치하면 앞바퀴 굴림의 구동방식 (전륜)
FR: 엔진이 차체 앞에 위치하면 뒤바퀴 굴림의 구동방식 (후륜)
Volkswagen
MQB 플랫폼을 상속받아 자동차를 생산할 Volkswagen 생성자 함수입니다.
1 | var Volkswagen = function(){ |
MQB 와 Volkswagen 두개의 생성자 함수가 준비되었습니다. Volkswagen 은 MQB 플랫폼을 상속받아 자동차를 만들어야 하기 때문에 생성자 빌려쓰기 와 프로토타입 링크 참조값 변경 을 통하여 두 클래스간 상속 관계를 맺어 주도록 하겠습니다.
생성자 빌려쓰기
prototype link
의 참조값 변경으로 부모 prototype object
는 상속 받을 수 있지만 부모 생성자가 만드는 인스턴스 멤버는 상속 받을 수 없기 때문에 부모 생성자가 만든 인스턴스의 멤버를 참조할 수 있도록 MQB.apply( this, arguments )
코드를 호출해 줍니다.
1 | var Volkswagen = function(){ |
생성자 빌려쓰기 후 Volkswagen 인스턴스를 생성하면 인스턴스 멤버로 부모 생성자인 MQB 의 인스턴스 멤버도 참조할 수 있습니다. 다음으로 prototype link
의 참조값 변경을 통하여 프로토타입 상속을 구현을 완성해 보겠습니다.
프로토타입 링크 참조값 변경
임의의 객체 F
를 사용하여 부모 생성자의 prototype
을 상속받고, 해당 객체의 인스턴스를 자식 객체 prototype link
의 참조값으로 사용합니다.
1 | var F = function(){}; |
상속 후 Volkswagen.prototype.__proto__
찍어보면 MQB.prototype
를 참조하고 있음을 알 수 있습니다.
1 | Volkswagen.prototpye = { |
이렇게 하면 상속 자체는 모두 끝이 납니다. 하지만 상속을 통하여 코드를 잘 작성하기 위해서 주의해야할 점이 있습니다.
반드시 prototype
상속을 위해 앞에서 보여드린 상속 코드 이후 아래와 같이 자식 생성자 함수의 prototype object
에 멤버를 추가해야 한다는 점 입니다. 역순으로 할 경우 자식 생성자 함수의 prototype object
에 선언된 모든 멤버는 가비지 컬렉터에 의해서 날아가게 됩니다.
1 | Volkswagen.prototype.setFuel = function( fuelType ){ |
MQB 를 상속받은 Volkswagen 를 통하여 GTI, GTD 같은 차들을 만들 수 있으며 마력이나 구동방식 셋팅시 MQB 가 제공하는 함수를 통하여 미리 설계된 범위 내에서 안전한 차량을 생산할 수 있습니다.
1 | var golf_gti = new Volkswagen(); |
마지막으로 좀 더 깔끔하게 상속을 처리를 위해 앞에서 살펴본 prototype link
참조값 변경 코드를 아래와 같이 inherit
함수로 만들어 줍니다.
1 | var inherit = function( Parent, Child ){ |
개선된 함수는 아래와 같은 기능을 합니다.
- 파라미터로 부모, 자식 생성자 함수를 받습니다.
- 임의의 생성자 함수
F
를 통해 자식 생성자 함수의prototype link
를 변경합니다. constructor
속성을 추가하여 생성자가 누구인지 알려줍니다.- 명시하지 않을 경우 체인을 타고 부모
prototype object
의constructor
를 참조하기 때문에 부모 생성자를 참조하게 됩니다.
- 명시하지 않을 경우 체인을 타고 부모
- 마지막으로
prototype
체인을 타지 않고 부모 생성자의prototype
멤버를 바로 호출하기 위한super
키워드를 추가합니다.
재사용 가능한 상속 함수를 만듦으로써 prototype
을 사용한 상속도 이제 손쉽게 할 수 있게 되었습니다. 이제 자바스크립트에서 넘어야할 많은 산들중 큰 산을 하나 넘었습니다.
기왕 산을넘은 김에 old school 방식이 아닌 모던한 방법도 알아볼까요?
조금 허무할 수 도 있습니다. 너무 심플하거든요.
모던하게 자바스크립트 상속 구현하기
ECMAScript 2015 문법을 사용한 상속 구현이지만 내부적으로는 old school 방식과 동일하게 상속을 처리합니다. 아래 두가지 방법을 소개해 드립니다.
- ECMAScript 2015 (ES6) 문법인
Object.create
메소드 사용구글링 하다가 Classical inheritance with Object.create() 이라는 페이지를 찾았습니다. 모던한 방법이 아닌 클래시컬한 방법이라고 하네요…ㅠ
- ECMAScript 2015 (ES6) 문법인
class
,extends
키워드 사용
Object.create
Object.create
메서드는 지정된 prototype object
및 속성을 갖는 새 객체를 만듭니다.
1 | Volkswagen.prototype = Object.create(MQB.prototype); |
위 코드를 사용하면 임의의 생성자 함수를 직접 만들 필요가 없이 내부적으로 아래와 같이 prototype link
참조를 변경해 줍니다.
1 | Volkswagen.prototpye = { |
prototype
상속은 잘 되었지만 앞서 만든 inherit
함수에 추가한 constructor
나 super
같은 내용은 처리해 주지 않습니다. 따라서 아래와 같이 기존에 만든 상속 함수에 prototype link
변경 부분만 수정하여 좀 더 간결한 상속 함수로 개선해 줍니다.
1 | var inherit = function( Parent, Child ){ |
상속 함수 자체는 많이 심플해 졌지만 부모 생성자의 인스턴스 멤버에 접근하기 위해서는 여전히 자식 함수에서 부모 생성자 함수 빌려쓰기를 해줘야 합니다.
이제 class
, extends
키워드를 사용해 더 심플해질 차례입니다.
class, extends
class
키워드를 사용하여 부모 클래스를 MQB
를 선언합니다.
1 | class MQB { |
부모 클래스 MQB
를 상속받을 Volkswagen
클래스는 extends
키워드를 사용하여 선언합니다.
1 | class Volkswagen extends MQB { |
상속이 완료되었습니다. old school 방식처럼 생성자 빌려쓰기, constructor, super 를 처리하기 위한 어떠한 작업도 필요 없습니다. 코드에서 보듯이 super 키워드로 부모 생성자를 초기화 할 수 있으며, super.getTitle 과 같이 부모 클래스의 prototype object
를 바로 참조할 수 도 있습니다. 인스턴스.constructor
를 찍어보면 아래와 같이 인스턴스의 생성자 함수인 Volkswagen
가 잘 찍히는 것도 확인해 볼 수 있습니다.
1 | const golf = new Volkswagen(); |
마무리
prototype
이 무엇이며 언제 사용되는지와 prototype
을 사용하여 상속을 구현하는 몇가지 방법들도 살펴보았습니다. 부족한 내용이지만 조금이나마 도움이 되었기를 바라며 es6 이상의 개발 환경에 있든 아니든 prototype
을 활용하여 좀 더 즐겁게 개발하실 수 있기를 바랍니다.
잘 못된 내용이나 개선점이 있으면 피드백을 남겨주시면 반영하도록 하겠습니다.
읽어주셔서 감사합니다.