본문 바로가기
자바스크립트(JavaScript)/자바스크립트

[JavaScript] Class

by yerica 2024. 12. 10.

Class는 왜 나왔는가?

ES6 이전에는 클래스 대신 프로토타입 체이닝을 통해 클래스 비스무리하게 구현해 왔었다.

이후 ES6 버전에서 클래스가 추가되며 객체 지향 프로그로그래밍에서 사용되는 다양한 기능을 자바스크립트에서 사용할 수 있게 되었다. (엔진 내부적으로는 프로토타입 방식으로 작동되지만)

1) 객체 지향 프로그래밍(Object-Oriented Programming)이란?
객체 지향 프로그래밍이란 하나의 모델이 되는 청사진(blueprint)( = 클래스 Class)을 만들고, 그 청사진을 바탕으로 한 객체( = 인스턴스 Instance)를 만드는 프로그래밍 패턴을 말한다.

* 클래스(Class)
일종의 원형(original form)으로, 객체를 생성하기 위한 아이디어나 청사진을 말한다. 
클래스명은 보통 대문자로 시작하며 일반명사로 만들고, cf.일반적인 함수는 적절한 동사를 포함하여 소문자로 시작한다. 클래스에 정의된 속성과 메소드는 인스턴스에서 이용되며, 객체를 만들기 위한 생성자(constructor)함수를 포함한다.

* 인스턴스(Instance)
클래스를 통해 만들어진 객체를 인스턴스 객체(Instance Object)라고 한다. 각 인스턴스는 클래스의 고유한 속성과 메서드(객쳉에 딸린 함수)를 가진다. 생성자를 통해 함수에 인자를 넣듯이 속성을 넣을 수 있다.
2) 프로토타입(prototype)이란?
클래스 기반 언어에서는 클래스 내부에 모든 속성과 메소드가 정의되어있다.
해당 클래스를 기반으로한 객체가 인스턴스로 생성되면 이 객체는 클래스 내부에 정의되어있는 속성과 메소드에 접근하여 사용할 수 있는 형태이다. 프로토타입은 이런 클래스와 아주 유사하며 javascript의 모든 객체 프로토타입은 값을 할당하는 시점에 결정된다.

JavaScript는 흔히 프로토타입 기반 언어(prototype-based language)라 불린다.
모든 객체들이 메소드와 속성들을 상속받기 위한 명세로 프로토 타입 객체를 가진다는 의미이다.
클래스 처럼 객체의 인스턴스를 위한 명세와 같은 역할을 하는데 객체 본인만이 가진 속성과 메소드에도 접근할 수 있으면서 프로토타입의 것들에도 접근할 수 있다. javascript에서 함수를 생성할 때 프로토타입 속성이 함수에 붙여진다. 예를 들어 new 키워드로 함수를 호출할 때마다 생성되는 인스턴스는 함수 프로토 타입의 모든 속성을 상속한다. 속성과 메소드들은 각 객체 인스턴스가 아니라 객체 생성자의 prototype 속성에 정의되어 있다.

이렇게 자바스크립트의 모든 객체는 자신의 부모 역할을 하는 객체와 연결되어있고 이 부모 객체를 프로토타입이라고 한다. 이런 방식으로 클래스를 상속하여 사용하는 것 같이 객체 지향 프로그래밍 방식을 사용할 수 있다.
3) 자바스크립트에서 클래스는 사실 표현 방식이 다른 함수이다.
함수에서 함수 표현식과 함수 선언으로 정의할 수 있듯이 class도 class 선언class 표현식 두가지 방식을 제공한다. 함수(function)과 클래스의 중요한 차이점 중 하나는 함수는 이후에 정의를 하더라도 호이스팅할 수 있지만, 클래스는 반드시 먼저 정의가 된 상태여야 사용가능하다는 것이다. (호이스팅되지 않는다.)

1. 클래스 선언 ( Class Declaration )

클래스 선언은 가장 직관적이고 일반적인 클래스 정의 방법으로, class 키워드클래스명을 작성하여 선언된다.
대부분의 클래스는 고정된 이름을 갖고, 클래스 이름을 다른 곳에서 사용할 필요가 있기 때문에 클래스 표현식보다 많이 사용된다.
선언된 클래스는 new 클래스명()으로 호출하면 정의한 메서드가 들어있는 클래스 객체를 생성하는데,
이를 인스턴스(Instance)라고 한다.
// class 선언
class 클래스명 {
  // 여러 메서드를 정의할 수 있음
  constructor() { ... }
  method1() { ... }
  method2() { ... }
  method3() { ... }
  ...
}
1) 클래스와 인스턴스 차이
className은 클래스를 선언한 것이기 때문에 콘솔로 출력하면 클래스 정의 자체가 출력된다.
하지만 con1은 new 키워드를 통해 인스턴스화 한 것이다. 그렇기 때문에 콘솔로 출력하면 해당 인스턴스의 객체가 출력되고, 객체에는 name 속성이 Class 1로 설정되어 있기 때문에 className{name : 'Class 1'} 으로 출력된다.
마지막으로 con2는 클래스 표현식인데 클래스 인스턴스처럼 변수에 할당되어 있지만 인스턴스 객체를 생성한 것이 아닌 클래스 자체가 할당되어 있으므로 콘솔로 출력했을 때 클래스의 정의를 출력한다.

// 클래스 선언
class className {
   constructor() {
      this.name = 'Class 1'
   }
}

// 클래스 인스턴스
const con1 = new className();

// 클래스 표현식
const con2 = class className {
   constructor() {
      this.name = 'Class 2'
   }
}

// 출력
console.log(className) // 선언된 className 출력
console.log(con1); // className의 인스턴스
console.log(con2); // 클래스 표현식으로 정의된 클래스


결과적으로 console.log(className)과 console.log(con2)는 클래스 정의를 출력하고, console.log(con1)은 인스턴스 객체를 출력했기 때문에 아래와 같이 보이는 것이다.

 

2. 클래스 표현식(Class Expression)

클래스 표현식은 동적이거나 일시적인 클래스를 생성해야 할 때 사용된다.
클래스 { } 구문을 변수에 할당하는 방식으로 클래스를 정의하는 방법으로, 클래스의 이름을 로컬 스코프에 한정시킬 수 있고, 익명 클래스를 생성하거나 클래스를 동적으로 생성하는데 유용하다.
즉, 클래스 본문 외부에서는 클래스명을 담고있는 변수를 통해 클래스에 접근할 수는 있지만 클래스명을 직접 사용하는것은 불가능하다는 이야기이다.

* 동적 클래스 생성 : 클래스를 변수에 할당하고, 이를 함수나 배열 등에서 동적으로 생성한다.
* 익명 클래스 : 클래스를 익명으로 정의하여, 일회성으로 사용하거나 이름을 노출하지 않고 클래스를 다룰 수 있다.
* 클로저와 결합 : 클래스 표현식은 클로저와 잘 결합되며, 즉시 실행 함수(IIFE)나 고차 함수와 함께 사용할수 있다.
// class 표현식(1) : 이름이 없는 경우
let 변수명 = class {
  // 여러 메서드를 정의할 수 있음
  constructor() { ... }
  method1() { ... }
  method2() { ... }
  method3() { ... }
  ...
}
console.log(변수명.name); // 출력: "변수명"

// class 표현식(2) : 이름이 있는 경우
let 변수명 = class 클래스명 {
  // 여러 메서드를 정의할 수 있음
  constructor() { ... }
  method1() { ... }
  method2() { ... }
  method3() { ... }
  ...
}
console.log(변수명.name); // 출력: "클래스명"
아래의 예시에서 MyClass의 name속성을 출력하면 MyClass가 가지고 있는 클래스명이 뜨다.
하지만, 클래스명으로 검색하면 존재하지 않는 다는 레퍼런스오류가 뜬다.
 
클래스명은 로컬스코프에 한정되기 때문에 className안의 printClassName() 메서드에서는 className.name으로 불러올 경우 제대로 className이 뜨는것을 확인할 수 있다.
const MyClass = class className {
   constructor (height) {
      this.height = height
   }
   printClassName () {
      console.log(className.name) // className
   }
}

console.log(MyClass.name) // className
console.log(classNmae.name) // Uncaught ReferenceError: className is not defined

MyClass.printClassName() // Uncaught TypeError: MyClass.printClassName is not a function
const obj = new MyClass()
obj.printClassName() // className

 

2-2. 클래스 표현식은 왜 사용하는가?

클래스 표현식을 사용하면 클래스 선언보다 범위가 더 좁아지고, 클래스의 이름이 로컬 스코프에 한정된다는 특성이 있다.

그럼에도 불구하고 클래스 표현식을 사용하는 이유는 몇 가지 유용한 용도와 이점이 있기 때문이다.

1) 동적으로 클래스를 생성할 때 유용하다.
클래스 표현식은 동적으로 클래스를 생성하거나 일회성으로 사용하고자 할 때 유용하다.
예를 들어, 클래스 이름을 코드 실행 중에 동적으로 할당하거나, 일시적으로 생성한 클래스를 변수에 저장하여 필요할 때 사용한다.

const createClass = (name) => {
  return class {
    constructor() {
      this.name = name;
    }
    printName() {
      console.log(this.name);
    }
  };
};

const MyDynamicClass = createClass("Dynamic");
const obj = new MyDynamicClass();
obj.printName();  // "Dynamic"​

위 예시에서 createClass 함수는 name 을 인자로 받아 동적으로 클래스를 생성한다.
class 표현식을 사용하면 이렇게 동적으로 생성되는 클래스 이름을 직접 지정할 필요가 없고, 코드 실행 중에 name
을 기반으로 클래스를 만들 수 있다.
2) 즉시 실행을 위한 클로저와의 결합
클래스 표현식은 즉시 실행 함수 표현식(IIFE) 과 결합하여, 즉시 실행되는 클래스 구조를 만들 때 유용하다.
이렇게 하면 클래스를 선언하면서 동시에 특정 로직을 실행할 수 있다.

const MyClass = (() => {
  return class {
    constructor() {
      this.name = "IIFE Class";
    }
    printName() {
      console.log(this.name);
    }
  };
})();

const obj = new MyClass();
obj.printName();  // "IIFE Class"​


위 코드에서, 클래스 표현식은 즉시 실행되어 MyClass 클래스를 반환한다.
이런 방식은 클래스 이름을 외부에서 직접 사용하지 않거나, 즉시 실행되는 클래스가 필요한 경우에 유용하다.

3) 이름 충돌을 피할 수 있다.
클래스 선언은 전역 스코프에서 이름을 사용한다.
이 경우 동일한 이름의 클래스를 다른 곳에서 재정의하는 경우 이름 충돌이 발생할 수 있다.
하지만 클래스 표현식은 로컬 스코프에 클래스 이름을 한정시키므로, 이름 충돌을 방지할 수 있다.

class className {
   constructor() {
      this.name = 'Class 1'
   }
}

// className이라는 클래스를 두번 선언했을 경우
class className {
   constructor() {
      this.name = 'Class 2'
   }
}

// 출력
console.log(className)


className을 한번만 선언했을 경우 다음과 같이 콘솔창에 클래스의 내용이 뜬다.

하지만 위 코드에서처럼, className라는 클래스 표현식을 두 번 선언하려 하면 SyntaxError가 발생한다.


하지만 클래스 표현식을 사용하는 경우, 변수에 클래스를 할당하여 동일한 이름의 클래스를 재정의하거나 충돌을 피할 수 있다.

const con1 = class className {
   constructor() {
      this.name = 'Class 1'
   }
}

// className이라는 클래스를 두번 선언했을 경우
const con2 = class className {
   constructor() {
      this.name = 'Class 2'
   }
}

// 출력
console.log(con1);
console.log(con2);


클래스 명이 동일한 className이어도 콘솔창에 오류 없이 뜨는것을 확인할 수 있다.

이와같이 변수에 담아 클래스를 선언하면 이름 충돌을 방지할 수 있으나, 외부에서 클래스명으로 호출이 불가능하기 때문에 변수명으로 호출해야한다.

 

4) 클래스를 값으로 다룰 수 있다.
클래스 표현식은 다른 함수나 변수에 값을 할당하는 방식으로 사용되므로, 클래스를 값으로 다루는 것과 같은 효과를 가질 수 있다. 예를 들어, 클래스를 배열이나 객체의 속성으로 사용할 수 있다.

const classList = [
  class { constructor() { this.name = "Class A"; } },
  class { constructor() { this.name = "Class B"; } }
];

const objA = new classList[0]();
const objB = new classList[1]();

console.log(objA.name);  // "Class A"
console.log(objB.name);  // "Class B"​

이 예시처럼 클래스를 배열이나 객체의 요소로 저장 할 수 있다.
이런 유연성은 일반적인 클래스 선언에서는 할 수 없는 작업이다.
5) 익명 클래스 사용
클래스 표현식을 사용할 때, 클래스를 익명으로 정의하여 한 번만 사용하는 경우 유용하다.
예를 들어, 클래스의 이름을 외부에서 사용할 필요가 없거나, 클래스의 이름을 숨기고 싶을 때 사용된다.

const MyClass = class {
  constructor() {
    this.name = "Anonymous Class";
  }
  printName() {
    console.log(this.name);
  }
};

const obj = new MyClass();
obj.printName();  // "Anonymous Class"​


여기서 MyClass는 변수명이고, 클래스 자체는 익명 클래스로 작성되었다.
클래스 이름을 외부에 노출시키지 않으려면 클래스 표현식을 사용하는 것이 좋다.


3. 클래스 본문( class body )

클래스 본문(class body)은 중괄호{}로 묶여 있는 class의 내부를 의미한다.
이 클래스 본문에서 constructor나 메서드같은 클래스 멤버를 정의한다.
클래스 바디는 성능향상을 위해 엄격한 모드(strict mode)에서 실행되는데, 그렇지 않다면 조용한 오류가 발생할 수도 있다.

4. 생성자( constructor )

생성자 메서드인 constructor는 클래스로 생성된 객체를 생성하고 초기화 하기 위한 특수한 메서드이다.
constructor는 클래스 안에 한 개만 존재할 수 있다.
(만약 여러개 constructor가 존재한다면 SyntaxError가 발생)
constructor는 부모 클래스의 constructor를 호출하기 위해 super키워드를 사용할 수 있다.

5. 프로토타입 메서드( prototype method )

1) 메서드(Method)란?
메서드(Method)와 함수(Function)는 매우 유사한 개념이지만, 어디서 정의되느냐와 어떻게 사용되느냐에 따라 구분된다. 결과를 먼저 말하자면, 모든 메서드는 함수지만, 모든 함수는 메서드가 아니다.

* 함수(Function)
함수는 독립적으로 존재하는 코드 블록으로, 특정 작업을 수행하기 위해 정의된다.
자바스크립트에서 함수는 변수에 할당되거나, 다른 함수나 객체와 무관하게 독립적으로 호출될 수 있다.
- 정의 : 함수는 function 키워드를 사용하거나 화살표 함수등으로 정의할 수 있다.
- 호출 : 함수는 이름을 통해 독립적으로 호출된다.

// 함수 정의
function greet(name) {
    console.log(`Hello, ${name}!`);
}

// 함수 호출
greet('Alice');  // 출력: Hello, Alice!​
// 위 예시에서 greet는 독립적인 함수이기 때문에, 객체나 클래스와 관련이 없이 어디서든 호출할 수 있다.

* 메서드(Method)
메서드는 객체나 클래스의 속성으로 정의된 함수이다. 즉, 특정 객체나 클래스에 속한 함수인 것이다.
메서드는 해당 객체나 클래스의 데이터를 처리하거나, 해당 객체와 관련된 작업을 수행하기 위해 사용된다.
- 정의 : 메서드는 객체의 속성으로 정의된 함수이다. 객체의 프로퍼티처럼 사용되며, this를 통해 객체의 속성에 접근할 수 있다.
- 호출 : 메서드는 객체나 클래스 인스턴스를 통해 호출된다.

// 객체에 메서드 정의
const person = {
    name: 'Alice',
    // greet는 객체 person의 메서드
    greet: function() {
        console.log(`Hello, ${this.name}!`);
    }
};

// 메서드 호출 : 객체 person을 통해 호출할 수 있다.
person.greet();  // 출력: Hello, Alice!
// 클래스에 매서드 정의
class Person {
    constructor(name) {
        this.name = name;
    }
    // greet는 Person의 메서드
    greet: function () {
        console.log(`Hello, ${this.name}!`);
    }
}

// new 키워드로 인스턴스 객체 생성
const person = new Person('Bob');
// 인스턴스객체로 생성된 person을 통해 메서드 호출
person.greet();  // 출력: Hello, Bob!​

구분 함수( Function ) 메서드( Method )
정의 독립적인 코드 블록으로, 객체나 클래스와 무관하게 정의됨. 객체나 클래스의 속성으로 정의된 함수.
호출 함수 이름을 직접 호출함. 객체나 클래스 인스턴스를 통해 호출됨.
this this는 함수 내에서 글로벌 객체호출하는 객체에 따라 다름. this는 호출된 객체나 클래스의 인스턴스를 가리킴.
2) 매서드 축약해서 사용하기
매서드도 함수처럼 축약해서 사용할 수 있다.
const obj = {
  foo: function () {},
  bar: function () {},
};

const obj = {
  foo() {},
  bar() {},
};​
3) generator function을 메서드로
ES6에서부터 도입된 Generator 함수는 일반 함수와는 다르게 실행을 중간에 멈추거나 다시 시작할 수 있는 특수한 함수다. 이 함수는 끝에 별표가 있는 function* 키워드로 정의되며, yield 키워드를 사용하여 실행을 중단하거나 값을 반환할 수 있다. Generator 함수는 이터레이터(Iterator)를 생성하며, 호출 시 바로 실행되지 않고 이터레이터에 객체를 반환한다. 이후 next(), return(), throw()와 같은 인스턴스 메서드를 호출하여 단계적으로 실행을 제어한다.

Generator 함수를 메서드로 사용

function* anotherGenerator(i) {
  yield i + 1;
  yield i + 2;
  yield i + 3;
}

const obj = {
   g: function* (i) { // 메서드를 단축해서 쓰지 않을 경우
     yield i;
     yield* anotherGenerator(i);
     yield i + 10;
   }
}
const obj = {
   *g(i) { // 단축해서 쓸 경우
     yield i;
     yield* anotherGenerator(i);
     yield i + 10;
   }
}

var gen = generator(10);

console.log(gen.next().value); // 10
console.log(gen.next().value); // 11
console.log(gen.next().value); // 12
console.log(gen.next().value); // 13
console.log(gen.next().value); // 20
4) 속성 계산명(computed property name)
단축 구문에서 속성 계산명도 지원한다.
var bar = {
  foo0: function () {
    return 0;
  },
  foo1() {
    return 1;
  },
  ["foo" + 2]() {
    return 2;
  },
};

console.log(bar.foo0()); // 0
console.log(bar.foo1()); // 1
console.log(bar.foo2()); // 2​

6. Getter / Setter

1) 객체의 프로퍼티
객체의 프로퍼티에는 두 종류가 있다.
바로 데이터 프로퍼티(data property)와 접근자 프로퍼티(accessor property)이다.
접근자 프로퍼티의 본질은 함수로 값을 획득(get)하고 설정(set)하는 역할을 담당하는데, 외부 코드에서는 함수가 아닌 일반적인 프로퍼티처럼 보인다.

접근자 프로퍼티는 'getter(획득자)'와 'setter(설정자)' 메소드로 표현된다.
객체 리터럴 안에서 getter와 setter 메서드는 get 과 set으로 나타낼 수 있다.