💡 JavaScript의 클래스
클래스는 동일한 모양의 객체를 더 쉽게 생성하도록 도와주는 문법입니다.
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: "B+",
age: 21,
study() {
console.log("자바스크립트 공부중!");
},
introduce() {
console.log("Hello!");
},
};
만약 한명의 학생이 더 필요하다면 다음과 같이 새로운 변수를 만들어주면 됩니다. 이때 studentA와 studentB에 저장된 객체는 동일한 프로퍼티를 갖는데 값만 다를 뿐입니다. 이는 동일한 모양의 객체라고 볼 수 있습니다.
이렇게 동일한 모양의 객체를 여러개 생성하게 되면 중복 코드가 발생하게 된 것입니다. 학생이 현재는 2명 뿐인 코드지만 훨씬 더 많아지면 코드가 어마어마하게 길어질 것입니다. 이때 사용하는 것이 자바스크립트의 클래스라는 문법입니다.
• 클래스 선언하기
클래스는 앞서 이야기 했듯 객체를 생성하는 틀입니다. 그리고 클래스 이름은 앞글자를 대문자로 쓰는 파스칼 표기법을 사용합니다.
class Student {
// 필드
name;
grade;
age;
// 생성자
constructor(name, grade, age) {
this.name = name;
this.grade = grade;
this.age = age;
}
}
클래스를 선언했다면 다음으로는 필드를 선언해야 합니다. 필드는 클래스가 생성할 객체가 갖는 프로퍼티를 의미합니다. 필드를 선언했다면 다음으로는 생성자를 선언합니다. 생성자는 특수한 메서드로 실질적으로 객체를 생성하는 함수입니다. 생성자에서는 매개변수로 프로퍼티 값을 받아 this.프로퍼티의 값으로 할당합니다. 이때 this는 객체이며 현재 만들고 있는 객체를 의미합니다. 따라서 이 생성자 메서드는 현재 만들고 있는 객체의 프로퍼티 값을 매개변수로 전달받은 값으로 설정하는 역할을 합니다.
const studentA = new Student("수현", "B-", 24);
생성자를 만들어 주었다면 이제 클래스를 호출하여 객체를 생성할 수 있습니다. 클래스를 이용하여 새로운 객체를 생성할 때에는 new 클래스명 형태로 클래스의 생성자 함수를 호출합니다. 이때 인수로는 각각의 프로퍼티명을 전달합니다. 그럼 생성자가 호출되어 this.name에는 매개변수 name에 저장된 "수현"을, this.grade에는 매개변수 grade에 저장된 "B-"를, this.age에는 매개변수 age에 저장된 24를 저장합니다.
console.log(studentA);
// → { name: "수현", grade: "B-", age: 24 }
실제로 tsx를 이용하여 해당 객체를 콘솔에 출력하여 보면 이렇게 나타납니다.
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);
console.log(studentB);
studentB.study();
studentB.introduce();
다음으로는 클래스가 생성할 객체의 메서드도 설정해주었습니다.
• this 활용하기
class Student {
(...)
introduce() {
console.log(`안녕하세요 ${this.name}입니다!`);
}
}
let studentB = new Student("수현", "A+", 27);
studentB.introduce(); // 안녕하세요 수현입니다!
this는 현재 만들고 있는 객체를 의미한다고 하였습니다. 따라서 메서드에 다음과 같이 this를 활용하여 객체 프로퍼티의 값을 활용할 수도 있습니다.
• 상속 (extends)
class StudentDeveloper extends Student {}
Student 클래스를 기반으로 추가적인 필드와 메서드를 갖는 클래스인 StudentDeveloper를 상속을 통해 선언해주었습니다. StudentDeveloper클래스는 Student클래스를 확장(상속)하기 때문에 Student 클래스에 정의된 모든 필드와 메서드를 자동으로 갖게 됩니다.
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라는 메서드입니다. super를 호출하고 인수로 name, grade, age를 전달하면 슈퍼클래스(확장 대상 클래스)의 생섵자를 호출합니다. 따라서 이 메서드는 this.name, this.grade, this.age의 값을 설정하게 됩니다.
💡 TypeScript의 클래스
타입스크립트에서는 클래스의 필들르 선언할 때 타입 주석으로 타입을 함께 정의해주어야 합니다. 그렇지 않으면 함수 매개변수와 동일하게 암시적 any 타입으로 추론되는데 엄격한 타입 검사 모드가 설정되어 있을 경우에는 오류가 발생하게 됩니다.
class Employee {
// 필드
name: string = "";
age: number = 0;
position: string = "";
// 메서드
work() {
console.log("일하는중!");
}
}
추가적으로, 생성자에서 각 필드의 값을 초기화하지 않을 경우 초기값도 필드에서 함께 명시해주어야 합니다.
class Employee {
// 필드
name: string;
age: number;
position?: string;
// 생성자
constructor(name: string, age: number, position: string) {
this.name = name;
this.age = age;
this.position = position;
}
// 메서드
work() {
console.log("일하는중!");
}
}
만약 생성자 함수에서 필드의 값들을 잘 초기화해 준다면 필드 선언 시 초기값은 생략해도 됩니다. 또한 이 클래스가 생성하는 객체의 특정 프로퍼티를 선택적 프로퍼티로 만들고 싶다면 다음과 같이 필드명 뒤에 물음표를 붙여주면 됩니다.
• 클래스는 타입입니다.
class Employee {
(...)
}
const employeeC: Employee = {
name: "",
age: 0,
position: "",
work() {},
};
타입스크립트의 클래스는 타입으로도 사용할 수 있습니다. 클래스를 타입으로 사용하면 해당 클래스가 생성하는 객체의 타입과 동일한 타입이 됩니다. 변수 employeeC의 타입을 Employee 클래스로 정의했습니다. 따라서 이 변수는 name, age, position 프로퍼티와 work 메서드를 갖는 객체 타입으로 정의됩니다.
• 상속
class ExecutiveOfficer extends Employee {
// 필드
officeNumber: number;
// 생성자
constructor(name: string, age: number, position: string, officeNumber: number) {
super(name, age, position);
this.officeNumber = officeNumber;
}
}
클래스의 상속을 이용할 때 파생 클래스(확장하는 클래스)에서 생성자를 정의하였다면 반드시 super 메서드를 호출하여 슈퍼 클래스(확장되는 클래스)의 생성자를 호출해야 하며, 호출 위치는 생성자의 최상단에 위치해야합니다.
💡 접근 제어자
• 접근 제어자의 기능
접근 제어자(Access Modifier)는 타입스크립트에서만 제공되는 기능으로 클래스의 특정 필드나 메서드를 접근할 수 있는 범위를 설정하는 기능입니다. 타입스크립트에서는 다음과 같은 3개의 접근 제어자를 사용할 수 있습니다.
⓵ public : 모든 범위에서 접근 가능
⓶ private : 클래스 내부에서만 접근 가능
⓷ protected : 클래스 내부 또는 파생 클래스 내부에서만 접근 가능
• 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 = "shyunu";
employee.age = 30;
employee.position = "frontend Developer";
첫번째 접근 제어자는 public입니다. public은 어디서든지 이 프로퍼티에 접근할 수 있음을 의미합니다. 위와 같이 필드의 접근 제어자를 지정하지 않으면 기본적으로 public 접근 제어자를 가지게 됩니다.
class Employee {
// 필드
public name: string;
public age: number;
public position: string;
...
}
하지만 이와 같이 직접 public 접근 제어자를 명시하는 것도 가능합니다.
• 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 = "shyunu"; // ❌
employee.age = 30;
employee.position = "frontend Developer";
특정 필드나 메서드의 접근 제어자를 private으로 설정하면 클래스 내부에서만 이 필드에 접근할 수 있게 됩니다. name 필드를 private으로 설정하였기 때문에 클래스 외부에서는 접근이 불가능합니다. 그러나 클래스 내부에서는 work와 같은 메서드처럼 접근이 자유롭습니다.
• protected
class Employee {
// 필드
private name: string; // private 접근 제어자 설정
protected age: number;
public position: string;
(...)
// 메서드
work() {
console.log(`${this.name} 일함`);
}
}
class ExecutiveOfficer extends Employee {
// 메서드
func() {
this.name; // ❌ private인 변수는 파생클래스에서조차 접근할 수 없다.
this.age; // 만약 파생클래스에서의 접근은 허용하고자 한다면 protected 접근제어자를 붙여주면 된다.
}
}
const employee = new Employee("수현", 27, "devloper");
employee.name = "shyunu"; // ❌
employee.age = 30; // ❌
employee.position = "frontend Developer";
protected 접근 제어자는 private과 public의 중간으로 클래스 외부에서는 접근이 안되지만 클래스 내부와 파생 클래스에서는 접근이 가능하도록 설정하는 접근 제어자입니다. 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 같은 접근 제어자가 설정되면 자동으로 필드도 함께 선언되기 때문입니다. 따라서 동일한 이름으로 필드를 중복 선언할 수 없게 되며 중복된 필드 선언 모두 제거하여야 합니다.
constructor(private name: string, protected age: number, public position: string) {
// this.name = name;
// this.age = age;
// this.position = position;
}
또한, 접근 제어자가 설정된 매개변수들은 this.필드 = 매개변수 가 자동으로 수행됩니다. 따라서 위 코드의 name, age, position은 모두 this 객체의 프로퍼티 값으로 자동 설정되기 때문에 다음과 같이 생성자 내부의 코드를 제거해도 됩니다.
class Employee {
// 생성자
constructor(
private name: string,
protected age: number,
public position: string
) {}
// 메서드
work() {
console.log(`${this.name}는 현재 근무중입니다.`);
}
}
그러므로 타입스크립트에서는 클래스를 사용할 때 보통 생성자 매개변수에 접근 제어자를 설정하여 필드 선언과 생성자 내부 코드를 생략하는 것이 훨씬 간결하고 빠르게 코드를 작성할 수 있다는 장점이 있습니다.
💡 인터페이스와 클래스
타입스크립트의 인터페이스는 클래스의 설계도 역할을 할 수 있습니다. 인터페이스를 이용해 클래스에 어떤 필드들이 존재하고, 어떤 메서드가 존재하는지 정의할 수 있습니다.
interface CharacterInterface {
// interface는 무조건 public 필드만 정의할 수 있다.
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 키워드와 함께 클래스에서 사용하면 이제부터 이 클래스가 생성하는 객체는 모두 이 인터페이스 타입을 만족하도록 클래스를 구현해야 합니다.
출처) 한 입 크기로 잘라먹는 타입스크립트(TypeScript)_이정환
'📍 프로그래밍 언어 > TypeScript' 카테고리의 다른 글
[ TypeScript ] 제네릭으로 확장하기: 인터페이스, 클래스, 프로미스 (0) | 2024.12.22 |
---|---|
[ TypeScript ] 제네릭 이해하기: 타입 변수와 메서드 타입 정의(map, forEach) (1) | 2024.12.22 |
[ TypeScript ] 인터페이스의 활용: 기본 개념부터 확장과 선언 합치기 (0) | 2024.12.21 |
[ TypeScript ] 함수 오버로딩과 커스텀 타입가드로 복잡한 타입 관리하기 (0) | 2024.12.19 |
[ TypeScript ] 함수 타입: 정의와 표현식, 호출 시그니처, 타입 호환성 (1) | 2024.12.19 |