6 분 소요

📝class란?

객체 지향 프로그래밍(OOP, Object-Oriented Programming)

  • 객체 지향 프로그래밍에서는 모든 데이터를 객체(object)로 취급합니다. 이러한 객체가 바로 프로그래밍의 중심이 됩니다.
  • 객체(object)란 간단히 설명하자면 실생활에서 우리가 인식할 수 있는 사물로 설명할 수 있습니다. 이러한 객체의 상태 (state)와 행동(behavior)을 구체화하는 형태의 프로그래밍이 바로 객체 지향 프로그래밍입니다. 이때 객체를 만들어 내기 위한 설계와 같은 계념을 클래스(class)라고 합니다.

class

  • 클래스는 ES6에서 추가된 기능입니다. 클래스가 도입 되기전에는 정리하지 않고 바로 객체로 만들수 있었습니다.
  • Class는 객체를 생성하기 위한 템플릿입니다. 클래스는 데이터와 이를 조작하는 코드를 하나로 추상화합니다.

class 선언

  • class 키워드를 이용해 Person 이라는 class를 생성합니다. constructor(생성자)를 이용해서 objec를 만들때 필요한 데이터를 전달합니다.

    전달받은 데이터를 두가지 fields에 할당해 줍니다 (name,age).

    예제에는 클레스에는 이름과 나이라는 fields가 있고 speak 메소드가 존제합니다.

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  speak() {
    console.log(`${this.name}: hello!`);
  }
}

object 생성

  • 새로운 object 객체를 만들때에는 new라는 키워드를 사용합니다.
class Person {
  constructor(name, age) {
    //fields
    this.name = name;
    this.age = age;
  }

  //methods
  speak() {
    console.log(`${this.name}: hello!`);
  }
}

const ellie = new Person("ellie", 20);
console.log(ellie.name);
console.log(ellie.age);
ellie.speak();
  • 새로운 객체 ellie를 생성후 값을 보면 ellie, 20, ellie: hello! 라는 값이 console에 출력되는걸 볼수있습니다.

  • class Person{...} 문법 구조가 진짜 하는 일은 다음과 같습니다.

    1. Person라는 이름을 가진 함수를 만듭니다. 함수 본문은 생성자 매서드 constructor에서 가져옵니다. 생성자 메서드가 없으면 본문이 비워진 채로 함수가 만들어집니다.

    2. speak같은 클래스 내에서 정의한 메서드를 Person.prototype에 저장합니다.

    3. class Person {
        constructor(name, age) {
          //fields
          this.name = name;
          this.age = age;
        }
        //methods
        speak() {
          console.log(`${this.name}: hello!`);
        }
      }
      
      // 클래스는 함수입니다.
      alert(typeof Person); // function
      
      // 정확히는 생성자 메서드와 동일합니다.
      alert(Person === Person.prototype.constructor); // true
      
      // 클래스 내부에서 정의한 메서드는 Person.prototype에 저장됩니다.
      alert(Person.prototype.speak); //   console.log(`${this.name}: hello!`);
      
      // 현재 프로토타입에는 메서드가 두 개입니다.
      alert(Object.getOwnPropertyNames(Person.prototype)); // constructor, speak
      

📝Getter & Setter

  • 객체의 프로퍼티는 두 종류가 있습니다.

    • 첫 번째 종류는 데이터 프로퍼티(data property) 입니다. 지금까지 사용한 모든 프로퍼티는 데이터 프로퍼티입니다.

    • 두 번째는 접근자 프로퍼티(accessor property) 라 불리는 새로운 종류의 프로퍼티입니다. 접근자 프로퍼티의 본질은 함수인데, 이 함수는 값을 획득(get)하고 설정(set)하는 역할을 담당합니다. 그런데 외부 코드에서는 함수가 아닌 일반적인 프로퍼티처럼 보입니다.

예제를 통해서 알아보겠습니다.
class User {
  constructor(firstName, lastName, age) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
  }
}

const user1 = new User("sim", "son", -1);

console.log(user1); // User {firstName: 'sim', lastName: 'son', age: -1}
  • 예제와 같이 class를 사용하는 사용자가 의도와 다르게 사용되었을때가 있습니다. age를 -1로 설정한것은 실수일것입니다. 이러한 상황에서 getter와 setter를 사용해서 방지할수 있습니다.

    • //첫번째 예제입니다.
      class User {
        constructor(firstName, lastName, age) {
          this.firstName = firstName;
          this.lastName = lastName;
          this.age = age;
        }
      
        get age() {
          return this._age;
        }
        set age(value) {
          this._age = value < 0 ? 0 : value;
        }
      }
      
      const user1 = new User("sim", "son", -1);
      
      console.log(user1.age); /// 0
      
      • set은 값을 설정하기 때문에 value를 값으로 줘야합니다.
      • get과 set의 age에는 _를넣어서 콜 스텍 에러를 방지해줍니다.
      • 위의 예제를 보면 age값을 value 값이 0보다 작으면 0으로 표기하고 아니라면 값을 표기하도록 설정해줬습니다.
    • //두번째 예제입니다.
      class User {
        constructor(firstName, lastName, age) {
          this.firstName = firstName;
          this.lastName = lastName;
          this.age = age;
        }
      
        get firstName() {
          return this._firstName.toUpperCase();
        }
        set firstName(value) {
          this._firstName = value;
        }
      }
      
      const user1 = new User("sim", "jun", 10);
      
      console.log(user1.firstName); // SIM
      

📝Public & Private

  • public, private은 최근에 추가된 기능입니다.

  • class Experiment {
      publicField = 2;
      #privateField = 0;
    }
    
    const experiment = new Experiment();
    
    console.log(experiment.publicField); // 2
    console.log(experiment.privateField); // undefined
    
    • 위 예제와 같이 publicField는 class 외부에서도 접근과 변경이 가능합니다
    • #privateField는 내부에서는 값이 변경가능하고 접근이 가능합니다. 하지만 외부에서는 이 값을 읽을수도 변경할수도 없습니다.

📝상속 & 다양성

  • 예를 들어 삼각형, 동그라미, 직사각형 등의 도형을 만드는 어플이 있다고 가정해보겠습니다. 이러한 도형들을 class로 정의할때 어떻게 정의할지 생각해보겠습니다. 도형을보면 넓이, 높이, 색상 그리고 드로잉 등의 메소드 들이 있을것 입니다. 이러한 속성들을 보면 도형을 표현하기 위한 반복되는 속성들이 있습니다. 계속 반복해서 코드를 만들기보다 한번에 정의한 뒤 속성값을 제사용한다면 유지보수도 쉽고 반복해서 코드를 작성하지 않아도 됩니다.

  • 예제를 보겠습니다.

    • class Shape {
        //3개의 필드를 생성해주었습니다.
        constructor(width, height, color) {
          this.width = width;
          this.height = height;
          this.color = color;
        }
        //메소드입니다.
        draw() {
          console.log(`drawing ${this.color} color of`);
        }
      
        getArea() {
          return this.width * this.height;
        }
      }
      
      class Rectangle extends Shape {}
      class Triangle extends Shape {}
      
      const rectangle = new Rectangle(20, 20, "blue");
      rectangle.draw(); // drawing blue color of
      
      const triangle = new Triangle(20, 20, "red");
      triangle.draw(); // drawing red color of
      
      • 📌 필드(field) 클래스를 구성하는 요소 중 하나로 클래스 내부 멤버입니다.어떠한 데이터를 저장하기 위한 역할을 담당하며 클래스 내부의 생성자와 메소드 밖에 정의가 됩니다. 즉 필드란 클래스 안에서 독립적으로 선언되는 변수를 뜻합니다.

      • extends 키워드는 클래스를 다른 클래스의 자식으로 만들기 위해 class 선언 또는 class 식에 사용됩니다. 위 예제에서 extends를 이용하여 Shape을 연장합니다. Shape에 있는 모든것들이 Rectangle에 포함됩니다.

      • 만약 상속받은 getArea 메소드를 Triangle에서 사용해야 한다면 getArea의 메소드로는 삼각형의 값을 구할수 없습니다 (width * height / 2). 이러한 경우 다양성을 이용하여 필요한 함수를 오버라이딩 합니다.

        • class Shape {
            constructor(width, height, color) {
              this.width = width;
              this.height = height;
              this.color = color;
            }
          
            draw() {
              console.log(`drawing ${this.color} color of`);
            }
          
            getArea() {
              return this.width * this.height;
            }
          }
          
          class Rectangle extends Shape {}
          class Triangle extends Shape {
            //getArea 메소드를 오버라이딩 하여 사용합니다.
            getArea() {
              return (this.width * this.height) / 2;
            }
          }
          
          const rectangle = new Rectangle(20, 20, "blue");
          console.log(rectangle.getArea()); // 400
          const triangle = new Triangle(20, 20, "red");
          console.log(triangle.getArea()); // 200
          
      • 오버라이딩을 사용해서 구현해보았습니다. 하지만 이렇게 오버라이딩을 하면 Triangle에서 getArea는 계속 오버라이딩 한 값으로만 계산이됩니다. 이 경우에는 super를 이용해서 부모의 메소드 값을 가져올수 있습니다. 예제를 보면서 설명하겠습니다.

        • class Shape {
            constructor(width, height, color) {
              this.width = width;
              this.height = height;
              this.color = color;
            }
          
            draw() {
              console.log(`drawing ${this.color} color of`);
            }
          
            getArea() {
              return this.width * this.height;
            }
          }
          
          class Rectangle extends Shape {}
          class Triangle extends Shape {
            //super를 이용해 부모의 draw를 호출했습니다.
            draw() {
              super.draw();
              console.log("🎈");
            }
            getArea() {
              return (this.width * this.height) / 2;
            }
          }
          
          const rectangle = new Rectangle(20, 20, "blue");
          console.log(rectangle.getArea());
          const triangle = new Triangle(20, 20, "red");
          
          //여기서 drawing red color of 는 super를 이용해 부모인 Shape에서 가져온 draw메소드입니다. 아래 🎈 오버라이딩한 값입니다.
          triangle.draw(); // drawing red color of
          //🎈
          console.log(triangle.getArea());
          
extends 상속 오류
class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  // ...
}

class Rabbit extends Animal {
  constructor(name, earLength) {
    this.speed = 0;
    this.name = name;
    this.earLength = earLength;
  }

  // ...
}

// 동작하지 않습니다!
let rabbit = new Rabbit("흰 토끼", 10); // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
  • 이유는 다음과 같습니다.

    • 상속 클래스의 생성자에선 반드시 super(...)를 호출해야 하는데, super(...)를 호출하지 않아 에러가 발생했습니다. super(...)this를 사용하기 전에 반드시 호출해야 합니다.

    그런데 왜 super(...)를 호출해야 하는 걸까요?

    당연히 이유가 있습니다. 상속 클래스의 생성자가 호출될 때 어떤 일이 일어나는지 알아보며 이유를 찾아봅시다.

    자바스크립트는 ‘상속 클래스의 생성자 함수(derived constructor)’와 그렇지 않은 생성자 함수를 구분합니다. 상속 클래스의 생성자 함수엔 특수 내부 프로퍼티인 [[ConstructorKind]]:"derived"가 이름표처럼 붙습니다.

    일반 클래스의 생성자 함수와 상속 클래스의 생성자 함수 간 차이는 new와 함께 드러납니다.

    • 일반 클래스가 new와 함께 실행되면, 빈 객체가 만들어지고 this에 이 객체를 할당합니다.
    • 반면, 상속 클래스의 생성자 함수가 실행되면, 일반 클래스에서 일어난 일이 일어나지 않습니다. 상속 클래스의 생성자 함수는 빈 객체를 만들고 this에 이 객체를 할당하는 일을 부모 클래스의 생성자가 처리해주길 기대합니다.

    이런 차이 때문에 상속 클래스의 생성자에선 super를 호출해 부모 생성자를 실행해 주어야 합니다. 그렇지 않으면 this가 될 객체가 만들어지지 않아 에러가 발생합니다.

    아래 예시와 같이 this를 사용하기 전에 super()를 호출하면 Rabbit의 생성자가 제대로 동작합니다.

    • class Animal {
        constructor(name) {
          this.speed = 0;
          this.name = name;
        }
      
        // ...
      }
      
      class Rabbit extends Animal {
        constructor(name, earLength) {
          super(name);
          this.earLength = earLength;
        }
      
        // ...
      }
      
      // 이제 에러 없이 동작합니다.
      let rabbit = new Rabbit("흰 토끼", 10);
      alert(rabbit.name); // 흰 토끼
      alert(rabbit.earLength); // 10
      

댓글남기기