[JavaScript] 자바스크립트 - 클래스

배창민·2025년 11월 20일
post-thumbnail

자바스크립트 클래스

class 기본 문법 · 상속 · static


1. 클래스 기본 문법 (class basic syntax)

1-1. class 선언과 인스턴스 생성

class Student {
  // 생성자를 통해 인스턴스 생성 및 초기화
  constructor(name) {
    this.group = 1;   // 고정 값
    this.name = name; // 전달받은 값
  }

  // 프로토타입 메서드
  introduce() {
    console.log(`안녕 나는 ${this.group}반 학생 ${this.name}이야`);
  }
}

const student = new Student('홍길동');
student.introduce();

console.log(typeof Student);                         // function
console.log(Student === Student.prototype.constructor); // true
console.log(Student.prototype.introduce);            // 프로토타입 메서드
console.log(Object.getOwnPropertyNames(Student.prototype));
// ['constructor', 'introduce']

핵심 정리:

  • class Student { ... } 로 클래스를 선언
  • new Student() 를 호출하면 인스턴스가 생성되고 constructor 가 자동 실행
  • 클래스 몸체에 정의한 메서드는 기본적으로 프로토타입 메서드
    Student.prototype.introduce 로 접근 가능
  • typeof Student === 'function'
    → 클래스도 함수의 한 종류

1-2. 생성자 함수 vs class 문법 차이

같은 역할을 생성자 함수로도 만들 수 있다.

function Teacher(name) {
  this.group = 1;
  this.name = name;
}

Teacher.prototype.introduce = function () {
  console.log(`안녕 나는 ${this.group}반 교사 ${this.name}이야`);
};

const teacher = new Teacher('유관순');
teacher.introduce();

// Student(); // TypeError: Class constructor Student cannot be invoked without 'new'
Teacher();   // 에러 안 남 (단, this가 전역을 가리키게 되는 문제 있음)

클래스와 생성자 함수의 주요 차이:

  1. new 없이 호출

    • class 생성자: new 없이 호출하면 무조건 에러
    • 일반 함수: 그냥 일반 함수 호출로 동작
  2. 메서드 열거 여부

    • 클래스 내부 메서드: enumerable: false
      for..in 순회에서 제외
  3. 호이스팅 방식

    • 클래스: 선언 이전에 사용하면 에러가 나서
      마치 호이스팅이 없는 것처럼 보인다
    • 함수 선언문: 함수 호이스팅 발생
  4. 자동 strict mode

    • 클래스 본문 내부 코드는 자동으로 use strict 적용

→ 결론: 클래스 문법은 생성자 함수와 비슷하게 보이지만,
실제로는 다른 객체 생성 메커니즘이라고 보면 된다.


1-3. class 표현식 (class expression)

익명 클래스 표현식

const Tutor = class {
  teach() {
    console.log('이해했지?');
  }
};

new Tutor().teach();

기명 클래스 표현식

const Tutee = class MyTutee {
  learn() {
    console.log('이제 알겠어');
    console.log(MyTutee); // 이 이름은 클래스 내부에서만 사용 가능
  }
};

new Tutee().learn();
// console.log(MyTutee); // ReferenceError

동적으로 클래스를 만들어 반환

function makeTutee(message) {
  return class {
    feedback() {
      console.log(message);
    }
  };
}

const SecondTutee = makeTutee('10점 만점에 10점');
new SecondTutee().feedback();

정리:

  • 클래스도 일급 객체

    • 다른 표현식 안에서 정의 가능
    • 변수/프로퍼티에 할당 가능
    • 인수로 전달 가능
    • 반환값으로 사용 가능

1-4. getter / setter

class Product {
  constructor(name, price) {
    // setter가 여기서 호출됨
    this.name = name;
    this.price = price;
  }

  // getter
  get name() {
    console.log('get name 동작');
    return this._name;
  }

  get price() {
    console.log('get price 동작');
    return this._price;
  }

  // setter
  set name(value) {
    console.log('set name 동작');
    this._name = value;
  }

  set price(value) {
    console.log('set price 동작');
    if (value <= 0) {
      console.log('가격은 0보다 작을 수 없다');
      this._price = 0;
      return;
    }
    this._price = value;
  }
}

const phone = new Product('전화기', 23000);
console.log(phone.name + ', ' + phone.price);

const computer = new Product('컴퓨터', -1500000);
console.log(computer.name + ', ' + computer.price);

포인트:

  • get name() {…}, set name(value) {…} 같은 형태로 정의
  • 외부에서는 obj.name 처럼 일반 프로퍼티처럼 접근
  • 실제 값은 _name, _price 같은 내부 프로퍼티에 저장
  • 이런 패턴으로 검증 로직, 로그 출력 등을 끼워 넣을 수 있다

1-5. public field (클래스 필드)

class Book {
  // 클래스 필드
  name = '모던 JavaScript';
  // 초기값을 안 줄 수도 있음
  price;

  introduce() {
    console.log(`${this.name} 재밌는 책이지`);
  }
}

const book = new Book();
book.introduce();

console.log(book.name);            // '모던 JavaScript'
console.log(Book.prototype.name);  // undefined (프로토타입에는 없음)
console.log(book.price);           // undefined

정리:

  • 클래스 몸체에 name = "값" 형태로 클래스 필드 정의
  • 각 인스턴스가 자기만의 필드 값을 가진다
  • Book.prototype 이 아니라 인스턴스 객체 자체에 저장
  • 함수(메서드)는 여전히 prototype 쪽에 두는 걸 권장
    → 필드를 함수로 쓰면 인스턴스마다 함수가 복사되기 때문

2. 클래스 상속 (class inheritance)

2-1. 상속 기본 문법

class Animal {
  constructor(name, weight) {
    this.name = name;
    this.weight = weight;
  }

  eat(foodWeight) {
    this.weight += foodWeight;
    console.log(`${this.name} 은(는) ${foodWeight}kg 먹고 ${this.weight}kg 이 되었다`);
  }

  move(lostWeight) {
    if (this.weight > lostWeight) {
      this.weight -= lostWeight;
    }
    console.log(`${this.name} 은(는) 움직여서 ${lostWeight}kg 빠져서 ${this.weight}kg 이 되었다`);
  }
}

const animal = new Animal('동물', 30);
animal.eat(1);
animal.move(0.5);

extends 로 상속하는 클래스:

class Human extends Animal {
  develop(language) {
    console.log(`${this.name} 은(는) ${language} 로 개발한다`);
  }
}

const human = new Human('수강생', 70);
human.eat(2);          // 부모 메서드 사용
human.move(1);
human.develop('JavaScript'); // 자식 메서드

동작 원리:

  • extends 는 내부적으로 프로토타입 기반 상속을 사용

  • Human.prototype.[[Prototype]]Animal.prototype 을 가리키게 된다

  • Human 인스턴스에서 메서드를 찾을 때

    1. Human.prototype
    2. 없다면 Animal.prototype 순으로 검색

2-2. 메서드 오버라이딩

부모 메서드를 완전히 바꾸지 않고, 기능을 확장하고 싶을 때 오버라이딩을 사용한다.

class Tiger extends Animal {
  attack(target) {
    console.log(`${this.name} 이(가) ${target} 을(를) 공격한다`);
  }

  // Animal의 move를 확장
  move(target) {
    // 부모 메서드 호출
    super.move(0.1);
    // 추가 동작
    this.attack(target);
  }
}

const tiger = new Tiger('백두산 호랭이', 90);
tiger.move('슬픈 눈망울의 사슴');

포인트:

  • super.move(...)부모 메서드 호출
  • 이후에 추가 동작(공격)을 붙여서 기능을 확장
  • 같은 이름의 메서드를 재정의하므로 오버라이딩 이라고 부름

2-3. constructor 오버라이딩와 super

자식 클래스에 constructor 가 없으면, 자바스크립트가 자동으로 아래와 같은 생성자를 넣어 준다.

class Tiger extends Animal {
  constructor(...args) {
    super(...args);
  }
}

직접 생성자를 작성하면 반드시 super(...) 를 호출해야 한다.

class Deer extends Animal {
  constructor(name, weight, legLength) {
    // super(name, weight) 보다 먼저 this에 접근하면 에러
    // this.name = name;  // 에러

    super(name, weight);  // 부모 생성자 호출
    this.legLength = legLength;
  }

  hide(place) {
    console.log(`${this.name} 은(는) ${place} 에 숨는다`);
  }
}

const deer = new Deer('슬픈 눈망울의 사슴', 40, 1);
deer.hide('동굴 안');

super 가 필수인지:

  • 상속받는 클래스의 생성자는 내부적으로 [[ConstructorKind]]: "derived" 로 구분된다
  • 이런 생성자는 직접 빈 객체를 만들지 않고,
    부모 생성자가 this 객체를 만들어 줄 것이라고 기대한다
  • 그래서 super(...) 를 호출하지 않으면 this 자체가 준비되지 않아 에러가 발생한다

3. 정적 메서드와 정적 프로퍼티 (static)

3-1. 정적 메서드 (static method)

특정 인스턴스가 아니라 클래스 전체에 필요한 기능을 만들 때 사용한다.

class Student {
  constructor(name, height) {
    this.name = name;
    this.height = height;
  }

  // 정적 메서드
  static compare(studentA, studentB) {
    return studentA.height - studentB.height;
  }
}

const students = [
  new Student('유관순', 165.5),
  new Student('홍길동', 180.5),
  new Student('선덕여왕', 159.5)
];

// 키 오름차순 정렬
students.sort(Student.compare);
console.log(students);
  • static compare 처럼 클래스 선언부 안에서 static 키워드 사용
  • 개별 학생 인스턴스가 아니라 배열 정렬 시 사용하는 유틸 메서드
    → 인스턴스가 아니라 클래스에 묶여 있는 게 자연스럽다

정적 메서드를 나중에 직접 프로퍼티로 붙여도 된다.

Student.staticMethod = function () {
  console.log('이건 Student에 직접 붙인 정적 메서드');
};

Student.staticMethod();

3-2. 팩토리 메서드 예시

정적 메서드로 객체 생성 로직을 감싸는 패턴도 자주 쓰인다.

class User {
  constructor(id, registDate) {
    this.id = id;
    this.registDate = registDate;
  }

  // 팩토리 정적 메서드
  static registUser(id) {
    return new this(id, new Date());
  }
}

const user01 = User.registUser('user01');
console.log(user01);
  • new User(...) 를 직접 쓰는 대신
    User.registUser('user01') 같은 형태로 생성
  • 내부에서 new this(...) 를 호출해서,
    상속 구조에서도 그대로 재사용 가능

3-3. 정적 프로퍼티 (static property)

클래스 수준에서 공유할 데이터를 담을 수 있다.

class Animal {
  // 정적 프로퍼티
  static planet = '지구';

  constructor(name, weight) {
    this.name = name;
    this.weight = weight;
  }

  eat(foodWeight) {
    this.weight += foodWeight;
    console.log(`${this.name} 이(가) ${foodWeight}kg 먹고 ${this.weight}kg 이 되었다`);
  }

  move(lostWeight) {
    if (this.weight > lostWeight) {
      this.weight -= lostWeight;
    }
    console.log(`${this.name} 이(가) 움직여서 ${lostWeight}kg 빠져서 ${this.weight}kg 이 되었다`);
  }

  // 정적 메서드
  static compare(animalA, animalB) {
    return animalA.weight - animalB.weight;
  }
}

// 정적 프로퍼티를 나중에 추가하는 것도 가능
Animal.staticProperty =
  'static으로 선언해도 결국 클래스 자체에 프로퍼티를 직접 할당하는 것과 같다';

console.log(Animal.planet);
console.log(Animal.staticProperty);

정리:

  • static planet = '지구' → 클래스 레벨에 붙는 값
  • Animal.planet 으로 접근
  • 인스턴스(new Animal)에서는 접근 불가 (animal.planet X)

3-4. 정적 멤버 상속

정적 메서드와 정적 프로퍼티도 상속된다.

class Human extends Animal {
  develop(language) {
    console.log(`${this.name} 이(가) ${language} 로 개발한다`);
  }
}

const humans = [
  new Human('홍길동', 70),
  new Human('선덕여왕', 50),
  new Human('신사임당', 60)
];

// 정적 메서드 상속 사용
humans.sort(Human.compare);
humans[0].develop('JavaScript');

// 정적 프로퍼티 상속
console.log(Human.planet);
console.log(Human.staticProperty);

// 프로토타입 체인 확인
console.log(Human.__proto__ === Animal);                   // true (정적 영역)
console.log(Human.prototype.__proto__ === Animal.prototype); // true (인스턴스 영역)

동작 방식:

  • class B extends A
    B.__proto__ === A
    → 정적 메서드·프로퍼티를 상속
  • 동시에 B.prototype.__proto__ === A.prototype
    → 인스턴스 메서드도 상속

마무리 요약

  • 클래스 기본 문법

    • constructor 로 인스턴스 초기화
    • 메서드는 기본적으로 프로토타입 메서드
    • 생성자 함수와 달리 new 없이 호출 불가, 자동 strict mode
  • 클래스 표현식

    • 클래스도 일급 객체라 표현식 안에서 정의·전달·반환 가능
  • getter / setter, public field

    • 게터/세터로 프로퍼티 접근에 부가 로직을 끼워 넣을 수 있다
    • 클래스 필드 문법으로 인스턴스 필드를 간결하게 정의할 수 있다
  • 상속과 super

    • extends 로 프로토타입 기반 상속 구성
    • 메서드 오버라이딩 시 super 로 부모 메서드 호출
    • 파생 클래스 생성자에서는 this 사용 전에 super(...) 필수
  • static 메서드·프로퍼티

    • 인스턴스가 아닌 클래스 전체에 묶어야 할 기능/데이터에 사용
    • 팩토리 메서드 패턴 구현에 자주 쓰이고
    • extends 를 통해 정적 멤버도 그대로 상속된다
profile
개발자 희망자

0개의 댓글