타입스크립트에서 인터페이스는 흔히, 객체 타입을 정의할 때 사용된다.
하지만, 실제로 인터페이스는 단순한 객체 타입의 정의를 넘어 다양한 기능을 제공한다.
이번 글에서는 타입스크립트에서 인터페이스의 다양한 기능들을 알아보겠다.
1️⃣ 인터페이스(interface)의 사전적 의미
인터페이스의 사전적 의미는 다음과 같다.
- 전기 신호의 변환으로 중앙 처리 장치와 그 주변 장치를 서로 잇는 부분.
- 키보드나 디스플레이 등처럼 사람과 컴퓨터를 연결하는 장치
출처 : Oxford Languages
위 두 정의에서 '잇는' , '연결' 이라는 키워드가 보인다.
두 키워드를 통해 추론한다면, 인터페이스란 일종의 매개체라고 할 수 있다.
서로 다른 무언가를 이어주는 매개체가 바로 인터페이스란 것이다.
우리가 흔히, 웹사이트에서 보는 UI 의 약자도 User Interface 이다.
우리가 UI를 사용하여 웹사이트의 서비스를 이용하는 것을 생각한다면,
왜 interface 단어를 사용했는지 납득이 갈 것이다.
타입스크립트에서 인터페이스 역시 일종의 매개체 역할을 한다.
인터페이스는 개발자와 코드 사이에 타입 시스템을 명확히 해줌으로써
더 안정적이고 유지보수가 쉬운 소프트웨어 개발을 가능하게 하는 것이다.
이제 본격적으로 타입스크립트에서 인터페이스의 여러 기능들에 대해 알아보자.
2️⃣ 객체 형태 정의
인터페이스의 가장 대표적인 기능이다.
우선, 인터페이스를 사용하지 않았을 때의 문제점을 알아보자.
아래 예시를 확인하자.
let person1 : object = {
name: 'kim',
age : 20
}
person1 객체의 object 타입을 할당해주었다.
위 코드의 문제점은 object 타입이 너무 광범위 하다는 것이다.
object 타입만 보고서는 person1 안에 어떠한 프로퍼티가 들어가는지 예측할 수가 없다.
또한, 아래와 같은 문제도 발생한다.

object 타입이 너무 광범위 하므로, 컴파일러 입장에서는 person1 프로퍼티에 name이 있는지,
또한 height 프로퍼티를 추가할 수 있는지 알 수 없기에 위와 같은 오류가 발생하는 것이다.
따라서, 다음과 같이 수정해야 한다.
let person1 : {name : string, age: number} = {
name: 'kim',
age : 20
}
// 접근 가능
person1.name
// 타입에 height 프로퍼티가 없으므로 오류
person1.height = 170;
person1 객체가 어떠한 프로퍼티로 이루어져있는지 매우 명확해졌다.
그러나, 위의 코드도 문제가 발생한다.
아래 예시를 확인하자.
let person1: { name: string; age: number } = {
name: "kim",
age: 20,
};
let person2: { name: string; age: number } = {
name: "jung",
age: 24,
};
let person3: { name: string; age: number } = {
name: "han",
age: 23,
};
let person4: { name: string; age: number } = {
name: "lee",
age: 21,
};
일일이 {name: string, age: number} 타입을 설정해주는 게 번거롭다.
또한, 모든 person 객체에 height 프로퍼티를 추가하고 싶다면
모든 person의 타입을 하나하나 수정해야 되는 것이다.
바로 다음과 같이 말이다.
let person1: { name: string; age: number; height: number } = {
name: "kim",
age: 20,
height: 170,
};
let person2: { name: string; age: number; height: number } = {
name: "jung",
age: 24,
height: 160,
};
let person3: { name: string; age: number; height: number } = {
name: "han",
age: 23,
height: 180,
};
let person4: { name: string; age: number; height: number } = {
name: "lee",
age: 21,
height: 150,
};
이러한 상황에서 interface 가 빛을 발휘한다.
interface 는 개발자와 타입 사이의 매개체 역할을 해주어 이러한 문제를 깔끔히 해결해준다.
interface Person {
name : string,
age : number,
height : number
}
let person1: Person = {
name: "kim",
age: 20,
height: 170,
};
let person2: Person = {
name: "jung",
age: 24,
height: 160,
};
let person3: Person = {
name: "han",
age: 23,
height: 180,
};
let person4: Person = {
name: "lee",
age: 21,
height: 150,
};
흔히, interface 로 정의하는 타입은 첫 글자를 대문자로 한다.
interface를 통해 Person 타입을 정의한 것이다.
이제 타입을 수정할 때, 일일이 수정하는 것이 아닌 Person 타입만 수정하면 된다.
또한, 코드의 가독성도 매우 좋아진 것을 확인할 수 있다.
3️⃣ 선택적(optional), 읽기 전용(readonly) 프로퍼티
만약에, 어떠한 프로퍼티는 존재할 수도, 존재하지 않을 수도 있다고 가정하자.
위의 예시로 들자면 height 프로퍼티가 존재할 수도, 존재하지 않을 수 있다면 어떻게 해야 될까?
단순히 아래와 같이 interface를 정의하면 오류가 발생한다.
interface Person {
name : string,
age : number,
}
let person1: Person = {
name: "kim",
age: 20,
height: 170,
};

이럴 때는 선택적(Optional) 프로퍼티를 이용하면 된다.
프로퍼티의 : 앞에 ?(물음표)를 붙이기만 하면 된다.
interface Person {
name: string;
age: number;
height?: number;
}
// height는 선택자 프로퍼티이므로 없어도 가능
let person1: Person = {
name: "kim",
age: 20,
};
// 가능
let person2: Person = {
name: "lee",
age: 22,
height: 170,
};
이제 오류가 발생하지 않는다.
JS 에서 옵셔널 체이닝을 알고 있다면 쉽게 이해되는 부분이다.
이번에는 읽기 전용(readonly) 프로퍼티에 대해 알아보자.
현재는 프로퍼티에 접근하여 수정이 가능하다.
interface Person {
name: string;
age: number;
height?: number;
}
let person1: Person = {
name: "kim",
age: 20,
};
// name 수정
person1.name = 'lee'
인터페이스에서는 readonly 라는 읽기 전용 기능을 통해 프로퍼티의 수정을 막을 수 있다.

4️⃣ 함수 타입 정의
아래와 같이 사칙연산을 수행하는 함수가 있다.
const add = (a: number, b: number): number => a + b;
const substract = (a: number, b: number): number => a - b;
const multiply = (a: number, b: number): number => a * b;
const divide = (a: number, b: number): number => a / b;
위의 함수들은 모두 인자의 타입과 return 의 타입이 같다는 공통점이 있다.
객체 타입에서 인터페이스 사용을 이해했다면, 위 코드의 문제점이 무엇인지 알 수 있을 것이다.
인터페이스는 함수의 타입을 정의할 때도 사용할 수 있으며,
객체에서처럼 위 코드의 문제점을 개선할 수 있다.
interface CalculationFunc{
(a: number, b:number) : number
}
const add : CalculationFunc = (a,b) => a+b
const substract : CalculationFunc = (a,b) => a-b
const multiply : CalculationFunc = (a,b) => a*b
const divide : CalculationFunc = (a,b) => a/b
참고로, 함수 정의 인터페이스는 함수 표현식에서만 사용이 가능하다.
일반적인 함수 선언식에서는 사용이 불가능하다.
arrow function 을 사용했는지는 상관이 없다.
아래 예시를 확인하면 선언식과 표현식의 차이를 명확히 이해할 수 있다.
interface CalculationFunc {
(a: number, b: number): number;
}
// 함수 표현식 (사용 가능)
const add1: CalculationFunc = (a, b) => a + b;
// 함수 표현식 (사용 가능)
const add2: CalculationFunc = function(a,b){
return a+b
}
// 함수 선언식(사용 불가, 에러 발생)
function add3: CalculationFunc (a,b){
return a+b
}
5️⃣ 인덱스 시그니처
지금까지 interface를 통해 객체 안에 프로퍼티들을 제어하였다.
다시 위의 Person 예시를 들고 오겠다.
interface Person {
name : string,
age : number,
height : number
}
위 예시에서 Person 타입을 부여 받은 객체는 반드시 name, age, height 프로퍼티를
가져야 한다. 그렇지 않으면 오류가 난다.
그렇다면 객체 안에 어떠한 프로퍼티가 들어갈지 정확히 예측할 수 없는 상황에서는 어떻게 해야 될까 ?
가령, 객체 안에 hometown, footsize 등 예측할 수 없는 이름의
더 많은 프로퍼티가 들어갈 수도 있다면 말이다.
일단 어떤 프로퍼티가 올지 알 수 없으므로 위의 예시처럼 사용은 불가능하다.
그렇다고 옵셔널 체이닝을 사용하기에는 프로퍼티의 이름을 예측할 수 없으므로 불가능하다.
이러한 상황일 때, 인덱스 시그니처 기능을 사용하면 된다.
인덱스 시그니처 기능은 단순히 key 의 타입과 value의 타입만 명시하여
객체 조작에 있어 더욱 유연성과 확장성을 갖게 해준다.
인덱스 시그니처의 기본 형태는 다음과 같다.
{[key: keyType]: ValueType}
아마 위의 형태만 본다면 감이 오지 않을 것이다. 바로 예시를 확인하자.
interface Person {
[key: string]: string | number,
}
let person1: Person = {
name: "kim",
age: 20,
height: 170,
};
// hometown 은 string 이고, Seoul은 string 이므로 가능
person1['hometown'] = 'Seoul'
// footsize 는 string 이고 255 는 number 이므로 가능
person1['footsize'] = 255
// isMarried는 string이지만 false 는 boolean 이므로 불가능
person1['isMarried'] = false
key : string 을 통해 객체에 올 수 있는 key는 string 타입만 올 수 있게끔 명시를 하였다.
또한 이어서 valueType을 string | number 로 지정함으로써,
string 또는 number 타입만 올 수 있게 한 것이다.
위와 같이 타입을 정의하면 동적으로 프로퍼티를 추가하거나 제거할 수 있다.
만약, name과 age는 필수 속성으로 하고 다른 프로퍼티는 동적으로 제어하고 싶다면
아래와 같이 할 수 있다.
interface Person {
[key: string]: string | number,
name : string,
age : number
}
let person1: Person = {
name: "kim",
age: 20,
height: 170,
};
person1['hometown'] = 'Seoul'
person1['footsize'] = '255'
// age 속성이 없기에 에러가 발생한다.
let person2: Person = {
name: "kim",
height: 170,
};
6️⃣ 클래스 타입 정의
클래스에서 타입을 정의할 때도 인터페이스를 사용할 수 있다.
먼저, 인터페이스를 사용하지 않고 클래스를 정의해보자.
class Car {
brand: string;
price: number;
constructor(brand: string, price:number) {
this.brand = brand;
this.price = price;
}
drive(): void{
console.log(`The car made by ${this.brand} is driving !`)
}
}
이제, Car 클래스와 동일하게 drive 메서드를 가진, Train, Airplane 클래스를 만들고자 한다.
여기서 우리는 인터페이스를 사용하여 혹시나 drive 메서드를 넣지 않았을 때,
에러를 발생시켜 문제를 예방할 수 있다.
// 인터페이스에서 메서드 정의
interface Vehicle {
drive(): void;
}
class Train implements Vehicle {
drive() {
console.log("The Train is moving! ");
}
}
class Airplane implements Vehicle {
drive() {
console.log("The Train is moving! ");
}
}
class Car implements Vehicle {
brand: string;
price: number;
constructor(brand: string, price: number) {
this.brand = brand;
this.price = price;
}
drive(): void {
console.log(`The car made by ${this.brand} is moving !`);
}
}

여기서 우리는 추가적으로 인터페이스에 메서드도 정의할 수 있음을 확인할 수 있다.
또한, 클래스에 인터페이스를 적용시키기 위해서는 implements 키워드를 사용해야 된다.
마지막으로 중요한 것은, Car 클래스에는 drive 메서드 이외에도
brand, price 프로퍼티가 정의되어 있다.
지금까지 인터페이스의 기능들을 이해했다면,
객체 리터럴에 Vehicle 타입을 할당하고, 다른 프로퍼티를 추가하면 에러가 난다는 것을 알 것이다.
그러나, 클래스에서는 추가적인 프로퍼티나 메서드를 가질 수 있다.
interface Vehicle {
drive(): void;
}
// 가능
class Car implements Vehicle {
brand: string;
price: number;
constructor(brand: string, price: number) {
this.brand = brand;
this.price = price;
}
drive(): void {
console.log(`The car made by ${this.brand} is moving !`);
}
}
// 불가능
let car: Vehicle = {
brand: "KIA",
price: 100,
drive(): void {
console.log(`The car made by ${this.brand} is moving !`);
},
};

7️⃣ 인터페이스 확장
인터페이스는 객체지향(OOP) 처럼 상속과 유사한 기능을 지원한다.
정확히는 상속이 아니라 확장이다.
다음과 같이 인터페이스를 정의하자.
interface Person {
name: string;
age: number;
}
interface Developer {
name: string;
age: number;
skill: string;
}
Developer 인터페이스를 유심히 보면, Person 인터페이스의 모습이 그대로 담겨져 있다.
따라서, 굳이 새롭게 정의하는 것보다 미리 정의해 둔 Person 인터페이스를 사용하는 것이 훨씬 효율적이다.
이러한 상황일 때, 인터페이스 확장이 강력한 기능이 되어준다.
바로 적용해보자.
interface Person {
name: string;
age: number;
}
interface Developer extends Person {
skill: string;
}
let developer: Developer = {
name: "kim",
age: 20,
skill: "TS",
};
위와 같이 extends 기능을 통해 Person 인터페이스를 확장하여 Developer 인터페이스를 정의했다.
즉, 타입 정의의 중복을 방지하고 추가로 skill 프로퍼티의 타입을 추가한 것이다.
여기까지, TS에서 인터페이스의 분석을 마치며 이번 글을 요약하겠다.
- 인터페이스는 무언가와 무언가를 잇는 매개체 역할을 한다.
- TS에서 인터페이스는 타입을 정의할 때 다양하고 강력한 기능들을 제공한다.
- 인터페이스를 통해 객체의 구조를 명확히 정의하고, 코드의 재사용성을 높이며 타입의 안정성을 강화할 수 있다.
'TypeScript' 카테고리의 다른 글
| [TypeScript] 바닐라JS(TS)로 CSR에 기반한 컴포넌트 만들기 (1) | 2024.09.26 |
|---|---|
| [TypeScript] tsconfig.json 주요 옵션 정리 (1) | 2024.09.25 |
| [TypeScript] 제네릭(Generic) 정복하기 (0) | 2024.09.22 |