1. 클래스 기초
1. 클래스 & 인스턴스란?
1) 클래스: 객채의 청사진으로, 객체의 형태 및 포함되어야 할 메소드를 정의한다.
- 관례상 클래스를 정의할 때 맨 첫글자를 대문자로 작성한다.
- 생성자(constructor): 인스턴스를 생성할 때 초기화하기 위한 메소드이다.
-> 인스턴스를 생성할 때 받은 인자는 생성자를 통해 클래스의 필드에 저장되며 이는 결국 프로퍼터(클래스 내에서 사용하는 변수)로 변환된다.
2) 인스턴스: 클래스의 구조에 따라 생성된 객체
class Department {
name: string;
constructor(n: string) {
this.name = n;
}
}
const accounting = new Department("Accounting");
console.log(accounting);
2. 클래스의 this
- 클래스를 사용하면 this를 자주 볼 수 있는데 여기서 this는 개별 인스턴스를 의미한다. 따라서 this를 쓰지 않고 그냥 변수처럼 불러올 경우 클래스 외부에 있는 변수를 찾아올 수 있다.
class Department {
name: string;
constructor(n: string) {
this.name = n;
}
describe() {
console.log(`Department: ${name}`);
console.log(`Department: ${this.name}`);
}
}
const accounting = new Department("Accounting");
accounting.describe();
3. public & private
- private은 클래스 내부에서 지정한 메소드를 제외하고는 외부에서 해당 프로퍼티의 값을 변화시키지 못하게 한다. private을 쓰는 이유는 큰 프로젝트일수록 통일되고 일관적인 코드를 쓰는 것이 좋기도 하고, 메소드 안에 validation이라든지 여러 조건을 체크하고 값을 변화시키는 과정이 있을 수도 있는데 외부에서 이를 무시하고 값을 추가하지 못하게 막을 수 있기 때문이다.
- public은 private의 반대 개념으로, 클래스 내부와 외부를 가리지 않고 접근할 수 있는 프로퍼티다. 클래스 프로퍼티는 디폴트가 public으로 설정되어 있다.
- 프로퍼티 앞에 private/public이라고 쓰면 된다.
- JS에서는 public과 private의 개념이 꽤나 최근까지도 없었기 때문에 컴파일 버전에 따라 private으로 설정하고 외부에서 접근했어도 문제로 인식하지 않는 경우가 있다.
class Department {
public name: string;
private employees: string[] = [];
constructor(n: string) {
this.name = n;
}
addEmployee(employee: string) {
this.employees.push(employee);
}
printEmployeeInfo() {
console.log(this.employees.length);
console.log(this.employees);
}
}
const accounting = new Department("Accounting");
accounting.addEmployee("Max");
accounting.addEmployee("Anna");
accounting.printEmployeeInfo();
4. 약식 초기화
- 일일이 프로퍼티값과 초기값을 매치해줄 필요없이 아래와 같이 약식으로 작성 가능하다.
class Department {
private employees: string[] = [];
constructor(private id: string, public name: string) {
}
}
5. Readonly
- readonly는 초기화 이후 값이 변해서는 안되는 프로퍼티를 의미한다.
class Department {
private employees: string[] = [];
constructor(private readonly id: string, public name: string) {}
addEmployee(employee: string) {
this.id = "D0002";
this.employees.push(employee);
}
}
2. 상속
1. 상속이란?
- 클래스를 정의할 때 이미 정의된 클래스의 내용을 그대로 활용할 수 있는데 이를 상속이라고 한다. 클래스 명 옆에 extends 상속받을 클래스명을 입력하면 된다.
- 자식 클래스에서 생성자를 작성할 때 부모 클래스로부터 물려받은 것은 super를 통해 호출한다. 생성자 내에서 super는 하나만 사용되고, this 키워드가 사용되기 이전에 작성되어야 한다.
- 자식 클래스는 한 클래스로부터만 상속받을 수 있다.
class Department {
private employees: string[] = [];
constructor(private readonly id: string, public name: string) {}
describe() {
console.log(`Department ID: ${this.id}`);
console.log(`Department NAME: ${this.name}`);
}
addEmployee(employee: string) {
this.employees.push(employee);
}
printEmployeeInfo() {
console.log(this.employees.length);
console.log(this.employees);
}
}
const accounting = new Department("D0001", "Accounting");
class ITDepartment extends Department {
admins: string[];
constructor(id: string, admins: string[]) {
super(id, "IT");
this.admins = admins;
}
}
const it = new ITDepartment("D0002", ["Kim"]);
console.log(it);
2. 메소드 재정의와 protected
- 자식 클래스는 부모 클래스에서 정의한 메소드를 재정의할 수 있다. 만약 메소드 재정의 시 부모 클래스에서 상속받은 private 프로퍼티를 수정해야 하는 경우가 있다면 protected를 활용할 수 있다.
- protected는 private과 유사하게 클래스 외부에서 속성에 함부로 접근할 수 없게 한다는 공통점이 있지만, private은 딱 그 클래스를 사용한 인스턴스에서만 프로퍼티에 접근할 수 있는 반면 protected는 그 클래스를 상속받은 자식 클래스에서도 해당 프로퍼티에 접근할 수 있게 한다는 차이가 있다.
class Department {
protected employees: string[] = [];
constructor(private readonly id: string, public name: string) {}
addEmployee(employee: string) {
this.employees.push(employee);
}
}
const accounting = new Department("D0001", "Accounting");
class ITDepartment extends Department {
admins: string[];
constructor(id: string, admins: string[]) {
super(id, "IT");
this.admins = admins;
}
addEmployee(employee: string) {
if(employee === "Max"){
return;
}
this.employees.push(employee);
}
}
const it = new ITDepartment("D0002", ["Kim"]);
console.log(it);
3. getter & setter
1) 획득자(getter):
- getter는 프로퍼티가 함수나 메소드를 사용하여 값을 가져올 수 있게 하는 기능이다. 생긴 건 함수지만 본질은 프로퍼티라는 말이다.
- getter는 궁극적으로 값이기 때문에 반드시 무언가를 return해야 하며, 이를 호출할 때는 메소드가 아닌 프로퍼티에 접근하듯 중괄호{} 없이 호출해야 한다.
2) 설정자(setter):
- getter는 말 그대로 받아오는 역할만을 하기 때문에 값을 할당할 수는 없다. 따라서 값을 할당하려면 setter를 사용해야 한다.
const thisYear: number = new Date().getFullYear();
class Student {
constructor(private name: string, private bornYear: number = thisYear) {}
get koreanAge() {
return thisYear - this.bornYear + 1;
}
set koreanAge(val) {
if (thisYear - val + 1 >= 20) {
throw new Error("어른은 빠지세요~~!");
}
this.bornYear = val;
}
}
let kim = new Student("Kim");
kim.koreanAge = 2006;
console.log(kim.koreanAge);
4. 정적 프로퍼티 & 메소드(Static property & method)
- 정적 프로퍼티 & 메소드는 클래스를 인스턴스화시키지 않고 사용할 수 있는 프로퍼티와 메소드를 의미한다. 인스턴스에서는 호출할 수 없다.
- 정적 프로퍼티 & 메소드는 static 설정을 해두지 않은 부분들에서는 접근할 수 없다. 또한 클래스 내에서 this로 호출할 수 없고, 호출하고자 한다면 클래스명을 대신 써야 한다.
- static을 공부하다 보면 한가지 의문점이 드는데 인스턴스화 안하고 그냥 쓸 수 있게 만들 거면 전역변수와 함수로 만들어버리면 되지 왜 static을 쓰냐는 점이다. 우리는 이미 static을 배우기 전부터 이를 수도 없이 써왔는데 너무나도 친숙한 Math 클래스가 바로 그것이다. Math.floor(), Math.ceil(), Math.pow() 등 Math 클래스 아래 많은 static 메소드들이 예쁘게 묶여 보기좋게(?) 사용되어져 왔다. 이처럼 인스턴스 프로퍼티 & 메소드 없이 static으로 구성된 클래스를 유틸리티 클래스라고 하고, static 이런 경우에 자주 사용된다고 한다.
class Student {
static thisYear: number = new Date().getFullYear();
constructor(
private name: string,
private bornYear: number = Student.thisYear
) {}
get koreanAge() {
return Student.thisYear - this.bornYear + 1;
}
set koreanAge(val) {
if (Student.thisYear - val + 1 >= 20) {
throw new Error("어른은 빠지세요~~!");
}
this.bornYear = val;
}
}
let kim = new Student("Kim");
kim.koreanAge = 2006;
console.log(kim.koreanAge);
5. 추상 클래스 & 메소드(Abstract class & method)
- 추상 메소드는 클래스 내에 정의된 메소드의 구조 정도만 정의해두고 정확한 내용 구현은 자식 클래스에게 일임하는 메소드를 의미한다.
- 추상 메소드를 하나라도 포함한 클래스를 추상 클래스라고 한다. 추상 클래스는 그 자체로 인스턴스를 만들 수는 없다. 오로지 상속을 위해 존재하는 클래스이다.
abstract class Student {
static thisYear: number = new Date().getFullYear();
constructor(
protected name: string,
protected bornYear: number = Student.thisYear
) {}
abstract describe(this: Student): void;
}
class SmartStudent extends Student {
private scores: number[] = [];
constructor(name: string, bornYear: number) {
super(name, bornYear);
}
addScore(score: number) {
this.scores.push(score);
}
describe() {
console.log(this.scores);
}
}
let kim = new SmartStudent("Kim", 2006);
kim.addScore(97);
kim.addScore(100);
kim.addScore(87);
kim.describe();
6. Singleton & private constructor
- 싱글튼 패턴은 특정 클래스의 인스턴스가 하나만 생성될 수 있도록 제한한다. 이 패턴은 정적 메소드 & 프로퍼티를 사용할 수 없다.
- private consturctor는 클래스 밖에서 인스턴스를 생성할 수 없게 한다. 싱글튼 패턴을 구현할 때 사용하지만 클래스를 정의하고 클래스 밖에서 new Instance()를 하지 못하게 하는 것은 싱글은커녕 제로(?)로 만들어버리는 것이므로 매우 이상하다. 이때 static 프로퍼티를 사용하면 이를 해결할 수 있다. static instance: 클래스 자기자신; 으로 설정해버리면 인스턴스 없이도 불러낼 수 있게 된다.
class Department {
constructor(
private readonly id: string,
public name: string,
protected employees: string[]
) {}
}
class ITDepartment extends Department {
admins: string[] = [];
private static instance: ITDepartment;
private constructor(id: string, admins: string[], employees: string[]) {
super(id, "IT", employees);
this.admins = admins;
}
static getInstance() {
if (ITDepartment.instance) {
return this.instance;
}
this.instance = new ITDepartment("D0002", ["Kim"], ["Kim", "Lee", "Park"]);
return this.instance;
}
}
const it = ITDepartment.getInstance();
console.log(it);
3. 인터페이스
1. 인터페이스란?
- 객체의 구조를 설명만 한다. 즉, 클래스는 인스턴스화를 통해 거푸집으로 찍어내듯이 객체를 만들어낼 수 있지만 인터페이스는 틀만 정해주기 때문에 초기값을 지정해주거나(에러 발생) 구체적인 내용을 할당하진 못한다.
- 인터페이스 내에도 메소드도 넣을 수 있지만 역시 메소드의 구체적인 내용을 넣는 것이 아니라 구조만 짠다.
- 인터페이스는 JS에는 없고 TS에만 있는 기능이다. 따라서 컴파일을 한다고 해도 JS 파일엔 아무런 변화도 일어나지 않는 순수한 개발 전용 기능이다.
- 클래스와 마찬가지로 맨 앞글자를 대문자로 쓰는 것이 관례이다.
interface Person {
name: string;
age: number;
greet(phrase: string): void;
}
let user1: Person;
user1 = {
name: "Max",
age: 30,
greet(phrase: string) {
console.log(phrase + ' ' + this.name);
}
};
user1.greet("Hello,");
2. 인터페이스를 사용하는 이유(클래스와 인터페이스)
- interface는 type과 기능적으로 별 차이 없어 보이지만 기술적으로 약간의 차이점을 보인다. 인터페이스는 객체의 구조만을 설명하기 위해 쓰고, 확장이 가능하며, TS의 고급타입 중 일부를 적용할 수 없다.
참고: 타입 vs 인터페이스 비교
- 인터페이스는 주로 서로 다른 클래스 간에 기능을 공유하기 위해 사용되며, 상속과는 다르게 클래스에 다수의 인터페이스를 적용할 수 있다.
- 추상 클래스와도 비슷하게 느껴지지만 상속을 통해서만 구현할 수 있는 추상클래스와 여러 클래스들이 공유할 수 있는 인터페이스는 탄생 목적(?)이 다르게 느껴진다.
- 인터페이스를 적용해 직접 객체를 만들 때는 인터페이스의 룰을 엄격하게 지켜야하지만(인터페이스에서 지정한 내용만 넣어야 하며 마음대로 뺄 수 없음) 클래스를 통해 인터페이스를 적용할 경우 인터페이스에서 지정한 내용 이외에도 내용을 추가할 수 있다.
interface Greetable {
name: string;
greet(phrase: string): void;
}
class Person implements Greetable {
name: string;
age = 30;
constructor(n:string) {
this.name = n;
}
greet(phrase: string) {
console.log(phrase + ' ' + this.name);
}
}
let user1: Greetable;
user1 = new Person("Max");
console.log(user1);
3. 인터페이스의 readonly
- 인터페이스 프로퍼티에도 readonly 속성을 추가할 수 있다.(public, private은 X)
cf) type도 readonly 속성 사용 가능하다.
- 인터페이스에서 readonly 설정된 프로퍼티는 클래스에 적용될 때 별다른 설정이 없더라도 변경 불가능하다.
interface Greetable {
readonly name: string;
greet(phrase: string): void;
}
class Person implements Greetable {
name: string;
age = 30;
constructor(n: string) {
this.name = n;
}
greet(phrase: string) {
console.log(phrase + " " + this.name);
}
}
let user1: Greetable;
user1 = new Person("Max");
3. 인터페이스 확장하기
- 인터페이스는 extends를 통해 확장이 가능하다.
interface Person {
name: string;
age: number;
}
interface Greetable extends Person {
greet(): void;
}
class GreetWithWho implements Greetable {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet() {
if (this.age < 10) {
console.log("안녕, " + this.name);
} else {
console.log("안녕하세요, " + this.name);
}
}
}
let user1: Greetable;
user1 = new GreetWithWho("Max", 30);
user1.greet();
let user2: Greetable;
user2 = new GreetWithWho("Anna", 5);
user2.greet();
4. 인터페이스로 함수구조 정하기
type AddFnType = (a: number, b: number) => number;
let addType: AddFnType;
addType = (n1: number, n2: number) => {
return n1 + n2;
};
console.log(addType(3, 5));
interface AddFnInterface {
(a: number, b: number): number;
}
let addInterface: AddFnInterface;
addInterface = (n1: number, n2: number) => {
return n1 + n2;
};
console.log(addInterface(7, 4));
5. 선택적 매개변수와 프로퍼티(Optional parameter & property)
- 선택적 매개변수와 프로퍼티는 없어도 문제가 안되는 매개변수와 프로퍼티를 의미하고, 물음표(?)를 통해 지정한다.
- 선택적 프로퍼티는 인터페이스는 물론 클래스에서도 적용 가능하다. 클래스에서 프로퍼티를 옵셔널로 설정하면 constructor도 그에 맞게 조건부로 바꿔야 하며, 클래스에 적용된 인터페이스에도 옵셔널을 적용해야 한다.
interface Greetable {
name?: string;
nickname?: string;
age: number;
greet(): void;
}
class GreetWithWho implements Greetable {
name?: string;
age: number;
constructor(age: number, name?: string) {
if (name) {
this.name = name;
}
this.age = age;
}
greet() {
console.log("안녕하세요, " + this.name);
}
}
let user1: Greetable;
user1 = new GreetWithWho(30);