엄격한 값 제약을 강제하고, 코드 명확성을 높이며, 오류를 방지하는 강력한 기능인 TypeScript 리터럴 타입을 살펴보세요. 실용적인 예제와 고급 기술을 통해 학습합니다.
TypeScript 리터럴 타입: 정확한 값 제약 마스터하기
JavaScript의 상위 집합인 TypeScript는 동적인 웹 개발 세계에 정적 타이핑을 도입합니다. 가장 강력한 기능 중 하나는 리터럴 타입이라는 개념입니다. 리터럴 타입을 사용하면 변수나 속성이 가질 수 있는 정확한 값을 지정할 수 있어, 향상된 타입 안정성을 제공하고 예기치 않은 오류를 방지할 수 있습니다. 이 글에서는 리터럴 타입의 문법, 사용법, 그리고 이점을 실용적인 예제와 함께 깊이 있게 탐구할 것입니다.
리터럴 타입이란 무엇인가?
string
, number
, 또는 boolean
과 같은 전통적인 타입과 달리, 리터럴 타입은 광범위한 값의 범주를 나타내지 않습니다. 대신, 특정하고 고정된 값을 나타냅니다. TypeScript는 세 가지 종류의 리터럴 타입을 지원합니다:
- 문자열 리터럴 타입: 특정 문자열 값을 나타냅니다.
- 숫자 리터럴 타입: 특정 숫자 값을 나타냅니다.
- 불리언 리터럴 타입:
true
또는false
라는 특정 값을 나타냅니다.
리터럴 타입을 사용하면 데이터의 실제 제약 조건을 반영하는 더 정확한 타입 정의를 만들 수 있으며, 이는 더 견고하고 유지보수하기 쉬운 코드로 이어집니다.
문자열 리터럴 타입
문자열 리터럴 타입은 가장 흔하게 사용되는 리터럴 타입입니다. 이를 통해 변수나 속성이 미리 정의된 문자열 값 집합 중 하나만 가질 수 있도록 지정할 수 있습니다.
기본 문법
문자열 리터럴 타입을 정의하는 문법은 간단합니다:
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
이는 만약 TypeA
가 TypeB
에 할당 가능하면 결과 타입은 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 프로젝트에서 리터럴 타입을 효과적으로 사용하려면 다음 모범 사례를 고려하세요:
- 제약 조건 강제를 위해 리터럴 타입 사용하기: 코드에서 변수나 속성이 특정 값만 가져야 하는 부분을 식별하고 리터럴 타입을 사용하여 이러한 제약 조건을 강제하세요.
- 리터럴 타입을 유니언 타입과 결합하기: 리터럴 타입을 유니언 타입과 결합하여 더 유연하고 표현력이 풍부한 타입 정의를 만드세요.
- 가독성을 위해 타입 별칭 사용하기: 타입 별칭을 사용하여 리터럴 타입에 의미 있는 이름을 부여하여 코드의 가독성과 유지보수성을 향상시키세요.
- 리터럴 추론 활용하기:
const
변수를 사용하여 TypeScript의 리터럴 추론 기능을 활용하세요. - enum 사용 고려하기: 논리적으로 관련이 있고 내부적인 숫자 표현이 필요한 고정된 값 집합의 경우, 리터럴 타입 대신 enum을 사용하세요. 그러나 런타임 비용 및 특정 시나리오에서 덜 엄격한 타입 검사 가능성과 같은 리터럴 타입 대비 enum의 단점에 유의하세요.
- 복잡한 시나리오를 위해 조건부 타입 사용하기: 다른 타입에 의존하는 타입을 정의해야 할 때, 조건부 타입을 리터럴 타입과 함께 사용하여 매우 유연하고 강력한 타입 시스템을 만드세요.
- 엄격함과 유연성의 균형 맞추기: 리터럴 타입은 뛰어난 타입 안정성을 제공하지만, 코드를 과도하게 제약하지 않도록 주의하세요. 리터럴 타입을 사용할지 여부를 선택할 때 엄격함과 유연성 사이의 장단점을 고려하세요.
리터럴 타입 사용의 이점
- 향상된 타입 안정성: 리터럴 타입을 사용하면 더 정확한 타입 제약 조건을 정의할 수 있어, 유효하지 않은 값으로 인한 런타임 오류의 위험을 줄입니다.
- 코드 명확성 향상: 변수 및 속성에 허용되는 값을 명시적으로 지정함으로써, 리터럴 타입은 코드를 더 읽기 쉽고 이해하기 쉽게 만듭니다.
- 더 나은 자동 완성: IDE는 리터럴 타입을 기반으로 더 나은 자동 완성 제안을 제공하여 개발자 경험을 향상시킬 수 있습니다.
- 안전한 리팩토링: 리터럴 타입은 TypeScript 컴파일러가 리팩토링 과정에서 발생하는 모든 타입 오류를 잡아주기 때문에 자신감을 가지고 코드를 리팩토링하는 데 도움이 될 수 있습니다.
- 인지 부하 감소: 가능한 값의 범위를 줄임으로써, 리터럴 타입은 개발자의 인지 부하를 낮출 수 있습니다.
결론
TypeScript 리터럴 타입은 엄격한 값 제약을 강제하고, 코드 명확성을 향상시키며, 오류를 방지할 수 있는 강력한 기능입니다. 문법, 사용법, 그리고 이점을 이해함으로써, 리터럴 타입을 활용하여 더 견고하고 유지보수하기 쉬운 TypeScript 애플리케이션을 만들 수 있습니다. 색상 팔레트 및 API 엔드포인트 정의부터 다양한 언어 처리 및 타입-안전한 이벤트 핸들러 생성에 이르기까지, 리터럴 타입은 개발 워크플로우를 크게 향상시킬 수 있는 광범위한 실용적인 응용 프로그램을 제공합니다.