Class 이해하고, 사용하기

JJ·2023년 5월 25일
0

자바스크립트

목록 보기
3/5

Class

대개 자바스크립트를 소개할 때, 명령형, 함수형, 프로토타입 기반의 객체 지향을 지원하는 멀티 패러다임 프로그래밍 언어라고 한다. 여기서 객체 지향이라는 의미는 프로그램을 표현할 때, 즉 코드를 절차지향적으로 구성하는 것이 아니라 여러 개의 독립적 단위, 다시말해 객체의 집합으로 프로그램을 표현하려는 방식을 의미한다. 잠시 객체에 대해서 알아보자.

객체지향 프로그래밍을 통해 구현된 프로그램은 객체의 집합으로 구성되는데, 여기서 객체는 그 사전적의미를 살펴보면 실제 세계에 존재(사물)하거나 생각할수 있는 것(개념)을 객체라고 하며, 이는 주변에서 흔히 볼 수 있는 책상, 의자, 컴퓨터, 자동차 등도 모두 객체에 해당한다. 그리고 이러한 객체는 각각 고유한 특징이나 성질을 나타내는 속성이 있고, 이를 통해 객체를 인식하거나 구별할 수 있다.

객체는 상태와 메서드로 구성되는데, 예를 들어 원(circle)이라는 개념을 객체로 만들게 된다면, 원의 반지름이라는 속성은 상태가 되고, 반지름을 사용하여 계산할 수 있는 여러가지 공식들은 메서드가 된다. 이를 코드로 표현한다면 아래와 같다.

const circle = {
  radius: 10,
  // 지름
  getDiameter() {
    return 2 * this.radius;
  },
  // 둘레
  getPerimeter() {
    return 2 * Math.PI * this.radius;
  },
  // 넓이
  getArea() {
    return Math.PI * this.radius ** 2;
  }
}

즉, 객체란 상태를 나타내는 데이터(프로퍼티)와 이것을 조작할 수 있는 동작(메서드) 하나의 단위로 묶은 복합적인 자료구조라고 할 수 있다.
***

클래스와 생성자 함수

자바스크립트에서는 객체 지향 프로그래밍을 구현하기 위해 생성자 함수라는 방식으로 함수를 정의하였고, 이후 ES6에서 클래스를 도입하였다. 물론, 클래스가 도입되기 이전의 방식인 생성자 함수로도 객체 지향 프로그래밍을 구현하는데 무리가 없었다.

// ES5
let Circle = (function () {
	// 생성자 함수
  function Circle(radius) {
  	this.radius = radius;
	}
  // 프로토타입 메서드
  Circle.prototype.getDiameter = function () {
  	return 2 * this.radius;
	};
	// 생성자 함수 반환
	return Circle;
}());

// 인스턴스 생성
let circle1 = new Circle(10);
circle1.getDiameter(); // 20

그렇다면, 클래스를 도입하게 된 이유는 무엇일까?

여러가지 이유가 있다. 우선, 기존에 존재하는 다른 객체지향 언어(자바, C#)를 사용하는 프로그래머들이 보다 빠르게 자바스크립트를 사용할 수 있게 하기 위한 이유도 있으나 클래스와 생성자 함수간에는 몇가지 차이가 존재한다.

  1. new 연산자 없이 클래스를 사용하면 에러가 발생한다.
  2. extends와 super 키워드
  3. 호이스팅이 발생하지 않는 것처럼 동작
  4. 암묵적 strict mode
  5. 열거 불가능

클래스

클래스는 객체 지향 프로그래밍에서 특정 객체를 생성하기 위해 변수와 메서드를 정의하는 일종의 틀로, 객체를 정의하기 위한 상태와 메서드로 구성된다.

"wikipedia"

생성(정의)

클래스 사용 방법을 알아보자.

우선, 클래스는 class 키워드를 사용하며 정의하며, 일반적으로 생성자 함수를 정의하는 것과 동일하게 파스칼케이스로 명명한다. 또한 함수와 마찬가지로 표현식으로도 정의할 수 있다.

// 클래스 선언문
	class Circle {}
// 익명 클래스 표현식
	const Circle = class {}
// 기명 클래스 표현식
  const Circle = class MyClass {}

메서드

클래스의 함수 몸체는 0개 이상의 메서드를 정의할 수 있고, 메서드의 종류에는 constructor(생성자), 프로토타입 메서드, 정적 메서드로 총 3 가지가 있다.

// 클래스 선언문
class Circle {
  // constructor(생성자)
  constructor(radius) {
    // 인스턴스 생성 및 초기화
    this.radius = radius; // 이떄, radius 프로퍼티는 public하다.
  }
  // 프로토타입 메서드
  getDiameter() {
    return 2 * this.radius;
  }
  // 정적 메서드
  static diameterFormula() {
    console.log(`2 * radius`);
  }
  static getDiameter(radius) {
    return 2 * radius;
  }
}
// 인스턴스 생성
const circle = new Circle(10);

// 프로퍼티 참조
console.log(circle.radius); // 10
// 프로토타입 메서드 호출
circle.getDiameter(); // 20
// 정적 메서드 호출
Circle.diameterFormula(); // 2 * radius
Circle.getDiameter(); // 20

constructor

인스턴스의 생성 및 초기화를 위한 메서드로, constructor는 클래스 내부에 단 한개만 존재할 수 있다. 만약 이를 생략하게 되면 암묵적으로 빈 constructor가 정의되며, 빈 객체를 생성한다. 인스턴스 프로퍼티의 추가는 constructor 내부에서 추가해야한다는 것이 기존의 방식이었으나, es13 명세에서 public, private, static 키워드가 추가되었고, 또한 constructor 외부에서도 인스턴스 프로퍼티를 추가할 수 있게 되었다.

// 기존 방식
class Circle {
  constructor(radius) {
    this.radius = radius;
  }
}
// 신규
class Circle {
  radius = 10;
  this.pi = Math.PI; // class 몸체에서 인스턴스를 추가하는 경우 this에 바인딩 시 오류가 발생한다.
}

프로토타입 메서드

생성자 함수의 경우 프로토타입 메서드 생성 시에 명시적으로 프로토타입 메서드를 추가해야 하지만, class 의 경우 명시적으로 추가하지 않아도 프로토타입 메서드가 된다.

// 생성자 함수
function Circle(radius) {
  this.radius = radius;
  // 프로토타입 메서드
  Circle.prototype.getDiameter = function () {
    return 2 * this.radius;
  }
}
const circle1 = new Circle(10);
circle1.getDiameter(); // 20

// 클래스
class Circle {
  constructor(radius) {
    this.radius = radius;
  }
  // 프로토타입 메서드
  getDiameter() {
    return 2 * this.radius
  }
}
const circle2 = new Circle(10);
circle2.getDiameter(); // 20

정적 메서드

인스턴스를 생성하지 않아도 호출할 수 있는 메서드를 의미한다. 생성자 함수의 경우 명시적으로 정적 메서드를 추가했지만, 클래스의 경우 static 키워드로 정적 메서드를 만들 수 있다.

// 생성자 함수
function Circle(radius) {
  this.radius = radius;
}
Circle.getDiameter = function (radius) {
  return 2 * radius;
}
Circle.getDiameter(10); // 20

// 클래스
class Circle {
  constructor(radius) {
    this.radius = radius;
  }
  static getDiameter(radius) {
    return 2 * radius;
  }
}
Circle.getDiameter(10); // 20

프로토타입 메서드와 정적 메서드를 살펴보면 한가지 차이점이 보인다. 프로토타입 메서드의 경우 호출 시에 인스턴스의 프로퍼티를 참조하지만, 정적 메서드의 경우 인스턴스의 프로퍼티를 참조할 수 없다.

다시 말해, 만약 프로토타입 메서드를 사용한다면 별도의 인수를 전달하지 않고 사용해야 하고, 정적 메서드를 사용한다면 필요한 인수를 전달하여 사용해야 한다.


상속과 확장

클래스에서는 상속과 확장을 통해 코드의 재사용성, 유지보수, 추상화, 다형성 등에 이점이 있다. 그렇다면, 어떤 방법으로 저러한 이점을 가지는지 알아보자.

클래스에서는 superclass와 subclass라는 것이 있다. 상속을 통해 클래스를 확장하게 될 때, 확장된 클래스를 subclass라 하고, subclass에게 상속된 클래스를 superclass라고 한다. 각각의 용어는 부모클래스와 자식클래스로 부르기도 하며, 개인적으론 후자가 더 이해하기 편했다.

클래스에서의 상속과 확장은 기존 클래스(superclass)를 상속받아 새로운 클래스(subclass)를 확장하여 정의하는 것이다.

extends

클래스에서 상속받을 클래스(subclass)를 정의할 때 사용되는 키워드이다.

// superclass
class BigCircle {}
// subclass
class SmallCircle extends BigCircle {}

생성자 함수의 경우 인스턴스를 생성하게 되면, 프로토타입 체인이 생성된다. 클래스의 경우에도 역시 인스턴스를 생성하므로, 프로토타입 체인이 생성되지만, 추가적으로 클래스 간의 프로토타입 체인이 생성된다. 때문에, 프로토타입 메서드와 정적 메서드 둘 다 상속이 가능해진다.

super

클래스에서 상속을 위해 사용되는 키워드로, extends가 상속받을 클래스를 정의하는데에 사용되었다면, super는 함수처럼 호출하거나 this 처럼 참조할 수 있는 키워드이다. super를 호출하게 되면 superclass의 constructor를 호출하게 되고, 참조하게 되면 superclass의 메서드를 호출하게 된다. 정리하면, super 라는 키워드는 상속의 대상이 되는 것(subclass)에서 상속의 주체(superclass)의 프로퍼티와 메서드를 사용하기 위한 키워드이다.

호출 예시를 살펴보자. (superclass의 프로퍼티 사용)

// superclass
class Parent {
	constructor(a, b) {
    this.a = a;
    this.b = b;
  }
}
// subclass
class Child extends Parent {
  // 아래의 과정이 암묵적으로 생성된다. 물론 명시적으로 작성해도 결과는 동일하다.
  // constructor(...args) { super(...args) }
}

const children = new Child(3, 4);
console.log(children); // children {a: 3, b: 4}

위의 예시의 경우, subclass가 superclass 내부의 프로퍼티를 그대로 갖는 형태이므로, constructor를 생략하였고, 그 결과 subclass를 호출했을 때, 그 내부에서 암묵적으로 정의된 constructor에서 super를 호출하면서 superclass의 constructor로 그 인수(3, 4)를 전달하였다.

만약 subclass에서 superclass로부터 상속받은 프로퍼티에 새로운 프로퍼티를 추가하는 경우라면 constructor를 생략할 수 없다. 물론, 이 과정에서 명시적으로 subclass 내부에 constructor로 super를 통해 superclass의 constructor로 필요한 인수를 전달할 수 있다.

// superclass
class Parent {
  constructor(a, b) {
    this.a = a;
    this.b = b;
  }
}
// subclass
class Child extends Parent {
  constructor(a, b, c) {
    super(a, b);
    this.c = c;
  }
}

const children = new Child(3, 4, 5);
console.log(children); // Child {a: 3, b: 4, c: 5}

다음은 참조 예시이다. (superclass의 메서드 사용)

// superclass
class Circle {
  constructor(radius) {
    this.radius = radius;
  }
  getDiameter() {
    return 2 * this.radius;
  }
}
// subclass
class SmallCircle extends Circle {
  getDiameter() {
    return super.getDiameter();
  }
}

const circle = new SmallCircle(10);
console.log(circle.getDiameter()); // 20

글을 마치며

오늘은 클래스에 대해서 짧게 알아보았다. 클래스를 사용하게 되면, 기존 코드를 재사용할 수 있어 실제 프로젝트에서 유용할 것 같고, 생성자 함수 방식과 비교해서 가독성이 더 좋아 보인다. 이전에는 사실 클래스나 생성자 함수를 사용하지 않았는데, 이번 기회로 정리하면서 이후 틈틈히 작은 프로젝트를 하면서 연습해야 할 것 같다.

profile
한줄 한줄

0개의 댓글