본문 바로가기

내일배움캠프 TIL

내일 배움 캠프 23_07_26 TIL

TypeScript에서의 제네릭 타입에 대해서 알게 되었는고 또한 객체지향 프로그래밍에 대해서도 알게되었고

객체 지향 설계 원칙 - S.O.L.I.D 에 대해서도 알게되었다 내용은 아래에 서술하고 

이미 존재하는 React 프로젝트에서 TypeScript로 전환하는 과정을 맨 아래에 서술해두겠다.

 

[partial<T>]

interface Person {
  name: string;
  age: number;
}

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

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

이런식으로 Person 인터페이스 안에 있는
속성중에서만 사용한다면 field 인자를 유연하게 구성할 수 있다

[Required<T>]

interface Person {
  name: string;
  age: number;
  address?: string; // 속성 명 뒤에 붙는 ?가 뭘까요
}

이 코드처럼 address는 있어도되고 없어도 된다는 의미이고
그 외 name,age는 반드시 존재해야하는 속성임을 의미한다.


[Readonly<T>]

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

const mutableConfig: DatabaseConfig = {
  host: "localhost",
  port: 3306,
};

const immutableConfig: Readonly<DatabaseConfig> = {
  host: "localhost",
  port: 3306,
};

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

모든 속성을 읽기전용으로 만들어서 불변의 객체로 취급할 수 있다.

[Pick<T,K>]

interface Person {
  name: string;
  age: number;
  address: string;
}

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

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

Pick<T,K>는
type SubsetPerson = Pick<Person, "name" | "age">;
이 부분에서 원하는 속성만 선택해서 할당을 받을 수 있게 만들어준다.

[Omit<T,K>]

interface Person {
  name: string;
  age: number;
  address: string;
}

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

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

Omit<T,K>는
원하는 속성만 제외해서 상속받는 코드이다
type SubsetPerson = Omit<Person, "address">;
여기서는 address를 제외하고 name,age만 할당받겠다는 코드이다.

https://www.typescriptlang.org/ko/docs/handbook/utility-types.html
TypeScript 의 유틸리티들을 정리해놓은 사이트


객체지향 프로그램

클래스
클래스란 객체 지향 프로그램의 핵심 구성 요소 중 하나이며
객체를 만들기 위한 틀이다
클래스에서는 같은 종류의 객체들이 공통으로 가지는 속성과 메서드를 정의한다
**** 여기서 속성이란 객체의 성질을 결정하는 것이다.
**** 여기서 메서드란 객체의 성질을 변화시키거나 객체에서 제공하는 기능들을 사용하는 창구이다.

클래스 정의하기
class 키워드를 사용하고
클래스의 속성과 메서드를 정의하고 new 키워드를 사용해 객체를 생성할 수 있다.

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();

예를들어 이렇게 코드를 작성해서 정의할 수 있다

클래스는 접근 제안자로 접근을 제한할 수 있는데
public - 클래스 외부에서도 접근이 가능하다
private - 클래스 내부에서만 접근이 가능하다
-private 으로 선언된 클래스의 속성을 보거나 편집하고 싶다면 getter/setter 메서드를 준비해놓아야한다.
protected - 클래스 내부와 해당 클래스를 상속받은 자식 클래스에서만 접근이 가능하다.
이렇게 세개가 있고 아무것도 지정을 안한다면 public으로 선언된다


class Person {
  private name: string;
  private age: number;

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

  public sayHello() {
    console.log(`안녕하세요! 제 이름은 ${this.name}이고, 나이는 ${this.age}살입니다.`);
  }
}

이런식으로 속성앞에 접근제한자를 지정해준다


상속을 통해 기존 클래스의 속성과 메서드를 물려받아서 새로운 클래스를 정의할 수 있고

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(); // 출력: 동물 소리~

이런식으로 extends 로 상속받아서 사용할 수 있다

서브타입과 슈퍼타입이라는 개념도 있는데
예를 들면 any 는 모든 것의 슈퍼타입이고
Animal을 예로들면 Dog,Cat의 슈퍼타입은 Animal이며 Dog,Cat은 Animal의 서브타입이다

upcasting과 downcasting은 슈퍼타입, 서브타입으로 변환할 수 있는 타입 변화 기술로

업캐스팅의 예 ------------------------------------

let dog: Dog = new Dog('또순이');
let animal: Animal = dog; // upcasting 발동! 
animal.eat(); // 에러. 슈퍼타입(Animal)으로 변환이 되어 eat 메서드를 호출할 수 없어요! 

이런식으로 타입을 변화시킨다


다운캐스팅의 예 -------------------------------------------
let animal: Animal;
animal = new Dog('또순이');

let realDog: Dog = animal as Dog;
realDog.eat(); // 서브타입(Dog)로 변환이 되었기 때문에 eat 메서드를 호출할 수 있죠!

이런식으로 슈퍼타입 -> 서브타입으로 변환하는것을 downcasting 이라고 한다
이 경우에는 as 키워드로 명시적으로 타입 변환을 해줘야 합니다.
-------------------------------------------------------------------

추상 클래스란
클래스와는 다르게 인스턴스화를 할 수 없는 클래스이고

- 추상 클래스의 목적은 **상속을 통해 자식 클래스에서 메서드를 제각각 구현**하도록 강제를 하는 용도입니다!
- 물론, 추상 클래스도 최소한의 기본 메서드는 정의를 할 수 있습니다.
- 하지만, 골자는 **핵심 기능의 구현은 전부 자식 클래스에게 위임을 하는 것**이에요!

추상 클래스의 사용 방법으로는
abstract 키워드를 사용하여 정의하며
추상 클래스는 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;
  }
}

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

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

const rectangle = new Rectangle(4, 6);
rectangle.printArea();


- Shape 클래스의 `abstract getArea(): number;` 를 주목해주세요. 이 코드가 바로 추상 함수입니다.
- 이 추상 클래스를 상속 받은 자식 클래스들은 반드시 getArea 함수를 구현해야 합니다!


-------------------------------------------------------------------------------------------------

인터페이스란

- 인터페이스는 TypeScript에서 객체의 타입을 정의하는데 사용됩니다!
- 인터페이스는 **객체가 가져야 하는 속성과 메서드를 정의합니다.**
- 인터페이스를 구현한 객체는 인터페이스를 반드시 준수해야해요! **규약**과 같아서 어길 수가 없습니다.
- 이렇게 인터페이스를 사용하면 코드의 안정성을 높이고 유지 보수성을 향상시킬 수 있습니다.


### ☑️ 구현부 제공 여부

- 추상 클래스
    - 클래스의 기본 구현을 제공합니다.
- 인터페이스
    - 객체의 구조만을 정의하고 기본 구현을 제공하지 않습니다.

### ☑️ 상속 메커니즘

- 추상 클래스
    - 단일 상속만 지원합니다.
- 인터페이스
    - 다중 상속을 지원합니다.
    - 즉, 하나의 클래스는 여러 인터페이스를 구현할 수 있어요!

### ☑️ 구현 메커니즘

- 추상 클래스
    - 추상 클래스를 상속받은 자식 클래스는 반드시 추상 함수를 구현해야 합니다.
- 인터페이스
    - 인터페이스를 구현하는 클래스는 인터페이스에 정의된 모든 메서드를 전부 구현해야 합니다.

### ☑️ 언제 쓰면 좋을까요?

- **기본 구현을 제공하고 상속을 통해 확장**하는데 초점을 맞추고 싶다면 → `추상 클래스`
- 객체가 완벽하게 **특정 구조를 준수하도록 강제**하고 싶다면 → `인터페이스`

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

- S(SRP. 단일 책임 원칙) → ⭐⭐⭐⭐⭐ 매우 중요 ⭐⭐⭐⭐⭐
    
    ### ☑️ SRP 원칙
    
    - 클래스는 **하나의 책임**만 가져야 한다는 매우 기본적인 원칙입니다.
    - 특히, 5개의 설계 원칙 중 **가장 기본적이고 중요한 원칙**이에요! **절대 잊어버리시면 안됩니다!**
    - 예를 들어서, 유저 서비스라는 클래스가 있다고 가정을 해보죠.
    - 이 유저 서비스에서는 유저 관련된 액션만 해야되고 다른 액션을 해서는 안됩니다.

잘못된 사례-----------------------------------

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

올바른 사례 --------------------------------------

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

-------------------------------------------------------


- O(OCP. 개방 폐쇄 원칙) → 인터페이스 혹은 상속을 잘 쓰자!
    - 클래스는 확장에 대해서는 열려 있어야 하고 수정에 대해서는 닫혀 있어야 한다는 원칙입니다.
    - 클래스의 **기존 코드를 변경하지 않고도 기능을 확장**할 수 있어야 합니다.
    - 즉, `인터페이스`나 `상속`을 통해서 이를 해결할 수가 있어요!
        - 부모 클래스의 기존 코드 변경을 하지 않고 기능을 확장하는데 아무런 문제가 없으니까요!

---------------------------------------------------------

### LSP 원칙

- **서브타입은 기반이 되는 슈퍼타입을 대체**할 수 있어야 한다는 원칙입니다.
- 다시 말해, 자식 클래스는 부모 클래스의 기능을 수정하지 않고도 부모 클래스와 호환되어야 합니다.
- 다시 말해서, **논리적으로 엄격하게 관계가 정립**이 되어야 한다는 얘기입니다.

잘못된 사례

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

class Penguin extends Bird {
  // 으잉? 펭귄이 날 수 있나요? 펭귄이 펄럭펄럭~ 한다는 것은 명백한 위반이죠.
}


올바른 사례

abstract class Bird {
  abstract move(): void;
}

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

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

class Penguin extends NonFlyingBird {} // 이제 위배되는 것은 아무것도 없네요!
--------------------------------------------------------------------------------

### DIP 원칙

- DIP는 Java의 Spring 프레임워크나 Node.js의 Nest.js 프레임워크와 같이 **웹 서버 프레임워크 내에서 많이 나오는 원칙**이에요.
- 이 원칙은 하위 수준 모듈(구현 클래스)보다 **상위 수준 모듈(인터페이스)에 의존**을 해야한다는 의미입니다!
- 예를 들어서, 데이터베이스라는 클래스가 있다고 가정을 해보겠습니다.
- 데이터베이스의 원천은 로컬 스토리지가 될 수도 있고 클라우드 스토리지가 될 수도 있어요.
- 이 때, 데이터베이스의 원천을 로컬 스토리지 타입 혹은 클라우드 스토리지 타입으로 한정하는 것이 아닙니다.
- 그보다 상위 수준인 스토리지 타입으로 한정을 하는 것이 맞아요!

사용예시

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("클라우드 데이터");

-----------------------------------------------------------------------------------------------------------

타입 스크립트에 관한 추가 학습 자료
- 공식 매뉴얼
    - https://www.typescriptlang.org/docs/
- 공식 튜토리얼
    - https://www.typescriptlang.org/docs/handbook/intro.html
- 온라인 책
    - https://radlohead.gitbook.io/typescript-deep-dive/getting-started



React에 TypeScript 적용

yarn add typescript @types/node @types/react @types/react-dom @types/jest --dev

으로 타입 스크립트 적용에 필요한 라이브러리들을 package.json에 의존성을 추가

이렇게 되면 package.json 에 의존성 라이브러리들이 추가가되고

 

그 후 

tsc --init 명령어를 터미널에서 실행하면

tsconifg.json 파일을 생성이되는데

 

그 안에 

{
  "compileOnSave": true,
  "compilerOptions": {
    "target": "es2016",
    "jsx": "react-jsx",
    "module": "esnext",
    "sourceMap": true,
    "removeComments": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "noFallthroughCasesInSwitch": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true
  },
  "include": ["./src/"],
  "exclude": ["node_modules/*"]
}

 

이렇게 정의를 해주고 기본적인 설정은 끝이나고 사실

tsconfig.json 에는 아주 많은 설정들이 존재하기에

https://www.typescriptlang.org/tsconfig 이 공식문서에서 필요한 부분을 확인하고 추가해주면 될 것이다.

 

그  다음 index.js 파일을 index.tsx 파일로 확장자를 변경해주고

 

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);

내용은 이렇게 수정하면 기본적인 세팅은 완료가 된다

 

또한 원래는 uuid를 사용하여 id를 세팅해 주었었는데

 

원래 방식대로 사용하려하면 모듈을 못찾는 에러가 발생을하여 react-uuid 라이브러리를 

yarn add react-uuid 를 설치해준후

 

src폴더안에 @types라는 폴더를 생성해준 후

이 안에서 모듈을 정의해 주어야 하는데 파일명은 uuid를 사용했기에

uuid.d.ts 파일명으로 정의해주었고

그 안에 코드는 

declare module "react-uuid" {
  const uuid: () => string;
  export default uuid;
}

이렇게 정의를 해 주었다

'내일배움캠프 TIL' 카테고리의 다른 글

내일 배움 캠프 23_07_31 TIL  (0) 2023.07.31
내일 배움 캠프 23_07_27 TIL  (0) 2023.07.27
내일 배움 캠프 23_07_25 TIL  (0) 2023.07.25
내일 배움 캠프 23_06_14 TIL  (0) 2023.06.14
내일 배움 캠프 23_06_13 TIL  (0) 2023.06.13