한국어

엄격한 값 제약을 강제하고, 코드 명확성을 높이며, 오류를 방지하는 강력한 기능인 TypeScript 리터럴 타입을 살펴보세요. 실용적인 예제와 고급 기술을 통해 학습합니다.

TypeScript 리터럴 타입: 정확한 값 제약 마스터하기

JavaScript의 상위 집합인 TypeScript는 동적인 웹 개발 세계에 정적 타이핑을 도입합니다. 가장 강력한 기능 중 하나는 리터럴 타입이라는 개념입니다. 리터럴 타입을 사용하면 변수나 속성이 가질 수 있는 정확한 값을 지정할 수 있어, 향상된 타입 안정성을 제공하고 예기치 않은 오류를 방지할 수 있습니다. 이 글에서는 리터럴 타입의 문법, 사용법, 그리고 이점을 실용적인 예제와 함께 깊이 있게 탐구할 것입니다.

리터럴 타입이란 무엇인가?

string, number, 또는 boolean과 같은 전통적인 타입과 달리, 리터럴 타입은 광범위한 값의 범주를 나타내지 않습니다. 대신, 특정하고 고정된 값을 나타냅니다. TypeScript는 세 가지 종류의 리터럴 타입을 지원합니다:

리터럴 타입을 사용하면 데이터의 실제 제약 조건을 반영하는 더 정확한 타입 정의를 만들 수 있으며, 이는 더 견고하고 유지보수하기 쉬운 코드로 이어집니다.

문자열 리터럴 타입

문자열 리터럴 타입은 가장 흔하게 사용되는 리터럴 타입입니다. 이를 통해 변수나 속성이 미리 정의된 문자열 값 집합 중 하나만 가질 수 있도록 지정할 수 있습니다.

기본 문법

문자열 리터럴 타입을 정의하는 문법은 간단합니다:


type AllowedValues = "value1" | "value2" | "value3";

이는 "value1", "value2", 또는 "value3" 문자열만 가질 수 있는 AllowedValues라는 타입을 정의합니다.

실용적인 예제

1. 색상 팔레트 정의하기:

UI 라이브러리를 구축하고 있으며 사용자가 미리 정의된 팔레트의 색상만 지정하도록 보장하고 싶다고 상상해 보세요:


type Color = "red" | "green" | "blue" | "yellow";

function paintElement(element: HTMLElement, color: Color) {
  element.style.backgroundColor = color;
}

paintElement(document.getElementById("myElement")!, "red"); // 유효함
paintElement(document.getElementById("myElement")!, "purple"); // 오류: '"purple"' 타입의 인수는 'Color' 타입의 매개변수에 할당할 수 없습니다.

이 예제는 문자열 리터럴 타입이 어떻게 허용된 값의 엄격한 집합을 강제하여 개발자가 실수로 유효하지 않은 색상을 사용하는 것을 방지하는지 보여줍니다.

2. API 엔드포인트 정의하기:

API로 작업할 때, 종종 허용된 엔드포인트를 지정해야 합니다. 문자열 리터럴 타입이 이를 강제하는 데 도움이 될 수 있습니다:


type APIEndpoint = "/users" | "/posts" | "/comments";

function fetchData(endpoint: APIEndpoint) {
  // ... 지정된 엔드포인트에서 데이터를 가져오는 구현
  console.log(`Fetching data from ${endpoint}`);
}

fetchData("/users"); // 유효함
fetchData("/products"); // 오류: '"/products"' 타입의 인수는 'APIEndpoint' 타입의 매개변수에 할당할 수 없습니다.

이 예제는 fetchData 함수가 유효한 API 엔드포인트로만 호출될 수 있도록 보장하여, 오타나 잘못된 엔드포인트 이름으로 인한 오류 위험을 줄여줍니다.

3. 다른 언어 처리하기 (국제화 - i18n):

글로벌 애플리케이션에서는 여러 언어를 처리해야 할 수 있습니다. 문자열 리터럴 타입을 사용하여 애플리케이션이 지정된 언어만 지원하도록 보장할 수 있습니다:


type Language = "en" | "es" | "fr" | "de" | "zh";

function translate(text: string, language: Language): string {
  // ... 텍스트를 지정된 언어로 번역하는 구현
  console.log(`Translating '${text}' to ${language}`);
  return "Translated text"; // 플레이스홀더
}

translate("Hello", "en"); // 유효함
translate("Hello", "ja"); // 오류: '"ja"' 타입의 인수는 'Language' 타입의 매개변수에 할당할 수 없습니다.

이 예제는 애플리케이션 내에서 지원되는 언어만 사용되도록 보장하는 방법을 보여줍니다.

숫자 리터럴 타입

숫자 리터럴 타입을 사용하면 변수나 속성이 특정 숫자 값만 가질 수 있도록 지정할 수 있습니다.

기본 문법

숫자 리터럴 타입을 정의하는 문법은 문자열 리터럴 타입과 유사합니다:


type StatusCode = 200 | 404 | 500;

이는 200, 404, 또는 500 숫자만 가질 수 있는 StatusCode라는 타입을 정의합니다.

실용적인 예제

1. HTTP 상태 코드 정의하기:

숫자 리터럴 타입을 사용하여 HTTP 상태 코드를 나타낼 수 있으며, 이를 통해 애플리케이션에서 유효한 코드만 사용되도록 보장할 수 있습니다:


type HTTPStatus = 200 | 400 | 401 | 403 | 404 | 500;

function handleResponse(status: HTTPStatus) {
  switch (status) {
    case 200:
      console.log("Success!");
      break;
    case 400:
      console.log("Bad Request");
      break;
    // ... 다른 케이스들
    default:
      console.log("Unknown Status");
  }
}

handleResponse(200); // 유효함
handleResponse(600); // 오류: '600' 타입의 인수는 'HTTPStatus' 타입의 매개변수에 할당할 수 없습니다.

이 예제는 유효한 HTTP 상태 코드의 사용을 강제하여, 부정확하거나 비표준 코드를 사용하여 발생하는 오류를 방지합니다.

2. 고정된 옵션 나타내기:

숫자 리터럴 타입을 사용하여 설정 객체 내의 고정된 옵션을 나타낼 수 있습니다:


type RetryAttempts = 1 | 3 | 5;

interface Config {
  retryAttempts: RetryAttempts;
}

const config1: Config = { retryAttempts: 3 }; // 유효함
const config2: Config = { retryAttempts: 7 }; // 오류: '{ retryAttempts: 7; }' 타입은 'Config' 타입에 할당할 수 없습니다.

이 예제는 retryAttempts에 가능한 값을 특정 집합으로 제한하여, 설정의 명확성과 신뢰성을 향상시킵니다.

불리언 리터럴 타입

불리언 리터럴 타입은 true 또는 false라는 특정 값을 나타냅니다. 문자열이나 숫자 리터럴 타입보다 활용도가 낮아 보일 수 있지만, 특정 시나리오에서는 유용할 수 있습니다.

기본 문법

불리언 리터럴 타입을 정의하는 문법은 다음과 같습니다:


type IsEnabled = true | false;

하지만, true | false를 직접 사용하는 것은 boolean 타입과 동일하기 때문에 중복됩니다. 불리언 리터럴 타입은 다른 타입과 결합되거나 조건부 타입에서 더 유용합니다.

실용적인 예제

1. 설정을 사용한 조건부 로직:

불리언 리터럴 타입을 사용하여 설정 플래그에 따라 함수의 동작을 제어할 수 있습니다:


interface FeatureFlags {
  darkMode: boolean;
  newUserFlow: boolean;
}

function initializeApp(flags: FeatureFlags) {
  if (flags.darkMode) {
    // 다크 모드 활성화
    console.log("Enabling dark mode...");
  } else {
    // 라이트 모드 사용
    console.log("Using light mode...");
  }

  if (flags.newUserFlow) {
    // 새로운 사용자 플로우 활성화
    console.log("Enabling new user flow...");
  } else {
    // 기존 사용자 플로우 사용
    console.log("Using old user flow...");
  }
}

initializeApp({ darkMode: true, newUserFlow: false });

이 예제는 표준 boolean 타입을 사용하지만, 나중에 설명할 조건부 타입과 결합하여 더 복잡한 동작을 만들 수 있습니다.

2. 구별된 유니언 (Discriminated Unions):

불리언 리터럴 타입은 유니언 타입에서 판별자(discriminator)로 사용될 수 있습니다. 다음 예제를 살펴보세요:


interface SuccessResult {
  success: true;
  data: any;
}

interface ErrorResult {
  success: false;
  error: string;
}

type Result = SuccessResult | ErrorResult;

function processResult(result: Result) {
  if (result.success) {
    console.log("Success:", result.data);
  } else {
    console.error("Error:", result.error);
  }
}

processResult({ success: true, data: { name: "John" } });
processResult({ success: false, error: "Failed to fetch data" });

여기서 불리언 리터럴 타입인 success 속성은 판별자 역할을 하여, TypeScript가 if 문 내에서 result의 타입을 좁힐 수 있도록 합니다.

리터럴 타입과 유니언 타입 결합하기

리터럴 타입은 유니언 타입(| 연산자 사용)과 결합될 때 가장 강력합니다. 이를 통해 여러 특정 값 중 하나를 가질 수 있는 타입을 정의할 수 있습니다.

실용적인 예제

1. 상태 타입 정의하기:


type Status = "pending" | "in progress" | "completed" | "failed";

interface Task {
  id: number;
  description: string;
  status: Status;
}

const task1: Task = { id: 1, description: "Implement login", status: "in progress" }; // 유효함
const task2: Task = { id: 2, description: "Implement logout", status: "done" };       // 오류: '{ id: number; description: string; status: string; }' 타입은 'Task' 타입에 할당할 수 없습니다.

이 예제는 Task 객체에 대해 허용된 상태 값의 특정 집합을 강제하는 방법을 보여줍니다.

2. 장치 타입 정의하기:

모바일 애플리케이션에서는 다양한 장치 타입을 처리해야 할 수 있습니다. 문자열 리터럴 타입의 유니언을 사용하여 이를 나타낼 수 있습니다:


type DeviceType = "mobile" | "tablet" | "desktop";

function logDeviceType(device: DeviceType) {
  console.log(`Device type: ${device}`);
}

logDeviceType("mobile"); // 유효함
logDeviceType("smartwatch"); // 오류: '"smartwatch"' 타입의 인수는 'DeviceType' 타입의 매개변수에 할당할 수 없습니다.

이 예제는 logDeviceType 함수가 유효한 장치 타입으로만 호출되도록 보장합니다.

타입 별칭(Type Aliases)과 함께 리터럴 타입 사용하기

타입 별칭(type 키워드 사용)은 리터럴 타입에 이름을 부여하는 방법을 제공하여 코드를 더 읽기 쉽고 유지보수하기 좋게 만듭니다.

실용적인 예제

1. 통화 코드 타입 정의하기:


type CurrencyCode = "USD" | "EUR" | "GBP" | "JPY";

function formatCurrency(amount: number, currency: CurrencyCode): string {
  // ... 통화 코드에 따라 금액 서식을 지정하는 구현
  console.log(`Formatting ${amount} in ${currency}`);
  return "Formatted amount"; // 플레이스홀더
}

formatCurrency(100, "USD"); // 유효함
formatCurrency(200, "CAD"); // 오류: '"CAD"' 타입의 인수는 'CurrencyCode' 타입의 매개변수에 할당할 수 없습니다.

이 예제는 통화 코드 집합에 대해 CurrencyCode 타입 별칭을 정의하여, formatCurrency 함수의 가독성을 향상시킵니다.

2. 요일 타입 정의하기:


type DayOfWeek = "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday";

function isWeekend(day: DayOfWeek): boolean {
  return day === "Saturday" || day === "Sunday";
}

console.log(isWeekend("Monday"));   // false
console.log(isWeekend("Saturday")); // true
console.log(isWeekend("Funday"));   // 오류: '"Funday"' 타입의 인수는 'DayOfWeek' 타입의 매개변수에 할당할 수 없습니다.

리터럴 추론

TypeScript는 종종 변수에 할당하는 값을 기반으로 리터럴 타입을 자동으로 추론할 수 있습니다. 이는 특히 const 변수로 작업할 때 유용합니다.

실용적인 예제

1. 문자열 리터럴 타입 추론하기:


const apiKey = "your-api-key"; // TypeScript는 apiKey의 타입을 "your-api-key"로 추론합니다

function validateApiKey(key: "your-api-key") {
  return key === "your-api-key";
}

console.log(validateApiKey(apiKey)); // true

const anotherKey = "invalid-key";
console.log(validateApiKey(anotherKey)); // 오류: 'string' 타입의 인수는 '"your-api-key"' 타입의 매개변수에 할당할 수 없습니다.

이 예제에서 TypeScript는 apiKey의 타입을 문자열 리터럴 타입 "your-api-key"로 추론합니다. 그러나 상수가 아닌 값을 변수에 할당하면, TypeScript는 일반적으로 더 넓은 범위의 string 타입으로 추론합니다.

2. 숫자 리터럴 타입 추론하기:


const port = 8080; // TypeScript는 port의 타입을 8080으로 추론합니다

function startServer(portNumber: 8080) {
  console.log(`Starting server on port ${portNumber}`);
}

startServer(port); // 유효함

const anotherPort = 3000;
startServer(anotherPort); // 오류: 'number' 타입의 인수는 '8080' 타입의 매개변수에 할당할 수 없습니다.

조건부 타입(Conditional Types)과 함께 리터럴 타입 사용하기

리터럴 타입은 조건부 타입과 결합될 때 훨씬 더 강력해집니다. 조건부 타입을 사용하면 다른 타입에 의존하는 타입을 정의할 수 있어, 매우 유연하고 표현력이 풍부한 타입 시스템을 만들 수 있습니다.

기본 문법

조건부 타입의 문법은 다음과 같습니다:


TypeA extends TypeB ? TypeC : TypeD

이는 만약 TypeATypeB에 할당 가능하면 결과 타입은 TypeC가 되고, 그렇지 않으면 결과 타입은 TypeD가 된다는 의미입니다.

실용적인 예제

1. 상태를 메시지에 매핑하기:


type Status = "pending" | "in progress" | "completed" | "failed";

type StatusMessage = T extends "pending"
  ? "Waiting for action"
  : T extends "in progress"
  ? "Currently processing"
  : T extends "completed"
  ? "Task finished successfully"
  : "An error occurred";

function getStatusMessage(status: T): StatusMessage {
  switch (status) {
    case "pending":
      return "Waiting for action" as StatusMessage;
    case "in progress":
      return "Currently processing" as StatusMessage;
    case "completed":
      return "Task finished successfully" as StatusMessage;
    case "failed":
      return "An error occurred" as StatusMessage;
    default:
      throw new Error("유효하지 않은 상태입니다");
  }
}

console.log(getStatusMessage("pending"));    // Waiting for action
console.log(getStatusMessage("in progress")); // Currently processing
console.log(getStatusMessage("completed"));   // Task finished successfully
console.log(getStatusMessage("failed"));      // An error occurred

이 예제는 조건부 타입을 사용하여 각 가능한 상태를 해당 메시지에 매핑하는 StatusMessage 타입을 정의합니다. getStatusMessage 함수는 이 타입을 활용하여 타입-안전한 상태 메시지를 제공합니다.

2. 타입-안전한 이벤트 핸들러 만들기:


type EventType = "click" | "mouseover" | "keydown";

type EventData = T extends "click"
  ? { x: number; y: number; } // 클릭 이벤트 데이터
  : T extends "mouseover"
  ? { target: HTMLElement; }   // 마우스오버 이벤트 데이터
  : { key: string; }             // 키다운 이벤트 데이터

function handleEvent(type: T, data: EventData) {
  console.log(`Handling event type ${type} with data:`, data);
}

handleEvent("click", { x: 10, y: 20 }); // 유효함
handleEvent("mouseover", { target: document.getElementById("myElement")! }); // 유효함
handleEvent("keydown", { key: "Enter" }); // 유효함

handleEvent("click", { key: "Enter" }); // 오류: '{ key: string; }' 타입의 인수는 '{ x: number; y: number; }' 타입의 매개변수에 할당할 수 없습니다.

이 예제는 이벤트 타입에 따라 다른 데이터 구조를 정의하는 EventData 타입을 만듭니다. 이를 통해 각 이벤트 타입에 대해 올바른 데이터가 handleEvent 함수에 전달되도록 보장할 수 있습니다.

리터럴 타입 사용을 위한 모범 사례

TypeScript 프로젝트에서 리터럴 타입을 효과적으로 사용하려면 다음 모범 사례를 고려하세요:

리터럴 타입 사용의 이점

결론

TypeScript 리터럴 타입은 엄격한 값 제약을 강제하고, 코드 명확성을 향상시키며, 오류를 방지할 수 있는 강력한 기능입니다. 문법, 사용법, 그리고 이점을 이해함으로써, 리터럴 타입을 활용하여 더 견고하고 유지보수하기 쉬운 TypeScript 애플리케이션을 만들 수 있습니다. 색상 팔레트 및 API 엔드포인트 정의부터 다양한 언어 처리 및 타입-안전한 이벤트 핸들러 생성에 이르기까지, 리터럴 타입은 개발 워크플로우를 크게 향상시킬 수 있는 광범위한 실용적인 응용 프로그램을 제공합니다.