클래스는 동일한 모양의 객체를 더 쉽게 생성하도록 도와주는 문법이다.
빵 틀
에 비유할 수 있다.예를 들어 학생을 객체로 표현한다면 다음과 같이 할 수 있다.
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명의 학생들을 이렇게 일일이 만드는 것은 굉장히 수고로운 일이다.
이럴때 바로 자바스크립트의 클래스
라는 문법을 사용하면 된다.
class Student {
}
필드
란 이 클래스가 생성할 객체가 갖는 프로퍼티를 의미한다.
학생 객체는 name, age, grade 프로퍼티를 갖기 때문에 다음과 같이 필드를 선언한다.
class Student {
// 필드
name;
age;
grade;
}
생성자는 특수한 메서드로 실질적으로 객체를 생성하는 함수이다.
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를 활용해 객체 프로퍼티의 값을 활용하는 것 또한 가능하다.
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개의 접근 제어자를 사용할 수 있다.
실습과 함께 3가지 접근 제어자를 살펴보자.
첫번째 접근 제어자는 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으로 설정하면 클래스 내부
에서만 이 필드에 접근할 수 있게 된다.
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 메서드 처럼 접근이 자유롭게 가능하다.
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
키워드와 함께 사용하면 이제부터 이 클래스가 생성하는 객체는 모두 이 인터페이스 타입을 만족하도록 클래스를 구현해야 한다.