ES6 클래스(class)
ES6 클래스는 ECMAScript 2015(ES6)에서 도입된 새로운 객체 지향 프로그래밍 문법이다. 기존의 프로토타입 기반 상속보다 클래스 기반 언어와 유사한 문법을 제공하여 개발자들이 객체 지향 프로그래밍을 더욱 쉽게 사용하도록 돕는다.
class는 기본적으로 프로토타입 기반 상속이다.
class 문법은 자바스크립트의 프로토타입 기반 상속을 더욱 쉽게 사용할 수 있도록 "문법적 설탕(syntactic sugar)"을 제공하는 것이지 상속을 받는 전혀 다른 방식이 아니다.
문법적 설탕(syntactic sugar)?
프로그래밍 언어에서 기존에 존재하는 기능을 더 쉽고 간결하게 사용할 수 있도록 제공하는 문법을 의미한다.
자바 스크립트에선
- 화살표 함수 (Arrow functions)
- 구조 분해 할당 (Destructuring assignment)
- 스프레드 연산자 (Spread operator)
등이라고 할 수 있다.
특징
- class 키워드: 클래스를 정의하기 위해 class 키워드를 사용한다.
- 생성자(constructor): 클래스의 인스턴스를 생성하고 초기화하는 특별한 메서드이다.
- 메서드 정의: 클래스 내부에 일반 메서드를 정의할 수 있다.
- 상속(extends): extends 키워드를 사용하여 다른 클래스를 상속받을 수 있다.
- super 키워드: 부모 클래스의 생성자나 메서드를 호출할 때 사용한다.
- 정적 메서드(static): 클래스의 인스턴스 없이 호출할 수 있는 정적 메서드를 정의할 수 있다.
// 클래스 정의
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log(`안녕하세요, 제 이름은 ${this.name}이다.`);
}
}
// 클래스 상속
class Student extends Person {
constructor(name, age, grade) {
super(name, age); // 부모 클래스의 생성자 호출
this.grade = grade;
}
study() {
console.log(`${this.name} 학생은 ${this.grade}학년이다.`);
}
}
// 인스턴스 생성
const person1 = new Person('홍길동', 30);
const student1 = new Student('임꺽정', 20, 3);
// 메서드 호출
person1.sayHello(); // "안녕하세요, 제 이름은 홍길동이다."
student1.sayHello(); // "안녕하세요, 제 이름은 임꺽정이다."
student1.study(); // "임꺽정 학생은 3학년이다."
// 정적 메서드 예시
class MathUtils {
static add(a, b) {
return a + b;
}
}
console.log(MathUtils.add(5, 3)); // 8
클래스의 구성 요소와 사용 순서
1. 클래스 선언
- class 키워드를 사용하여 클래스를 선언한다.
- 클래스 이름은 일반적으로 대문자로 시작한다.(이 부분은 생산자 함수와 비슷하다)
// 클래스 정의
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
2. 생성자 (constructor)
- constructor 메서드는 클래스의 인스턴스를 초기화한다.
- new 연산자를 사용하여 클래스의 인스턴스를 생성할 때 자동으로 호출된다.
- 인스턴스의 초기 속성을 설정하는 데 사용된다.
// constructor로 인스턴스를 초기화
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
// 인스턴스 생성
const person1 = new Person('홍길동', 30);
const student1 = new Student('임꺽정', 20, 3);
3. 메서드 (methods)
- 클래스 내부에 정의된 함수를 메서드라고 한다.
- 메서드는 클래스의 인스턴스에서 호출할 수 있는 함수이다.
- 인스턴스의 동작을 정의하는 데 사용된다.
// 일반 메서드 예시
sayHello() {
console.log(`안녕하세요, 제 이름은 ${this.name}이다.`);}
study() {
console.log(`${this.name} 학생은 ${this.grade}학년이다.`);}
// 메서드 호출
person1.sayHello(); // "안녕하세요, 제 이름은 홍길동이다."
student1.sayHello(); // "안녕하세요, 제 이름은 임꺽정이다."
student1.study(); // "임꺽정 학생은 3학년이다."
4. 정적 메서드 (static methods)
- static 키워드를 사용하여 정의된 메서드를 정적 메서드라고 한다.
- 일반 메서드와 마찬가지로 반드시 클래스 내부에 있어야한다.
- 정적 메서드는 클래스의 인스턴스 없이 클래스 자체에서 직접 호출할 수 있다.
- 클래스와 아무 상관없이 완전히 독립된 내용으로도 만들 수있다.
- 클래스 관련 유틸리티 함수를 만들 때 사용된다.
// 정적 메서드 예시 : 일반 메서드와 마찬가지로 반드시 클래스 내부에 있어야한다.
class MathUtils {
static add(a, b) {
return a + b;
}
}
//new를 사용하지 않아도 사용 가능
console.log(MathUtils.add(5, 3)); // 8
일반 메서드 vs 정적 메서드
호출 방식
- 일반 메서드 : 클래스의 인스턴스를 생성한 후, 인스턴스에서 호출해야 한다.
- 정적 메서드 : 클래스 이름으로 직접 호출할 수 있다.
// 일반 메서드 : new를 통해 인스턴스를 생성해야한다.
const person1 = new Person('홍길동', 30);
const student1 = new Student('임꺽정', 20, 3);
// 메서드 호출 : 인스턴트에 . 연산자(점 연산자)를 사용해서 호출해야한다.
person1.sayHello(); // "안녕하세요, 제 이름은 홍길동이다."
student1.sayHello(); // "안녕하세요, 제 이름은 임꺽정이다."
student1.study(); // "임꺽정 학생은 3학년이다."
//정적 메서드
static add(a, b) {
return a + b;
}
//new를 사용하지 않아도 사용 가능하지만 클래스에 .연산자를 사용해서 호출해야한다.
console.log(MathUtils.add(5, 3)); // 8
this 바인딩
- 일반 메서드 : 메서드를 호출한 인스턴스를 가리킨다. 따라서 일반 메서드는 인스턴스 속성에 접근할 수 있다.
- 정적 메서드 : 클래스 자체를 가리키므로, 인스턴스 속성에 접근할 수 없다.
class Person {
constructor(name, age) {
this.name = name; // 인스턴스 속성 초기화
this.age = age; // 인스턴스 속성 초기화
}
}
// 인스턴스 생성
const person1 = new Person("홍길동", 30);
// 인스턴스 속성 접근
console.log(person1.name); // 출력: 홍길동
// 인스턴스를 생성하지 않고 속성에 접근하면 오류 발생
// console.log(Person.name); // 오류 발생
- 인스턴스 속성이라는 것은 클래스 내부에 들어있는 this.name = name; (this.속성명 = 값;)류의 코드이다.
- 클래스를 생성할때 constructor를 사용해서 말 그대로 초기화하기 때문에 값이 없다.
- 아예 메모리 할당이 되지 않기 때문에 undefined나 null같은 값조차 없다.
- 따라서 그냥 접근할 경우 오류가 나고, new 생성자를 사용해서 인스턴스 속성에 값을 부여해야만 사용할 수 있는 것이다.
- 마찬가지 이유로 그냥 class 내부에 static 메서드를 정의한다고 해도 내부의 인스턴스 속성에 접근이 불가능하다.
- new로 인스턴스를 생성하기 전에는 인스턴스 속성 값 자체가 비어있기 때문이다.
물론 예외 상황은 있긴하지만 정적 메서드 자체가 유틸리티를 위해 사용하는 것이기 때문에 굳이 서로의 역할과 다르게 사용할 이유가 전혀없다. 정적 메서드를 사용하는 이유 자체가 코드의 가독성과 효율성을 위함인데 굳이 용도 외의 방식으로 사용할 필요가 없다.
class MyClass {
static myStaticMethod() {
console.log("정적 메서드 호출");
}
}
const instance = new MyClass();
// 권장되는 호출 방식
MyClass.myStaticMethod(); // "정적 메서드 호출"
// 가능은 하지만 비권장 호출 방식
instance.myStaticMethod(); // "정적 메서드 호출"
5. 속성 (fields)
- 클래스 필드는 클래스 레벨에서 변수를 선언할 수 있게해준다.
- 기본 값을 할당할 수 있고, 생성자 밖이나 안에서 선언이 가능하다.
클래스 필드(Class Fields)
JavaScript에서 클래스 내부에 변수(속성)를 선언하는 새로운 방법이다.
ES2022(ECMAScript 2022) 표준에 추가되어 이전에는 생성자 내부에서만 선언할 수 있었던 인스턴스 속성을 클래스 본문에서도 직접 선언하고 초기화할 수 있게 되었다.
간단히 말하자면 이전에는 생성자(constructor)를 사용해서 this.속성명 = 값; 형태로 초기화 하거나 할당을 했어야 했다.
하지만 이젠 아예 일반적인 변수처럼 직접 선언을 하는 것이 가능해졌다.
또한 클래스 필드는 속성이지 변수가 아니기 때문에 let, const 같은 변수를 사용하지는 않는다.
class Person {
name = "기본 이름"; // 클래스 필드 선언 및 초기화
age; // 클래스 필드 선언 (초기값 없음)
constructor(age) {
this.age = age; // 생성자 내부에서 클래스 필드 초기화
}
물론 클래스 필드를 사용하는 것은 직관적이기 때문에 단순한 속성 초기화를 간편하게 만들어주지만,
그럼에도 굳이 constructor를 사용하는 것은 복잡한 초기화 로직, 매개변수 처리, 상속 관련 작업, 필수 속성 초기화 등은 여전히 생성자의 역할이기 때문이다.
6. 상속 (inheritance)
- extends 키워드를 사용하여 다른 클래스를 상속받을 수 있다.
- super 키워드를 사용하여 부모 클래스의 생성자나 메서드를 호출할 수 있다.
- 기존 클래스의 기능을 확장하거나 재사용하는 데 사용된다.
- super 키워드는 생성자(constructor) 또는 메서드 내에서만 사용할 수 있다.
class Student extends Person {
constructor(name, age, grade) {
super(name, age); // 부모 클래스의 생성자 호출
this.grade = grade;
}
Student 클래스는 extend를 사용해서 Person 클래스를 상속 받았다. 때문에 Student 클래스 내부에 name과 age 속성이 없더라도 super 키워드를 사용해서 해당 속성을 받아올 수 있다.
sayHello() {
super.sayHello(); // 부모 클래스 메서드 호출
console.log(`I am ${this.age} years old.`);
}
이런식으로 메서드 안에 super를 통해 부모 클래스의 메서드를 받아올 수 있다.
클래스 필드는 받아오지 못한다
super자체가 생성자를 호출하는 것이기 때문에 클래스 필드를 사용해서 명시적으로 정의된 속성은 가져오지 못한다. 때문에 다른 방식을 사용해야한다.
아래 예시 처럼 부모 요소에서 클래스필드의 속성과 값을 받아오는 메서드를 작성하고 상속 받은 클래스에서 해당 메서드를 불러와야한다.
class Parent {
name = "Parent Name"; // 클래스 필드 정의
getParentName() {
return this.name;
}
}
class Child extends Parent {
getChildName() {
return super.getParentName(); // 부모 클래스의 메서드 호출
}
}
const child = new Child();
console.log(child.getChildName()); // 출력: Parent Name
생성자 함수 VS 클래스
공통점
- 객체 생성 : 둘 다 new 연산자를 사용하여 객체를 생성한다.
- 객체 초기화 : 둘 다 객체의 초기 상태를 설정하는 역할을 합니다. 생성자 함수의 경우 함수 자체, 클래스의 경우 constructor 메서드가 이 역할을 수행한다.
- 프로토타입 기반 : JavaScript의 프로토타입 기반 상속을 사용한다.
차이점
- 문법
- 생성자 함수 : 일반 함수를 사용하여 객체를 생성
- 클래스 : class 키워드를 사용하여 객체를 생성
- 호출 가능 여부
- 생성자 함수 : 일반 함수로서도 호출이 가능하다.
- 클래스 : 반드시 new 연산자를 사용해 호출해야 한다.
- 상속
- 생성자 함수 : 프로토타입 체인을 직접 조작해야 하므로 상속 구현이 복잡하다.
- 클래스 : extends 및 super 키워드를 사용하여 상속을 더 쉽게 구현할 수 있다.
- 정적 메서드:
- 생성자 함수 : 프로토타입 객체에 직접 메서드를 추가해야 한다.
- 클래스 : static 키워드를 사용하여 정적 메서드를 정의할 수 있다.
- 엄격 모드
- 생성자 함수 : 엄격 모드를 명시적으로 선언해야한다.
- 클래스 : 블록 내의 코드는 항상 엄격 모드(strict mode)로 실행된다.
- 호이스팅
- 생성자 함수 : 는 일반 함수처럼 호이스팅된다.
- 클래스 : 호이스팅되지 않는다.
장점
- 가독성 향상: 클래스 기반 언어와 유사한 문법으로 코드의 가독성이 향상된다.
- 객체 지향 프로그래밍 지원 강화: 상속, 다형성 등 객체 지향 프로그래밍의 핵심 개념을 더욱 쉽게 구현할 수 있다.
- 유지 보수성 향상: 클래스를 사용하여 코드를 구조화하면 유지 보수가 용이해진다.
주의사항은 다음과 같다.
- ES6 클래스는 프로토타입 기반 상속을 추상화한 문법일 뿐, 내부적으로는 프로토타입 체인을 사용하여 상속을 구현한다.
- class 키워드는 호이스팅되지 않는다. 즉, 클래스 선언 전에 클래스를 사용할 수 없다.