[6] 클래스

Doozuu·2023년 10월 3일
0

TypeScript

목록 보기
10/13

📌 자바스크립트의 클래스

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

  • 베이킹을 예시로 들자면 같은 모양의 빵을 찍어내기 위한 빵 틀에 비유할 수 있다.

예를 들어 학생을 객체로 표현한다면 다음과 같이 할 수 있다.

let studentA = {
  name: "이정환",
  grade: "A+",
  age: 27,
  study() {
    console.log("열심히 공부 함");
  },
  introduce() {
    console.log("안녕하세요!");
  },
};

만약 이때 한명의 학생이 더 필요하다면 다음과 같이 새로운 변수를 만들어야 한다.

let studentA = {
  name: "이정환",
  grade: "A+",
  age: 27,
  study() {
    console.log("열심히 공부 함");
  },
  introduce() {
    console.log("안녕하세요!");
  },
};

let studentB = {
  name: "홍길동",
  grade: "A+",
  age: 27,
  study() {
    console.log("열심히 공부 함");
  },
  introduce() {
    console.log("안녕하세요!");
  },
};

이때 studentA와 studentB에 저장된 객체는 동일한 프로퍼티를 갖는다.
이렇게 동일한 모양의 객체를 여러개 생성해야 하면 어쩔 수 없이 중복 코드가 발생하게 된다.

2명이 아닌 100명, 1000명의 학생들을 이렇게 일일이 만드는 것은 굉장히 수고로운 일이다.

이럴때 바로 자바스크립트의 클래스 라는 문법을 사용하면 된다.


클래스 선언하기

  1. 클래스 선언은 다음과 같이 이루어진다.
class Student {
    
}
  1. 클래스를 선언했다면 다음으로는 필드를 선언해야 한다.

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

class Student {
  // 필드
  name;
  age;
  grade;
}
  1. 필드를 선언했다면 다음으로는 생성자를 선언한다.

생성자는 특수한 메서드로 실질적으로 객체를 생성하는 함수이다.

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

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

생성자에서는 매개변수로 프로퍼티 값을 받아 this.프로퍼티의 값으로 할당한다.
이때 this는 객체이며 현재 만들고 있는 객체를 의미한다.

따라서 이 생성자 메서드는 현재 만들고 있는 객체의 name, grade, age 프로퍼티의 값을 매개변수로 전달받은 값으로 설정하는 역할을 한다.

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

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

클래스를 이용해 새로운 객체를 생성할 때에는 new 클래스이름 형태로 클래스의 생성자 함수를 호출한다.
이때 인수로는 각각 name, grade, age를 전달한다.

그럼 결과적으로 다음과 같이 생긴 객체가 생성된다.

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

클래스가 생성할 객체의 메서드도 설정해 줄 수 있다.

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

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

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

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

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

studentB.study(); // 열심히 공부 함
studentB.introduce(); // 안녕하세요!

this 활용하기

앞서 클래스 내부에서 this는 현재 만들고 있는 객체를 의미한다고 했다.
따라서 메서드에 다음과 같이 this를 활용해 객체 프로퍼티의 값을 활용하는 것 또한 가능하다.

class Student {
  (...)

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

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

studentB.introduce(); // 안녕하세요 이정환 입니다!

상속

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

class StudentDeveloper extends Student {
}

이렇게 되면 StudentDeveloper 클래스는 Student 클래스에 정의된 모든 필드와 메서드를 자동으로 갖게 된다.

StudentDeveloper 클래스만의 새로운 필드나 메서드도 다음과 같이 정의할 수 있다.

이때 StudentDeveloper 클래스에서 Student 클래스의 생성자를 함께 호출해줘야 한다.
그렇지 않으면 생성되는 객체의 name, grade, age 값이 제대로 설정되지 않는다.
따라서 다음과 같이 super 라는 메서드를 호출한다.

class StudentDeveloper extends Student {
  // 필드
  favoriteSkill;

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

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

super를 호출하고 인수로 name, grade, age를 전달하면 슈퍼 클래스(확장 대상 클래스)의 생성자를 호출한다.
따라서 this.name, this.grade, this.age의 값을 설정하게 된다.



📌 타입스크립트의 클래스

타입스크립트에서는 클래스의 필드를 선언할 때 타입 주석으로 타입을 함께 정의해주어야 한다.

그렇지 않으면 암시적 any 타입으로 추론되는데 엄격한 타입 검사 모드(strict 옵션이 true로 설정되어 있을 경우)일 때에는 오류가 발생하게 된다.

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

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

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

만약 다음과 같이 생성자 함수에서 필드의 값들을 잘 초기화 해 준다면 필드 선언시의 초기값은 생략해도 된다.

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("일함");
  }
}

클래스는 타입이다.

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

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

class Employee {
  (...)
}

const employeeC: Employee = {
  name: "",
  age: 0,
  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 : 클래스 내부 또는 파생 클래스 내부에서만 접근 가능

실습과 함께 3가지 접근 제어자를 살펴보자.

public

첫번째 접근 제어자는 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, "devloper");

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

다음과 같이 public 접근 제어자를 직접 명시하는것도 가능하다.

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

  ...
}

const employee = new Employee("이정환", 27, "devloper");

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

Private

private은 제한된, 사적인 이라는 뜻으로 특정 필드나 메서드의 접근 제어자를 private으로 설정하면 클래스 내부에서만 이 필드에 접근할 수 있게 된다.

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

  ...

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

const employee = new Employee("이정환", 27, "devloper");

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

name 필드를 private으로 설정했으므로 클래스 외부에서는 접근이 불가하다. 그러나 클래스 내부에서는 work 메서드 처럼 접근이 자유롭게 가능하다.

Protected

proteced 접근제어자는 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, "devloper");

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

Employee 클래스를 확장(상속)하는 파생 클래스 ExecutiveOfficer를 선언한 다음 메서드 func를 만들었다.
이 메서드에서는 name과 age에 접근하는데 이때 name은 private으로 접근이 불가하지만 age는 protected이므로 파생 클래스에서는 접근이 가능하다.
그러나 클래스 외부에서는 접근이 불가하다.

필드 생략하기

접근 제어자는 다음과 같이 생성자의 매개변수에도 설정할 수 있다.

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} 일함`);
  }
}

그러나 생성자에 접근 제어자를 설정하면 동일한 이름의 필드를 선언하지 못하게 된다.

그 이유는 생성자 매개변수에 name, age, position 처럼 접근 제어자가 설정되면 자동으로 필드도 함께 선언되기 때문이다.
따라서 동일한 이름으로 필드를 중복 선언할 수 없게 된다.

따라서 다음과 같이 중복된 필드 선언을 모두 제거해 주어야 한다.

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
  ) {}

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

인터페이스 CharacterInterface는 name, moveSpeed 프로퍼티와 move 메서드를 갖는 객체 타입을 정의한다.

그런데 이 인터페이스를 클래스에서 implements 키워드와 함께 사용하면 이제부터 이 클래스가 생성하는 객체는 모두 이 인터페이스 타입을 만족하도록 클래스를 구현해야 한다.

profile
모든게 새롭고 재밌는 프론트엔드 새싹

0개의 댓글