icon
2장 : 타입스크립트 기본 타입

객체와 인터페이스


실제 애플리케이션에서는 이러한 기본 타입들이 조합된 훨씬 더 복잡한 데이터 구조, 즉 객체(Object) 를 다루는 경우가 대부분입니다. 타입스크립트는 이러한 객체의 구조를 명확하게 정의하고 관리할 수 있도록 다양한 강력한 기능을 제공하는데, 그중 핵심적인 것이 바로 인터페이스(Interface) 입니다.

이 절에서는 타입스크립트에서 객체의 타입을 어떻게 정의하는지, 그리고 인터페이스가 무엇이며 어떻게 활용되는지 자세히 알아보겠습니다.


객체 리터럴 타입

가장 간단하게 객체의 타입을 정의하는 방법은 객체 리터럴 타입 (Object Literal Types) 을 사용하는 것입니다. 이는 중괄호 {} 안에 객체가 가질 속성(property)의 이름과 해당 속성의 타입을 직접 명시하는 방식입니다.

// 사람 정보를 담는 객체의 타입을 정의합니다.
let person: {
  name: string;
  age: number;
  isStudent: boolean;
};

// 정의된 타입에 맞춰 객체를 할당합니다.
person = {
  name: "김민준",
  age: 25,
  isStudent: true,
};

console.log(person.name);    // 김민준
console.log(person.age);     // 25

// 오류: 타입 정의에 없는 속성을 추가할 수 없습니다.
// person.address = "서울"; // Error: Property 'address' does not exist on type '{ name: string; age: number; isStudent: boolean; }'.

// 오류: 필수 속성을 누락할 수 없습니다.
// person = {
//   name: "박서준",
//   age: 30
// }; // Error: Property 'isStudent' is missing in type '{ name: string; age: number; }' but required in type '{ ... }'.

// 오류: 속성의 타입이 맞지 않으면 할당할 수 없습니다.
// person = {
//   name: "최유리",
//   age: "스물셋", // Error: Type 'string' is not assignable to type 'number'.
//   isStudent: false,
// };

위 예시에서 볼 수 있듯이, 객체 리터럴 타입은 객체가 가져야 할 속성의 이름, 그리고 각 속성의 타입을 엄격하게 검사합니다. 정의되지 않은 속성을 추가하거나, 필수 속성을 누락하거나, 혹은 속성의 타입이 일치하지 않을 경우 컴파일 시점에 즉시 오류를 발생시켜줍니다. 이는 런타임 오류를 사전에 방지하는 매우 중요한 기능입니다.

선택적 속성

때로는 객체의 특정 속성이 필수적이지 않고 선택적으로 존재할 수 있도록 하고 싶을 때가 있습니다. 이럴 때는 속성 이름 뒤에 물음표(?)를 붙여 선택적 속성 (Optional Properties) 으로 만들 수 있습니다.

let car: {
  brand: string;
  model: string;
  year?: number; // year 속성은 선택적입니다.
};

car = {
  brand: "Hyundai",
  model: "Sonata",
  year: 2023,
};

car = {
  brand: "Kia",
  model: "K5",
  // year 속성을 생략해도 오류가 발생하지 않습니다.
};

console.log(car.year); // 2023 또는 undefined

year?: numberyear 속성이 number 타입이거나, 아예 존재하지 않아도 된다는 의미입니다.

읽기 전용 속성

객체의 특정 속성이 처음 할당된 이후에는 변경될 수 없도록 만들고 싶을 때는 속성 이름 앞에 readonly 키워드를 붙여 읽기 전용 속성 (Readonly Properties) 으로 만들 수 있습니다.

let userProfile: {
  readonly id: string;
  name: string;
};

userProfile = {
  id: "user_abc_123",
  name: "이수진",
};

// userProfile.id = "new_id"; // Error: Cannot assign to 'id' because it is a read-only property.
userProfile.name = "이수진님"; // name은 변경 가능합니다.

console.log(userProfile.id); // user_abc_123

readonly 속성은 객체가 생성될 때만 값을 할당할 수 있으며, 그 이후에는 수정이 불가능합니다. 이는 불변성(Immutable)을 유지해야 하는 데이터에 유용하게 사용됩니다.


인터페이스

객체 리터럴 타입으로 객체의 형태를 정의하는 것은 간단한 경우에 유용합니다. 하지만 동일한 객체 타입을 여러 번 사용하거나, 복잡한 타입을 정의하고 확장해야 할 때는 인터페이스 (Interface) 를 사용하는 것이 훨씬 효과적입니다.

인터페이스는 타입스크립트에서 객체의 '모양(Shape)'을 정의하는 강력한 도구입니다. 특정 객체가 어떤 속성과 메서드를 가져야 하는지 명세하는 청사진과 같습니다.

// 'Person'이라는 이름의 인터페이스를 정의합니다.
interface Person {
  name: string;
  age: number;
  isStudent?: boolean; // 선택적 속성도 정의 가능
  readonly nationalId: string; // 읽기 전용 속성도 정의 가능
}

// Person 인터페이스를 따르는 객체를 생성합니다.
let student: Person = {
  name: "김영희",
  age: 22,
  isStudent: true,
  nationalId: "981234-5678901",
};

let teacher: Person = {
  name: "박선생",
  age: 40,
  nationalId: "789012-3456789",
  // isStudent 속성은 선택적이므로 생략해도 오류가 없습니다.
};

console.log(student.name);   // 김영희
// student.nationalId = "새로운ID"; // Error: nationalId는 읽기 전용입니다.

interface 키워드를 사용하여 인터페이스를 정의하고, 정의된 인터페이스 이름을 타입으로 사용합니다. 인터페이스는 객체 리터럴 타입과 마찬가지로 선택적 속성(?)과 읽기 전용 속성(readonly)을 가질 수 있습니다.

인터페이스의 장점

  1. 재사용성: 한 번 정의된 인터페이스는 여러 곳에서 재사용될 수 있어 코드의 중복을 줄이고 일관성을 유지할 수 있습니다.
  2. 명확성: 복잡한 객체 구조를 명확한 이름으로 추상화하여 코드의 가독성을 높입니다.
  3. 협업 용이성: 팀원 간에 데이터 모델에 대한 명확한 약속을 공유할 수 있어 협업 효율을 높입니다.
  4. 확장성: 인터페이스는 다른 인터페이스를 확장(extends) 하거나 구현(implements) 할 수 있어 유연한 타입 정의가 가능합니다.

함수 타입 정의

인터페이스는 객체 속성뿐만 아니라, 함수의 타입 (Function Types in Interfaces) 도 정의할 수 있습니다. 이는 특정 형태의 함수를 요구할 때 유용합니다.

interface MathOperation {
  (x: number, y: number): number; // 매개변수 타입과 반환 타입 정의
}

let add: MathOperation = function (a, b) {
  return a + b;
};

let multiply: MathOperation = function (num1, num2) {
  return num1 * num2;
};

console.log(add(5, 3));     // 8
console.log(multiply(4, 6)); // 24

// 오류: 반환 타입이 맞지 않습니다.
// let invalidOp: MathOperation = function (a, b) {
//   return "결과: " + (a + b); // Error: Type 'string' is not assignable to type 'number'.
// };

MathOperation 인터페이스는 두 개의 number 타입 매개변수를 받고 number 타입을 반환하는 함수의 형태를 정의합니다. 이 인터페이스를 따르는 함수들은 해당 형태를 반드시 지켜야 합니다.

인덱스 시그니처

객체의 속성 이름이 정해져 있지 않고 동적으로 변할 수 있을 때 인덱스 시그니처 (Index Signatures) 를 사용하여 정의할 수 있습니다. 이는 예를 들어 객체를 딕셔너리(Dictionary)나 맵(Map)처럼 사용할 때 유용합니다.

interface StringDictionary {
  [key: string]: string; // 문자열 키에 문자열 값을 가집니다.
}

let myDictionary: StringDictionary = {};
myDictionary["hello"] = "안녕하세요";
myDictionary["apple"] = "사과";
myDictionary["book"] = "책";

console.log(myDictionary["hello"]); // 안녕하세요

// 오류: [key: string]에 string 값만 허용하므로 숫자를 할당할 수 없습니다.
// myDictionary["year"] = 2024; // Error: Type 'number' is not assignable to type 'string'.

interface NumberIndexableArray {
  [index: number]: string; // 숫자 인덱스에 문자열 값을 가집니다.
}

let myStringArray: NumberIndexableArray = ["a", "b", "c"];
console.log(myStringArray[0]); // a

인덱스 시그니처는 [key: TKey]: TValue 형태로 정의하며, TKeystring 또는 number 타입만 가능하고, TValue는 해당 키에 해당하는 값의 타입입니다.

인터페이스 확장

인터페이스는 다른 인터페이스를 확장(extends) 할 수 있습니다. 이는 기존 인터페이스의 속성을 모두 가져오면서 새로운 속성을 추가하거나 기존 속성을 재정의하여 더 구체적인 인터페이스를 만들 때 사용됩니다. 객체 지향 프로그래밍의 상속과 유사한 개념입니다.

interface Shape {
  color: string;
}

interface Circle extends Shape { // Shape 인터페이스를 확장합니다.
  radius: number;
}

interface Rectangle extends Shape { // Shape 인터페이스를 확장합니다.
  width: number;
  height: number;
}

let myCircle: Circle = {
  color: "blue",
  radius: 10,
};

let myRectangle: Rectangle = {
  color: "red",
  width: 20,
  height: 30,
};

console.log(myCircle.color);    // blue
console.log(myRectangle.width); // 20

CircleRectangle 인터페이스는 Shape 인터페이스를 확장했기 때문에 color 속성을 별도로 정의하지 않아도 자동으로 포함됩니다. 이는 코드의 재사용성을 높이고 관련된 타입들을 계층적으로 관리하는 데 큰 도움이 됩니다.


이번 절에서는 타입스크립트에서 객체의 타입을 정의하는 핵심적인 방법인 객체 리터럴 타입인터페이스에 대해 알아보았습니다. 특히 인터페이스는 복잡한 객체 구조를 체계적으로 관리하고, 코드의 재사용성 및 협업 효율을 높이는 데 필수적인 도구임을 이해하셨기를 바랍니다.