[TIL] 230726

이세령·2023년 7월 26일
0

TIL

목록 보기
67/118

TypeScript

ts실습을 위한 프로젝트 세팅

  1. 디렉토리 생성(영문)
  2. npm init -y package.json 생성
  3. 빌드 및 실행을 위한 package.json에서 scripts 수정
    "start": "tsc && node ./dist/index.js",
    "build": "tsc --build",
    "clean": "tsc --build --clean"
}, 
  1. 코드를 담을 src 디렉토리 생성

실행할 때

  1. 빌드 필요
    npm run build
  2. 실행
    npm run start

enum vs object literal

  • object literal
const obj = {
  a: [1,2,3],
  b: 'b',
  c: 4
}

→ const나 let으로 key+value로 구성된 객체를 정의

enum에 비해 다른 type을 사용할 수 있다.

유틸리티 타입

Partial

유연하게 타입 속성 선택 가능

interface 객체{
  name: string;
  age: number;
}

const updatePerson = (person: 객체, fields: Partial<객체>): 객체 => {
  return { ...person, ...fields };
};

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

Required

모든 속성 필수

interface 객체{
  name: string;
  age: number;
  address?: string; // 필수 속성 
}

type 객체이름 = Required<객체>;

Readonly

객체를 상수화

interface 객체 {
  host: string;
  readonly port: number; // 인터페이스에서도 readonly 타입 사용 가능해요!
}

Pick <T, K>

특정 속성만 선택

interface 객체 {
  name: string;
  age: number;
  address: string;
}

type SubsetPerson = Pick<객체, "name" | "age">; // name과 age만 사용 

const person: SubsetPerson = { name: "Spartan", age: 30 };

Omit <T, K>

특정 속성 제외하고

interface 객체 {
  name: string;
  age: number;
  address: string;
}

type SubsetPerson = Omit<객체, "address">;

const person: SubsetPerson = { name: "Alice", age: 30 };

별다방 프로그램 만들기

// 유저 인터페이스 정의
interface User {
  id: number;
  name: string;
  role: "admin" | "customer";
}

// 음료 인터페이스 정의
interface Beverage {
  name: string;
  price: number;
}

// 주문 인터페이스 정의
interface Order {
  orderId: number;
  customerId: number;
  customerName: string;
  beverageName: string;
  status: "placed" | "completed" | "picked-up";
}

// 음료 목록
let beverages: Beverage[] = [];

// 주문 목록
let orders: Order[] = [];

// admin 권한 체크 함수
const checkAdmin = (user: User) => {
  return user.role === "admin";
};

// customer 권한 체크 함수
const checkCustomer = (user: User) => {
  return user.role === "customer";
};

// 음료 목록에 등록하는 함수
const addBeverage = (user: User, name: string, price: number): void => {
  if (!checkAdmin(user)) {
    console.log("권한이 없습니다.");
    return;
  }
  const newBeverage: Beverage = { name, price };
  beverages.push(newBeverage);
};

// 음료 삭제 함수
const removeBeverage = (user: User, beverageName: string): void => {
  if (!checkAdmin(user)) {
    console.log("권한이 없습니다.");
    return;
  }
  beverages = beverages.filter((beverage) => beverage.name !== beverageName);
};

// 음료 조회 기능 함수
const getBeverage = (user: User): Beverage[] => {
  if (!user) {
    return [];
  }
  return beverages;
};

// 음료 찾기 함수
const findBeverage = (beverageName: string): Beverage | undefined => {
  return beverages.find((beverage) => beverage.name === beverageName);
};

// 음료 주문 기능
const placeOrder = (user: User, beverageName: string): number => {
  if (!checkCustomer(user)) {
    console.log("손님이 아닙니다.");
    return -1;
  }
  if (!findBeverage(beverageName)) {
    console.log("해당 음료는 목록에 없습니다.");
    return -1;
  }
  const newOrder: Order = {
    orderId: orders.length + 1,
    customerId: user.id,
    customerName: user.name,
    beverageName,
    status: "placed",
  };
  orders.push(newOrder);
  return newOrder.orderId;
};

// 음료 준비 완료 기능
const completeOrder = (user: User, orderId: number): void => {
  if (!checkAdmin(user)) {
    console.log("권한이 없습니다.");
    return;
  }
  // 해당 주문 찾기
  const order = orders.find((order) => order.orderId === orderId);

  // 해당 주문 상태 변경 후 준비 완료 출력
  if (order) {
    order.status = "completed";
    console.log(
      `${order.customerName}님 주문하신 ${order.beverageName}가 준비되었습니다!`
    );
  }
};

// 음료 수령 기능 - 고객
const pickUpOrder = (user: User, orderId: number): void => {
  if (!checkAdmin(user)) {
    console.log("권한이 없습니다.");
    return;
  }
  // 해당 주문 찾기
  const order = orders.find(
    (order) => order.orderId === orderId && order.customerId === user.id
  );

  // 해당 주문 상태 변경 후 준비 완료 출력
  if (order && order.status === "completed") {
    order.status = "picked-up";
    console.log(
      `[어드민 메시지] 고객 ID[${order.customerId}]님이 주문 ID[${orderId}]을 수령했습니다.`
    );
  }
};

클래스

만들기 위한 틀

ex) 유저, 아이템 등

  • 예제

       class Person {
         name: string;
         age: number;
       
         constructor(name: string, age: number) { // 생성자 
           this.name = name;
           this.age = age;
         }
       
         sayHello() {
           console.log(`안녕하세요! 제 이름은 ${this.name}이고, 나이는 ${this.age}살입니다.`);
         }
       }
       
       const person = new Person('Spartan', 30);
       person.sayHello();

    생성자: 클래스 당 1개, 속성을 초기화할 때 주로 사용

  • 접근 제한자
    public - 자유로움

    private - 클래스 내부에서만 접근 가능(주로 private을 사용해서 getter/setter 메서드 활용)

    protected - 클래스 내부 + 상속받은 자식 클래스에서만 접근 가능

상속

상위 클래스에서 물려받아, 하위 클래스를 재정의 한다.
상속할 때 extends
기본 클래스의 생성자 함수를 실행할 때 super() 호출

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과 다를게 하나도 없어요!
}

→ Animal이 부모, Dog나 Cat이 자식 클래스

  • 오버라이딩(overriding)
    상위 클래스의 메서드를 하위에서 재정의

  • 오버로딩(overloading)
    같은 이름의 메서드를 다른 매개변수 타입이나 개수를 다르게 정의

  • 서브타입, 슈퍼타입

    서브타입 : 두 개의 타입 A와 B가 있고 B가 A의 서브타입이면 A가 필요한 곳에는 어디든 B를 안전하게 사용할 수 있다.

    슈퍼타입: 두 개의 타입 A와 B가 있고 B가 A의 슈퍼타입이면 B가 필요한 곳에는 어디든 A를 안전하게 사용할 수 있다.

    Animal은 Dog, Cat의 슈퍼타입. Dog, Cat은 Animal의 서브타입!

  • super()

    자식 클래스가 부모 클래스를 참조하는데 사용

  • upcasting

    서브 → 슈퍼

    let dog: Dog = new Dog('또순이');
    let animal: Animal = dog; // upcasting, TS가 자동으로 변환해준다. 

    서브 타입 객체를 슈퍼타입 객체로 다루면 유연하게 활용이 가능할 때

  • downcasting

    슈퍼 → 서브

    let animal: Animal;
    animal = new Dog('또순이');
    
    let realDog: Dog = animal as Dog;

    서브타입의 메서드를 사용해야 할 때 사용한다.

추상 클래스

상속으로 다른 자식 클래스에서 메서드 각각 구현하도록 만들고 싶을 때, 인스턴스화는 불가능

기본 구현을 제공하고 상속으로 확장할 때 사용한다.
추상 클래스 선언 abstract

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;
  }
}

class Rectangle extends Shape {
  width: number;
  height: number;

  constructor(width: number, height: number) {
    super();
    this.width = width;
    this.height = height;
  }

  getArea(): number { // 사각형의 넓이를 구하는 공식은 가로 X 세로
    return this.width * this.height;
  }
}

인터페이스

TS에서 객체 타입을 정의할 때 사용, 구조만 만듬
상속처럼 인터페이스 모양 조건을 사용하고 싶을 때 implements
ex) class Child implements Person

객체가 가져야 하는 속성과 메서드 정의

특정 구조를 준수하도록 강제할 때 사용한다.

객체 지향 설계 원칙 SOLID

  • SRP 단일 책임 원칙
    클래스는 하나의 책임만 가져야한다.
  • OCP 개방 폐쇄 원칙
    인터페이스 or 상속을 잘 쓰자
  • LSP 리스코프 치환 원칙
    서브타입은 기반이 되는 슈퍼타입을 대체할 수 있어야한다.
    → 논리적으로 엄격하게 관계가 정립되어야 한다.
  • ISP 인터페이스 분리 원칙
    무의미한 메소드의 구현을 막아야한다.
    → 필요한 만큼만 정의, 클래스는 필요한 인터페이스들을 구현
  • DIP 의존성 역전 원칙
    하위보다 상위 수준 모듈(인터페이스)에 의존해야한다.

도서관 프로그램 실습

  • npm init -y 실행 시 주의점
    디렉토리가 영어로 되어있어야 description만 생기지 않는다.
  enum Role {
  LIBRARIAN, // 사서
  MEMBER, // 멤버
}

// 유저 추상 클래스
abstract class User {
  constructor(public name: string, public age: number) {} // 생성자 -> 꼭 필요한 로직들 선언
  abstract getRole(): Role; // 추상클래스 -> 구현 강제
}

// 맴버 클래스
class Member extends User {
  constructor(public name: string, public age: number) {
    super(name, age);
  }
  getRole(): Role {
    return Role.MEMBER;
  }
}

// 사서 클래스
class Librarian extends User {
  constructor(public name: string, public age: number) {
    super(name, age);
  }
  getRole(): Role {
    return Role.LIBRARIAN;
  }
}

// 책 클래스
class Book {
  constructor(
    public title: string,
    public author: string,
    public publishedDate: Date
  ) {}
}

// RentManager 인터페이스
interface RentManager {
  getBooks(): Book[]; // 도서 목록
  addBook(user: User, book: Book): void; // 도서 추가
  removeBook(user: User, book: Book): void; // 도서 폐기
  rentBook(user: Member, book: Book): void; // 사용자가 책을 빌릴 때
  returnBook(user: Member, book: Book): void; // 사용자가 책을 반납할 때
}

// Library 클래스
class Library implements RentManager {
  private books: Book[] = [];

  // 유저의 대여 이력
  private rentedBooks: Map<string, Book> = new Map<string, Book>();

  // getBooks : books를 깊은 복사
  getBooks(): Book[] {
    return JSON.parse(JSON.stringify(this.books));
  }

  // addBook, removeBook 사서만 호출 가능
  addBook(user: User, book: Book): void {
    if (user.getRole() !== Role.LIBRARIAN) {
      console.log("사서가 아닙니다.");
      return;
    }
    this.books.push(book);
  }
  removeBook(user: User, book: Book): void {
    if (user.getRole() !== Role.LIBRARIAN) {
      console.log("사서가 아닙니다.");
      return;
    }
    const index = this.books.indexOf(book); // 특정 문자열의 위치 검색
    if (index !== -1) {
      this.books.splice(index, 1);
    }
  }
  // rentBook 유저만 호출 가능, 다른 책을 대여한 유저는 대여 불가
  rentBook(user: Member, book: Book): void {
    if (user.getRole() !== Role.MEMBER) {
      console.log("유저가 아닙니다.");
      return;
    }
    // 책이 빌려져있는 상태인지 확인
    if (this.rentedBooks.has(user.name)) {
      console.log(`${user.name}님은 이미 다른 책을 대여중입니다.`);
    } else {
      this.rentedBooks.set(user.name, book);
      console.log(`${user.name}님이 [${book.title}] 책을 대여했습니다.`);
    }
  }

  // returnBook 책을 빌린 사람들만 반납가능
  returnBook(user: Member, book: Book): void {
    if (user.getRole() !== Role.MEMBER) {
      console.log("유저만 도서를 반납할 수 있습니다.");
      return;
    }

    if (this.rentedBooks.get(user.name) === book) {
      this.rentedBooks.delete(user.name);
      console.log(`${user.name}님이 [${book.title}] 책을 반납했어요!`);
    } else {
      console.log(`${user.name}님은 [${book.title}] 책을 빌린적이 없어요!`);
    }
  }
}

// TEST
function main() {
  const myLibrary = new Library();
  const librarian = new Librarian("르탄이", 30);
  const member1 = new Member("예비개발자", 30);
  const member2 = new Member("독서광", 28);

  const book = new Book("TypeScript 문법 종합반", "강창민", new Date());
  const book2 = new Book("금쪽이 훈육하기", "오은영", new Date());
  const book3 = new Book("요식업은 이렇게!", "백종원", new Date());

  myLibrary.addBook(librarian, book);
  myLibrary.addBook(librarian, book2);
  myLibrary.addBook(librarian, book3);
  const books = myLibrary.getBooks();
  console.log("대여할 수 있는 도서 목록:", books);

  myLibrary.rentBook(member1, book);
  myLibrary.rentBook(member2, book2);

  myLibrary.returnBook(member1, book);
  myLibrary.returnBook(member2, book2);
}

main();

이외 새로 알게된 정보

  • 많이 사용하는 기술 살펴보기
    state of js -> JS에서 사용하고 있는 것을 볼 수 있다.

  • React에서 컴포넌트 return type을 React.FC 이런식으로도 사용하지만 요즘에는 const AddTodo = ({ todos, setTodos }: Props):JSX.Element => {}도 사용하고 JSX.Element를 생략하기도 한다.

  • 상태 관리 라이브러리인 redux를 많이 사용하지만, zustand, jotai가 최신 라이브러리이기 때문에 사용하기 편하다.
    zustand + immer를 사용해서 프로젝트를 진행해보는 것도 좋다.

TypeScript 실습을 진행해보면서 옛날에 학교에서 배웠던 객체지향 프로그래밍의 기억이 새록새록 났다..
좋은 코드를 짜기 위해서 명확하게 정의하는 것이 좋은데 객체 지향 설계 원칙 SOLID를 통해 다시한번 짚고 넘어갈 수 있었다.
결국은 JS를 활용하고 잘 활용하기 위해서는 JS를 계속 공부해야한다.
문서 읽는 것에 익숙해질겸 문서로 공부해야겠다.

profile
https://github.com/Hediar?tab=repositories

0개의 댓글