Thumbnail

7분

JavaScript Object(1) - Prototype

들어가면서

자바스크립트에서는 배열과같은 기능부터 JavaScript 위에 구축된 브라우저 APIs에 이르기까지 대부분의 것들이 객체입니다.
사용자는 관련된 함수들과 변수들을 효율적인 패키지로 추상화하거나 편리한 데이터 컨테이너로 작동하는 객체를 만들 수 있습니다.
언어에 대한 지식을 가지고 더 멀리 나아고자 한다면 자바스크립트의 객체 기반의 본질을 이해하는 것이 중요합니다.
- Javascript 객체 소개 - mdn

자바스크립트는 객체 지향 언어이지만 다른 객체 지향 언어들과는 약간 다른 특성을 가지고 있습니다. 이 차이점은 자바스크립트의 프로토타입 때문입니다. 이 글에서는 자바스크립트의 객체와 프로토타입에 대해 깊게 알아보겠습니다.

OOJS - 객체 지향 자바스크립트(Object-Oriented Javascript)

OOP의 기본 컨셉은 프로그램 내에서 표현하고자 하는 실 세계(real world)의 일들을 객체를 사용해서 모델링 하고, 객체를 사용하지 않으면 불가능 혹은 무지 어려웠을 일들을 쉽게 처리하는 방법을 제공한다는 것입니다.

객체 지향 프로그래밍의 핵심은 실세계의 사물을 객체로 모델링하고, 객체를 이용해 복잡한 문제를 쉽게 해결하는 것입니다. 자바스크립트 역시 객체 지향 언어로, 1995년 12월에 프로토타입 기반의 객체 지향 언어로 탄생했습니다.

자바스크립트의 객체 지향 설계는 셀프 라는 언어에 영향을 받았습니다. 그리고 1995년 12월, 프로토타입 형태의 객체 지향 언어인 자바스크립트가 만들어졌습니다.

셀프는 프로토타입 개념 기반의 객체 지향 프로그래밍 언어로 1980년대 1990년대에 언어 디자인을 위한 실험적인 테스트 시스템으로 처음 사용되었습니다.

자바스크립트의 클래스는 ES6부터 도입되었지만, 내부적으로는 여전히 프로토타입 기반의 객체를 사용합니다.

ES6 Class는 ES5의 Object 생성자 함수의 syntactic sugar입니다.
syntactics sugar: 프로그래밍 언어 내에서 더 쉽게 읽거나 더 쉽게 표현할 수 있게 만들어진 해당 언어 내에 syntax를 뜻합니다.
- Brad Traversy

자바스크립트에서 프로토타입 기반의 객체는 무엇을 뜻하는 걸까요?

프로토타입 객체

자바스크립트 모든 객체들이 메소트와 속성을 상속 받기 위한 템플릿으로써 프로토타입 객체를 가진다는 의미입니다.
프로토타입 객체도 또 다시 상위 프로토타입 객체로부터 메소드와 속성을 상속 받을 수도 있고 그 상위 프로토타입 객체도 마찬가지입니다.
이를 프로토타입 체인이라 부르며 다른 객체에 정의된 메소드와 속성을 한 객체에서 사용할 수 있도록 하는 근간입니다.
_- 프로토타입 기반 언어? - MDN*

자바스크립트에서 모든 객체는 메소드와 속성을 상속받기 위한 템플릿 역할을 하는 프로토타입 객체를 가집니다. 프로토타입 객체는 상위 프로토타입 객체로부터 메소드와 속성을 상속받을 수 있으며, 이를 프로토타입 체인이라 부릅니다.

객체 인스턴스와 프로토타입 간의 연결은 다음과 같이 이루어집니다.

  1. 객체의 프로토타입: Object.getPrototypeOf(obj)
  2. 프로토타입 속성: 생성자의 prototype 속성

이 두 가지 개념의 차이를 이해하는 것이 중요합니다. 첫 번째는 개별 객체의 속성이고, 두 번째는 생성자의 속성입니다.

예제를 통한 이해

다음과 같은 Person() 생성자 함수를 작성했다고 가정해봅시다.

function Person(first, last, age, gender, interests) {
  // 속성 정의
  this.first = first
  this.last = last
  this.age = age
  this.gender = gender
  this.interests = interests
 
  // 메소드 정의
  this.bio = function () {
    alert(this.first + " " + this.last + " " + this.age)
  }
  this.greeting = function () {
    alert("Hi! I'm " + this.name.first + ".")
  }
}

이 생성자를 사용해 객체를 하나 생성해보겠습니다.

var person1 = new Person("Bob", "Smith", 32, "male", ["music", "skiing"])

생성자 함수를 이용해 person1 객체를 생성했습니다. 이 객체에는 생성자 함수에서 정의한 속성과 메서드가 포함되어 있습니다.

prototype-2.png

person1의 프로토타입 객체인 Person()에 정의된 멤버들이 보입니다. (first, last, age ...) 뿐 만 아니라 valueOf, toString과 같이 Person()의 프로토타입 객체인 Object에 정의된 다른 멤버들도 보입니다.

이렇게 상속된 값들도 다 같이 보인다는 것은 프로토타입 체인이 동작하는 증거입니다.

prototype-2

실제로는 Object에 정의되어있는 valueOf 메소드를 person1에서 호출하면 어떻게 될까요?

prototype-3.png

이 메소드는 호출된 객체(여기서는 person1)의 값을 단순 반환합니다. 프로토타입 체인 관점에서 보자면 다음 흐름과 같습니다.

  1. 브라우저는 우선 person1객체에 valueOf() 메소드를 체크합니다.
  2. 없기 때문에 person1의 프로토타입 객체 인(Person() 생성자의 프로토타입)에 valueOf() 메소드를 체크합니다.
  3. 여전히 없기 대문에 Person() 생성자의 프로토타입 객체의 프로토타입 객체(Object() 생성자의 프로토타입)가 valueOf() 메소드를 가지고 있는 체크합니다. 여기에 있으니 호출하면서 끝납니다.

여기서 중요한 점은 프로토타입 체인에서 한 객체의 메소드와 속성들이 다른 객체로 복사되는 것이 아닌 프로토타입 체인을 통해 타고 올라가며 접근할 뿐입니다.

그런데 실제로 person1.valueOf() 처럼 상속받은 멤버들은 실제로 어디에 정의되어 있을까요?

Object.prototype

Object 에는 수 많은 속성과 메소드들이 있습니다.
그러나 실제로 상속받은 멤버들은 몇 개 되지 않습니다. 일부는 상속 되었지만 나머지는 아닙니다. 왜그럴까요?

실제로 상속 받는 멤버들은 prototype 속성에 정의되어 있습니다. Object. 가 아닌 Object.prototype.로 접근 되는 것입니다.

prototype 속성도 하나의 객체이며 프로토타입 체인을 통해 상속되는 속성과 메소드를 담아두는 버킷으로 주로 사용되는 객체입니다. Object.is(), Object.keys()prototype 버킷에 정의되지 않은 멤버들은 상속되지 않습니다.

object-prototype.png

ex-prototype.png

Primitive 객체 String, Date, Number, Array의 프로토타입도 확인해보는 것도 좋습니다.

string-prototype.png

우리가 string 변수를 선언하고 사용할 때 replacesplit을 쓸 수 있는 이유가 여기에 있습니다.

다시 한번 정리한다면,

  • prototype 속성에 정의되어 있는 멤버는 상속되어집니다.
  • 인스턴스가 속성을 사용하는 방식은 본인 객체와 프로토타입 체인에 의해서 부모 객체를 타고 올라가면서 찾는 방식입니다.

prototype 속성은 Javascript에서 가장 헷갈리는 명칭중 하나입니다. 보통 this가 현재 객체의 프로토타입 객체를 가리킬 것이라 오해하지만 그렇지 않습니다. (프로토타입 객체는 __proto__ 속성으로 접근 가능한 내장 객체입니다.)

thisprototype 속성을 가리킵니다. 그리고 prototype속성은 상속되는 속성이 정의된 객체를 가리킵니다.

this-prototype.png

여기 thisPersonprototype 속성을 가리키고 그래서 프로토타입에 속성과 메소들을 정의할 수 있습니다.

자바스크립트에서 함수

함수 역시 객체의 하나입니다.
못 미더우시면 Function() 생성자 레퍼런스 페이지를 확인해보세요.
프로토타입 속성 - mdn

자바스크립트에서 함수 또한 객체의 하나일 뿐이라는 것이죠.

  • 자바스크립트에서 거의 모든 것은 객체입니다.: null, undefined를 제외한 모든 것은 객체입니다.
  • 그리고 모든 객체들은 프로토타입 객체를 가집니다.

객체 인스턴스

  • var foo1 = new Foo('bar')
  • var foo2 = { name: 'bar' }
  • var foo3 = new Object(); foo3.name = 'bar';
  • var foo4 = Object.create(foo1);

자바스크립트에서는 객체 인스턴스를 만드는 방법은 꽤나 많습니다.

다음은 객체 인스턴스를 만드는 방법 4가지를 설명합니다.

  1. 생성자 함수
  2. Object() 생성자
  3. 객체 리터럴
  4. Object.create()

1. 생성자 함수

생성자 함수는 클래스의 자바스크립트 버전입니다.

자바스크립트는 객체와 그 기능을 정의하기 위해 생성자 함수라고 불리는 특별한 함수를 사용할 수 있습니다.

function Person(name) {
  this.name = name
  this.greeting = function () {
    alert("Hi! I'm " + this.name + ".")
  }
}

생성자 함수는 단순히 속성과 메소드를 정의합니다. 그리고 이를 위해 this를 사용하고 있는 것을 볼 수 있습니다.

이것은 객체 인스턴스가 만들어질 때마다 객체의 name 속성이 생성자 함수 호출에서 전달된 name 값과 같아질 것이라고 말하고 있습니다.

var person1 = new Person("Bob")
person1.greeting()
 
// alert('Hi! I\'m Bob')

new 키워드가 브라우저에게 우리가 새로운 객체 인스턴스를 만들고 싶어한다는 것을 알려줍니다.

사실 위 처럼 메소드를 직접 정의해놓으면 생성자 함수를 호출할 때마다 다시 정의하게 되어 최선의 방법은 아닙니다.
해결책은 prototype에 함수를 정의해놓고 사용하는 방법이 있습니다.

프로토타입 수정하기

사실 일반적인 방식으로는 속성은 생성자 함수에서, 메소드는 프로토타입에서 정의합니다.
생성자 함수에는 속성에 대한 정의만 있으며 메소드는 별도의 블럭으로 구분할 수 있으니 코드를 읽기가 훨씬 쉬워집니다.

위에서 greeting() 을 생성자 함수 안에 메소드를 선언했지만, 이를 프로토타입에 정의하는 방식으로 변경해보겠습니다.

function Person(name) {
  this.name = name
}
 
var person1 = new Person("Bob")
 
Person.prototype.greeting = function () {
  alert("Hi! I'm " + this.name + ".")
}
 
person1.greeting()

나중에 선언했지만 person1에서 바로 greeting() 메소드를 사용할 수 있습니다.

실제로 프로토타입 객체는 모든 인스턴스에서 공유되는 개념이기 때문에 즉시 별도의 갱신 과정 없이 접근이 가능합니다.

프로토타입에 속성을 정의할 수는 있으나 그렇게 하지 않는 이유는 namespace 가 전역 범위를 가리키므로 코드가 의도대로 동작되지 않을 수 있기 때문입니다.

Person.prototype.fullName = this.name.first + " " + this.name.last
 
// 여기서 this는 전역을 가리키므로 undefined undefined으로 나타날 것입니다.

2. Object() 생성자

최초의 object 역시 생성자를 가지고 있습니다.

var person1 = new Object()
person1.name = "Bob"
person1.greeting = function () {
  alert("Hi! I'm " + this.name + ".")
}

점 표기법이나 괄호 표기법을 이용해 속성과 메소드를 추가할 수 있습니다.

객체 인스턴스를 일회성으로 만들어서 바로 사용하기 때문에 재사용에 있어서 차이가 있습니다.

3. 객체 리터럴

객체 리터럴 구문은 더 간결하며 new Object() 와 성능 차이는 없습니다.

아마 개발하면서 가장 많이 사용하는 syntax 중 하나라고 생각됩니다.

Object() 생성자와 마찬가지로 일회성으로 사용하기 때문에 재사용에 있어서 차이가 있습니다.

var person1 = {
  name: "Bob",
  greeting: function () {
    alert("Hi! I'm " + this.name + ".")
  },
}

4. Object.create()

하지만 몇몇 사람들은 객체 인스턴스들을 생성할 때 먼저 생성자를 만들기를 귀찮아 합니다.

위에서 살펴봤던 재사용성이 없는 객체 리터럴, new Object() 같은 것들도 재사용성을 갖게 할 수 있을까요?

Object 객체에는 create() 메소드가 있습니다. 이미 존재하는 객체 인스턴스를 이용하여 새로운 인스턴스를 만들 수 있는 메소드입니다.

위 예시에서 만들었던 person1을 이용하여 person2를 만들어보겠습니다.

var person2 = Object.create(person1)

create() 메소드가 실제로 하는 일은 주어진 객체를 프로토타입 객체로 삼아서 새로운 객체를 만드는 것입니다.

위 예시는 person2가 person1을 프로토타입 객체로 삼습니다.

create-prototype.png

객체 모습이 매우 비슷하지만 살짝 다른 객체 인스턴스를 만들 때 좋지 않을까 생각이 들기도 합니다.

그러면 Class 상속이랑 느낌이 비슷한 것 같다는 생각도 듭니다.

프로토타입에서는 체인을 통해 상속과 비슷한 로직이 이루어진다고 본 것 같은데, 그건 그거고 상속은 또 따로 있는 것일까요?

실제로 Prototype에서의 상속은 이와 어떤 차이가 있으며 어떻게 이루어지는 걸까요?

프로토타입 상속

프로토타입 체인, 체인을 통해 멤버들을 탐색하는 것도 보았습니다.
하지만 이는 대부분 브라우저가 알아서 처리하는 로직이었습니다.
그러면 우리가 직접 객체를 생성하고 상속하려면 어떻게 해야 할까요? - 프로토타입 상속 - mdn

예제를 보면서 이해해보도록 합시다.

function Person(first, last, age, gender, interests) {
  this.name = {
    first,
    last,
  }
  this.age = age
  this.gender = gender
  this.interests = interests
}
 
Person.prototype.greeting = function () {
  alert("Hi! I'm " + this.name.first + ".")
}
 
Person.prototype.farewell = function () {
  alert(this.name.first + " has left the building. Bye for now!")
}

속성은 생성자 함수에, 메소드는 prototype에 정의되어 있습니다.

Person 객체를 상속받고 몇 가지를 추가한 Teacher 객체를 만들어보겠습니다.

  • subject 속성 추가
  • greeting() 메소드 변경: 좀 더 공손하게

Function.prototype.call()

call() 메소드는 주어진 this 값 및 각각 전달된 인수와 함께 함수를 호출합니다.
- Function.prototype.call() - mdn

Teacher 객체에서 Person 생성자를 호출하기 위해 사용하는 메소드가 Function 프로토타입에 정의된 call() 메소드입니다.

function Teacher(first, last, age, gender, interests, subject) {
  Person.call(this, first, last, age, gender, interests)
 
  this.subject = subject
}

call() 메소드의 첫 번째 인자는 다른 곳에서 정의된 함수를 현재 컨텍스트에서 실행할 수 있도록 합니다.

다시 말해, Person 또한 생성자 함수이므로 생성자를 Teacher(= this) 컨텍스트에서 실행하게 된다는 뜻입니다. (첫 번째 인자를 전달하지 않으면, this 의 값은 전역 객체에 바인딩 됩니다.)

엄격 모드(strict mode)에서는 스코프가 함수 컨텍스트 내로 한정되기 때문에 첫 번째 인자를 전달하지 않으면 this는 undefined를 가지게 됩니다.
strict-call.png

그리고 call() 함수의 나머지 인자로 Person에 필요한 인자를 전달합니다.

이렇게 하면 속성에 관해서는 상속받기가 끝이 났습니다.

그러나 greeting() farewell() 과 같이 prototype에 정의된 메소드들은 어떻게 상속받아야 할까요?

prototype에 정의된 메소드도 상속받기

현재 위 상태에서 PersonTeacher 프로토타입을 확인해본다면 method는 상속받은 상태가 아닙니다.

inherit-prototype.png

Teacher가 메소드도 상속받으려면 어떻게 해야 할까요?

  • Object.create() 사용
  • 만들어진 prototype.constructor값을 Person에서 Teacher로 수정
Teacher.prototype = Object.create(Person.prototype)

앞에서 적었듯이 prototype도 객체 이므로 Object.create를 통해 객체 인스턴스를 만들 수 있습니다.

그런데 모든 값이 Person.prototype 객체 인스턴스 값으로 되어있기 때문에 추후 문제 소지가 있는 부분은 수정해야 합니다.

Teacher.prototype.constructor = Teacher

이렇게 되면 우리가 의도한대로 모든 멤버들에 대해 상속이 완료되었습니다.

상속한 메소드 수정하기

Teacher() 에 새로운 greeting() 함수를 정의하도록 합시다. (정확히는 상속된 메소드를 overriding 하는 것입니다.)

Teacher.prototype.greeting = function () {
  alert("Hello. My name is " + this.name.last + ", and I teach " + this.subject + ".")
}

여러 방법이 있겠지만 가장 간단한 방법으로 프로토타입을 통해 overriding하는 방법입니다.

마무리하며

ES6(ECMAScript 2015)에서 도입된 클래스 구문(Class syntax)으로 인해 객체를 더 쉽고 명확하게 재사용할 수 있게 되었습니다. 최신 브라우저들은 이 새로운 클래스 구문을 지원하지만, 일부 구형 브라우저(예: 인터넷 익스플로러)에서는 동작하지 않을 수 있습니다. 따라서 하위 호환성을 고려할 필요가 있으며, 이 때문에 프로토타입에 대해 알아두는 것이 중요합니다.

다음 포스트에서는 자바스크립트에서 클래스 구문이 어떻게 사용되는지, 그리고 이번 포스트에서 다루지 못한 객체의 다양한 메서드들을 살펴볼 예정입니다.

Reference

마지막 업데이트

3/26/2023


Avatar

JHSeo

배우는 것을 좋아하고 관심이 많은 웹 엔지니어 입니다. 느리더라도 꾸준하게 성장하려고 노력하는 개발자입니다.