4.Node.js/JavaScript&TypeScript

[TypeScript] 2편. 객체 설계의 핵심, Interface vs Type Alias 완벽 정리와 타입 추론 원리

쿼드큐브 2025. 12. 12. 09:38
반응형
반응형

 

2편. 객체 설계의 핵심, Interface vs Type Alias 완벽 정리와 타입 추론 원리

📚 목차
1. 인터페이스 (Interface): 객체 타입 정의의 기본
2. type vs interface: 언제 무엇을 사용해야 할까요?
3. 객체 타입 심화: 유연하고 안전한 구조 만들기
4. 타입 추론 (Type Inference) 이해하기: 자동으로 타입 유추

 

Interface vs Type Alias 삽화 이미지
Interface vs Type Alias 삽화 이미지

 

📂 [GitHub 예시 코드 보러가기] (https://github.com/cericube/nodejs-tutorials) /TypeScript

 

1. 인터페이스 (Interface): 객체 타입 정의의 기본

JavaScript의 객체는 매우 유연하여 다양한 속성과 구조를 가질 수 있지만, 반대로 말하면 어떤 속성이 존재하는지 사전에 알기 어렵고, 실수로 잘못된 속성을 써도 런타임까지 오류를 알기 어렵습니다.


TypeScript에서는 이러한 문제를 해결하기 위해 인터페이스(Interface)라는 기능을 제공합니다.

 

✔️ 인터페이스란?
인터페이스는 객체가 가져야 할 속성(property)과 메서드(method)의 이름과 타입을 사전에 정의하는 일종의 설계도입니다.

▸ 객체에 어떤 속성이 있어야 하는지 명확히 규정할 수 있습니다.
▸ 자동 완성, 타입 체크, 문서화 등에 큰 도움이 됩니다.
▸ 클래스(class)와도 함께 사용될 수 있습니다

 

🔷 인터페이스 정의 및 사용법

// 'User'라는 이름의 인터페이스를 정의합니다.
interface User {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
  // 선택적 속성: 있어도 되고 없어도 되는 값
  phoneNumber?: string;
  // 메서드 정의: 함수 타입도 포함 가능
  greet(): string;
}

// 이 객체는 반드시 User 인터페이스의 구조를 따라야 합니다.
const user1: User = {
  id: 1,
  name: "김민준",
  email: "minjun.kim@example.com",
  isActive: true,
  greet() {
    return `안녕하세요, ${this.name}입니다.`;
  }
};

console.log(user1.greet()); // 출력: 안녕하세요, 김민준입니다.

▸ id, name, email, isActive: 필수 속성
▸ phoneNumber: 선택적(optional) 속성 → 있어도 되고 없어도 됩니다
▸ greet(): 문자열을 반환하는 메서드 정의

 

✔️ 오류 예시: 속성 누락 또는 타입 불일치

const user2: User = {
  id: 2,
  name: "이서연",
  email: "seoyeon@example.com",
  // isActive가 누락되었고 greet 메서드도 없음 → 오류 발생
  // Property 'isActive' is missing in type ...
  greet() {
    return "안녕하세요!";
  }
};

TypeScript는 컴파일 단계에서 User 인터페이스 정의와 실제 객체 구조를 자동으로 비교하여 누락된 속성이나 잘못된 타입을 미리 알려줍니다.

 

🔷 인터페이스 확장 (Extending Interfaces)

인터페이스는 다른 인터페이스를 확장(extend) 할 수 있습니다.

이렇게 하면 기존 인터페이스의 속성은 그대로 유지하면서 새로운 속성을 추가할 수 있습니다.

// User 인터페이스를 확장한 AdminUser
interface AdminUser extends User {
  role: "admin" | "super-admin"; // 역할은 두 가지 중 하나
  permissions: string[];         // 권한 목록
}

const admin: AdminUser = {
  id: 100,
  name: "홍길동",
  email: "jihoon.park@example.com",
  isActive: true,
  role: "admin",
  permissions: ["read", "write", "delete"],
  greet() {
    return `관리자 ${this.name}입니다.`;
  }
};

console.log(admin.greet()); // 출력: 관리자 박지훈입니다.

확장의 장점
▸ 코드 재사용: 공통 속성을 별도로 정의해 두고 여러 타입에 공유 가능
▸ 유지보수 용이: 수정할 때 한 군데만 고치면 됨
▸ 의미 있는 타입 분리: 일반 사용자와 관리자 같은 역할 구분에 효과적

 

2. type vs interface: 언제 무엇을 사용해야 할까요?

TypeScript를 처음 접하신 분들께서 가장 자주 헷갈려하시는 부분 중 하나가 바로 type 키워드를 이용한 타입 별칭(Type Alias)과 interface를 이용한 인터페이스 정의입니다.


두 문법 모두 객체의 구조를 정의할 수 있다는 공통점이 있지만, 그 용도와 동작 방식에는 중요한 차이점들이 존재합니다.

항목 interface type
확장(상속) 방식 extends 키워드를 사용하여 확장 & 연산자(인터섹션)를 사용하여 결합
정의할 수 있는 대상 객체의 모양(structure) 정의에 특화 객체 외에도 기본 타입, 유니언, 튜플 등 다양한 타입 정의 가능
선언 병합
(Declaration Merging)
동일한 이름의 interface를 여러 번 선언하면 자동으로 병합됨 type은 동일한 이름으로 여러 번 선언 시 오류 발생
공식 문서 권장 용도 객체 구조를 정의할 때 추천(라이브러리, 클래스, API 응답 등) 유니언 타입, 튜플, 복합 타입 등을 정의할 때 추천

▸ 객체의 구조를 정의할 때는 interface를 사용하는 것이 일반적입니다.
▸ 특히 확장성(extends)과 선언 병합(declaration merging)이 가능하므로 라이브러리 타입 정의나 객체 지향 설계에 적합합니다.
▸ 반면, type은 유니언 타입("A" | "B"), 튜플, 함수 타입, 기본 타입 조합 등 보다 다양하고 복합적인 타입 정의에 적합합니다.

상황 추천
객체 구조를 정의해야 할 때 interface ✅
타입 확장을 자주 해야 할 때 interface ✅
유니언, 튜플, 복합 타입이 필요한 경우 type ✅
기본 타입 또는 다른 타입을 조합할 때 type ✅
동일 이름으로 병합이 필요한 경우 interface만 가능

 

✔️ 예제 1: type으로 유니언 타입 정의하기

type은 이렇게 문자열 리터럴 유니언 타입을 선언할 때 매우 유용합니다.

// 상태 값을 제한된 문자열로 표현
type Status = "pending" | "success" | "error";

// 해당 타입을 변수에 적용
let currentStatus: Status = "success";

// 잘못된 값은 오류 발생
// currentStatus = "done"; // ❌ Error: Type '"done"' is not assignable to type 'Status'.

 

✔️ 예제 2: interface로 객체 타입 정의하기

interface는 객체의 구조를 명확하게 정의하고, 필요한 경우 선택적 속성도 지정할 수 있어 실무 API 응답 타입 등에 많이 사용됩니다.

interface Response {
  status: "success" | "error";
  data: any;
  message?: string; // 선택적 프로퍼티
}

const res1: Response = {
  status: "success",
  data: { id: 1, name: "Jane" }
};

const res2: Response = {
  status: "error",
  data: null,
  message: "요청에 실패했습니다."
};

 

✔️ 예제 3: interface 확장 vs type 확장

두 방식 모두 유사하게 확장 가능하지만, interface는 extends 문법을 사용하고, type은 & 연산자(intersection)를 통해 확장합니다.

// interface 확장
interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

const myDog: Dog = {
  name: "Coco",
  breed: "Poodle"
};
// type 확장 (인터섹션 방식)
type Animal = { name: string };
type Dog = Animal & { breed: string };

const myDog: Dog = {
  name: "Coco",
  breed: "Poodle"
};

 

✔️ 예제 4: 선언 병합 차이점

// interface는 같은 이름으로 선언 가능하며 병합됨
interface User {
  name: string;
}

interface User {
  age: number;
}

// 병합된 결과:
const user: User = {
  name: "Jane",
  age: 30
};
// type은 같은 이름으로 다시 선언하면 오류 발생
type Admin = {
  role: string;
};

// 아래는 오류 발생
// type Admin = { level: number }; // ❌ Duplicate identifier 'Admin'
반응형

 

3. 객체 타입 심화: 유연하고 안전한 구조 만들기

TypeScript를 사용하면 객체의 구조를 훨씬 정밀하게 제어할 수 있습니다.
▸ 어떤 속성은 선택적으로 정의할 수 있고,
▸ 어떤 속성은 생성 후 절대 수정할 수 없도록 만들 수 있습니다.
▸ 경우에 따라서는 컴파일러보다 개발자가 타입을 더 잘 알고 있는 상황도 있기 때문에, 타입 단언(Type Assertion)을 통해 타입을 직접 지정해 줄 수도 있습니다.
▸ 추가로, 실무에서 자주 사용하는 계산된 프로퍼티(Computed Property)를 이용하면 키 이름이 동적으로 결정되는 객체도 타입 안전하게 다룰 수 있습니다.

개념 설명 키워드
선택적 속성 있어도 되고 없어도 되는 속성 ?
읽기 전용 속성 생성 후 변경이 불가능한 속성 readonly
계산된 프로퍼티 표현식 결과를 키 이름으로 사용하는 속성 [expr]
타입 단언 개발자가 직접 타입을 지정 (확신이 있을 때) as

 

 

🔷 선택적 속성 (Optional Properties)

객체의 모든 속성이 항상 필수일 필요는 없습니다.
TypeScript에서는 속성 이름 뒤에 ?를 붙이면 해당 속성을 선택적 속성(optional property) 으로 만들 수 있습니다.

interface Profile {
  nickname: string;   // 필수 속성
  age?: number;       // 선택적 속성
}

const profile1: Profile = {
  nickname: "TypeScriptLover",
}; // ✅ age가 없어도 오류 없음

const profile2: Profile = {
  nickname: "JSExpert",
  age: 30,
}; // ✅ age가 있어도 문제 없음

 

✔️ 선택적 속성과 number | undefined의 차이

interface A {
  age?: number;
}
//→ age 라는 키 자체가 없어도 된다
//→ {}도 A 타입으로 허용
const a1: A = {};                  // ✅ OK
const a2: A = { age: 10 };         // ✅ OK

interface B {
  age: number | undefined;
}
//age 키는 항상 존재해야 한다
//→ 값만 undefined일 수 있음
//→ { age: undefined }는 허용되지만, {}는 B 타입이 될 수 없음
const b1: B = { age: undefined };  // ✅ OK
// const b2: B = {};               // ❌ Property 'age' is missing

 

 

🔷 읽기 전용 속성 (readonly)

readonly 키워드를 사용하면 해당 속성은 객체 생성 이후에는 값을 변경할 수 없습니다.
이는 데이터의 불변성(immutability)을 보장하여 예기치 않은 값 변경으로 인한 오류를 방지할 수 있습니다.

interface Point {
  readonly x: number;
  readonly y: number;
}

const p: Point = { x: 10, y: 20 };

// p.x = 5; // ❌ 오류 발생! Cannot assign to 'x' because it is a read-only property.

 

✔️ const vs readonly 구분하기

두 키워드는 자주 헷갈리지만, 의미하는 대상이 다릅니다.

const p: Point = { x: 10, y: 20 };

// p = { x: 0, y: 0 }; // ❌ const: 변수 자체 재할당 불가
// p.x = 5;            // ❌ readonly: 프로퍼티 값 변경 불가
interface Config {
  apiBaseUrl: string;
  timeoutMs: number;
}

// 전체를 읽기 전용으로 만들고 싶을 때
const config: Readonly<Config> = {
  apiBaseUrl: "https://api.example.com",
  timeoutMs: 5000,
};

// config.timeoutMs = 3000; // ❌ 읽기 전용이라 수정 불가

 

🔷 계산된 프로퍼티 (Computed Property)와 동적 키

실무에서는 키 이름이 코드 실행 시점에 결정되는 객체도 자주 등장합니다.

이럴 때 JavaScript/TypeScript는 계산된 프로퍼티 이름(computed property name) 문법을 제공합니다.

const fieldName = "email";

const user = {
  name: "Alice",
  [fieldName]: "alice@example.com", // => "email": "alice@example.com"
};

console.log(user.email);    // "alice@example.com"
console.log(user["email"]); // "alice@example.com"

▸ [fieldName] 부분이 계산된 프로퍼티 이름입니다.
▸ fieldName 값이 "email"이므로 최종 결과는 { name: "Alice", email: "..." }와 동일합니다.

 

🔷 Type Assertion (as 키워드)

TypeScript의 타입 시스템은 강력하지만, 때로는 개발자가 타입에 대해 컴파일러보다 더 잘 알고 있는 상황이 존재합니다.

이럴 경우, 타입 단언(Type Assertion)을 통해 컴파일러에게 "이 값은 내가 보장하는 이 타입이야"라고 명시할 수 있습니다.

 

가장 일반적인 문법은 as 키워드를 사용하는 것입니다

type CanvasLike = {
  id: string;
};

const unknownValue: unknown = { id: 'main_canvas' };

// Type Assertion: unknown → CanvasLike
const canvasLike = unknownValue as CanvasLike;

console.log('canvasLike.id:', canvasLike.id);

단언은 타입 검사를 우회하기 때문에, 절대로 무분별하게 사용해서는 안 됩니다.

 

✔️ 타입 단언 사용 시 주의점

타입 단언은 타입 검사를 우회하는 기능입니다.
실제로는 <canvas>가 아닌 다른 요소가 들어오거나, 아예 요소가 없는데도 억지로 HTMLCanvasElement라고 단언해 버리면, 런타임에서 치명적인 오류가 발생할 수 있습니다.
가능하다면 다음과 같이 런타임 체크(타입 좁히기)를 먼저 고려하는 것이 더 안전합니다.

const el = document.getElementById("main_canvas");

if (el instanceof HTMLCanvasElement) {
  // 여기서는 TypeScript가 el을 HTMLCanvasElement로 인식합니다.
  const ctx = el.getContext("2d");
  ctx?.fillRect(0, 0, 100, 100);
}

▸ type(타입 별칭)이나 interface는 전부 “타입스크립트 전용 개념”이라서 instanceof에 쓸 수 없습니다.

 

✔️ instanceof를 쓸 수 있는 경우
▸ class로 만든 인스턴스 (가장 일반적)
▸ 전통적인 생성자 함수로 만든 인스턴스
▸ Array, Date, RegExp, Error 등 내장 생성자 인스턴스
▸ Symbol.hasInstance를 구현한 함수/클래스에 대해 커스텀 체크

 

4. 타입 추론 (Type Inference) 이해하기: 자동으로 타입 유추

TypeScript의 가장 큰 장점 중 하나는 타입 추론(Type Inference)입니다.


타입 추론이란 개발자가 타입을 명시적으로 선언하지 않아도, TypeScript가 변수의 초기값이나 반환 값 등을 보고 자동으로 적절한 타입을 유추하는 기능입니다.

덕분에 코드가 간결해지고, 타입 안정성도 유지할 수 있습니다.

상황 타입 추론 가능 여부 타입 명시 필요 여부
변수 선언 + 초기값 있음 ✅ 가능 ⛔ 생략 가능
변수 선언 + 초기값 없음 ❌ 불가 ✅ 필요
함수 반환값 ✅ 가능 (매개변수에 타입 지정 시) ⛔ 생략 가능
함수 매개변수 ❌ 불가 ✅ 반드시 필요

 

🔷 타입 추론의 기본 원리

TypeScript는 변수에 초기값이 주어졌을 때, 또는 함수가 값을 반환할 때, 그 값을 기준으로 타입을 자동으로 판단합니다.

 

✔️ 변수 선언 시의 타입 추론

let favoriteColor: string = "blue";처럼 타입을 명시하지 않아도, TypeScript가 "blue"라는 값을 보고 string 타입으로 이해합니다.

let favoriteColor = "blue";
// ⤷ TypeScript는 "blue"가 문자열이므로 favoriteColor를 `string` 타입으로 추론합니다.

favoriteColor = "red";    // ✅ OK
// favoriteColor = 10;    // ❌ 오류: 'number'는 'string'에 할당할 수 없습니다.

 

✔️ 함수 반환값의 타입 추론

함수의 매개변수는 타입을 명시했기 때문에, 반환 타입도 명확하게 추론할 수 있습니다.

function calculateSum(a: number, b: number) {
  return a + b;
}

let result = calculateSum(5, 3);
// ⤷ TypeScript는 반환되는 값이 숫자이므로, result의 타입을 `number`로 추론합니다.

console.log(result * 2); // ✅ 16 출력

 

🔷 타입 명시가 필요한 경우

대부분의 경우 타입 추론만으로 충분하지만, 다음과 같은 상황에서는 타입을 명시적으로 지정해 주는 것이 좋습니다

 

1. 초기값 없이 선언한 변수

let data;
// ⤷ 아무 값도 지정하지 않으면 TypeScript는 `any` 타입으로 추론합니다.

data = 10;         // ✅ OK
data = "hello";    // ✅ OK (any는 어떤 타입이든 허용되므로 안전하지 않음)

// 해결방법
let count: number;
// count = "text"; // ❌ 오류: 'string'은 'number'에 할당될 수 없습니다.

 

2. 함수의 매개변수

함수의 매개변수는 초기값이 없기 때문에, TypeScript는 타입을 자동으로 추론할 수 없습니다.

따라서 반드시 타입을 명시해 주어야 합니다.

// 오류 예시:
// function multiply(a, b) {
//   return a * b;
// }
// ❌ 오류: 매개변수 'a'의 타입이 암시적으로 'any'입니다.

function multiply(a: number, b: number) {
  return a * b;
}

console.log(multiply(4, 5)); // ✅ 20 출력

 


※ 게시된 글 및 이미지 중 일부는 AI 도구의 도움을 받아 생성되거나 다듬어졌습니다.

반응형

 

반응형