[JS]Class 뜯어보기

Class라고 쓰고 Prototype이라고 읽는다

Posted by Dev X on September 9, 2020

✨Javascript의 Class

1
2
3
4
5
6
7
8
9
10
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    sayHi() {
        console.log(`Hi! my name is ${this.name}`);
    }
}

OOP(객체 지향)을 공부해본 사람들에게 class는 익숙한 개념일 것입니다. JS의 클래스 또한 객체 지향적 개념에서 만들어진 것이기 때문에, 객체를 정의하는 틀이라는 의미를 갖습니다. 가장 흔히 말해지는 비유가 붕어빵과 붕어빵 틀입니다. 객체가 붕어빵이라면 붕어빵을 만드는 틀이 class인 것이죠.

JS의 Class는 ES6에서 새로 추가된 문법이지만 새롭게 추가된 기능은 아닙니다. JS의 class는 함수로 구현되어 있으며, 표현식선언식의 두가지 방법으로 사용이 가능합니다.

1
2
3
4
5
6
7
8
const PersonClass = class {
}//표현식

class Person{
}// 선언식

class {
}// 익명 선언식

또 class는 함수이기 때문에 익명 선언식으로 사용 할 수도 있습니다. (함수와 다른 점이 있다면 class는 호이스팅이 이루어지지 않습니다.)

즉, JS의 class 문법은 class가 없던 시절 class를 흉내내 객체를 생성하던 방식을 간편하게 이용하기 위해 만들어진 Syntax Sugar라는 것이죠. 따라서 class를 완전히 이해하기 위해서는 함수로 class를 구현하는 방식을 이해해야 합니다. 함수로 객체를 생성하는 방식을 천천히 살펴보며 JS class의 핵심인 prototype에 대해 알아보겠습니다.

함수로 객체를 생성하는 4가지 패턴

JS는 객체 지향 언어이기 때문에 class 문법이 없었다해도 다양한 방법으로 class를 구현해왔습니다. 아래는 class를 함수로 구현하는 패턴의 몇가지 예시이고, 의사 클래스 패턴에 다다를수록 발전되는 모습을 볼 수 있습니다.

  • Functional (함수 패턴)
  • Functional-shared (함수 공유 패턴)
  • Prototypal (프로토타입 패턴)
  • Pseudo-classical (의사 클래스 패턴)

Functional

초보 개발자들이 객체 생성 함수를 만들 때 널리 사용하는 방식입니다. 함수 내에서 객체를 생성해 속성과 메서드를 추가한 후, 객체를 돌려줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
const Person = function (name, age) {
    const person = {};
    person.name = name;
    person.age = age;

    person.sayHi = function () {
        console.log(`Hi! my name is ${this.name}!`);
    };
    return person;
};

let person = Person("Peter", 18);
person.sayHi();

장점: 구조가 단순해 이해하기 쉽습니다. 변수를 클로저 스코프 안에 두고 사용하기 때문에 private하게 쓸 수 있습니다.

단점: 객체를 생성할때마다 메서드도 함께 생성되어 새로운 메모리 주소에 할당되기 때문에 메모리 관리가 비효율적입니다.

Functional-shared

Functional 패턴의 중복 메서드 생성 문제를 해결하기 위해 고안된 방식입니다. 레퍼런스 참조를 이용해, 객체의 메서드에 공용으로 사용할 함수를 참조시킵니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const _extend = function (to, from) {
    Object.assign(to, from);
};

const sharedMethods = {
    sayHi: function () {
        console.log(`Hi! my name is ${this.name}!`);
    },
};

const Student = function (name, age) {
    const student = {};
    student.name = name;
    student.age = age;
    _extend(student, sharedMethods);
    return student;
};

const student = Student("Peter", 18);
student.sayHi(); //Hi! my name is Peter!

//참조 함수의 값을 수정해도 인스턴스들에 적용되지 않는다
sharedMethods.sayHi = function () {};
student.sayHi(); //Hi! my name is Peter!

장점: 모든 객체의 메서드가 같은 함수를 참조하기 때문에 메모리를 과도하게 소비하던 문제가 해결되었습니다.

단점: 공용으로 사용하는 함수를 수정하고 싶어도, 재할당시 객체의 메서드와 참조 함수의 메모리 주소가 달라지기 때문에 변경된 내용이 객체의 메서드에도 적용되지 않습니다.

💾Prototype

Prototypal 패턴을 들어가기 전에 프로토타입에 대해 먼저 알아보겠습니다. 자바스크립트는 프로토타입 기반 언어라고 합니다. class 문법이 추가되었다해도, class 기반 언어로 바뀌지 않고 프로토타입으로 class를 구현하고 있습니다.

프로토타입은 번역하면 ‘원형’이라는 의미를 갖습니다. JS에서는 ‘특정 객체들이 공용으로 참조 할 수 있는 메모리’이라는 의미로 사용되고 있고, 그 객체들을 생성하는 함수는 prototype 속성prototype 객체를 갖고 있습니다.

prototype 속성과 prototype 객체

prototype 속성: Prototype 객체를 참조합니다.

prototype 객체: prototype 속성에 정의되는 메소드들이 저장되는 버킷입니다.

constructor: prototype 객체가 생성될때 함께 만들어지는 생성자 속성입니다. 함수 자신을 참조합니다.

__proto__: Person의 인스턴스 객체들이 갖고 있는 속성입니다. Person prototype 객체를 참조합니다.

이런 prototype 속성을 갖고 있는 객체를 만드는 방법은 Object.create 메서드를 이용하는 방법과 new 연산자를 사용하는 방법 두 가지가 있습니다. Object.create를 사용하는 Prototypal 패턴부터 알아보겠습니다.

Prototypal

Functional-shared 패턴의 ‘참조한 메서드를 수정할 수 없었던 단점’을 보안하는 패턴입니다.

1
Object.create(proto[, propertiesObject])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const Person = function (name, age) {
    const person = Object.create(personMethod);
    person.name = name;
    person.age = age;
    return person;
};

const personMethod = {
    sayHi: function () {
        console.log(`Hi! my name is ${this.name}!`);
    },
};

let person = Person("Peter", 18);
person.sayHi(); // Hi! my name is Peter!

personMethod.sayHi = function () {
    console.log("changed");
};

person.sayHi(); // changed

Object.create 메서드는 인자로 받은 객체를 프로토타입 객체로 삼아 새로운 객체를 생성합니다. 예를 들어 위의 코드에서 person은 Object.create이 생성한 personMethod를 프로토타입으로 삼는 객체입니다. 따라서 personMethod.sayHi의 값이 변경되더라도 person의 prototype은 여전히 personMethod이기 때문에 변경된 내용이 person에게도 적용됩니다.

장점: 메서드가 인스턴스에 속하지 않고 객체의 프로토타입에 속합니다. (Functional의 단점 보완) 프로토타입의 메서드를 변경한 내용이 인스턴스들에게도 적용됩니다. (Functional-Shared의 단점 보완)

단점: 객체를 생성한 후, 속성을 추가하고, 반환하는 과정을 반드시 거쳐야 합니다.

Pseudo-classical

Prototypal 패턴에서 더 발전된 객체 생성 패턴입니다. new 연산자와 prototype 속성을 이용해 코드가 간략화되었습니다.

1
2
3
4
5
6
7
8
9
10
11
const Person = function (name, age) {
    this.name = name;
    this.age = age;
};

Person.prototype.sayHi = function () {
    console.log(`Hi! my name is ${this.name}!`);
};

let person = new Person("Peter", 18);
person.sayHi();

우선 보이는 달라진 점은 Person 함수가 객체를 생성하지 않고, 아무것도 return 하지 않는다는 점입니다. 인스턴스는 Person 생성자 함수 내부에서 생성되지 않고 new 연산자를 통해 생성됩니다.

new 연산자로 생성자 함수를 호출되었을때 일어나는 일입니다.

  1. 새로운 객체(인스턴스)를 생성합니다.
  2. 생성 객체의 __proto__ 를 생성자 함수의 prototype 객체로 설정합니다.
  3. 생성자 함수의 this로 새로 생성된 객체가 binding되어 실행됩니다.
  4. return이 명시되지 않았다면 생성 객체를 반환합니다. (일반적으로 선호되는 방법)
  5. return이 명시되어 있다면 해당 값을 반환합니다.

new 연산자가 알아서 처리해준 일을 다시 코드로 작성하자면 이렇습니다.

1
2
3
4
5
6
7
const Person = function (name, age) {
    let this = Object.create(프로토타입 객체)
    this.name = name;
    this.age = age;
    return this
};

다시 원래의 코드를 살펴보겠습니다. new Person(‘Peter’,18)이 실행되기 전 new 연산자가 Person의 prototype을 상속하는 새로운 객체를 생성합니다. 그리고 그 객체를 this로 binding해 Person 함수가 실행됩니다. this.name과 this.age는 새로 생성된 객체의 속성으로 추가되며 마지막엔 return값이 명시되지 않았기 때문에 생성 객체가 반환되었습니다.

또 객체가 이미 prototype 객체를 갖고 있고, prototype 속성에 메소드를 직접 추가하기 때문에 Object.create의 인자로 사용할 객체를 따로 만들 필요가 없습니다.

장점: class를 사용하지 않고 객체를 생성하는 최적화 된 방법입니다.

단점: 패턴를 이해하는데 사전 지식이 필요합니다.

💿class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    // sayHi = function () {
    sayHi() {
        console.log(`Hi! my name is ${this.name}!`);
    }
}

const person = new Person("Peter", 18);
person.sayHi();

프로로타입과 constructor에 대해 배워보았으니 class를 다시 살펴보겠습니다.

new 연산자는 Pseudo-classical 패턴에서와 마찬가지로 Person의 Prototype 객체를 __proto__로 참조하는 객체를 생성합니다. 그리고 이 객체를 this로 바인딩해 Person의 constructor 함수가 실행되며, 속성 값을 설정한 후 생성 객체를 return합니다.

constructor: 객체를 초기화하기 위한 목적으로 사용되는 메소드입니다. Person prototype 객체가 생성될 때 함께 생성되기 때문에, override하지 않아도 기존의 constructor를 통해 객체가 생성됩니다. Person class로 인스턴스를 생성 할 때 실행되며, new 연산자가 아닌 경로로 실행하려 할 시 에러가 발생합니다. (Uncaught TypeError: Class constructor Person cannot be invoked without ‘new’)

Prototype methods: class의 프로토타입 메소드들입니다. body 스코프 안에 선언되며, method이기 때문에 let 또는 const같은 선언 키워드를 사용하지 않습니다. 또 메서드이기 때문에 축약형 선언이 가능합니다. (7,8번 줄 참조)

Instance properties: 인스턴스의 속성입니다. 메서드 내에서만 정의되며 초기 값은 일반적으로 constructor안에서 설정됩니다.

그렇다면 class가 prototype으로 구현되었다는 것을 확실히하기 위해 Class와 Instance의 속성 값을 확인해보겠습니다.

console.dir값

Class(Person)의 속성입니다. prototype 속성이 constructor와 sayHi 메소드를 갖고 있습니다.

console.dir값

Instance(person)의 속성입니다. __proto__가 Person의 prototype 객체를 참조하고 있기 때문에 Person Class와 같은 constructor와 sayhi를 갖고 있습니다.

[Posting Reference]

Classes
Instantiation Patterns in JavaScript
The Evolution of JavaScript Instantiation Patterns
프로토타입 메서드와 proto가 없는 객체
prototype