[TypeScript] 인터페이스 개념정리
오늘은 TypeScript의 핵심 기능 중 하나인 인터페이스(Interface)에 대해 개념정리 해보고자 한다.
인터페이스란?
TypeScript에서 인터페이스는 타입 체크를 위한 강력한 도구라고 할 수 있다.
인터페이스는 객체의 구조를 정의하며, 코드 내에서 객체가 특정 구조를 따르도록 강제한다
=> 이를 통해 코드의 가독성과 안정성을 높일 수 있다!
인터페이스는 실제 구현이 아닌 '계약'과 같은 개념이다.
즉, 특정 객체가 가져야 할 프로퍼티와 메서드를 선언하지만, 실제 구현은 해당 인터페이스를 이용하는 객체에게 맡긴다.
(TypeScript 컴파일러는 객체가 인터페이스에 명시된 구조를 준수하는지 확인하여 타입 안정성을 보장한다.)
JavaScript는 동적 타입 언어이기 때문에 런타임에 타입 관련 오류가 발생할 수 있지만,
TypeScript의 인터페이스를 사용하면 컴파일 단계에서 이러한 오류를 미리 잡아낼 수 있다는 이점이 있다.
기본 인터페이스 사용법
가장 간단한 인터페이스부터 살펴보겠다.
아래 코드는 학생 정보를 나타내는 인터페이스다.
interface Student {
id: number;
name: string;
age: number;
major: string;
}
function introduceStudent(student: Student): void {
console.log(`안녕하세요, 저는 ${student.major}를 전공하는 ${student.name}입니다.`);
}
const newStudent = {
id: 12345,
name: "김철수",
age: 22,
major: "컴퓨터공학"
};
introduceStudent(newStudent); // 안녕하세요, 저는 컴퓨터공학을 전공하는 김철수입니다.
위 코드에서 Student 인터페이스는 학생 객체가 가져야 할 프로퍼티들을 정의한다.
- introduceStudent 함수는 파라미터로 Student 타입의 객체를 받는다.
- newStudent 객체는 Student 인터페이스의 모든 프로퍼티를 가지고 있기 때문에 introduceStudent 함수에 전달될 수 있다.
🚨만약 major 프로퍼티가 없는 객체를 전달하려고 한다면, TypeScript 컴파일러는 오류를 발생시킨다.🚨
const invalidStudent = {
id: 67890,
name: "박영희",
age: 21
// major 프로퍼티가 없음
};
introduceStudent(invalidStudent); // 오류: 'major' 프로퍼티가 없습니다.
이처럼 인터페이스를 사용하면 객체의 구조를 명확히 정의하고, 코드의 예측 가능성을 높일 수 있다!
선택적 프로퍼티
모든 프로퍼티가 항상 필요한 것은 아니다.
인터페이스에서 선택적 프로퍼티(Optional Properties)는 프로퍼티 이름 뒤에 물음표(?)를 붙여 표시한다.
interface CourseRegistration {
studentId: number;
courseId: string;
semester: string;
grade?: string; // 선택적 프로퍼티
completionDate?: Date; // 선택적 프로퍼티
}
function registerCourse(registration: CourseRegistration): void {
console.log(`학번 ${registration.studentId}의 학생이 ${registration.courseId} 과목을 ${registration.semester}에 등록했습니다.`);
if (registration.grade) {
console.log(`최종 성적: ${registration.grade}`);
}
}
const newRegistration = {
studentId: 12345,
courseId: "CS101",
semester: "2023-1"
};
registerCourse(newRegistration); // 정상 작동
위 코드에서 grade와 completionDate는 선택적 프로퍼티이다.
이들은 있어도 되고 없어도 되며, 없더라도 TypeScript는 오류를 발생시키지 않는다.
(선택적 프로퍼티는 API 응답이나 사용자 입력처럼 일부 정보가 누락될 수 있는 상황에서 유용함.)
읽기 전용 프로퍼티
인터페이스에서 프로퍼티를 읽기 전용(Readonly)으로 만들 수도 있다.
읽기 전용 프로퍼티는 객체가 처음 생성될 때만 값을 할당할 수 있고, 그 후에는 변경할 수 없다.
interface University {
readonly id: number;
name: string;
location: string;
foundedYear: number;
}
const myUniversity: University = {
id: 1,
name: "서울대학교",
location: "서울시 관악구",
foundedYear: 1946
};
myUniversity.name = "서울국립대학교"; // 정상 동작
myUniversity.id = 2; // 오류: 'id'는 읽기 전용 프로퍼티입니다.
위 코드에서 id 프로퍼티는 readonly 키워드로 읽기 전용으로 선언되었다.
따라서 myUniversity 객체가 생성된 후에 id 값을 변경하려고 하면 오류가 발생한다!
(이는 상수(constant) 값이나 식별자와 같이 변경되면 안 되는 프로퍼티를 보호하는 데 유용함.)
함수 타입 인터페이스
인터페이스는 객체의 프로퍼티뿐만 아니라 함수의 형태도 정의할 수 있다.
함수 타입 인터페이스는 매개변수의 타입과 반환 타입을 지정한다!
interface MathOperation {
(x: number, y: number): number;
}
const add: MathOperation = function(x, y) {
return x + y;
};
const multiply: MathOperation = function(x, y) {
return x * y;
};
console.log(add(5, 3)); // 8
console.log(multiply(5, 3)); // 15
위 코드에서 MathOperation 인터페이스는 두 개의 숫자 매개변수를 받아 숫자를 반환하는 함수의 형태를 정의한다.
add와 multiply 함수는 이 인터페이스를 구현하므로, 두 개의 숫자 매개변수를 받아 숫자를 반환해야 한다.
(함수 타입 인터페이스는 콜백 함수나 이벤트 핸들러와 같이 특정 시그니처를 가진 함수를 정의할 때 유용함)
클래스와 인터페이스
TypeScript에서 클래스는 인터페이스를 구현(implement)할 수 있다.
이는 클래스가 특정 계약을 준수하도록 강제하는 방법이다.
interface Vehicle {
brand: string;
model: string;
year: number;
start(): void;
stop(): void;
}
class Car implements Vehicle {
brand: string;
model: string;
year: number;
constructor(brand: string, model: string, year: number) {
this.brand = brand;
this.model = model;
this.year = year;
}
start() {
console.log(`${this.brand} ${this.model} 시동을 겁니다.`);
}
stop() {
console.log(`${this.brand} ${this.model} 시동을 끕니다.`);
}
honk() {
console.log("빵빵!");
}
}
const myCar = new Car("현대", "아반떼", 2020);
myCar.start(); // 현대 아반떼 시동을 겁니다.
myCar.honk(); // 빵빵!
위 코드에서 Car 클래스는 Vehicle 인터페이스를 구현한다.
이는 Car 클래스가 Vehicle 인터페이스에 정의된 모든 프로퍼티와 메서드를 포함해야 함을 의미한다.
Car 클래스는 인터페이스에 명시된 요구사항을 충족하면서도 honk와 같은 추가 메서드를 가질 수 있다.
클래스와 인터페이스를 함께 사용하면 코드의 구조를 더 명확하게 설계할 수 있고, 객체 지향 프로그래밍의 원칙을 쉽게 적용할 수 있는 것 같다.
인터페이스 확장
인터페이스는 다른 인터페이스를 확장(extend)할 수 있으며, 이를 통해 인터페이스 간 재사용성을 높이고 코드 중복을 줄일 수 있다.
interface Person {
name: string;
age: number;
}
interface Employee extends Person {
employeeId: string;
department: string;
work(): void;
}
const newEmployee: Employee = {
name: "이지은",
age: 28,
employeeId: "E12345",
department: "개발팀",
work: function() {
console.log(`${this.name}이(가) ${this.department}에서 일하고 있습니다.`);
}
};
newEmployee.work(); // 이지은이(가) 개발팀에서 일하고 있습니다.
위 코드에서 Employee 인터페이스는 Person 인터페이스를 확장하고 있다.
따라서 Employee 인터페이스는 Person의 모든 프로퍼티(name, age)와 자신의 프로퍼티(employeeId, department, work)를 모두 포함한다!!!
💡인터페이스 확장은 여러 인터페이스를 동시에 확장할 수도 있다!💡
interface Student {
studentId: string;
major: string;
study(): void;
}
interface InternEmployee extends Person, Student {
companyName: string;
internshipPeriod: number;
}
const intern: InternEmployee = {
name: "김민수",
age: 23,
studentId: "S54321",
major: "정보통신공학",
companyName: "테크 기업",
internshipPeriod: 6, // 개월
study: function() {
console.log(`${this.name}이(가) ${this.major}을(를) 공부합니다.`);
},
work: function() {
console.log(`${this.name}이(가) ${this.companyName}에서 인턴으로 일합니다.`);
}
};
위 코드에서 InternEmployee 인터페이스는 Person과 Student 두 인터페이스를 모두 확장한다.
따라서 InternEmployee 인터페이스는 세 인터페이스의 모든 프로퍼티와 메서드를 포함해야 한다.
하이브리드 타입
TypeScript에서는 함수와 객체의 특성을 모두 가진 하이브리드 타입을 정의할 수 있다.
이는 JavaScript의 유연한 함수 객체 특성을 활용한 것이다.
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function getCounter(): Counter {
const counter = function(start: number): string {
return `카운터가 ${start}에서 시작합니다.`;
} as Counter;
counter.interval = 123;
counter.reset = function() {
console.log("카운터를 리셋합니다.");
};
return counter;
}
const counter = getCounter();
console.log(counter(10)); // 카운터가 10에서 시작합니다.
console.log(counter.interval); // 123
counter.reset(); // 카운터를 리셋합니다.
위 코드에서 Counter 인터페이스는 함수로 호출할 수 있으면서도 interval 프로퍼티와 reset 메서드를 가지는 객체를 정의한다.
getCounter 함수는 이러한 하이브리드 타입의 객체를 생성하여 반환합니다.
🚨하이브리드 타입은 복잡한 구조를 표현해야 할 때 유용하지만, 코드의 복잡성을 높일 수 있으므로 신중하게 사용해야 한다!!!🚨