TypeScript 기본기 다지기
Table of Content
1. Object
•
클래스(Class)를 다루기 위해서 객체(Object)에서부터 연관성을 이어나가도록 설명하고자 함
새로운 폴더(metaverse/greeter/src/class)
•
object.ts
•
타입 관련 연습을 통해서도 몇번 객체에 대해서 나타나기도하고 Java등 다른 언어로부터 익숙 할 수 있지만 다시 한번 복습하는 생각으로 진입
// Vanilia JS(PURE) : Object only -> Prototype-based programming
// object
let robot = { // Members
// Property(속성)
name: "R2-D2",
model: "AstroMech",
status: "Active",
// Method(행동)
performTask: function (task: string) {
console.log(`${this.name} is performing ${task}.`);
},
}
// usage with "."" dot operator
console.log(robot.name); // Accessing property
robot.performTask("Getting ready to move") // Calling method
TypeScript
복사
◦
순수 JavaScript에서는 Object만 있으며 이것을 프로토타입 베이스 프로그래밍이라 함
▪
객체는 속성과 메서드라는 멤버를 가지고 있음
•
속성 : 객체의 속성으로 객체의 특징
•
메서드 : 객체의 행동 또는 역할, 객체의 함수는 메서드로 불리움
▪
객체의 멤버에 접근(사용)하기 위해서는 . 도트 연산자를 통해 객체를 사용하게 됨
여러 객체가 있는 상황에서 수정이 필요한 상황
•
object.ts
•
로봇 객체를 여러개 생성해야 할 때, 아래 처럼 계속해서 생성해 나가게 된다.
//...
// object
let robot1 = { // Members
// Property(속성)
name: "A2-F1",
model: "Protocol",
status: "Active",
// Method(행동)
performTask: function (task: string) {
console.log(`${this.name} is performing ${task}.`);
},
updateStatus: function (newStatus: string) {
this.status = newStatus;
console.log(`${this.name}'s status is now ${this.status}.`);
}
}
let robot2 = { // Members
// Property(속성)
name: "A5-F9",
model: "Protocol",
status: "Active",
// Method(행동)
performTask: function (task: string) {
console.log(`${this.name} is performing ${task}.`);
},
updateStatus: function (newStatus: string) {
this.status = newStatus;
console.log(`${this.name}'s status is now ${this.status}.`);
}
}
let robot3 = { // Members
// Property(속성)
name: "R2-D1",
model: "Protocol",
status: "InActive",
// Method(행동)
performTask: function (task: string) {
console.log(`${this.name} is performing ${task}.`);
},
updateStatus: function (newStatus: string) {
this.status = newStatus;
console.log(`${this.name}'s status is now ${this.status}.`);
}
}
// ...
TypeScript
복사
◦
하지만 이 처럼 1,000개를 만들어야 한다면?
◦
또한 만들었다 하더라도 여기에서 updateStatus() 와 같은 특정 메서드의 내부 구현을 변경해야 한다면? 1,000개의 객체를 모두 수정해야 하는 상황
▪
ex) console.log를 return문으로 바꿔야 한다는 요청..
•
개발자의 실수가 발생 될 수 있으며, 작업이 비효율적
2. Class
클래스와 인스턴스
•
class.ts 생성
•
위와 같이 Object에서의 비효율적 문제점들이 있었다.
•
아래와 같이 코드를 구성해보자.
class Robot { // Members
// Property 또는 Field(속성, 필드)
name: string;
model: string;
status: string = "Active";
// Constructor(생성자)
constructor(name: string, model: string) {
this.name = name;
this.model = model;
}
// Method(행동)
performTask(task: string) {
console.log(`${this.name} is performing ${task}.`);
}
updateStatus(newStatus: string) {
this.status = newStatus;
console.log(`${this.name}'s status is now ${this.status}.`);
}
}
// Create Instance of the Robot class
let r1 = new Robot("R2-D19a", "Optimus");
let r2 = new Robot("A2-12D", "Bumble");
let r3 = new Robot("C3-1AD", "MOTOS");
// Accessing properties and Calling methods
console.log(r1.name);
console.log(r3.status);
r1.performTask("Charging")
r2.performTask("Explore around")
r3.updateStatus("On Repair")
TypeScript
복사
•
object.ts의 기본 객체를 생성하던 것과 비교하여 달리 class를 활용하면서 코드의 가독성이 확보되고 있다.
◦
"Field"라는 용어는 주로 객체 지향 프로그래밍에서 클래스의 인스턴스 변수를 지칭하는 데 사용되 JavaScript 개념과 연결되면 "Property"라는 용어가 더 일반적이기도 해서 혼용되기도 한다. 근본적으로는 다른 표현이지만 객체 지향 프로그래밍을 위한 타입스크립트기 때문에 필드라는 표현으로 접근하는것이 좋다.
◦
생성 부분만 살펴보더라도, 모델과 이름만 넣어도 하나씩 객체를 생성해 나갈 수 있다.
◦
여기서 생성자라는 부분이 추가되었다.
▪
생성자는 클래스의 인스턴스가 생성될 때 호출되는 특별한 메서드로, 객체의 초기 상태를 설정하는 데 사용된다. 이를 통해 객체를 생성할 때 필요한 속성 값을 쉽게 전달할 수 있다.
•
클래스는 객체를 생성하기 위한 틀로 데이터와 함수를 하나로 묶은 것으로
◦
Java나 다른 객체지향프로그래밍 언어를 배우면서 클래스를 이해하는 가장 좋은 표현인 붕어빵 틀이라는 것을 기억하면 좋다. 또는 설계도 라는 표현을 많이 들었을 것이다.
▪
클래스는 객체를 생성하기 위한 설계도나 틀로, 인스턴스는 그 설계도를 바탕으로 실제로 만들어진 구체적인 객체를 의미한다.
▪
실무적으로는 인스턴스를 객체라는 용어를 혼용해서 사용되기도 한다. 중요한 부분은 두 용어 모두 클래스에서 생성된 것이라는 점
◦
순수 자바스크립트에서 가장 문제가 되었던 수정에 대한 부분도 클래스를 정의한 부분에서 수정해주면 모든 객체들은 동일한 수정이 적용된다.
•
이처럼 객체를 효율적으로 다룰 수 있도록하여 순수 JavaScript에선 다루기 어려운 객체지향프로그래밍을 효과적으로 다룰 수 있는 개념이다.
3. Constructor
클래스의 생성자
•
클래스는 필드, 메서드, 그리고 생성자로 구성되어 있다.
•
클래스를 통해 인스턴스를 만들어 낼 때 우리는 2개의 인수를 전달했었다.
let r1 = new Robot("R2-D19a", "Optimus");
//...
TypeScript
복사
◦
이때 이 인수(Argument)를 받아내는 클래스의 부분이 constructor라는 생성자 부분이다.
◦
new라는 키워드로 Robot 클래스의 생성자가 호출되고
◦
→ 호출 시점에 입력된 인수들은 클래스의 생성자로 전달되어 클래스의 속성(name과 model)을 초기화(할당)하는 데 사용된다
◦
→ 이후 해당 속성을 가진 객체 인스턴스가 생성 됨
◦
→ 이후 r1이라는 변수에 완성된 인스턴스 객체가 담기게 된다.
•
생성자는 이처럼 인스턴스 생성의 초기화와 관련되어 있기 때문에 필수적인 요소이다.
기본 생성자?
•
생성자가 없는데 인스턴스가 생성되는 경우가 있다. 아래 코드를 살펴보자.
class Pet {
category: string = "Cat";
name: string = "Chu";
}
let p1 = new Pet();
console.log(p1.name)
TypeScript
복사
◦
이와 같이 간단하게 클래스로 인스턴스를 만들었는데, 아무 인수를 전달하지 않고도 해당 인스턴스를 생성 할 수 있다.
◦
이것은 TypeScript가 기본 생성자(No-Args Constructor)를 자동으로 제공하기 때문
▪
클래스에 특별한 생성자를 명시하지 않은 경우 아래와 같이 기본 생성자가 숨어있게 된다.
class Pet {
category: string = "Cat";
name: string = "Chu";
constructor() {
}
}
TypeScript
복사
4. 클래스 사용 정리
클래스 선언 및 사용 코드 기초 예시
class User {
// [필드부분]
username: string; // 타입만을 표기하는 경우
email: string;
job: string = "Student"; // 기본값을 주는 경우
// [생성자부분]
constructor(username: string, email: string) {
this.username = username; // 매개변수로 초기화
this.email = email; // 매개변수로 초기화
}
// [메서드부분]
study(): void {
console.log(`${this.username} is studying.`);
}
}
// [객체 생성] (new 키워드와 arguments 입력)
let user1 = new User("홍길동", "hong@example.com");
let user2 = new User("김철수", "kim@example.com");
// [객체의 사용, 접근] (도트연산자 사용)
// 1. 인스턴스 자체 접근
console.log(user1); // 출력: User { username: '홍길동', email: 'hong@example.com' }
// 2. 인스턴스의 세부 필드 접근
console.log(user2.username); // 출력: 김철수
// 3. 메서드 호출
user1.study(); // 출력: 홍길동 is studying.
user2.study(); // 출력: 김철수 is studying.
TypeScript
복사
5. Inheritance
클래스의 상속
•
이제 객체를 클래스의 인스턴스로 만들고 있기 때문에 순수 자바스크립트때 보다 효율적으로 객체를 만들어 내고 있다.
•
그런데 추가된 기능들이 다른 “클래스” 자체를 수없이 만들어야 한다면? 객체를 비효율적으로 계속 만들던것과 큰 차이가 없지 않은가?
◦
로봇을 참고하여 청소, 요리 로봇 클래스를 추가한다면 다음 코드와 같다.
class CleaningRobot {
// Field(Property) 속성
name: string;
model: string;
cleanSchedule: string[];
status: string = "Active";
// Constructor(생성자)
constructor(name: string, model: string, cleanSchedule: string[]) {
this.name = name;
this.model = model;
this.cleanSchedule = cleanSchedule;
}
// Method(행동)
performTask(task: string) {
console.log(`${this.name} is performing ${task}.`);
}
updateStatus(newStatus: string) {
this.status = newStatus;
console.log(`${this.name}'s status is now ${this.status}.`);
}
performCleaning() {
console.log(`${this.name} is cleaning according to the schedule: ${this.cleanSchedule.join(", ")}.`)
}
}
class CookingRobot {
// Field(Property) 속성
name: string;
model: string;
availableMenus: string[];
status: string = "Active";
// Constructor(생성자)
constructor(name: string, model: string, availableMenus: string[]) {
this.name = name;
this.model = model;
this.availableMenus = availableMenus;
}
// Method(행동)
performTask(task: string) {
console.log(`${this.name} is performing ${task}.`);
}
updateStatus(newStatus: string) {
this.status = newStatus;
console.log(`${this.name}'s status is now ${this.status}.`);
}
performCooking() {
console.log(`${this.name} will cook one of the menus you choose from the following: ${this.availableMenus.join(", ")}.`);
}
}
TypeScript
복사
▪
각 기능로봇 클래스는 cleanSchedule, availableMenus 와 같은 별도의 필드를 가지고 있기도 하며, performCleaning(), performCooking()과 같은 별도 메서드도 가지고 있다.
▪
또한 기존 로봇을 참고했기 때문에 중복된 부분도 상당히 존재한다.
▪
여기서 기능 및 속성 수정이나 추가가 필요하다면 마치 처음 객체를 순수 자바스크립트로 구성하던 중복, 유지보수의 불리함이 동일하게 나타난다.
◦
이 때 각 기능 로봇이 참조했던, 로봇의 기본형태인 Robot 클래스를 그대로 사용하고, 이것을 상속받아 청소로봇, 요리로봇을 만들 수 있다.
▪
상속받는 클래스(ex: 청소로봇)가 클래스 선언부에 …extends Robot { ...} 처럼 키워드를 사용하여 구현
class CleaningRobot extends Robot {
// Field(Property) 속성
cleanSchedule: string[];
// Constructor(생성자)
constructor(name: string, model: string, cleanSchedule: string[]) {
super(name, model);
this.cleanSchedule = cleanSchedule;
}
// Method(행동)
override performTask() {
console.log(`${this.name} is cleaning according to the schedule: ${this.cleanSchedule.join(", ")}.`)
}
}
class CookingRobot extends Robot {
// Field(Property) 속성
availableMenus: string[];
// Constructor(생성자)
constructor(name: string, model: string, availableMenus: string[]) {
super(name, model);
this.availableMenus = availableMenus;
}
// Method(행동)
performTask() {
console.log(`${this.name} will cook one of the menus you choose from the following: ${this.availableMenus.join(", ")}.`);
}
}
// TEST
let c1 = new CleaningRobot("R2-D19a", "Optimus", ["Sun","Mon"]);
console.log(c1.cleanSchedule);
c1.performTask();
console.log(c1.name);
TypeScript
복사
◦
super는 부모클래스인 Robot의 멤버를 가리키며, 물려받은 필드의 값은 super를 통해 접근하여 값을 할당 시켜주게 된다.
▪
참고로 this는 현재 클래스의 멤버를 가리킴
▪
override 키워드는 자식 클래스에서 물려 받은 메소드를 변형해서 사용하기 위한 방법으로, 기능을 추가 또는 재정의하기 위해 사용 되는 것
•
동일한 메서드 이름이라면 자동으로 인식되므로 생략 가능
◦
조금 복잡해지기 시작하지만, 이는 곧 극단적으로 코드의 수가 늘어날 수록 더욱 간결하게 구조를 지킬 수 있는 방법이다.
▪
객체지향 프로그래밍 원칙에서,
•
OOP원칙의 OCP(Open/Closed Principle) 개방-폐쇄의 원칙인 확장에 열리고 수정에 닫힌 상태를 유지할 수 있다.
•
OOP원칙 중 SRP(Single Responsibility Principle) 단일 책임 원칙에서도 하나의 클래스는 하나의 책임만 가져야 한다는 부분에서, 예시의 청소 로봇 클래스는 관련 코드만 작성함으로써 명확성을 줄 수 있다.
▪
객체지향 프로그래밍 요소에서,
•
공통된 특성을 묶어두는 부모 클래스를 만들어 중복 코드를 최소화 시킬 수 있는 방안인 상속을 직접적으로 다루고 있다.
•
또한 동일한 메서드를 호출하지만 각 클래스의 특성에 맞게 동작하도록 구현되는 메서드 오버라이드(Overriding)를 통해 다형성을 보여주고 있다.
6. Visibility modifier
스코프의 제한
•
TypeScript에서 Visibility modifier라 불리우는 키워드들은 Java에서의 Access modifier와 유사한 개념으로 보면 된다.
•
이는 클래스의 멤버인 변수와 메서드에 접근하는 범위를 제어하는 요소이다.
◦
public - protected - private 으로 구분됨
▪
public : 모든 클래스에서 접근 가능, 기본값
▪
protected : 같은 클래스와 자식 클래스에서 접근 가능
▪
private : 해당 클래스 내에서만 접근 가능
◦
현재 모든 변수와 메서드는 public으로 설정되었기 때문에 사용, 수정 등이 자유롭게 진행되었던 것이다.
◦
로봇 부모 클래스의 필드를 쉽게 수정 할 수 없도록 은닉 해보자.
class Robot { // Members
// Property(속성)
private name: string;
private model: string;
protected status: string = "Active";
// Constructor(생성자)
constructor(name: string, model: string) {
this.name = name;
this.model = model;
}
// Getter for name
public getName(): string {
return this.name;
}
// Getter for model
public getModel(): string {
return this.model;
}
// Method(행동)
protected performTask(task: string) {
console.log(`${this.name} is performing ${task}.`);
}
protected updateStatus(newStatus: string) {
this.status = newStatus;
console.log(`${this.name}'s status is now ${this.status}.`);
}
}
class CleaningRobot extends Robot {
// Field(Property) 속성
private cleanSchedule: string[];
// Constructor(생성자)
constructor(name: string, model: string, cleanSchedule: string[]) {
super(name, model);
this.cleanSchedule = cleanSchedule;
}
// Getter for schedule
public getCleanSchedule(): string[]{
return this.cleanSchedule;
}
// Method(행동)
override performTask() {
console.log(`${this.getName()} is cleaning according to the schedule: ${this.cleanSchedule.join(", ")}.`)
}
}
class CookingRobot extends Robot {
// Field(Property) 속성
private availableMenus: string[];
// Constructor(생성자)
constructor(name: string, model: string, availableMenus: string[]) {
super(name, model);
this.availableMenus = availableMenus;
}
// Method(행동)
performTask() {
console.log(`${this.getName()} will cook one of the menus you choose from the following: ${this.availableMenus.join(", ")}.`);
}
}
// TEST
let c1 = new CleaningRobot("R2-D19a", "Optimus", ["Sun","Mon"]);
console.log(c1.getCleanSchedule());
c1.performTask();
console.log(c1.getName());
TypeScript
복사
◦
다음 처럼 이름이나 모델 값은 이제 getName(), getModel()과 같은 메서드를 통해서만 가져 올 수 있다. 수정하는 것은 setName() 과 같은 메서드를 추가로 구성해야 한다. 이를 Getter/Setter라 함
▪
이렇게 하면 클래스의 내부 상태를 보호하고, 클래스의 사용자가 의도하지 않은 방식으로 데이터를 수정하는 것을 방지할 수 있음
▪
객체 지향프로그래밍 (OOP)의 요소에서,
•
이것은 은닉화(Encapsulation)에 대한 부분을 구현한 부분으로
•
객체의 내부 상태(속성)와 행동(메서드)을 하나의 단위로 묶고, 외부에서 직접 접근할 수 없도록 제한하는 원칙임
•
이를 통해 데이터 보호 뿐만 아니라 직접 관련된 메서드를 통해 상호작용하기 때문에(이름을 얻기 위해 getName()메서드를 사용하는 것 처럼) 사용하고자 하는 기능을 명확하게 정의 할 수 있음
6. Interface
인터페이스
•
객체의 구조를 정의, 추상화하는 데 사용됨
◦
인터페이스는 특정 속성과 메서드의 형태를 정의(값X, 타입OK)
◦
여러 객체가 동일한 구조를 따르도록 하는 역할을 함
•
Java와 TypeScript의 추상화 개념은 비슷하지만, TypeScript에서는 인터페이스를 통해 필드와 메서드 모두를 정의할 수 있다는 점에서 더 유연한 부분이 있음
◦
하지만 이 부분 때문에 클래스와 혼동 될 수 있음
◦
결국 역할과 사용의 차이로 명확하게 구분하는 것이 중요.
▪
클래스 : 객체를 생성하기 위한 청사진으로 생성자를 포함,
▪
인터페이스 : 객체의 형태를 정의하는 것, 직접 객체를 생성 할 수 없음, 인터페이스를 상속받은 클래스(구현체)에서 생성해야 함
•
→ TypeScript에서는 Object Type을 특정 타입의 집합으로 지정하던 부분에서 사용하던 방식으로 이해
◦
인터페이스는 주로 타입 체크와 코드의 일관성을 유지하는 데 사용
◦
추후 요청과 응답의 형태를 제어하는 DTO에서 주로 사용될 것임
// 추후 자주 사용될 인터페이스의 모습, 주로 필드와 타입을 제한하는데 활용
// User DTO 인터페이스
interface UserDTO {
id: number; // 사용자 ID
name: string; // 사용자 이름
email: string; // 사용자 이메일
createdAt: Date; // 생성일
}
// API 응답 형태를 정의하는 인터페이스
interface ApiResponse {
success: boolean; // 요청 성공 여부
data: UserDTO; // 실제 데이터
message?: string; // 추가 메시지 (선택적)
}
TypeScript
복사
Related Posts
Search