본문 바로가기

스터디/모던 자바스크립트 딥 다이브

[딥 다이브 스터디 10장] 객체와 프로토타입 (feat. new, __proto__)

<모던 자바스크립트 딥 다이브> (이웅모, 위키북스) 를 읽고 공부한 내용입니다. 책의 내용을 그대로 적어 놓은 것이 아니기 때문에 오류가 있을 수 있습니다. 오류가 있다면 댓글을 통해 피드백 부탁드립니다.

💡 중점적으로 공부한 내용

✔ 객체를 생성하는 방식에 대해
✔ 객체의 메서드를 표현하는 방식에 대해
✔ new 키워드에 대해
✔ 프로토타입 체이닝 동작방식에 대해

10장 <객체 리터럴>

객체는 0개 이상의 프로퍼티로 구성된 집합이다. 프로퍼티는 key: value 쌍으로 구성된다. 이 경우 value가 함수라면 이 객체의 동작을 나타낸다 하여 관습적으로 '프로퍼티'보다는 '메서드'라고 부른다.

객체의 생성

1. 객체 리터럴 방식

자바스크립트 {} 를 이용해서 가장 기본적으로 객체를 생성할 수 있는 방식이다.

const person = {};

여기에 '이름' 이라는 프로퍼티와 인사를 하는 메서드를 정의해보겠다.

const person = {
  name: "Sang Yoon",
  sayHello: function () {
    return `Hello, my name is ${this.name}`;
  },
};

console.log(person.name); // 'Sang Yoon'
console.log(person.sayHello()); // 'Hello, my name is Sang Yoon'

메서드의 표현 방식 3가지

위 예제에서 sayHello는 메서드로, 3가지 방법으로 표현될 수 있다.

const person = {
  name: "Sang Yoon",
  sayHello: function () {
    return `Hello, my name is ${this.name}`;
  },
  sayHello2() {
    return `Hello, my name is ${this.name}`;
  },
  sayHello3: () => `Hello, my name is ${this.name}`,
};

console.log(person.name); // 'Sang Yoon'
console.log(person.sayHello()); // 'Hello, my name is Sang Yoon'
console.log(person.sayHello2()); // 'Hello, my name is Sang Yoon'
console.log(person.sayHello3()); // 'Hello, my name is undefined'
  1. sayHello 는 가장 기본적인 방식(정석)이다.
  2. sayHello2는 1의 축약형이다.
  3. sayHello3는 화살표 함수를 사용했다.

그런데 화살표 함수로 표현한 sayHello3는 this.name을 undefined로 출력했다. 객체 리터럴 방식에서 화살표 함수로 메서드를 표현하면 의도치 않은 결과를 반환한다. 이 경우 this는 person을 가리키는 것이 아니라 아무것도 가리키지 않는다. 화살표 함수의 this는 문맥적 컨텍스트(렉시컬 컨텍스트 환경)에서 조금씩 달라지는데, 자세한 이유는 여기서 다루지 않겠다. 객체 리터럴 방식에서 메서드를 표현할 때는 화살표 함수를 사용하면 안된다는 것을 염두해 두자. 다만 객체 리터럴 방식이 아닌 경우에 객체(인스턴스)를 생성한다면 오히려 화살표 함수를 사용하는 것이 더 좋다.

2. 함수와 클래스

객체 리터럴 방식으로 객체를 생성하는 것은 매우 간편하지만, 이 객체를 추상화하여 인스턴스를 생성하고 싶다면 객체 리터럴 방식은 무리가 있다. 왜냐하면, 똑같이 name, sayHello 메서드를 가지고 있는 사람 10만명을 생성하려면 객체 리터럴 방식으로 10만번을 만들어야 할 것이다. 이것은 비용이 많이 드는 일이다. 이것을 위해 객체지향언어에서는 객체를 추상화한다.

class는 ES6에서 도입된 문법이다. 그 전까지 자바스크립트에는 class 문법이 없었다. (충격적...) 자바스크립트는 프로토타입 언어이다. 그래서 프로토타입 체이닝 방식을 통해 인스턴스를 생성한다. 여기서 인스턴스는 객체지향언어에서 객체를 추상적으로 정의한 객체를 표현한 용어이다.
나는 (객체 == 인스턴스)는 true, (객체 === 인스턴스)는 false라고 말하고 싶다.

함수로 표현

class문법이 없었을 시절 인스턴스를 추상화하기 위해 아래와 같이 함수와 프로토타입 체이닝을 이용했다.

// 생성자 함수
function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayName = function () {
  return this.name;
};

Person.prototype.sayAge = function () {
  return this.age;
};

const person = new Person("yong", 20);

console.log(person.sayName()); // "yong"
console.log(person.sayAge()); // 20

클래스로 표현

자바스크립트에서 prototype 체이닝을 통해 인스턴스를 만드는 것은 기존 객체지향언어를 사용하던 프로그래머들에게 진입장벽이었다. 그래서 자바스크립트는 다른 객체지향언어의 문법을 지향하기 위해 ES6에서 클래스 문법을 도입했다. 사실 class문법을 사용하더라도 내부적으로는 함수로 생성하는 것과 마찬가지로 프로토타입 기반으로 동작하지만 몇 가지 차이가 있다.

 

그 차이는... <25장 클래스 - "클래스는 프로토타입의 문법적 설탕인가?"> 를 참고하자...

 

어쨌든 클래스로 표현하면 매우 직관적이다. (가독성은 상대적이다. 내가 클래스에 익숙해져있기 때문일 것...)

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  sayName() {
    return this.name;
  }
  sayAge() {
    return this.age;
  }
}

const person = new Person("yong", 20);

console.log(person.sayName()); // "yong"
console.log(person.sayAge()); // 20

3. Object.create

Object.create 라는 빌트인 메서드를 통해 객체를 생성할 수도 있다. 첫 번째 인자에는 상속을 받을 객체(부모 클래스), 두번째에는 옵션을 넣어준다.

const person = Object.create(null, {
  name: {
    value: "kim",
    writable: false,
    configurable: false,
  },
});

console.log(person) // { name: 'kim' }

특별히 상속받을 객체가 없으니 null을 넣어주었다. 결과적으로 person은 { name: 'kim' } 이라는 객체이다. (person1.name.value 는 undefined이다.)

value 아래로 옵션들이 몇 개 있는데,

  1. writable은 readonly인지 설정한다. 여기서는 false로 되어 있으니 값을 변경할 수 없다. 예를 들어 person.name = 'lee' 는 에러를 발생시킨다.
  2. configurable은 flase로 두면 프로퍼티를 삭제할 수 없도록 한다. delete person.name 은 에러가 난다.

(지극히 개인적인 의견) Object.create는 이제는 잘 사용하지 않는 문법일 것 같다. ES6 이전 자바스크립트는 클래스 문법이 없었고, 클래스 문법이 나온 상황에서 이제는 extends를 하면 된다. 또 그 시절에는 타입스크립트도 없었으니 울며 겨자먹기로 사용하는 문법이 아니었을까 라는 생각이 든다.


추가 학습) new 키워드와 프로토타입 체이닝

우아한 형제들 김민태님의 <자바스크립트, 타입스크립트 에센셜(패스트캠퍼스)> 강의를 참고했습니다.

 

함수와 클래스로 인스턴스를 생성할 때 new 키워드를 이용해서 객체를 생성하고 있다. prototype (클래스도 내부적으로는 프로토타입이므로) 으로 생성한 객체는 함수의 형태이다. new 는 특별한 기능을 많이 가지고 있는데, 함수를 new 키워드를 통해 호출하면 메모리 내부에 함수 이름으로 빈 객체를 생성하고 자동적으로 리턴한다.

또한 기존에 인스턴스를 정의할 때, class 문법이 없었으므로 함수로 인스턴스를 생성했다. 하지만 이렇게 되면 이 인스턴스를 사용하는 사용자 쪽에서는 이 함수가 인스턴스를 위한 생성자 함수인지, 그냥 함수인지 알 수가 없다. 그래서 인스턴스를 위한 생성자 함수라면 첫 글자를 대문자로 하는 컨벤션을 가지도록 했다. 하지만 문법적으로 오류가 나지 않기 때문에 개발상 문제가 생길 수도 있다. 그래서 class 문법이 도입된 이후에, class 문법으로 생성한 인스턴스는 반드시 new를 붙여야 한다. (안그러면 에러남)

function x() {
  return;
}

const obj = new x(); // new 로 빈 객체를 리턴했기 때문에 obj는 현재 빈 객체가 되었다.

obj.value = 10;
obj.getValue = function () {
  return this.value;
};

console.log(obj); // x { value: 10, getValue:[Function] }
console.log(obj.value); // 10
console.log(obj.getValue()); // 10

위와 같은 메커니즘으로 prototype 체이닝을 통해 x 자체에 속성과 메서드를 전달 할 수 있다.

x.prototype.value = 10;
x.prototype.getValue = function () {
  return this.value;
};

const y = new x();

console.log(y); // x {}
console.log(y.value); // 10
console.log(y.getValue()); // 10

y는 비어있는 객체다. 하지만 valuegetValue를 가지고 있다. x는 부모, y는 자식 클래스라고 생각하면 이해가 쉬울 것 같다. 내부적으로는 스코프와 컨텍스트 라는 개념과 연결되는 것인데, 이것은 자바스크립트가 y에 접근해서 valuegetValue를 찾으려 했지만 찾지 못해서 상위 컨텍스트(부모 클래스인 x)에서 찾은 것이다.

prototype

바로 위 예시에서, prototype속성에 넣고 있는 함수와 그렇지 않은 함수가 눈에 띈다. prototype속성에 넣는 함수는 인스턴스 객체에 나타나지만 그렇지 않은 함수는 나타나지 않는다.

그렇다면 prototype 으로 어떻게 메소드를 추가하는 것일까?

이것도 자바스크립트만의 메커니즘인데, new를 이용해서 만들어낸 객체가 해당 메소드를 사용하려하면 먼저 그 객체가 메소드를 가지고 있는지 확인한다. (__proto__) 따라서 앞서 prototype을 통해 메소드를 추가했으니 자바스크립트가 그 메소드를 찾게되어 사용할 수 있게 되는 것이다.

prototype은 앞의 함수를 가리키고 있기 때문에 아래와 같은 것도 가능하다.

function myName(name) {
  this.name = name;
}
const kim = new myName("kim");
console.log(kim.name); // kim

myName.prototype.age = 18;
console.log(kim); // myName { name: 'kim' }
console.log(kim.age); // 18

kim만 콘솔에 출력했을 때는 name밖에 나오지 않았는데, prototype으로 추가한 age라는 속성이 출력되었다.

조금 더 나아가면 prototype으로 추가한 속성을 받아서 객체까지 가면, 객체는 __proto__ 라는 속성이 prototype에 딸려온 속성과 연결을 짓게 되서 결국 함께 사용할 수 있게 되는 것이다.

프로토타입 체이닝의 다른 예시

자바스크립트의 객체는 빌트인 메서드로 toString이라는 것을 가지고 있다. 그 말은...

const person = {
  name: "Bob",
  age: 28,
};

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

person은 Obejct다. 따라서 person 자체가 toString 메서드를 가지고 있지 않아도, 자바스크립트는 person에서 toString을 찾다가 없으니까 프로토타입 체이닝을 타고 계속 올라가서 최상위 객체인 Object까지 가서야 toString 메서드를 찾은 것이다.

믹스인

프로토타입 체이닝을 연결시켜주면 아래와 같이 객체들을 연결시켜 줄수도 있다. 서로 다른 객체를 연결시켜주는 믹스인 기법의 간단한 예시라고 할 수 있겠다.

const person1 = {
  name: "Bob",
  age: 18,
};

const person2 = {
  name: "Jenny",
  favoriteColor: "blue",
};

const person3 = {
  name: "Son",
  hobby: "soccer",
};

여기서 person1을 person3에 연결시켜보겠다. (person1 -> person3)

person1.__proto__ = person3;
console.log(person1.hobby); // soccer
console.log(person3.age); // undefined

person1에는 hobby란 프로퍼티가 없었는데, __proto__로 연결시켜주면서 person3의 hobby에 연결됐다. (하지만 person3이 person1과 연결된 것은 아니다.)


💡 공부하며 느낀 점

prototype은 new 연산자의 암묵적인 기능과 자바스크립트 객체의 동적바인딩 기능을 이용해서 (더 나아가 __proto__ 속성의 기능까지) 객체에 메서드를 정의하는 메커니즘이라고 할 수 있을 것 같다. class와 prototype 방식을 비교하면 당연히 class 문법이 훨씬 낫다. (역시 신문물) 기존 prototype 방식은 너무 번잡하다.

앞으로는 class를 사용하는 일이 많겠지만 class 문법을 사용할지라도 내부적인 메커니즘은 거의 변함이 없을 것 같다. 따라서 더 많은 이해가 필요할 것 같다.

프로그래머스 데브코스 에서 팀원들과 <모던 자바스크립트 딥 다이브>를 스터디하면서 10장 객체 리터럴에 대해서 다시 한번 공부하면서 예전에 공부했던 내용들도 되짚어보았다. 객체에 대해서 더 파려면 정말 끝이 없겠지만, 일단 10장만을 기준으로는 이 정도 선에서 정리를 마무리 지었다. 앞으로 추가적인 학습이 필요한 부분은 역시 컨텍스트, this, 프로토타입, 클래스...등등 (너무 많아 😂) 이 있을 것 같다.

.
.
.

⁉ 깜짝 퀴즈 ⁉

아래 코드에서는 두 가지 문법적 오류가 있습니다... 어디일까요? (computed property name 에 관한 문제)

const person = {
  age: 20,
  name: "Sang Yoon",
  "last-name": "Yong",
  sayAge: function () {
    return this[age];
  },
  sayLastName(){
    return this.last-name;
  }
};

첫 번째는 person.sayAge 의 리턴 부분입니다. 여기서 this는 person을 가리키고 있고, this[age]에서 자바스크립트 엔진은 age를 식별자로 판단하기 때문에 age를 찾을 수 없다는 에러를 반환합니다. this.age 였다면 괜찮겠지만... computed property 방식으로 접근하기 위해서는 this["age"] 또는 this['age'] 여야 합니다.

두 번째는 person.sayLastName 의 리턴 부분입니다. last-name은 식별자 네이밍 규칙에 어긋납니다. 따라서 last-name이 computed property name 형식으로 "last-name" 로 되어있는 것 처럼 식별자 네이밍 규칙을 만족하지 않는 key에는 computed property 방식으로 접근해야 합니다. sayLastName은 this["last-name"] 또는 this['last-name'] 을 리턴해야 합니다.

 


읽어보면 좋은 포스팅

https://medium.com/@limsungmook/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%8A%94-%EC%99%9C-%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85%EC%9D%84-%EC%84%A0%ED%83%9D%ED%96%88%EC%9D%84%EA%B9%8C-997f985adb42

 

자바스크립트는 왜 프로토타입을 선택했을까

프로토타입으로 검색하면 으레 나오는 서두처럼 저 또한 자바스크립트를 처음 접했을 때 가장 당황스러웠던 게 프로토타입이었습니다.

medium.com