[JavaScript] 코드의 터널, Class로 열어!

hyeonbin·2023년 4월 29일
0

JS 계란반 스터디

목록 보기
12/12
post-thumbnail

📃 Class

📍 기본 개념

  • ES6에 도입된 클래스 문법은 객체 지향 프로그래밍에서 사용되는 중요한 개념이며, 객체의 설계도 또는 템플릿으로 생각할 수 있다.

  • 클래스를 사용해 객체를 생성하면 코드를 논리적으로 구성하고 유지보수하기 쉽게 만들 수 있다.

  • 사실상 기존의 프로토타입 기반 시스템 위에 구조적인 문법을 제공하는 것이며, 내부적으로는 프로토타입 방식으로 작동된다.

  • 즉, 클래스 문법을 사용하여 객체를 생성하고 상속하는 것은 내부적으로 여전히 프로토타입 체인과 관련이 있다.


✔️ ES5 프로토타입 vs ES6 클래스

  • 같은 결과를 출력하지만, 문법 모양만 다르고 내부 로직은 같은 구조라는 점을 기억하자!


📍 클래스 정의

  • ES6 클래스는 class 키워드를 사용하여 정의한다.

  • 클래스 필드의 선언과 초기화는 반드시 constructor 내부에서 실행하고, constructor 내부에 선언한 클래스 필드는 클래스가 생성할 인스턴스에 바인딩 된다.

  • 일반적으로 클래스 이름은 생성자 함수와 마찬가지로 파스칼 케이스 즉, 첫 글자를 대문자로 사용한다.

  class Me {
    constructor(name, age) {
      this.name = name;
      this.age = age;
    }

    sayHoo() {
      console.log(`sayHoo~ ${this.name}`);
    }
  }

  const me = new Me('덕배', 30);
  me.sayHoo(); // 출력: sayHoo~ 덕배

  console.log(me.age); // 30
  console.log(me instanceof Me); // true

  • 클래스는 클래스 선언문 이전에 참조할 수 없다. 그렇다고 호이스팅이 발생하지 않는 것은 아니다.

  • 클래스는 var 키워드로 선언한 변수처럼 호이스팅되지 않고, let, const 키워드로 선언한 변수처럼 호이스팅된다.

  const Me = '';

  {
    // 호이스팅이 발생하지 않는다면 ''가 출력
    console.log(Me);
    // ReferenceError: Cannot access 'Me' before initialization
    class Me {}
  }


📍 클래스 인스턴스 생성

  • 클래스를 사용해 인스턴스를 생성하려면 new 키워드를 사용해야 한다.

  • new 키워드를 사용하지 않고 constructor를 호출하면 타입 에러가 발생한다.

  // new 키워드 O
  class Me {}

  const me = new Me('덕배', 30);
  const mee = new Me('덕수', 35);

  // new 키워드 X
  class Me {}

  const me = Me('덕배', 30);
  const mee = Me('덕수', 35);
  // TypeError: Class constructor Me cannot be invoked without 'new'


📍 constructor

  • constructor는 인스턴스를 생성하고 클래스 필드를 초기화하기 위한 특수한 메소드다.
    -> 클래스 필드는 캡슐화된 변수를 말하며, 데이터 멤버 또는 멤버 변수라고도 한다. 쉽게 말해, 자바스크립트의 생성자 함수에서 this에 추가한 프로퍼티를 클래스 기반 클래스 필드라고 부른다.

  • constructor는 클래스 내에 한 개만 존재할 수 있고, 2개 이상이면 문법 에러가 발생한다.

  • 인스턴스를 생성할 때 new 키워드와 함께 호출한 것이 바로 constructor이며 constructor의 파라미터에 전달한 값은 클래스 필드에 할당된다.

  class Me {
    constructor(name, age) {
      // this는 클래스가 생성할 인스턴스를 가리킴
      // name, age는 클래스 필드
      this.name = name;
      this.age = age;
    }

    sayHoo() {
      console.log(`sayHoo~ ${this.name} 나이가 벌써 ${this.age}?`);
    }
  }

  // 인스턴스 생성
  const me = new Me('덕배', 30);

  me.sayHoo();
  // 출력: sayHoo~ 덕배 나이가 벌써 30?

  • constructor는 생략 가능하고, 생략하면 클래스에 constructor를 포함한 것과 동일하게 동작하며 빈 객체를 생성한다.

  • 그렇기에 인스턴스에 프로퍼티를 추가하려면 인스턴스를 생성한 후, 프로퍼티를 동적으로 추가해야 한다.

  • 단, constructor는 인스턴스의 생성과 동시에 클래스 필드의 생성과 초기화를 실행하니까, 클래스 필드 초기화가 필요하다면 constructor를 생략해서는 안된다.

  class Me {}

  const me = new Me();
  console.log(me); // 출력: Me {}

  me.num = 5;
  console.log(me); // 출력: Me { num: 5 }


📍 클래스 메소드 정의

  • 클래스의 메소드를 정의할 때는 객체 리터럴에서 사용하던 문법과 유사한 문법을 사용한다.
  class Calculator {
    plus(x, y) {
      return x + y;
    }
    minus(x, y) {
      return x - y;
    }
  }

  const calc = new Calculator();

  console.log(calc.minus(80, 50)); // 출력: 30
  console.log(calc.plus(20, 50));  // 출력: 70


  // [] 대괄호로 둘러싸서 메소드의 이름으로 사용할 수도 있음
  const methodName = 'sayHoo'; // 클래스 메소드 이름

  class Me {
    constructor({ name, age }) {
      this.name = name;
      this.age = age;
    }

    // 아래 메소드 이름은 `sayHoo`
    [methodName]() {
      return `sayHoo~ ${this.name} 나이가 벌써 ${this.age}?`;
    }
  }

  console.log(new Me({ name: '덕배쓰', age: 30 }).sayHoo());
  // 출력: sayHoo~ 덕배쓰 나이가 벌써 30?

✔️ Getter

  • getter는 클래스 필드에 값을 반환하는데 사용되며, 주로 클래스의 내부 상태를 외부에 노출하고, 값을 읽는 동작에 관여할 때 유용하게 사용된다.

  • getter는 메소드 이름 앞에 get 키워드를 사용해 정의한다. 이때 메소드 이름은 클래스 필드 이름처럼 사용된다.

  • getter는 호출하는 것이 아니라 프로퍼티처럼 참조하는 형식으로 사용하며 참조 시에 메소드가 호출된다. ( obj.property )

  • getter는 이름 그대로 무언가를 취득할 때 사용하므로 반드시 무언가를 반환해야 한다. 반환문이 없다면 undefined가 반환된다.

  class Me {
    constructor(arr = []) {
      this._arr = arr;
    }

    get info() {
      // getter는 반드시 무언가를 반환함
      return this._arr.length ? this._arr[0] : null;
    }
  }

  const me = new Me([1, 2]);

  // 필드 info에 접근하려면 getter가 호출됨
  console.log(me.info); // 출력: 1

✔️ Setter

  • setter는 클래스 필드에 값을 할당하는데 사용되며, 주로 클래스의 내부 상태를 외부에서 변경할 때 유용하게 사용된다.

  • setter는 메소드 이름 앞에 set 키워드를 사용해 정의한다. 이때 메소드 이름은 클래스 필드 이름처럼 사용된다.

  • setter는 호출하는 것이 아니라 프로퍼티처럼 할당하는 형식으로 사용하며 할당 시에 메소드가 호출된다. ( obj.property = value )

  • setter는 인자를 받아서 클래스 필드에 할당하는데, 이때 받은 값을 가공하거나 검증하는 등의 추가적인 로직을 수행할 수 있다.

  class Me {
    constructor(arr = []) {
      this._arr = arr;
    }

    get info() {
      // getter는 반드시 무언가를 반환함
      return this._arr.length ? this._arr[0] : null;
    }

    set info(elem) {
      // this._arr 개별 요소로 분리
      this._arr = [elem, ...this._arr];
    }
  }

  const me = new Me([1, 2]);

  // 클래스 필드 info에 접근하려면 getter가 호출됨
  console.log(me.info); // 출력: 1

  // 클래스 필드 info에 값을 할당하면 setter가 호출됨
  me.info = 100;
  console.log(me.info); // 출력: 100

  // 이후 배열
  console.log(me._arr); // 출력: [ 100, 1, 2 ]

✔️ 정적 메소드 (static)

  • 클래스의 정적 메소드를 정의할 때 static 키워드를 사용한다. 정적 메소드는 클래스의 인스턴스가 아닌 클래스 이름으로 호출한다. 따라서 클래스의 인스턴스를 생성하지 않아도 호출할 수 있다.

  • 클래스의 정적 메소드는 인스턴스로 호출할 수 없으며, 이것은 정적 메소드는 this를 사용할 수 없다는 것을 의미한다.

  class Me {
    constructor(prop) {
      this.prop = prop;
    }

    // 정적 메소드
    static staticMethod() {
      return 'staticMethod';
    }

    prototypeMethod() {
      return this.prop;
    }
  }

  // 정적 메소드는 클래스 이름으로 호출
  console.log(Me.staticMethod());

  const me = new Me(123);

  // 정적 메소드는 인스턴스를 호출할 수 없음
  console.log(me.staticMethod()); // TypeError: me.staticMethod is not a function


📍 클래스 상속

  • 클래스 상속을 통해 한 클래스의 기능을 다른 클래스에서 재사용할 수 있다.

  • 새롭게 정의할 클래스가 기존에 있는 클래스와 매우 유사하다면, 상속을 통해 그대로 사용하되 다른 점만 구현하면 된다.

✔️ extends 키워드

  • extends 키워드부모 클래스를 상속받는 자식 클래스를 정의할 때 사용한다.

  • 이 관계를 부모 클래스 - 자식 클래스 혹은 슈퍼 클래스 ( superclass ) - 서브 클래스 ( subclass ) 관계라고도 말한다.

  class Me {
    constructor(name) {
      this.name = name;
    }

    sayHoo() {
      console.log(`${this.name}는 나야나!`);
    }
  }

  // 자식 클래스
  class Mee extends Me {
    constructor(name, age) {
      super(name);
      this.age = age;
    }

    sayYoo() {
      console.log(`${this.name}는 또다른 나야나!`);
    }
  }

  // 인스턴스 생성
  const me = new Mee('김덕배', '김덕수');

  // 상속된 메소드 호출
  me.sayHoo(); // 출력: 김덕배는 나야나!

  // 자식 클래스의 메소드 호출
  me.sayYoo(); // 출력: 김덕배는 또다른 나야나!

깨알 상식!

  • 오버라이딩 ( Overriding ) 은 상위 클래스가 가지고 있는 메소드를 하위 클래스가 재정의하여 사용하는 방식이다.
  // 부모 클래스
  class Me {
    sayHoo() {
      console.log('나는 덕배');
    }
  }

  // 자식 클래스
  class Mee {
    // 부모 클래스의 메소드를 오버라이딩
    sayHoo() {
      console.log('나는 덕수');
    }
  }

  // 인스턴스 생성
  const me = new Mee();

  // 오버라이딩된 메소드 호출
  me.sayHoo(); // 출력: 나는 덕수

  • 오버로딩 ( Overloading ) 은 매개변수의 타입 또는 개수가 다른, 같은 이름의 메소드를 구현하고 매개변수에 의해 메소드를 구별하여 호출하는 방식이다.

  • 자바스크립트는 오버로딩을 지원하지 않지만 arguments 객체를 사용하여 구현할 수는 있다.

  // 함수 오버로딩을 시뮬레이션하기 위해 arguments 객체 사용
  function add() {
    if (arguments.length === 2) {
      return arguments[0] + arguments[1];
    } else if (arguments.length === 3) {
      return arguments[0] + arguments[1] + arguments[2];
    } else {
      console.error('인수 개수 잘못됨');
    }
  }

  // 오버로딩된 함수 호출
  console.log('1:', add(2, 3)); // 출력: 5
  console.log('2:', add(2, 3, 4)); // 출력: 9
  console.log('3:', add(2, 3, 4, 5)); // 출력: 인수 개수 잘못됨 undefined

✔️ super 키워드

  • super 키워드는 부모 클래스의 필드 또는 메소드를 참조할 때 또는 부모 클래스의 생성자를 호출할 때 사용한다. 즉, 부모 클래스의 인스턴스를 생성한다.

  • 자식 클래스의 constructor에서 super( )를 호출하지 않으면 this에 대한 참조 에러(ReferenceError)가 발생한다. 이것은 super 메소드를 호출하기 이전에는 this를 참조할 수 없음을 의미한다.

  class Me {
    constructor(name) {
      this.name = name;
    }

    sayHoo() {
      console.log(`${this.name}는 나야나!`);
    }
  }

  // 자식 클래스
  class Mee extends Me {
    constructor(name, age) {
      super(name); // super 키워드를 사용해 부모 클래스의 생성자 호출
      this.age = age;
    }

    sayYoo() {
      console.log(`${this.name}는 또다른 나야나!`);
    }
  }

  // 인스턴스 생성
  const me = new Mee('김덕배', '김덕수');

  // 상속된 메소드 호출
  me.sayHoo(); // 출력: 김덕배는 나야나!

  // 자식 클래스의 메소드 호출
  me.sayYoo(); // 출력: 김덕배는 또다른 나야나!

✔️ static 메소드와 prototype 메소드의 상속

  • 자식 클래스의 정적 메소드 내부에서도 super 키워드를 사용해 부모 클래스의 정적 메소드를 호출할 수 있다.

  • 왜? 자식 클래스는 프로토타입 체인에 의해 부모 클래스의 정적 메소드를 참조할 수 있기 때문이다.

  // static 메소드 상속
  class Me {
    static staticMethod() {
      console.log('나는 부모 클래스');
    }
  }

  class Mee extends Me {
    static staticMethod() {
      super.staticMethod();
      console.log('나는 자식 클래스');
    }
  }

  Mee.staticMethod();
  // 출력 :
  // 나는 부모 클래스
  // 나는 자식 클래스

  • 그러나, 자식 클래스의 프로토타입 매소드 내부에서는 super 키워드를 사용해 부모 클래스의 정적 메소드를 호출할 수 없다.

  • 왜? 자식 클래스의 인스턴스는 프로토타입 체인에 의해 부모 클래스의 정적 메소드를 참조할 수 없기 때문이다.

  // static 메소드와 prototype 메소드의 상속
  class Me {
    static staticMethod() {
      return '나는 덕배';
    }
  }

  class Mee extends Me {
    static staticMethod() {
      return `${super.staticMethod()} 하이!`;
    }

    prototypeMethod() {
      return `${super.staticMethod()} 하이루!`;
    }
  }

  console.log(Me.staticMethod()); // 출력: 나는 덕배
  console.log(Mee.staticMethod()); // 출력: 나는 덕배 하이!

  console.log(new Mee().prototypeMethod());
  // TypeError: (intermediate value).staticMethod is not a function


📍 접근 제어자

  • 클래스에서는 접근 제어자를 사용해 클래스의 속성 및 메소드의 접근을 제한할 수 있다.

  • public, protected, private 3가지 접근 제어자를 제공하며, 기본적으로 모든 멤버는 public으로 선언된다.

 ✓ public: 클래스 내부 및 외부에서 접근 가능
 ✓ protected: 클래스 내부 및 하위 클래스에서만 접근 가능
 ✓ private: 클래스 내부에서만 접근 가능

  • JavaScript에서는 TypeScript와 같은 정적 타입 언어와 달리, 명시적으로 public, protected, private 등의 접근 제어자를 사용하지는 않는다.

  • 따라서 아래 코드의 protected와 private 키워드는 주석으로 표시한 것처럼 실제로 동작하지 않는다.

  class Me {
    name; // public
    protected age;
    private email;

    constructor(name, age, email) {
      this.name = name;
      this.age = age;
      this.email = email;
    }

    getAge() {
      return this.age; 
      // protected 멤버는 클래스 내부에서 접근 가능
    }
  }

  const me = new Me('덕배', 30, 'duckbae@gmail.com')

  console.log(me.name);     // 덕배
  console.log(me.age);      // Error: age is protected
  console.log(me.email);    // Error: email is private
  console.log(me.getAge()); // 30
profile
할 수 있다고 믿는 사람은 결국 그렇게 된다 😄😊

0개의 댓글