본문 바로가기
  • soldonii's devlog
Javascript 공부/Advanced Javascript(-)

자바스크립트의 객체 지향 프로그래밍 2 : 클래스(Class) 사용

by soldonii 2019. 10. 20.

*Udemy의 "Advanced Javascript Concepts"강의에서 학습한 내용을 정리한 포스팅입니다.

*자바스크립트를 배우는 단계라 오류가 있을 수 있습니다. 틀린 내용은 댓글로 말씀해주시면 수정하겠습니다. 감사합니다. :)


1. 객체지향 프로그래밍의 마지막 단계

# OOP를 향한 마지막 5단계 : ES6 Classes

class는 ES6에서 추가된 Syntactic Sugar로, 만들고자 하는 대상에 대한 청사진을 그려주는 역할을 한다.

class Elf {
  constructor(name, weapon) {
    this.name = name;
    this.weapon = weapon;
  }
  attack() {
    return 'attack with ' + this.weapon;
  }
}

const peter = new Elf('Peter', 'stones');
peter.attack(); // 'attack with stones'
console.log(peter instanceof Elf); // true
const sam = new Elf('Sam', 'fire');
sam.attack(); // 'attack with fire'

- constructor 함수는 class를 이용하여 초기화를 할 때, 즉 class를 이용하여 새로운 것을 만들 때마다 실행되는 함수이다.

- 생성자 함수와 프로토타입을 사용할 때에는, 생성자 함수 외부에서 프로토타입에 접근한 후 메소드를 따로 부여했으나, class를 사용하면 하나의 class 안에 해당 개념을 하나로 묶을 수 있다. 즉, attack 메소드가 하나의 class 안에 포함되어 있으며, 따라서 데이터(state)와 동작(function)을 하나로 관리할 수 있고 class만 수정해주면 하위의 인스턴스들은 자동으로 update가 된다.

 

인스턴스란, class를 이용해서 생성된 각각의 객체들을 의미한다. 

const peter = new Elf('Peter', 'stones'); // Instantiating class
console.log(peter instanceof Elf); // true. // peter is instance of Elf

Elf 클래스를 이용해서 peter 변수에 초기화를 진행했으며, 이 때 peter 객체는 Elf 클래스의 인스턴스이다.

 

클래스를 이용해서 객체지향 프로그래밍에 가장 가까이 접근했지만, 사실 자바스크립트에서 class는 말 그대로 syntactic sugar에 불과하며, 실질적으로 존재하는 개념이 아닐 뿐 아니라 백그라운드에서는 여전히 prototypal inheritance를 이용하고 있다. 자바스크립트에서는 class도 class 자체로 존재하는 개념이 아니라, 객체에 불과하다.

 

위에서 왜 attack 함수는 constructor 함수 안에 포함시키지 않았을까? constructor 함수는 class를 이용해서 Instantiating, 즉 초기화가 진행될 때마다 실행되는 함수라고 했다. name과 weapon은 초기화할 때마다 값이 변경되므로 constructor 안에 존재해야 하지만, attack 함수는 모든 인스턴스에서 동일하게 작동하기 때문에 constructor 안에 포함시킬 수는 있지만 메모리 낭비가 발생한다.

 

2. this의 4가지 경우

// 1. new binding this
function Person(name, age) {
  this.name = name;
  this.age = age;
}
const person1 = new Person('Xavier', 55); 
// new 키워드를 사용하면, person1, new 키워드로 만들어진 객체가 담겨 있는 변수가 바로 this가 된다. 

new 키워드를 이용하여 생성자 함수로 객체를 생성할 경우, 생성된 객체가 담긴 변수가 바로 this이다.

 

// 2. implicit binding
const person = {
  name: 'Karen',
  age: 40,
  hi() {
    console.log('hi' + this.name); 
    // 객체 안에서 this를 호출하면 바로 그 객체가 this이다. 내포적인 개념.
  }
}

객체 안에서 this를 사용할 경우, 그 객체가 this이다.

 

// 3. explicit binding
const person3 = {
  name: 'Karen',
  age: 40,
  hi: function() {
    console.log('hi' + this.setTimeout)
  }.bind(window);
}

bind, apply, call의 경우에 해당되며 메소드에게 전달된 전달인자(argument)가 this가 된다. 명시적으로 this를 전달하는 것이다.

 

// 4. arrow function
const person4 = {
  name: 'Karen',
  age: 40,
  hi: function() {
    var inner = () => {
      console.log('hi' + this.name);
    }
    return inner();
  }
}
person4.hi(); // 'hi Karen'

this는 원래 dynamically scoped, 즉 어떠한 방식으로 함수를 호출하느냐에 따라 this의 값이 결정되는 개념이다. 하지만 화살표 함수를 사용하면 lexically scoped, 즉 this가 선언된 환경에 따라 결정되도록 할 수 있다고 했다. 

위 경우 inner 함수를 화살표 함수로 선언했기 때문에, inner() 이렇게 regular function call, 일반적인 함수 호출로 실행했다고 할지라도, this가 window가 되는 대신 선언된 환경인 person4 객체를 가리키게 된다. 함수 호출 방식에 따라 this가 달라지는 내용은 아래에 다시 한 번 간략하게 정리한다.

 

// 1. 일반적인 함수 호출(Regular Function Call) : this는 window
function test() {
  return 'what is ' + this;
}
test(); // 일반적인 함수 호출. this는 window를 가리킨다.
// 단, 'use strict', 즉 strict mode에서는 regular function call의 경우 this는 undefined.

// 2. dot notation을 활용한 메소드 호출 : this는 .앞에 등장하는 객체
const test2 = {
  name: 'test2 function',
  testing: function() {
    return 'I am testing what is ' + this;
  }
}
test2.testing(); // dot notation을 활용한 메소드 호출. this는 test2 객체를 가리킨다.

// 3. .call, .apply., .bind : this는 call, apply, bind의 첫번째 전달인자(argument)
const obj = {
  name: hyunsol,
  age: 29,
  location: seoul
}
function foo() {
  return `hi, my name is ${this.name}, ${this.age} years old.`;
}
foo.call(obj); // this는 obj이다.

// 4. new 키워드를 사용할 때의 this : 새로운 빈 객체가 생성된 후 this에 할당이 된다.
function foo() {
  console.log(this);
}
new foo(); // this는 {}

 

3. 프로토타입 상속(Prototypal Inheritance)

살펴보았듯이, OOP의 핵심이 되는 개념이 바로 프로토타입 상속이다. 프로토타입 상속과 클래스를 활용해서 객체지향 프로그래밍에 조금 더 다가가보자.

class Elf {
  constructor(name, weapon) {
    this.name = name;
    this.weapon = weapon;
  }
  attack() {
    return 'attack with ' + this.weapon;
  }
}

const fiona = new Elf('Fiona', 'ninja stars');
const ogre = {...fiona}; // fiona를 ogre에 복제한다.

console.log(ogre.__proto__); // {} // 어떤 클래스도 상속받고 있지 않다.
console.log(fiona.__proto__); // Elf {} // Elf 클래스를 상속받고 있다.
console.log(fiona === ogre); // false

피오나 공주 뿐 아니라, 슈렉을 만들고자 한다. 따라서 피오나 공주를 ogre 변수에 복제했으나, ogre는 어떤 클래스도 상속받지 않을 뿐 아니라, fiona 변수와 같은 대상을 가리키고 있지도 않다. 프로토타입 체인이 유실된 것이다. 이를 해결하기 위해서는?

 

class Character {
  constructor(name, weapon) {
    this.name = name;
    this.weapon = weapon;
  }
  attack() {
    return 'attack with ' + this.weapon;
  }
}

class Elf extends Character { // Character 클래스를 상속받도록 하는 SUB CLASSING
  
}
const fiona = new Elf('Fiona', 'ninja stars'); // Elf { name: 'Fiona', weapon: 'ninja stars'}

우선 피오나, 슈렉 모두 캐릭터이므로 클래스를 Character로 변경한 후, extends를 활용한다. extends를 활용해 하나의 클래스가 다른 클래스를 상속받도록 하는 행위를 SUB CLASSING이라고 한다. 위 코드에서 Elf 클래스가 Character 클래스를 상속받도록 지정하였다. Elf는 Character의 모든 프로퍼티를 상속받지만, 만약 Elf 클래스만의 고유한 프로퍼티가 필요하다면?

 

class Character {
  constructor(name, weapon) {
    this.name = name;
    this.weapon = weapon;
  }
  attack() {
    return 'attack with ' + this.weapon;
  }
}

class Elf extends Character { // Character 클래스를 상속받도록 하는 SUB CLASSING
  constructor(name, weapon, type) {
    super(name, weapon); // super
    this.type = type;
  }
}

const dolby = new Elf('Dolby', 'cloth', 'house');
dolby.attack(); // 'attack with cloth'

우선 Elf 클래스가 this 키워드를 이용해서 Elf 클래스를 이용해 생성된 객체에 프로퍼티를 지정해주기 위해서는 한가지 전제조건이 있다. 우선적으로 상속받고자 하는 상위 클래스(이 경우 Character 클래스)의 constructor 함수를 Elf 클래스로 불러와야 한다. 이를 불러오기 위해 필요한 것이 바로 super 키워드이다.

 

super 키워드를 통해 Character 클래스의 constructor 함수를 Elf 캐릭터에서 가져올 수 있으며, 이 때 super는 상위 클래스 내 constructor 함수의 parameter까지 그대로 가져와야 한다.

 

다시 정리해보자면 아래와 같다.

1. extends : Elf 클래스의 __proto__, 즉 Character 생성자 함수 내의 prototype 객체가 Elf 생성자 함수의 상위 프로토타입 체인이 되도록 링크를 연결한다. extends는 프로퍼티를 복사하는 것이 아니라, 프로토타입 체인을 형성하는 행위이다.

2. super : super(arg1, arg2, ...)는 상위 클래스의 constructor 함수를 실행시킨 후, Character 클래스를 통해 인스턴스가 초기화된다. 이 인스턴스는 Elf { name: 'Dolby', weapon: 'cloth'} 인 상태이다. 이후 Elf 클래스는 이제 this를 사용할 수 있게 되며, Elf 클래스의 constructor 함수가 실행되면서 type 프로퍼티가 인스턴스에 추가가 되는 방식이다.

 

class Character {
	constructor(name, weapon) {
    this.name = name;
    this.weapon = weapon;
  }
  attack() {
    return 'attack with ' + this.weapon;
  }
}

class Elf extends Character { // Character 클래스를 상속받도록 하는 SUB CLASSING
  constructor(name, weapon, type) {
    super(name, weapon); // super
    this.type = type;
  }
}

class Ogre extends Character {
  constructor(name, weapon, color) {
    super(name, weapon);
    this.color = color;
  }
  makeFort() {
    return 'strongest Fort in the world made';
  }
}

const dolby = new Elf('Dolby', 'cloth', 'house');
dolby.attack(); // 'attack with cloth'
dolby.makeFort(); // error
const shrek = new Ogre('Shrek', 'club', 'green');
shrek.attack(); // 'attack with club'
shrek.makeFort(); // 'strongest Fort in the world made'

마찬가지로 Ogre 클래스는 Character 클래스를 상속받고, 동시에 자신만의 프로퍼티를 가지고 있다.

 

class Character {
	constructor(name, weapon) {
    this.name = name;
    this.weapon = weapon;
  }
  attack() {
    return 'attack with ' + this.weapon;
  }
}

class Ogre extends Character {
  constructor(name, weapon, color) {
    super(name, weapon);
    this.color = color;
  }
  makeFort() {
    return 'strongest Fort in the world made';
  }
}
const shrek = new Ogre('Shrek', 'club', 'green');

console.log(Ogre.isPrototypeOf(shrek)); // false
console.log(Ogre.prototype.isPrototypeOf(shrek)); // true

console.log(dolby instanceof Elf); // true
console.log(dolby instanceof Character); // true

- Ogre는 생성자 함수이다. Ogre는 생성자 함수를 통해 생성된 객체 shrek의 프로토타입이 아니다. 

- Ogre 생성자 함수는 prototype 객체를 가지고 있으며, 이 prototype 객체가 shrek 객체의 프로토타입이다.(헷갈리지 말자!)

 

4. 객체지향 프로그래밍의 4가지 기둥

1) Encapsulation

데이터(state)와 동작(function)을 하나의 객체 안에 묶어서 관리한다.

2) Abstraction

모든 복잡함이 하나의 객체 안에 추상화되어, 유저는 단순한 instantiating 만으로 모든 프로퍼티들이 세팅된 객체를 생성할 수 있다.

3) Inheritance

다른 클래스를 상속받음으로써 코드가 DRY해지고, 메모리 공간 또한 절약하게 된다. 객체지향 프로그래밍에서 가장 중요한 개념이다.

4) Polymorphism

같은 메소드를 다른 객체에서 사용할 수 있으며, 사용하는 객체에 따라서 다른 결과물을 출력한다. 즉, 각 객체의 state에 따라 다르게 동작이 가능하다.

댓글