2024. 3. 5. 21:45ㆍ공부/내배캠 TIL
목차
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)
- ex ) Dog, Cat, Lion등의 다양한 동물을 인자로 받을 수 있는 함수를 만들고 싶을때,
- 필요성 : 서브타입 객체를 슈퍼타입 객체로 다루면 유연한 활용이 가능하다.
- 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을 작성할 예정이다.
'공부 > 내배캠 TIL' 카테고리의 다른 글
[Node.js_4기] TIL : Nest.js 2_인증과 인가 (24/03/14) (0) | 2024.03.14 |
---|---|
[Node.js_4기] TIL : Nest.JS_1 (24/03/06) (0) | 2024.03.06 |
[Node.js_4기] TIL : TypeScript_1 (24/03/04) (1) | 2024.03.04 |
[Node.js_4기] Redis, 트러블 슈팅 (24/02/28) (0) | 2024.02.28 |
[Node.js_4기] TIL : 트러블슈팅 (24/02/27) (1) | 2024.02.27 |