[Node.js_4기] TIL : TypeScript_2 (24/03/05)

2024. 3. 5. 21:45공부/내배캠 TIL

목차

 

1. 학습 내용

2. 내용 정리

3. 예제

4. 생각 정리

 

1. 학습 내용 

 

enum, object literal의 차이점과 언제 사용하면 좋을지 파악합니다.

Partial<T>, Required<T>, Readonly<T>, Pick<T, K>, Omit<T, K>를 이해합니다. (주요 유틸리티 타입)
클래스, 상속, 추상 클래스 및 인터페이스에 대해 이해합니다.
S.O.L.I.D 원칙을 기반으로 좋은 객체 지향 설계 방법을 이해합니다.

 

2. 내용 정리 

 

w4_01. enum과 object literal 비교

1) enum : 열거형 데이터 타입(상수의 그룹화)

  • 코드 가독성을 높이고 명확한 상수 값 정의
  • 컴파일시 자동으로 숫자값이 매핑되기 때문에 값을 할당하지 않아도 된다.(특정 값은 직접 할당)

2) object literal

  • 키+값 쌍의 객체 정의 방식
const obj = {
  a: [1,2,3],
  b: 'b',
  c: 4
}

 

  • enum에 비해 가지는 장점
    • enum의 멤버는 상수이기 때문에 num, str 타입만 대입 가능
    • object literal은 어떤 타입의 값도 대입할 수 있다.(유연한 구조)
    • 사용 전 값이 할당되어야 하기 때문에 런타임 에러 방지

3) 언제 무엇을 써야 하나?

  • enum : 상수 값(num,str)을 그룹화해야 할 때, 멤버의 값이 변하면 안될때
  • 멤버의 값이나 데이터 타입을 변경해도 될때, 복잡한 구조나 다양한 테이터 타입을 사용해야 할 때

w4_02. 유틸리티 타입

1) Partial <T>

  • Partial<T> ?
    • 타입 T의 모든 속성을 선택적으로 만든다. 이를 통해, 기존 타입의 일부 속성만 가지는 객체 생성
interface Person {
  name: string;
  age: number;
}

const updatePerson = (person: Person, fields: Partial<Person>): Person => {
  return { ...person, ...fields };
};
// fields가 가질 수 있는 경우의 수
// name, age, name&age / 기존에 없는 속성을 넣을 순 없다.

const person: Person = { name: "Spartan", age: 30 };
const changedPerson = updatePerson(person, { age: 31 });

2) Required <T>

  • Required<T> ?
    • T의 모든 속성을 필수적으로 가진다. 즉, 모든 속성이 제공되어야 하는 객체 생성
interface Person {
  name: string;
  age: number;
  address?: string; // 속성 명 뒤에 붙는 ?는 선택적 속성을 의미한다.
}

type RequiredPerson = Required<Person>;
// address까지 필수적으로 받게 된다.

3) Readonly <T>

  • Readonly  <T> ?
    • T의 모든 속성을 읽기 전용으로 만든다. 완전 불변 객체 생성
interface DatabaseConfig {
  host: string;
  readonly port: number; // 인터페이스에서도 readonly 타입 사용 가능해요!
} // 완전 불변 객체는 아님.

const mutableConfig: DatabaseConfig = {
  host: "localhost",
  port: 3306,
}; // 얼마든지 변할 수 있다.

const immutableConfig: Readonly<DatabaseConfig> = {
  host: "localhost",
  port: 3306,
}; // DatabaseConfig를 불변객체로 받아온다!

mutableConfig.host = "somewhere";
immutableConfig.host = "somewhere"; // 오류!

4) Pick<T,K>

  • Pick <T,K> ?
    • T에서 K 속성들만 선택하여 새로운 타입 생성. 일부 속성만 포함하는 객체 생성
interface Person {
  name: string;
  age: number;
  address: string;
}

type SubsetPerson = Pick<Person, "name" | "age">;

const person: SubsetPerson = { name: "Spartan", age: 30 }; // address를 포함하지 않는 객체

5) Omit<T, K>

  • Omit <T,K> ?
    • T에서 K 속성들만 제외하고 새로운 타입 생성. Pick과 반대 동작
interface Person {
  name: string;
  age: number;
  address: string;
}

type SubsetPerson = Omit<Person, "address">;

const person: SubsetPerson = { name: "Alice", age: 30 }; // address만 없는 객체

6) 이 외의 다양한 유틸리티 타입

TypeScript: Documentation - Utility Types (typescriptlang.org)

 

w5_01. 클래스

1) 클래스와 객체

  • 클래스 : 붕어빵 틀
    • 속성 : 객체의 성질
    • 메서드 : 객체의 성질을 수정, 객체에서 제공하는 기능들을 사용
  • 객체 : 붕어빵
    • 클래스 기반으로 생성됨. 클래스의 인스턴스(instance)라고도 함.

2) 클래스 정의

js와 다르지 않다. class 키워드로 정의하고, new class로 생성

  • 생성자 : 클래스의 인스턴스를 생성하고 초기화. (객체 속성 초기화 + 생성시의 초기화 로직)
  • 생성자는 클래스 내에서 constructor로 정의됨.
  • 클래스 내에 단 하나만 존재

3) 클래스 접근 제한자

  • public
    • 클래스 외부에서도 접근 가능. 기본 접근 제한자.
    • 민감하지 않은 객체 정보 열람, 누구나 클래스 내의 기능 사용해야 할 경우
  • private
    • 클래스 내부에서만 접근 가능한 제한자.
    • 속성을 보거나 편집하고 싶다면 getter/setter메서드를 준비
  • protected
    • 클래스 내부 + 상속받은 자식 클래스에서만 접근

w5_02. 상속

1) 상속

  • 클래스간 관계를 정의하는 개념.
  • 상속을 통해 기존 클래스의 속성과 메서드를 물려받은 새로운 클래스 정의(같은 코드 반복 불필요)
  • extends 키워드를 사용하여 구현
class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  makeSound() {
    console.log('동물 소리~');
  }
}

class Dog extends Animal {
  age: number;
  constructor(name: string) {
    super(name);
    this.age = 5;
  }
  makeSound() {
    console.log('멍멍!'); // 부모의 makeSound 동작과 달라요!
  }
  eat() { // Dog 클래스만의 새로운 함수 정의
    console.log('강아지가 사료를 먹습니다.');
  }
}

class Cat extends Animal { // Animal과 다를게 하나도 없어요!
}

const dog = new Dog('누렁이');
dog.makeSound(); // 출력: 멍멍!

const cat = new Cat('야옹이');
cat.makeSound(); // 출력: 동물 소리~

 

  • Animal : 부모 클래스, Dog : 자식 클래스
  • super 키워드 : 자식 클래스가 부모 클래스를 참조할 때 사용 - 자식 클래스에서 생성자를 정의할 때, 부모 클래스의 생성자를 호출할 때 사

2) 서브타입, 슈퍼타입

  • 서브타입 : 타입 A와 타입 B가 있을 때, B가 A의 서브타입이라면 : A대신 B를 사용할 수 있다.
  • 슈퍼타입 : 타입 A와 타입 B가 있을 때, B가 A의 슈퍼타입이라면 : B가 필요한 곳에 어디든 A를 사용할 수 있다.
  • any타입은 모든 타입의 슈퍼타입
    • Animal은 Dog, Cat의 슈퍼타입이고 Dog, Cat은 Animal의 서브타입이다.

3) upcasting, downcasting

  • Upcasting : 서브타입을 슈퍼타입으로 변환시키는 것. 이 경우 타입 변환이 암시적으로 이루어진다.(Ts가 알아서 해줌)
    • 필요성 : 서브타입 객체를 슈퍼타입 객체로 다루면 유연한 활용이 가능하다.
      • ex ) Dog, Cat, Lion등의 다양한 동물을 인자로 받을 수 있는 함수를 만들고 싶을때, 
        • Animal 타입을 받는다! (o) / union으로 새로운 타입을 만들어 받는다! (x)
  • Donwcasting : 슈퍼타입을 서브타입으로 변환시키는 것. as키워드로 명시적으로 타입변환 필요(쓸 일이 좀 적다)

w5_03. 추상 클래스

1) 추상 클래스

  • 인스턴스화 할 수 없는 클래스
  • 상속을 통해 자식 클래스에서 메서드를 각각 구현하기를 강제하는 용도(기본 메서드 정의 가능)
  • 핵심 기능은 전부 자식 클래스에게 위임한다
  • abstact 키워드를 사용하여 정의. 1개 이상의 추상 함수가 있는게 일반적임
abstract class Shape {
  abstract getArea(): number; // 추상 함수 정의!!!
  printArea() {
    console.log(`도형 넓이: ${this.getArea()}`);
  }
}

class Circle extends Shape {
  radius: number;
  constructor(radius: number) {
    super();
    this.radius = radius;
  }
  getArea(): number { // 원의 넓이를 구하는 공식은 파이 X 반지름 X 반지름
    return Math.PI * this.radius * this.radius;
  }
}

const circle = new Circle(5);
circle.printArea();

 

w5_04. 인터페이스

1) 인터페이스

  • 객체의 타입을 정의하는 용도로 객체가 가져야 하는 속성과 메서드를 정의
  • 인터페이스를 구현한 객체는 인터페이스를 반드시 준수(규약)

2) 클래스와의 차이

  • 추상 클래스 : 
    • 클래스 기본 구현 제공. 단일 상속만 지원. 추상 클래스를 상속받은 자식 클래스는 반드시 추상 함수를 구현
    • 기본 구현을 제공하고 상속을 통해 확장하고 싶을 때 사용
  • 인터페이스 : 
    • 구조만 정의(기본 구현 제공x), 다중 상속 지원(클래스:인터페이스 = 1:N), 인터페이스를 구현하는 클래스는 정의된 모든 메서드를 전부 구현해야 한다.
    • 객체가 특정 구조를 준수하게 강제하고 싶을 때 사용

w5_05. 객체 지향 설계 원칙 S.O.L.I.D

1) S (단일 책임 원칙 SRP) : 

  • 클래스 하나당 하나의 책임
  • 가장 기본적이고 중요한 원칙

2) O (개방 폐쇄 원칙 OCP) :

  • 확장에는 열려 있고, 수정에는 닫혀 있어야 함
  • 인터페이스, 혹은 상속을 통하여 기존 코드를 변경하지 않고 기능을 확장할 수 있어야 한다

3) L (리스코프 치환 원칙 LSP) : 

  • 서브타입은 슈퍼타입을 대체할 수 있어야 함(부모/자식 클래스간에 일관성이 있어야 한다)
  • 논리적으로 엄격하게 관계가 정립되어야 한다

4) I (인터페이스 분리 원칙 ISP) : 

  • 인터페이스는 클라이언트가 필요로 하는 메서드만 제공해야 함
  • 범용 인터페이스 하나 보다 여러개의 특화된 인터페이스가 더 좋다

5) D (의존성 역전 원칙 DIP) : 

  • 추상화에 의존해야 하며 구체화에 의존하면 안됨(abstact)
  • 웹 서버 프레임워크 내에서 많이 나오는 원칙으로, 하위수준 모듈(구현 클래스)보다 상위 수준 모듈(인터페이스)에 의존해야 한다.

3. 예제 

 

w5_05. 객체 지향 설계 원칙 S.O.L.I.D

1) S (단일 책임 원칙 SRP) : 클래스 하나당 하나의 책임

// Wrong
class UserService {
  constructor(private db: Database) {}

  getUser(id: number): User {
    // 사용자 조회 로직
    return this.db.findUser(id);
  }

  saveUser(user: User): void {
    // 사용자 저장 로직
    this.db.saveUser(user);
  }
  
  // 그런데 갑자기 이메일 발송 로직이 나타났다.
  sendWelcomeEmail(user: User): void { 
    // 단일 책임 원칙 위반
    const emailService = new EmailService();
    emailService.sendWelcomeEmail(user);
  }
}

// Good
class UserService {
  constructor(private db: Database) {}

  getUser(id: number): User {
    // 사용자 조회 로직
    return this.db.findUser(id);
  }

  saveUser(user: User): void {
    // 사용자 저장 로직
    this.db.saveUser(user);
  }
}

class EmailService {
  // 이메일 관련된 기능은 이메일 서비스에서 총괄하는게 맞습니다.
  // 다른 서비스에서 이메일 관련된 기능을 쓴다는 것은 영역을 침범하는 것이에요!
  sendWelcomeEmail(user: User): void {
    // 이메일 전송 로직
    console.log(`Sending welcome email to ${user.email}`);
  }
}

2) O (개방 폐쇄 원칙 OCP) : 확장에는 열려 있고, 수정에는 닫혀 있어야 함

// Wrong 
import * as mysql from 'mysql2';

export class UserRepository {
  save(sql: string, values: string[]) {
    await mysql.query(sql, values);
  }
  
  find(sql: string, values: string[]) {
    return await mysql.query(sql, values);
  }
}
// 확장에 열려있지 못하고, 변경에 닫혀있지 못하다.

// Good + 해당 예시에는 어댑터 패턴이 사용되어 있다고 한다.
// IDatabase-adapter.ts
export interface IDatabaseAdapter {
  save(sql: string, values: string[]): unknown[];
  find(sql: string, values: string[]): unknown[];
}

// MySQLDatabase-adapter.ts
import * as mysql from 'mysql2';

export class MySQLDatabaseAdapter implements IDatabaseAdapter { 
  save(sql: string, values: string[]) {
    mysql.query(sql, values);
  }
  find(sql: string, values: string[]) {
    return mysql.query(sql, values);
  }
}

// user-repository.ts
export class UserRepository {
  constructor(private readonly databaseAdapter: IDatabaseAdapter){}
  
  save(sql: string, values: string[]) {
    await this.databaseAdapter.query(sql, values);
  }
  
  find(sql: string, values: string[]) {
    return this.databaseAdapter.query(sql, values);
  }
}


3) L (리스코프 치환 원칙 LSP) : 서브타입은 슈퍼타입을 대체할 수 있어야 함

 

// Wrong
class Bird {
  fly(): void {
    console.log("펄럭펄럭~");
  }
}

class Penguin extends Bird {
	// 펭귄이 펄럭펄럭 납니까? 이상하지 않습니까, 당신?
}

// Good...?
abstract class Bird {
  abstract move(): void;
}

class FlyingBird extends Bird {
  move() {
    console.log("펄럭펄럭~");
  }
}

class NonFlyingBird extends Bird {
   move() {
    console.log("뚜벅뚜벅!");
  }
}

class Penguin extends NonFlyingBird {} // 그러하다

4) I (인터페이스 분리 원칙 ISP) :

  • 범용 인터페이스 하나 보다 여러개의 특화된 인터페이스가 더 좋다

5)  D (의존성 역전 원칙 DIP) : 추상화에 의존해야 하며 구체화에 의존하면 안됨(abstact)

// Good

interface MyStorage {
  save(data: string): void;
}

class MyLocalStorage implements MyStorage {
  save(data: string): void {
    console.log(`로컬에 저장: ${data}`);
  }
}

class MyCloudStorage implements MyStorage {
  save(data: string): void {
    console.log(`클라우드에 저장: ${data}`);
  }
}

class Database {
  // 상위 수준 모듈인 MyStorage 타입을 의존! 
  // 여기서 MyLocalStorage, MyCloudStorage 같은 하위 수준 모듈에 의존하지 않는게 핵심!
  constructor(private storage: MyStorage) {}

  saveData(data: string): void {
    this.storage.save(data);
  }
}

const myLocalStorage = new MyLocalStorage();
const myCloudStorage = new MyCloudStorage();

const myLocalDatabase = new Database(myLocalStorage);
const myCloudDatabase = new Database(myCloudStorage);

myLocalDatabase.saveData("로컬 데이터");
myCloudDatabase.saveData("클라우드 데이터");

 

참고

SOLID 준수해서 코딩해보기 (feat. Typescript) 👈 (velog.io)

solid하게 SOLID (2) | devlog.akasai

4. 생각 정리 

 

3) 유틸리티 타입

원래 있던 타입 T를 응용하는 타입.

 

4) 타입과 인터페이스의 차이
타입 : 별칭을 사용해 새로운 타입 정의
인터페이스 : 객체 구조 정의, 클래스에서 구현 가능


5) 클래스와 상속
클래스 : 객체의 설계도. 생성자, 메서드, 속성등을 포함
상속 : 클래스간 관계 정의. 부모 클래스의 기능을 자식 클래스가 상속받는다. 


6) 객체지향 설계 원칙S.O.L.I.D

단일 책임 원칙 SRP : 클래스 하나당 하나의 책임
개방 폐쇄 원칙 OCP : 확장에는 열려 있고, 수정에는 닫혀 있어야 함
인터페이스 분리 원칙 ISP : 인터페이스는 클라이언트가 필요로 하는 메서드만 제공해야 함
의존성 역전 원칙 DIP : 추상화에 의존해야 하며 구체화에 의존하면 안됨(abstact)
리스코프 치환 원칙 LSP : 서브타입은 슈퍼타입을 대체할 수 있어야 함

 

이것으로 TypeScript 내용 정리를 끝내겠다.

다음 TIL부터는 Express.js대신 사용하게 되는 Nest.js에 대한 내용과 실습들로 TIL을 작성할 예정이다.