본문 바로가기
Modern React Deep Dive/01장 리액트 개발을 위해 꼭 알아야 할 자바스크립트

1.3.1 클래스란 무엇인가?

by Whiimsy 2024. 4. 4.

1.3.1 클래스란 무엇인가?

클래스

  • 의미: 특정한 객체를 만들기 위한 일종의 템플릿과 같은 개념. 즉, 특정한 형태의 객체를 반복적으로 만들기 위해 사용.

  • 활용: 객체를 만드는 데 필요한 데이터나 이를 조작하는 코드를 추상화해 객체 생성을 더욱 편리하게 할 수 있음

  • 클래스가 나오기 이전(ES6)에는 클래스라는 개념이 없어 객체를 만드는 템플릿 같은 역할을 함수가 했음. 반대로 말하면, 자바스크립트에서 클래스로 하는 모든 것들을 함수로도 동일하게 표현할 수 있음

  • // 클래스 예제
    // Car 클래스 선언
    class Car {
      // constructor는 생성자. 최초에 생성할 때 어떤 인수를 받을지 결정할 수 있으며,
      // 객체를 초기화하는 용도로도 사용
      constructor(name) {
        this.name = name
      }
    
      // 메서드
      honk() {
        console.log(`${this.name}이 경적을 울립니다!`)
      }
    
      // 정적 메서드
      static hello() {
        console.log('저는 자동차입니다.')
      }
    
      // setter
      set age(value) {
        this.carAge = value
      }
    
      // getter
      get age() {
        return this.carAge
      }
    }
    
    // Car 클래스를 활용해 car 객체를 만듦
    const myCar = new Car('자동차')
    
    // 메서드 호출
    myCar.honk()
    
    // 정적 메서드는 클래스에서 직접 호출
    Car.hello()
    
    // 정적 메서드는 클래스로 만든 객체에서는 호출 불가능
    // Uncaught TypeError: myCar.hello is not a function
    myCar.hello()
    
    // setter를 만들면 값 할당 가능
    myCar.age = 32
    
    // getter로 값 가져오기 가능
    console.log(myCar.age, myCar.name) // 32 자동차

아래에서 클래스 내부 특징들에 대해 살펴보자.

constructor

  • 의미: 생성자로, 객체를 생성하는 데 사용하는 특수한 메서드

  • 특징: 단 하나만 존재할 수 있으며, 여러 개를 사용하면 에러 발생. 생성자에서 별다르게 수행할 작업이 없다면 생략 가능.

  • // 불가능
    class Car {
      constructor(name) {
        this.name = name
      }
    
      // SyntaxError: A class may only have one constructor
      constructor(name) {
        this.name = name
      }
    }
    
    // 가능
    class Car {
      // constructor는 없어도 가능
    }

프로퍼티

  • 의미: 클래스로 인스턴스를 생성할 때 내부에 정의할 수 있는 속성값

  • class Car {
      constructor(name) {
        // 값을 받으면 내부에 프로퍼티로 할당됨
        this.name = name
      }
    }
    
    const myCar = new Car('자동차') // 프로퍼티 값을 넘겨줌
  • 특징

    • 기본적으로 인스턴스 생성 시 constructor 내부에는 빈 객체가 할당돼 있는데 이 빈 객체에 프로퍼티의 키와 값을 넣어서 활용할 수 있게 도와줌
    • 다른 언어처럼 접근 제한자가 완벽하게 지원되는 것은 아니지만, #을 붙여서 private을 선언하는 방법이 ES2019에 추가됐고, 또 타입스크립트를 활용하면 다른 언어와 마찬가지로 private, protected, public을 사용할 수 있음. 물론 타입스크립트에서 가능한 것일 뿐, 자바스크립트에서는 기본적으로 모든 프로퍼티가 public임. 과거 private이 없던 시절에는 _를 붙여 접근해서는 안 된다는 의미의 코딩 컨벤션이 있긴 했지만 어디까지나 컨벤션일 뿐 기능적으로 private와 동일한 것은 아님.

getter와 setter

  • getter

    • 의미: 클래스에서 무언가 값을 가져올 때 사용. 사용하기 위해서는 get을 앞에 붙여야 하고, 뒤이어서 getter의 이름을 선언해야 함.

    • class Car {
        constructor(name) {
          this.name = name
        }
      
        get firstCharacter() {
          return this.name[0]
        }
      }
      
      const myCar = new Car('자동차')
      
      myCar.firstCharacter // 자
  • setter

    • 의미: 클래스 필드에 값을 할당할 때 사용. 마찬가지로 set이라는 키워드를 먼저 선언하고, 이어서 이름을 붙이면 됨.

    • class Car {
        constructor(name) {
          this.name = name
        }
      
        get firstCharacter() {
          return this.name[0]
        }
      
        set firstCharacter(char) {
          this.name = [char, ...this.name.slice(1)].join('')
        }
      }
      
      const myCar = new Car('자동차')
      
      myCar.firstCharacter // 자
      
      // '차'를 할당
      myCar.firstCharacter = '차'
      
      console.log(myCar.firstCharacter, myCar.name) // 차, 차동차

인스턴스 메서드

  • 의미: 클래스 내부에서 선언한 메서드. 실제로 자바스크립트의 prototype에 선언되므로 프로토타입 메서드로 불리기도 함.

  • class Car {
      constructor(name) {
        this.name = name
      }
    
      // 인스턴스 메서드 정의
      hello() {
        console.log(`안녕하세요, ${this.name}입니다.`)
      }
    }
    
    const myCar = new Car('자동차')
    
    myCar.hello() // 안녕하세요, 자동차입니다.
  • 위와 같이 새롭게 생성한 객체에서 클래스에서 선언한 hello 인스턴스 메서드에 접근할 수 있는 것을 확인할 수 있음. 이렇게 접근할 수 있는 이유는 앞서 프로토타입 메서드라고도 불리는 이유, 즉 메서드가 prototype에 선언됐기 때문.

  • const myCar = new Car('자동차')
    
    Object.getPrototypeOf(myCar) // {constructor: f, hello: f}
  • Object.getPrototypeOf를 사용하면, 인수로 넘겨준 변수의 prototype을 확인할 수 있음. 이에 대한 결과로 {constructor: f, hello: f}를 반환받아 Car의 prototype을 받은 것으로 짐작할 수 있음.

  • Object.getPrototypeOf(myCar) === Car.prototype // true
  • Object.getPrototypeOf(myCar)를 Car.prototype과 비교한 결과 true 반환. 이 외에 __proto__ 또한 해당 변수의 prototype을 확인할 수 있음.

  • myCar.__proto__ === Car.prototype // true
  • 하지만 __proto__는 가급적 사용해서는 안 되는 코드. 1장에서 설명했던 typeof null === 'object'와 유사하게 원래 의도한 표준은 아니지만 과거 브라우저가 이를 사용했기 때문에 유지되는, 호환성을 지키기 위해서만 존재하는 기능이기 때문.

  • 프로토타입 체이닝: 직접 객체에서 선언하지 않았음에도 프로토타입에 있는 메서드를 찾아서 실행을 도와주는 것. 모든 객체는 프로토타입을 가지고 있는데, 특정 속성을 찾을 때 자기 자신부터 시작해서 이 프로토타입을 타고 최상위 객체인 Object까지 훑음. 이 경우 myCar에서 시작해서 부모인 Car에서 hello를 찾는 프로토타입 체이닝을 거쳐 비로소 hello를 호출할 수 있게 됐음. 이와 비슷한 원리로 toString을 예로 들 수 있음. toString은 객체 어디에서도 선언하는 경우가 없지만 대부분의 객체에서 모두 사용할 수 있음. 이는 toString도 마찬가지로 프로토타입 체이닝을 거쳐 Object에 있는 toString을 만나기 때문

  • 결론적으로 이 프로토타입과 프로토타입 체이닝이라는 특성 덕분에 생성한 객체에서도 직접 선언하지 않은, 클래스에 선언한 hello() 메서드를 호출할 수 있고, 이 메서드 내부에서 this도 접근해 사용할 수 있게 됨

정적 메서드

  • 의미: 특이하게 클래스의 인스턴스가 아닌 이름으로 호출할 수 있는 메서드.

  • class Car {
      static hello() {
        console.log('안녕하세요!')
      }
    }
    
    const myCar = new Car()
    
    myCar.hello() // TypeError: myCar.hello is not a function
    Car.hello() // 안녕하세요!
  • 정적 메서드 내부의 this는 클래스로 생성된 인스턴스가 아닌, 클래스 자신을 가리키기 때문에 다른 메서드에서 일반적으로 사용하는 this를 사용할 수 없음. 이러한 이유로 리액트 클래스형 컴포넌트 생명주기 메서드인 static getDerivedStateFromProps(props, state)에서는 this.state에 접근할 수 없음.

  • 정적 메서드는 this에 접근할 수 없지만 인스턴스를 생성하지 않아도 사용할 수 있다는 점, 그리고 생성하지 않아도 접근할 수 있기 때문에 객체를 생성하지 않더라도 여러 곳에서 재사용이 가능하다는 장점이 있음. 이 때문에 애플리케이션 전역에서 사용하는 유틸 함수를 정적 메서드로 많이 활용하는 편.

상속

  • 의미: 리액트에서 클래스형 컴포넌트를 만들기 위해 extends React.Component 또는 extends React.PureComponent를 선언한 것을 본 적 있을 것. 이 extends는 기존 클래스를 상속받아 자식 클래스에서 이 상속받은 클래스를 기반으로 확장하는 개념이라 볼 수 있음.

  • class Car {
      constructor(name) {
        this.name = name
      }
    
      honk() {
        console.log(`${this.name} 경적을 울립니다!`)
      }
    }
    
    class Truck extends Car {
      constructor(name) {
        // 부모 클래스의 constructor, 즉 Car의 constructor를 호출
        super(name)
      }
    
      load() {
        console.log('짐을 싣습니다')
      }
    }
    
    const myCar = new Car('자동차')
    myCar.honk() // 자동차 경적을 울립니다!
    
    const truck = new Truck('트럭')
    truck.honk() // 트럭 경적을 울립니다!
    truck.load() // 짐을 싣습니다!
  • Car를 extends 한 Truck이 생성한 객체에서도, Truck이 따로 정의하지 않은 honk 메서드를 사용할 수 있는 것을 볼 수 있음. 이 extends를 활용하면 기본 클래스를 기반으로 다양하게 파생된 클래스를 만들 수 있음.