20210511 TypeSciprt 06 class, contructor(optional) & initialize(async), 접근제어자(private, protected), getter&setter, readonly, IndexSignature, static, singleton(싱글톤 패턴), 상속(extends, super), abstract

9 분 소요

TypeScript06

Class

클래스?

  • ojbect를 만드는 blueprint (설계도)
  • 클래스 이전에 object를 만드는 기본적인 방법은 function
  • JS는 es6 부터 class를 사용 가능 (접근 제한 키워드가 부족함)
  • OPP을 위한 초석
  • TypeScript 에서는 클래스도 사용자가 만드는 타입의 하나임 (JS보다 강력한 class 기능)


class 선언과 사용

class 선언&사용 : 기본 방법

  • class 키워드 사용
  • class 이름은 대문자
  • new 키워드를 이용하여 class를 통해 object를 생성할 수 있음
  • 기본 property에 값을 할당하여 class를 만들고 object를 생성하면 기본 property가 초기값으로 설정되어 object가 만들어짐
class Per {
  name: string = "kim";
}
const per = new Per();


class 선언&사용 : 생성자를 이용한 방법

  • constructor 키워드를 사용해서 나중에 property에 값을 무조건 넣게 지정할수 있다. (일단은, class 안에서 값 자체가 할당 안되어 있어도 compile error가 없음)
  • this 키워드를 사용해서 class에 있는 property를 class 내부에서 사용할 수 있음
  • object 생성시 class 안에 constructor를 이용하여 object를 생성하면서 값을 전달할 수 있다.
class Person0 {
  name; // property

  constructor(name: string) {
    this.name = name;
  }
}


contructor & initialize

  • strict 모드 옵션이 켜져있고 constructor가 없는 class의 property는 반드시 초기화를 시켜주어야 함으로 class 내부적으로 값을 할당 해주어야 한다. (초기화를 검사하는 옵션 : strictPropertyInitialization)
  • 만약, 내부적으로 값 할당 안하고 나중에 외부에서 값을 할당해 준다고 하더라도 compile error는 발생함 (class는 불완전하게 느끼고 runtime까지 고려 못함)
    • 예) age는 number이지만 할당 되지 않으면 나중에 값을 불러올때 undefined 가 됨으로
  • 만약 초기값을 할당 안하고 나중에 개발자가 신경써서 한다는 것을 !을 사용해서 표시하고 compile error를 일단은 벗어날수 있다.
class Person1 {
	name: string = 'Jerry';
	// age: number; // error 발생
  age!: number; // ! 사용할땐 주의하자 나중에 값할당 안하면 온전히 개발자 잘못이므로
}

const p1: Person1 = new Person1();
console.log(p1);
console.log(p1.age); // undefined 뜨게 됨 (type은 number라고 뜨지만)
p1.age = 2; // runtime 보완 해도 고려 안됨 (!를 사용 안하면)
console.log(p1.age); // ok (위에서 2로 할당 시켜 줬기 때문에)

초기화 할때 값을 할당하거나 안하고 싶은 경우 (type 안정성 유지)

  • constructor 에서 매개변수에 ?을 붙여 optional로 만든다.
  • 하지만, type은 안정화 시켜줘야 하기 때문에 contructor 안에 if 문을 통해서 undefined 시 해당 type의 기본 값을 할당하게 하여 어쨌거나 해당 type을 만족하도록 한다.
class Person2 {
  name: string = "Tom";
  age: number;

  constructor(age?: number) {
    if (age === undefined) {
      this.age = 1;
    } else {
      this.age = age;
    }
  }
}
const p2: Person2 = new Person2(); // OK
const p2_1: Person2 = new Person2(24); // OK


비동기적으로 초기화 하고 싶은 경우

  • contructor 키워드에는 async를 붙일 수 없음
  • 초기화 할수 있는 메서드를 만들어서 활용해야 함 (async 가능)
  • class의 메서드를 통해서 값을 할당을 통해 초기화 한다고 해도 class는 해당 메서드를 고려하지 못함으로 compile error가 발생함 그래서 여기에서도 property에 !을 사용함
class Person3 {
	name: string = 'asyn_test';
	// age: number; // error
  age!: number;

  async init() {
    this.age = 2
  }
}

const p3: Person3 = new Person3();
await p3.init();


정리

  • 생성자 함수가 없으면, 디폴트 생성자가 불린다.
  • 프로그래머가 만든 생성자가 하나라도 있으면, 디폴트 생성자는 사라진다. (즉, 우선적으로 contructor가 씌워진다.)
  • strict 모드에서는 프로퍼티를 선언하는 곳 또는 생성자에서 값을 할당해야 함
  • 프로퍼티 선언하는 곳 또는 생성자에서 값을 할당하지 않는 경우에는 !를 붙여 위험 표현
  • 클래스의 프로퍼티가 정의 되어 있지만 값을 대입하지 않으면 undefined임
  • 생성자에는 async를 설정할 수 없다.


접근 제어자 (access modifier : public, private, protected)

  • 접근 제어자는 property, method, contructor 모두 붙일 수 있음
  • TS는 아무 설정 없으면 class를 외부에서 모두 접근 가능함 (설정 안하면 default로 public인것임)
  • public을 명시해도 상관 없음
  • private로 해놓으면 밖에서 호출이 불가해짐 (private 키워드가 나오기 전에 암묵적으로 _를 붙여서 밖에서는 사용하지 마세요라고 모양을 통일해주었음)
    • contructor에 private 설정은 new 키워드를 통한 object를 만들수 없게 함
  • 그래서 private해 놓으면 언더바 _ 를 붙여주는 습관이 남아 있음
class Person4 {
	public name: string = '접근제어자'; // 내부 외부 접근
	private _age!: number; // 내부에서만 접근 '_' 붙임

	public constructor(age?: number) {
		if (age === undefined) {
			this._age = 1;
		} else {
			this._age = age;
		}
	}

	public async init() {}
}

const p4: Person4 = new Person4(444);
console.log(p4); // Person4 { name: 접근제어자, _age: 444} 로 다보이기는 함
console.log(p4._age) // error 접근 불가!!!!! private
p4.init(); // 가능


접근제어자로 contructor에서 바로 property까지 작성 시키는 방법 (꿀팁)

  • this 를 사용하고 property를 선언해 줄 필요 없이 constructor 파라미터에 접근제어자를 붙여 전체적으로 코드를 줄여서 사용이 가능함 (property 명과 constructor의 parameter 명이 같게 쓰는 경우)
  • 즉, Person5와 Person5_1는 같은 기능을 함 (private 설정도 가능함, object를 찍으면 볼순 있지만 접근해서 값을 바꾸거나 그것만 가져올 수는 없음)
// property 명과 constructor의 parameter 명이 같게 됨
class Person5 {
	public constructor(public name: string, private _age: number) {}
}

class Person5_1 {
	name: string;
	private _age: number;
	constructor(name: string, age: number) {
		this.name = name;
		this._age = age;
	}
}

const p5: Person5 = new Person5('접근 제어자를 활용한 방법', 5);
console.log(p5); // Person5 { name: '접근 제어자를 활용한 방법', _age: 5 }


getter & setter

  • 값을 가져오고 새로 세팅하기 직전에 무언가를 할수 있고, 해당 값의 가공도 가능함
  • get, set 키워드 사용
  • 내부적으로 활용하는 private property도 접근하게 할수 있음
  • getter와 setter의 짝으로 가지고 있어야 좋음
  • get이 없고 set만 있으면 compile 에러는 없지만 runtime으로 돌려보면 가져오는 경우는 없는 프로퍼티로 생각할 수 있어 undefind로 뜨게 됨
  • set이 없고 get만 있으면 해당 프로퍼티에 값을 할당하는 코드에 compile 에러를 내면서 알려줌 (이 데이터는 읽기만 가능하다라는 상황 연출 가능)
class Person6 {
	public constructor(private _name: string, public age: number) {}

	get name() {
	  // 직전에 할수 있는 무언가
		// console.log('get');
		return this._name;
	}

	set name(n: string) {
    // 직전에 할수 있는 무언가
		// console.log('set');
		this._name = n;
	}
}

const p6: Person6 = new Person6('raccoon', 6);
console.log(p6.name); // get을 하는 함수 : getter (사용하면 get, set하기 직전에 무언가를 할 수 있음 그리고 데이터 가공도 됨)
p6.name = 'changed!'; // set을 하는 함수 : setter
console.log(p6.name); // 다시 get raccoon -> changed!로 표현된것 확인


readonly property

  • readonly 키워드가 들어간 property
  • readonly 사용하면 해당 property는 값을 가져오는 것만 가능하고, 값 설정이 불가하다. (초기화시 값 할당만 가능)
  • contructor 아니면 기본 property 선언시 할당 가능
class Person7 {
	public readonly name: string = 'Tom';
	private readonly country: string; // 초기값도 가능함
	public constructor(private _name: string, private age: number) {
		this.country = 'korea'; // readonly 여도 constructor 안에서만 값 변경이 가능함 즉, readonly면 초기값을 지정하는 곳에서만 값을 할당하고 변경 가능하지 그외에서는 불가함
	}
	hello() {
		// this.country = "China" // 일단 private라서 안에서는 사용 가능하지만 readonly라서 값을 바꾸는 행위는 불가하고 가져는 것만 가능
	}
}

const p7: Person7 = new Person7('Tom', 7);
console.log(p7.name);
p7.name = "raccoon!" // error : 값을 바꾸는 행위를 했기 때문에 compile 에러 발생


Index Signature

  • 값이 동적으로 들어오는 경우에 사용 (딱히 값이 몇개가 들어올지 모를때, type만 만족시키는 property를 몇개든지 만들수 있음)
  • optional property 느낌이라서 최초값을 적어줄 필요는 없다. 있을 수도 있고 없을 수도 있기 때문에
  • index string으로 해놓으면 그냥 해당 조건의 모든 property에 type을 지정하게 되어 영향을 주게 됨
  • 추론할 수 없는 type이 지정되어 있으면 class 내부에서 property에 값을 넣어준다고 type이 추론되어 지정되진 않음 -> type을 명시해줘야 함
class Students {
	[index: string]: 'male' | 'female';

	mark: 'male' = 'male'; // mark에 값을 지정한다고 해서 male이라는 type이 되는 것은 아니기 때문에 error가 뜨기 때문에 명시적으로 type을 적어주어야 함
	// mark 라는 property는 항상 male이라는 type에 male값을 가지고 있다
}

const a = new Students();
a.mark = 'male';
a.jade = 'male';

console.log(a); // Students { mark: 'male', jade: 'male' }

const b = new Students();
b.chloe = 'female';
b.alex = 'male';
b.anna = 'female';

console.log(b); // Students { chloe: 'famale', alex: 'male', anna: 'famale' }


Static property & methods

  • static을 사용하면 object 상관없이 공통적으로 사용하고 싶은 메서드나, property를 사용 할 수 있게 됨
  • static으로 지정된 method 그리고 property는 object를 생성해서 사용하는게 아니라 선언된 class 명에서 시작하여 사용함 class명.static property명 or method명
  • private 에서 static 사용된 property라도 class 내부에서 자유롭게 사용이 가능함
  • static property의 값을 변경하는 코드를 일반 메서드에 담으면 모든 object에서 static 값을 변경시킬 수 있고 해당 값은 모든 object에서 볼때도 다 반영되어 있다.
  • static property를 일반 메서드에 담아 return 등을 시키면 모든 object에서 해당 값에 접근이 가능하다.
  • 즉, 다른 object에서 static property를 변경하면 다른 object에도 반영이 된다는 것 -> 공유 된다.
  • static method에서는 this를 통한 변수 사용은 못함 하지만, 일반 method에서는 static property 사용 가능
class Apple {
	public static CITY = 'seoul';
	private static COUNTRY = 'Korea';
	private name = 'kim';
	hi() {
		console.log(this.name);
	}
  static thanks() {
    console.log('감사합니다!')
  }
	public hello() {
		console.log('안녕하세요!', Apple.COUNTRY);
    // static property class 내부에서 자유롭게 사용가능
	}
	public change() {
		Apple.COUNTRY = 'USA';
	}
}

const p10 = new Apple();

// p10.thanks(); // error :  static이 붙여지면 해당 함수를 object에서는 메서드로 생각 안함

Apple.thanks(); // static method 클래스명에서 바로 호출하여 사용이 가능함
Apple.CITY; // static property

p10.hi(); // static method에서는 this를 통한 변수 사용은 못함 하지만
//일반 method에서는 static property 사용 가능

const p11 = new Apple();
p11.hello(); // 안녕하세요! Korea
p10.change();
p11.hello(); // 안녕하세요! USA


싱글톤 패턴 (Singleton pattern)

  • 단일객체 패턴
  • 어플리케이션이 실핻되는 중간에 class로부터 다른 하나의 object만 생성을 해서 사용하는 패턴
  • 단일 오브젝트를 공유하는 패턴
  • 생성자를 통해 생성되는 object를 하나만 만들어서 다른 변수에서는 전에 만들어둔 object를 불러와 재사용하는 것
  • new라는 키워드를 통해서 object를 생성하는 것을 막아야함 -> new를 직접 호출 할수 없는 상황이 됨 -> 다른 매개체를 만들어서 사용 할수 있게함
class ClassName {
	private static instance: ClassName | null = null; // 매개체로 instance static property
  // 초기에는 object가 존재하지 않으므로 null union으로 만듦

  public static getInstance(): ClassName {
    // new 생성자 대신 해당 object를 만들거나 가져오는 기능의 getInstance static 메서드

    if (ClassName.instance === null) {
		// ClassName으로 부터 만든 Object가 없으면, 만들어서 return 하는 로직 만들기
			ClassName.instance = new ClassName();
		}

		// ClassName으로 부터 만든 Object가 있으면 그것을 return
		return ClassName.instance;
	}

	private constructor() {}
}

const s1 = ClassName.getInstance();
const s2 = ClassName.getInstance();


console.log(s1 === s2); // true


상속 (inheritance)

  • extends 키워드를 활용해서 자식은 상속을 받음
  • protected 접근제어자 키워드 : 외부에서는 사용 불가하지만 상속받은 자식은 접근 가능
  • 상속 받은 자식을 통해서 property, method, contructor 사용시 자식이 아무것도 없으면 부모를 찾아서 해당 기능을 받아 사용함
  • overriding : 같은 이름의 property, method 또는 contructor가 겹치는 경우 자식이 우선되어 덮어씌워져서 기능을 함, 심지어 접근제어자도 overriding이 됨
  • super키워드는 부모의 contructor에 접근하여 세팅하거나 가져올 수 있음
  • 부모의 값을 쓰던 안쓰던 자식에서 contructor를 사용하면 항상 super을 안에 써서 부모 contructor에 값을 할당 시켜서 type 안정성을 지켜줘야 함
class Parent {
	constructor(protected _name: string, private _age: number) {}
	public print(): void {
		console.log(`이름은 ${this._name} 이고, 나이는 ${this._age} 입니다.`);
	}
	protected printName(): void {
		console.log(this._name, this._age);
	}
}

const super1 = new Parent('Mom', 30);
super1.print(); // 이름은 Tom 이고, 나이는 30 입니다.

class Child extends Parent {
	public _name = 'son'; // 접근 제어자 까지도 overriding이 됨
	public gender = 'male'; // 부모에 없는 property 생성
	constructor(age: number) {
		// 자식 자신만의 생성자를 만드는 경우 부모의 생성자도 setting을 해줘야함 공유하고 있어서 그래서 super 키워드를 사용함
		super('son', age);
		this.printName(); // 부모 메서드 사용
	}
}

const sub1 = new Child(7); // son 7
sub1.print(); // 이름은 Son 이고, 나이는 7 입니다.
sub1._name; // son

  • 자식의 constructor 안에서 this. 무언가를 호출하려는 경우 부모의 property가 setting 되어 있지 않다면 super 키워드를 통해서 먼저 세팅 해줘야함 (순서 주의)
  • 부모의 protected 함수를 통해서 자식이 그것을 받아 사용하고 부모의 protected에 부모 자신만의 private property를 사용하게 되면
  • 자식은 부모의 private property 까지 접근 가능하게 됨,
  • 그래서 서로 너무 얽히지 않도록 주의 해야함, 오염되지 않도록


abstract 키워드

  • 완전하지 않은 객체를 만들 수 있고, 상속과 같은 것으로 보완하게 안내하여 완성시켜 사용할 수 있다.
  • abstract 키워드를 사용하려면 abastract class여야 해서 class 키워드 앞에 abstract를 붙임
  • 함수 구현부 바디 없이 부모 작성 가능 -> 자식이 사용할 땐 이를 보충해서 사용할 수 있음
abstract class AbstractPerson {
	protected _name: string = 'Tom';

	abstract setName(name: string): void; // 함수지만 abstract 키워드를 붙임으로서 함수 구현부(바디)를 작성하지 않음
}

// 기능이 완전하지 않아서 new 키워드로 object를 생성할 수 없음

class DetailPerson extends AbstractPerson {
	setName(name: string): void {
		this._name = name;
	}
}

const ab1 = new DetailPerson();

ab1.setName('Jerry');