6 분 소요

자바스크립트의 클래스

클래스는 동일한 모양의 객체를 더 쉽게 생성하도록 도와주는 문법이다.

붕어빵을 예로 들면, 붕어빵을 찍어내는 틀은 클래스, 실제로 만들어진 붕어빵은 인스턴스 객체에 해당된다. 만약 붕어빵 틀이 없는 상태에서 붕어빵을 100개 만들려면 굉장히 귀찮은 작업들을 수작업으로 100번 반복해야 할 것이다. 이런 붕어빵 지옥은 상상만 해도 끔찍하다.

자바스크립트의 객체도 마찬가지이다. 동일한 모양의 객체를 여러 번 생성해야 하면 어쩔 수 없이 중복 코드가 발생하게 된다. 이럴 때 바로 클래스 문법을 사용한다.

클래스 선언 & 호출

자바스크립트에서 클래스 선언은 다음과 같이 이루어진다.

class Student {
  // 필드
  name;
  age;
  grade;

  //생성자
  constructor(name, grade, age) {
    this.name = name;
    this.grade = grade;
    this.age = age;
  }

  // 메소드
  study() {
    console.log("열심히 공부 중");
  }

  introduce() {
    console.log("안녕하세요.");
  }
}

필드클래스가 생성할 객체들이 갖는 프로퍼티를 의미한다. 학생 객체는 항상 name, age, grade 프로퍼티를 갖기 때문에 위와 같이 필드를 선언했다.

생성자는 실질적으로 객체를 생성하는 특수한 메소드이다. 생성자는 매개변수로 프로퍼티들의 값을 받아 this.프로퍼티의 값으로 할당한다. 이때 this는 현재 만들고 있는 객체를 의미한다. 따라서 생성자 메소드현재 만들고 있는 객체의 name, age, grade 프로퍼티의 값을 매개변수로 전달받은 값으로 설정하는 역할을 수행한다.

이렇게 생성자까지 만들어주었다면 이제 클래스를 호출하여 객체를 만들어 낼 수 있다.

const studentA = new Student("홍길동", "A+", 27);

클래스를 이용해 새 객체를 생성할 때에는 new 클래스이름 형태로 클래스의 생성자 함수를 호출한다. 이때 인수로는 각각 생성자 함수의 매개변수에 들어갈 값을 전달한다.

결과적으로 생성되는 객체는 다음과 같다.

{
    name: "홍길동",
    grade: "A+",
    age: 27
}

메소드는 클래스 내에 정의된 함수로, 각각의 객체들이 기본적으로 포함하게 된다. 이러한 메소드에서는 앞서 배운 this를 활용해 객체 프로퍼티의 값을 다양하게 활용할 수 있다.

class Student {
    (...)

    introduce() {
        console.log(`안녕하세요 ${this.name} 입니다.`);
    }
}

let studentB = new Student("홍길동", "A+", 27);

studentB.introduce(); // 안녕하세요 홍길동 입니다.

상속

만약 앞서 만든 Student 클래스를 기반으로 추가적인 필드와 메소드를 갖는 클래스를 선언하고 싶다면 다음과 같이 상속을 이용하면 편리하다.

class StudentDeveloper extends Student {}

위 StudentDeveloper 클래스는 Student 클래스를 확장(상속)한다. 이 확장은 앞서 배운 인터페이스의 확장과 기본적으로 비슷하다. 즉 StudentDeveloper 클래스는 Student 클래스에 정의된 모든 필드와 메소드를 자동으로 갖게 된다.

또한 StudentDeveloper 클래스만의 새로운 필드와 메소드도 다음과 같이 정의할 수 있다.

class StudentDeveloper extends Student {
  // 필드
  favoriteSkill;

  // 생성자
  constructor(name, grade, age, favoriteSkill) {
    super(name, grade, age);
    this.favoriteSkill = favoriteSkill;
  }

  // 메서드
  programming() {
    console.log(`${this.favoriteSkill}로 프로그래밍 함`);
  }
}

이때 주의해야 할 점은 StudentDeveloper 클래스에서 Student 클래스의 생성자를 함께 호출해줘야 한다. 그렇지 않으면 생성되는 객체의 name, grade, age 값이 제대로 설정되지 않는다.

따라서 super 메소드를 호출하여 인수로 name, grade, age를 전달하여 슈퍼 클래스(확장 대상 클래스)의 생성자를 호출한다.


타입스크립트의 클래스

타입스크립트에서는 클래스의 필드를 선언할 때 타입 주석으로 타입을 함께 정의해주어야 한다. 그렇지 않을 경우 함수 매개변수와 동일하게 암시적 any타입으로 추론되므로 엄격한 타입 검사 모드에서 오류가 발생한다.

추가로 생성자에서 각 필드의 값을 초기화하지 않을 경우에는 필드에서 초기값도 함께 명시해주어야 한다.

class Employee {
  // 필드
  name: string = "";
  age: number = 0;
  position: string = "";

  // 생성자
  constructor(name: string, age: number, position: string) {
    this.name = name;
    this.age = age;
    this.position = position;
  }

  // 메소드
  work() {
    console.log("일함");
  }
}

만약 이 클래스가 생성하는 객체의 특정 프로퍼티를 선택적 프로퍼티로 만들고 싶다면 다음과 같이 필드의 이름 뒤에 물음표를 붙여주면 된다.

class Employee {
    name: string = "";
    age: number = 0;
    position?: string = "";

    (...)
}

클래스는 타입?

타입스크립트의 클래스는 타입으로도 사용할 수 있다. 클래스를 타입으로 사용하면 해당 클래스가 생성하는 객체의 타입과 동일한 타입이 된다.

class Employee {
  (...)
}

const employeeC: Employee = {
  name: "",
  age: 0,
  position: "",
  work() {},
};

위 코드에서 employeeC의 타입을 Employee 클래스로 정의했다. 따라서 이 변수는 Employee의 name, age, position 프로퍼티와 work 메소드를 갖는 객체 타입으로 정의된다.

상속

타입스크립트에서 클래스의 상속을 이용할 때 파생 클래스에서 생성자를 정의했다면 반드시 super 메소드를 호출해 슈퍼 클래스의 생성자를 호출해야 하며, 호출 위치는 생성자의 최상단이어야 한다.

class ExecutiveOfficer extends Employee {
  officeNumber: number;

  constructor(
    name: string,
    age: number,
    position: string,
    officeNumber: number
  ) {
    super(name, age, position);
    this.officeNumber = officeNumber;
  }
}

접근 제어자

접근 제어자(Access Modifier)는 타입스크립트에서만 제공되는 기능으로, 클래스의 특정 필드나 메소드에 접근할 수 있는 범위를 설정하는 기능이다.

타입스크립트에서는 기본적으로 다음과 같은 3개의 접근 제어자를 사용할 수 있다.

  • public: 모든 범위에서 접근 가능
  • private: 클래스 내부에서만 접근 가능
  • protected: 클래스 내부 또는 파생 클래스 내부에서만 접근 가능

public

public 접근 제어자로 지정한 프로퍼티는 어디에서든지 접근할 수 있다. 앞서 만들었던 클래스와 같이 만약 필드의 접근 제어자를 지정하지 않으면 기본적으로 public 접근 제어자를 갖게 된다.

class Employee {
  // 필드
  name: string; // 자동으로 public
  age: number; // 자동으로 public
  position: string; // 자동으로 public

  // 생성자
  constructor(name: string, age: number, position: string) {
    this.name = name;
    this.age = age;
    this.position = position;
  }

  // 메서드
  work() {
    console.log("일함");
  }
}

const employee = new Employee("정주노", 27, "developer");

// public이기 때문에 마음대로 접근 및 수정할 수 있다.
employee.name = "홍길동";
employee.age = 30;
employee.position = "디자이너";

Private

private로 설정된 프로퍼티는 같은 클래스 내부에서만 접근할 수 있다.

class Employee {
  // 필드
  private name: string;
  public age: number;
  public position: string;

  (...)

  work() {
    console.log(`${this.name}이 일 중`); // 클래스 내에서는 접근 가능
  }
}

const employee = new Employee("정주노", 27, "developer");

employee.name = "홍길동"; // ❌ 오류
employee.age = 30;
employee.position = "디자이너";

위 코드에서는 name 필드를 private로 설정했기 때문에 클래스외부에서는 접근이 불가하다. 따라서 클래스 외부에서 접근하거나 수정할 경우 오류가 발생한다. 하지만 클래스 내부에서는 work 메소드 처럼 자유롭게 접근이 가능하다.

Protected

Protected는 private와 public의 중간으로 클래스 외부에서는 접근이 안되지만 클래스 내부와 파생 클래스에서는 접근이 가능하도록 한다.

class Employee {
  // 필드
  private name: string; // private 접근 제어자 설정
  protected age: number;
  public position: string;

  ...

  // 메서드
  work() {
    console.log(`${this.name}이 일함`); // 여기서는 접근 가능
  }
}

class ExecutiveOfficer extends Employee {
 // 메서드
  func() {
    this.name; // ❌ 오류
    this.age; // ✅ 가능
  }
}

const employee = new Employee("정주노", 27, "developer");

employee.name = "홍길동"; // ❌ 오류
employee.age = 30; // ❌ 오류
employee.position = "디자이너";

위 코드에서 Employee 클래스를 상속받는 파생 클래스 ExecutiveOfficer를 선언 후 Employee 클래스에서 protected 접근 제어자로 설정된 age 프로퍼티에 접근했을 때, 접근이 가능하다는 것을 알 수 있었다. 그러나 마찬가지로 클래스 외부에서는 접근이 불가능하다.

필드 생략하기

접근 제어자는 필드 뿐만 아니라 생성자의 매개변수에도 설정할 수 있는데, 이때 생성자의 매개변수에 접근 제어자를 설정하면 동일한 이름의 필드를 선언할 수 없게 된다. 그 이유는 생성자 매개변수에 접근 제어자가 설정되면 자동으로 필드도 함께 선언되기 때문이다. 따라서 동일한 이름으로 필드를 중복 선언할 수 없다.

class Employee {
  // 필드
  private name: string; // ❌
  protected age: number; // ❌
  public position: string; // ❌

  // 생성자
  constructor(
    private name: string,
    protected age: number,
    public position: string
  ) {
    this.name = name;
    this.age = age;
    this.position = position;
  }

  // 메서드
  work() {
    console.log(`${this.name} 일함`);
  }
}

따라서 다음과 중복된 필드 선언을 모두 제거할 수 있다.

class Employee {
  // 생성자
  constructor(
    private name: string,
    protected age: number,
    public position: string
  ) {
    this.name = name;
    this.age = age;
    this.position = position;
  }

  // 메서드
  work() {
    console.log(`${this.name} 일함`);
  }
}

또한 접근 제어자가 설정된 매개변수들은 this.필드 = 매개변수가 자동으로 수행된다. 따라서 위 코드에 name, age, position은 모두 this 객체의 프로퍼티 값으로 자동 설정되기 때문에 다음과 같이 생성자 내부의 코드를 제거할 수 있다.

// 최종 형태 !!
class Employee {
  // 생성자
  constructor(
    private name: string,
    protected age: number,
    public position: string
  ) {}

  // 메서드
  work() {
    console.log(`${this.name} 일함`);
  }
}

결론적으로 타입스크립트에서 클래스를 사용할 때에는 생성자 매개변수에 접근 제어자까지 설정하여 필드 선언과 동시에 생성자 내부 코드까지 생략하는 것이 훨씬 간결하고 빠르게 코드를 작성할 수 있다.

클래스의 설계도, 인터페이스

타입스크립트의 인터페이스는 클래스의 설계도 역할을 수행할 수 있다. 쉽게 말해 인터페이스를 이용해 클래스에 어떤 필드와 메소드가 존재하는지 정의할 수 있다.

interface CharacterInterface {
  name: string;
  moveSpeed: number;
  move(): void;
}

class Character implements CharacterInterface {
  constructor(
    public name: string,
    public moveSpeed: number,
    private extra: string
  ) {}

  mover(): void {
    console.log(`${this.moveSpeed} 속도로 이동`);
  }
}

위 코드처럼 인터페이스와 implements를 키워드를 통해 클래스를 선언할 경우, 앞으로 해당 클래스가 생성하는 객체는 모두 이 인터페이스 타입을 만족하도록 구현되어야 한다.


출처

위 포스트는 이정환 님의 인프런: 한 입 크기로 잘라먹는 타입스크립트(TypeScript)를 수강한 뒤 복습 차원에서 저의 생각을 정리 및 추가하여 업로드했음을 알립니다.


mdn web docs: Classes

댓글남기기