[내일배움캠프] React 플러스 (클래스, 상속, 추상 클래스, 인터페이스, 객체 지향 설계 원칙 S.O.L.I.D, Typescript 도서관 프로그램 실습)
[5-1] 클래스
1. 클래스?
: 객체 지향 프로그래밍(OOP)의 핵심 구성 요소 중 하나로, 객체를 만들기 위한 틀(template)
2. 클래스의 구성 요소
1) 속성(attribute): 객체의 성질
2) 메서드(method): 객체의 성질을 변화시키거나 객체에서 제공하는 기능들을 사용하는 창구
3. 객체?
: 클래스 기반으로 생성되는 인스턴스(instance)
4. 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();
5. Class 접근 제한자
1) public
- 클래스 외부에서도 접근 가능 (default)
- 누구나 해당 클래스의 특정 기능을 사용해야 할 때
2) private
- 클래스 내부에서만 접근 가능
- 보통 클래스의 속성은 대부분 private으로 접근 제한자 설정
- 클래스 속성을 조회/편집하고 싶다면 별도의 getter / setter 메서드 사용
3) protected
- 클래스 내부 & 해당 클래스를 상속받은 자식 클래스에서만 접근 가능
// 예시 코드
class Person {
private name: string;
private age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
// public은 생략 가능 (default 값이라)
// name, age가 private로 선언되었기 때문에 별도 수정은 불가 (최초 대입값만 볼 수 있다.)
// 만약 name, age 수정하고 싶다면 setter 추가해서 this.name, this.age 수정..
public sayHello() {
console.log(`안녕하세요! 제 이름은 ${this.name}이고, 나이는 ${this.age}살입니다.`);
}
}
[5-2][5-3] 상속
1. 상속?
: 객체 지향 프로그래밍에서 클래스 간 관계를 정의하는 개념
-> 상속을 통해 기존 클래스의 속성&메서드를 물려받은 새로운 클래스 정의 가능 (extends 키워드 사용)
// 1. 부모 class (base Class)
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
makeSound() {
console.log('동물 소리~');
}
}
// 2. 자식 class -> 커스텀해서 상속받기
class Dog extends Animal {
age: number;
constructor(name: string) {
super(name); // super: 부모 클래스를 참조하는 데 사용 (base Class의 생성자를 호출)
this.age = 5;
}
// 메서드 "오버라이딩" -> base Class의 정의를 따르지 않고, 재정의!
makeSound() {
console.log('멍멍!'); // 부모의 makeSound 동작과 달라요!
}
eat() { // Dog 클래스만의 새로운 함수 정의
console.log('강아지가 사료를 먹습니다.');
}
}
// 3. 자식 class -> 그대로 상속받기
class Cat extends Animal { // Animal과 다를게 하나도 없어요!
}
// 4. 실행
const dog = new Dog('누렁이');
dog.makeSound(); // 출력: 멍멍!
const cat = new Cat('야옹이');
cat.makeSound(); // 출력: 동물 소리~
2. 서브타입, 슈퍼타입
1) 서브 타입
: 두 개의 타입 A와 B가 있고 B가 A의 서브타입이면 A가 필요한 곳에는 어디든 B를 안전하게 사용할 수 있다.
2) 슈퍼 타입
: 두 개의 타입 A와 B가 있고 B가 A의 슈퍼타입이면 B가 필요한 곳에는 어디든 A를 안전하게 사용할 수 있다.
3) 예시
- any: 모든 것의 슈퍼타입
- Animal: Dog, Cat의 슈퍼타입
- Dog, Cat: Animal의 서브타입
3. 타입 변환: upcasting, downcasting
1) upcasting: 서브타입 -> 슈퍼타입
: 암시적 타입 변환 이루어져 별도의 타입 변환 구문이 필요 없음! 단지 슈퍼타입 변수에 대입만
let dog: Dog = new Dog('또순이');
let animal: Animal = dog; // 슈퍼 타입 변수에 서브 타입 객체를 넣으면 -> upcasting 발동!
animal.eat(); // 에러. 슈퍼타입(Animal)으로 변환이 되어 eat 메서드를 호출할 수 없어요! (eat은 Dog 클래스에만 있던 함수라)
2) downcasting: 슈퍼타입 -> 서브타입
: as 키워드를 통해 명시적 타입 변환 필요!
let animal: Animal; // 슈퍼 타입 Animal
animal = new Dog('또순이'); // 슈퍼 타입의 객체(인스턴스)로 Dog
let realDog: Dog = animal as Dog; // 타입 자체는 animal인데 Dog로 명시적 변환
realDog.eat(); // 서브타입(Dog)로 변환이 되었기 때문에 eat 메서드를 호출할 수 있죠!
[5-4] 추상 클래스
1. 추상 클래스?
: 클래스와 다르게, 인스턴스화 할 수 없는 Class ( = new 해서 생성 불가)
2. 추상 클래스의 목적
: 상속을 통해 자식 클래스에서 메서드를 각각 구현하도록 강제하기 위해
-> 최소한의 기본 메서드는 정의 가능하지만, 핵심 기능의 구현을 자식 class에 위임!
3. 사용 방법
-> abstract 키워드 사용, 1개 이상의 추상 함수
// 추상 클래스 생성
abstract class Shape {
// 추상 함수 정의!!! -> 자식 니가 스스로 해결해!
abstract getArea(): number;
// 얘는 그냥 함수
printArea() {
console.log(`도형 넓이: ${this.getArea()}`);
}
}
// 상속받은 자식 클래스 1
class Circle extends Shape {
// 필요한 속성 정의
radius: number;
// 생성자 호출
constructor(radius: number) {
super();
this.radius = radius;
}
// 추상 함수는 반드시 구현해주어야 한다. 아니면 에러남 ! (추상 클래스 상속했으므로)
getArea(): number { // 원의 넓이를 구하는 공식은 파이 X 반지름 X 반지름
return Math.PI * this.radius * this.radius;
}
}
// 상속받은 자식 클래스 2
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();
[5-5] 인터페이스
1. 인터페이스?
1) 객체의 "타입 정의"
2) 객체가 가져야 하는 "속성"과 "메서드" 정의
3) 규약과 같은 것 -> 인터페이스를 구현한 객체는 반드시 인터페이스를 준수해야 함!
-> 코드의 안정성, 유지 보수성 향상
2. 추상 클래스 VS 인터페이스
| 추상 Class | Interface | |
| 구현부 제공 여부 | 클래스의 기본 구현 제공 | 객체의 구조만 정의, 기본 구현X |
| 상속 메커니즘 | 단일 상속만 | 다중 상속 지원 (하나의 class가 여러 interface 구현 가능) |
| 구현 메커니즘 | 추상 클래스를 상속받은 자식 클래스는 반드시 추상 함수를 구현해야 함 |
인터페이스를 구현하는 클래스는 인터페이스에 정의된 모든 메서드 전부 구현해야 함 |
| 언제 쓰면 좋을까? | 기본 구현 제공 +상속을 통해 확장 | 객체가 특정 구조를 준수하도록 강제 |
[5-6][5-7][5-8] 객체 지향 설계 원칙 S.O.L.I.D.
1. S (SRP. 단일 책임 원칙)
1) class는 하나의 책임만 갖는다.
2) 예시
// 각각의 서비스르 독립적으로 분리해서 관리
// 예를 들어, 유저 서비스라는 class에서는 유저 관련된 액션만!
// 1. 유저 서비스
class UserService {
constructor(private db: Database) {}
getUser(id: number): User {
// 사용자 조회 로직
return this.db.findUser(id);
}
saveUser(user: User): void {
// 사용자 저장 로직
this.db.saveUser(user);
}
}
// 2. 이메일 서비스
class EmailService {
// 이메일 관련된 기능은 이메일 서비스에서 총괄하는게 맞습니다.
// 다른 서비스에서 이메일 관련된 기능을 쓴다는 것은 영역을 침범하는 것이에요!
sendWelcomeEmail(user: User): void {
// 이메일 전송 로직
console.log(`Sending welcome email to ${user.email}`);
}
}
2. O (OCP. 개방 폐쇄 원칙)
1) class: 확장에 대해서는 open & 수정에 대해서는 close
2) 즉, class의 기존 코드를 변경하지 않고도 기능을 확장할 수 있어야 한다!
3) 활용
- 인터페이스 -> 객체의 구조가 잘 정의되어 있으니, 참고해서 확장
- 상속 -> 부모 Class의 기본 기능 + 필요한 건 오버라이딩(재정의) 및 기능 추가
3. L (LSP. 리스코프 치환 원칙)
1) 서브타입은 기반이 되는 슈퍼타입을 대체할 수 있어야 한다.
2) 부모 Class의 기능을 수정하지 않고도, 자식 Class는 부모 Class와 호환되어야 함
3) 예시
// 추상 클래스 생성
abstract class Bird {
abstract move(): void; // 추상 함수 (강제)
}
// 부모를 상속받은 자식 class 1
class FlyingBird extends Bird {
move() {
console.log("펄럭펄럭~");
}
}
// 부모를 상속받은 자식 class 2
class NonFlyingBird extends Bird {
move() {
console.log("뚜벅뚜벅!");
}
}
// 펭귄은 자식 class 2를 상속받도록
class Penguin extends NonFlyingBird {} // 이제 위배되는 것은 아무것도 없네요!
4. I (ISP. 인터페이스 분리 원칙)
1) 클래스는 자신이 사용하지 않는 인터페이스의 영향을 받지 않아야 한다. (무의미한 메소드의 구현 막기)
2) 인터페이스를 너무 크게 정의하기 보다 필요한 만큼씩만 정의
5. D (DIP. 의존성 역전 원칙)
1) 하위 수준 모듈(구현한 클래스)보다 상위 수준 모듈(인터페이스)에 의존해야 함
2) 예시
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) {}
// MyLocalStorage가 들어오든, MyCloudStorage가 들어오든 각각의 구현에 맞게 save 되도록
saveData(data: string): void {
this.storage.save(data);
}
}
// 실행 코드에 MyLocalStorage, MyCloudStorage 만든 후
const myLocalStorage = new MyLocalStorage();
const myCloudStorage = new MyCloudStorage();
// 각각 Database라는 class의 생성자에 type을 다르게 넣어준다!
const myLocalDatabase = new Database(myLocalStorage);
const myCloudDatabase = new Database(myCloudStorage);
myLocalDatabase.saveData("로컬 데이터");
myCloudDatabase.saveData("클라우드 데이터");
[5-9][5-10][5-11] 세 번째 TypeScript 프로그램 실습: 도서관 프로그램
1. 프로그램 기능
-> 클래스, 상속, 인터페이스를 활용한 프로그램 작성
- 도서 추가 기능 - 사서
- 도서 삭제 기능 - 사서
- 도서 대여 기능 - 유저
- 도서 반납 기능 - 유저
2. 프로젝트 세팅
1) 프로젝트 디렉토리 생성

2) package.json과 tsconfig.json 생성하는 명령어
// node.js -> package.json 생성
npm init -y
// typescript -> tsconfig.json 생성
tsc --init --rootDir ./src --outDir ./dist --esModuleInterop --module commonjs --strict true --allowJS true --checkJS true
3) package.json의 "scripts" 항목 아래와 같이 변경
"scripts": {
"start": "tsc && node ./dist/index.js",
"build": "tsc --build",
"clean": "tsc --build --clean"
},
4) src 디렉토리 생성 후 -> src 폴더 하위에 index.ts 파일 생성
3. 도서관 프로그램 코딩
1) Role이라는 enum을 정의
enum Role {
LIBRARIAN, // 사서
MEMBER, // 멤버
}
2) User라는 추상 클래스를 정의 (name, age 인자 & 추상함수 getRole)
abstract class User {
constructor(public name: string, public age: number) {}
abstract getRole(): Role; // Role을 반환하는 추상 함수 -> User 클래스 상속받는 자식 클래스들은 getRole 구현 필수
}
3) User 클래스를 상속받는 Member 클래스 & Librarian 클래스 정의
// 추상 클래스 User를 상속받는 Member 클래스
class Member extends User {
constructor(name: string, age: number) {
super(name, age);
}
// 추상 클래스의 추상 함수 구현 (필수)
getRole(): Role {
return Role.MEMBER;
}
}
// 추상 클래스 User를 상속받는 Librarian 클래스
class Librarian extends User {
constructor(name: string, age: number) {
super(name, age);
}
getRole(): Role {
return Role.LIBRARIAN;
}
}
4) Book 클래스 정의 (title, author, publishDate 인자)
// Book 클래스 정의
class Book {
constructor(
public title: string,
public author: string,
public publishDate: Date
) {}
}
5) 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; // 사용자가 도서를 반납할 때 호출하는 함수
}
6) RentManager를 구현하는 Library 클래스 생성
// RentManager 인터페이스를 구현하는 Library 클래스
class Library implements RentManager {
private books: Book[] = [];
private rentedBooks: Map<string, Book> = new Map<string, Book>(); // rentedBooks는 멤버의 대여 이력을 관리
// books를 깊은 복사 하여 외부에서 books를 수정하는 것을 방지
getBooks(): Book[] {
return JSON.parse(JSON.stringify(this.books));
// return this.books; // [얕은 복사] 이렇게 하면 getBooks를 호출받은 객체가 books를 수정 가능하다고..?
}
// 사서만 호출할 수 있게
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(user: User, 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(user: User, 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}] 책을 빌린적이 없어요!`);
}
}
}
7) 테스트 함수 코드
function main() {
// 도서관 생성
const myLibrary = new Library();
// User 생성
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());
// 도서관에 도서 추가 (사서 ONLY)
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();
8) 테스트 코드 실행 & 결과
npm run build
npm run start

cf) 추가 학습 자료
- TypeScript 공식 매뉴얼: https://www.typescriptlang.org/docs/
The starting point for learning TypeScript
Find TypeScript starter projects: from Angular to React or Node.js and CLIs.
www.typescriptlang.org
- TypeScript 공식 튜토리얼: https://www.typescriptlang.org/docs/handbook/intro.html
Handbook - The TypeScript Handbook
Your first step to learn TypeScript
www.typescriptlang.org
- TypeScript 온라인 책: https://radlohead.gitbook.io/typescript-deep-dive/getting-started
시작하기 - TypeScript Deep Dive
타입스크립트는 자바스크립트로 컴파일이 되고 자바스크립트는 브라우저 또는 서버에서 실행될 것 입니다. 그래서 다음에 정의된 목록이 타입스크립트를 시작하는데 필요할 것 입니다.
radlohead.gitbook.io