🎭 Union Types: 여러 타입 중 하나로 정의하기 🎭
안녕, 친구들! 오늘은 TypeScript의 꿀잼 기능 중 하나인 Union Types에 대해 알아볼 거야. 🍯 Union Types라고 하면 뭔가 노동조합 같은 느낌이 들지 않아? 하지만 여기서 말하는 Union은 그런 게 아니라, 여러 타입을 하나로 '결합'한다는 의미야. 마치 여러 가지 재능을 가진 사람들이 모여 있는 재능넷처럼 말이지! 😉
🎨 Union Types란? 여러 타입 중 하나가 될 수 있는 값을 나타내는 방법이야. 쉽게 말해, "이 변수는 문자열일 수도 있고, 숫자일 수도 있어!"라고 말하는 거지.
자, 이제부터 Union Types의 세계로 빠져볼까? 준비됐어? 그럼 출발! 🚀
🌈 Union Types의 기본
Union Types를 사용하면 변수나 함수 매개변수가 여러 타입 중 하나일 수 있다고 선언할 수 있어. 이걸 어떻게 쓰냐고? 간단해! 파이프(|) 기호를 사용하면 돼.
let myVariable: string | number;
이렇게 하면 myVariable은 문자열이나 숫자 중 하나가 될 수 있어. cool하지? 😎
예를 들어볼까? 재능넷에서 사용자의 ID를 나타내는 변수를 만든다고 생각해보자. 사용자 ID는 문자열일 수도 있고, 숫자일 수도 있어.
let userId: string | number;
userId = 123; // OK
userId = "abc123"; // 이것도 OK
userId = true; // 에러! boolean은 안 돼요.
이렇게 하면 userId는 문자열이나 숫자만 받을 수 있고, 다른 타입을 할당하려고 하면 TypeScript가 "야, 그건 안 돼!"라고 말해줄 거야.
🚨 주의사항: Union Types를 사용할 때는 해당 타입들의 공통 메서드나 속성만 사용할 수 있어. 예를 들어, string | number 타입의 변수에서는 toString() 메서드는 사용할 수 있지만, toUpperCase() 같은 문자열 전용 메서드는 사용할 수 없어.
이제 기본은 알았으니, 좀 더 깊이 들어가볼까? 🏊♂️
🎭 Union Types와 함수
Union Types는 함수의 매개변수나 반환 값에도 사용할 수 있어. 이렇게 하면 함수가 여러 타입의 입력을 받거나, 여러 타입의 결과를 반환할 수 있지.
예를 들어, 재능넷에서 사용자의 프로필 정보를 가져오는 함수를 만든다고 생각해보자. 사용자 ID로 문자열이나 숫자를 받을 수 있게 하고 싶어.
function getUserProfile(userId: string | number): object {
// 사용자 프로필을 가져오는 로직
return {
id: userId,
name: "홍길동",
skills: ["프로그래밍", "디자인", "마케팅"]
};
}
console.log(getUserProfile(123)); // OK
console.log(getUserProfile("user456")); // 이것도 OK
이 함수는 문자열이나 숫자 형태의 userId를 받아서 사용자 프로필 객체를 반환해. 어떤 형태의 ID를 사용하든 함수가 잘 작동한다는 거지. 👍
💡 팁: 함수 내에서 Union Type의 매개변수를 사용할 때는 타입 가드(Type Guard)를 사용하면 더 안전하게 코드를 작성할 수 있어. 타입 가드에 대해서는 나중에 더 자세히 알아볼 거야!
자, 이제 Union Types가 함수에서 어떻게 사용되는지 알았지? 근데 여기서 끝이 아니야. Union Types는 더 복잡한 상황에서도 우리를 구원해줄 수 있어! 🦸♂️
🎨 리터럴 타입과 Union Types
Union Types의 진가는 리터럴 타입과 함께 사용할 때 더욱 빛을 발해. 리터럴 타입이 뭐냐고? 간단히 말해서 특정 값 자체를 타입으로 사용하는 거야.
예를 들어, 재능넷에서 사용자의 역할을 정의한다고 생각해보자. 사용자는 "admin", "user", "guest" 중 하나의 역할만 가질 수 있어.
type UserRole = "admin" | "user" | "guest";
let myRole: UserRole;
myRole = "admin"; // OK
myRole = "superuser"; // 에러! "superuser"는 UserRole에 없어요.
이렇게 하면 myRole 변수는 오직 "admin", "user", "guest" 중 하나의 값만 가질 수 있어. 다른 문자열을 할당하려고 하면 TypeScript가 에러를 발생시킬 거야.
이걸 활용해서 재능넷의 사용자 관리 함수를 만들어볼까?
function manageUser(userId: string | number, action: "ban" | "promote" | "demote"): void {
console.log(`User ${userId} is being ${action}ed`);
// 실제 사용자 관리 로직
}
manageUser("user123", "ban"); // OK
manageUser(456, "promote"); // OK
manageUser("admin789", "delete"); // 에러! "delete"는 허용된 action이 아니에요.
이 함수는 사용자 ID(문자열 또는 숫자)와 수행할 작업("ban", "promote", "demote" 중 하나)을 받아. 만약 허용되지 않은 작업을 시도하면 TypeScript가 컴파일 단계에서 에러를 발생시켜줘. 이렇게 하면 실수로 잘못된 작업을 수행하는 걸 방지할 수 있지!
🎭 재미있는 사실: 리터럴 타입과 Union Types를 함께 사용하면, 마치 열거형(enum)처럼 사용할 수 있어. 하지만 더 유연하고, 때로는 더 타입 안전해!
자, 이제 Union Types와 리터럴 타입의 조합이 얼마나 강력한지 알겠지? 이걸 잘 활용하면 코드의 안정성을 크게 높일 수 있어. 그럼 이제 좀 더 복잡한 예제로 넘어가볼까? 🚀
🧩 객체 타입과 Union Types
Union Types는 단순한 기본 타입뿐만 아니라 복잡한 객체 타입에도 적용할 수 있어. 이걸 이용하면 정말 다양한 상황을 표현할 수 있지.
예를 들어, 재능넷에서 사용자가 등록한 재능을 나타내는 타입을 만들어보자. 재능은 '기술 재능'과 '예술 재능' 두 가지 종류가 있다고 가정해볼게.
type TechnicalSkill = {
type: "technical";
language: string;
yearsOfExperience: number;
};
type ArtisticSkill = {
type: "artistic";
medium: string;
style: string;
};
type Skill = TechnicalSkill | ArtisticSkill;
function describeSkill(skill: Skill): string {
switch(skill.type) {
case "technical":
return `${skill.language} 개발자 (${skill.yearsOfExperience}년 경력)`;
case "artistic":
return `${skill.medium} ${skill.style} 아티스트`;
}
}
const coding: Skill = { type: "technical", language: "TypeScript", yearsOfExperience: 3 };
const painting: Skill = { type: "artistic", medium: "유화", style: "인상주의" };
console.log(describeSkill(coding)); // "TypeScript 개발자 (3년 경력)"
console.log(describeSkill(painting)); // "유화 인상주의 아티스트"
여기서 Skill 타입은 TechnicalSkill과 ArtisticSkill의 Union Type이야. 이렇게 하면 하나의 Skill 변수가 두 가지 타입의 객체 중 하나를 가질 수 있지.
그리고 describeSkill 함수는 이 Union Type을 매개변수로 받아서 적절한 설명을 반환해. 함수 내부에서는 skill.type을 확인해서 어떤 종류의 재능인지 구분하고, 그에 맞는 설명을 만들어내지.
🔍 주목할 점: 이런 방식을 '태그된 유니온(Tagged Union)' 또는 '판별 유니온(Discriminated Union)'이라고 불러. type 속성이 어떤 종류의 객체인지 '태그'하는 역할을 하기 때문이지.
이 방식을 사용하면 복잡한 데이터 구조도 타입 안전하게 다룰 수 있어. 예를 들어, 재능넷에서 사용자 프로필을 표현할 때 이런 방식을 사용할 수 있겠지?
type UserProfile = {
id: string | number;
name: string;
skills: Skill[];
};
const myProfile: UserProfile = {
id: "user123",
name: "김재능",
skills: [
{ type: "technical", language: "JavaScript", yearsOfExperience: 5 },
{ type: "artistic", medium: "디지털 아트", style: "미니멀리즘" }
]
};
myProfile.skills.forEach(skill => console.log(describeSkill(skill)));
이렇게 하면 사용자의 다양한 재능을 타입 안전하게 표현하고 다룰 수 있어. 멋지지 않아? 😎
자, 이제 Union Types가 얼마나 강력한지 알겠지? 하지만 아직 더 있어! Union Types를 제대로 활용하려면 '타입 가드'라는 개념도 알아야 해. 다음 섹션에서 자세히 알아보자! 🚀
🛡️ 타입 가드와 Union Types
타입 가드(Type Guard)는 Union Type의 변수를 사용할 때 특정 타입임을 보장하는 방법이야. 이걸 사용하면 Union Type의 변수를 더 안전하고 효과적으로 다룰 수 있지.
타입 가드에는 여러 가지 방법이 있어. 가장 기본적인 것부터 살펴볼까?
1. typeof 연산자 사용하기
function printId(id: string | number) {
if (typeof id === "string") {
console.log(id.toUpperCase());
} else {
console.log(id.toFixed(2));
}
}
printId("user123"); // USER123
printId(123.456); // 123.46
이 예제에서 typeof 연산자를 사용해 id가 문자열인지 숫자인지 확인하고 있어. TypeScript는 이 검사를 인식하고, 각 블록 내에서 id를 적절한 타입으로 처리해.
2. instanceof 연산자 사용하기
객체의 경우 instanceof를 사용할 수 있어. 재능넷의 사용자 시스템을 예로 들어볼까?
class NormalUser {
constructor(public username: string) {}
sayHello() {
console.log(`안녕하세요, ${this.username}입니다!`);
}
}
class AdminUser {
constructor(public username: string, public adminLevel: number) {}
performAdminTask() {
console.log(`${this.username} 관리자가 관리 작업을 수행합니다.`);
}
}
function greetUser(user: NormalUser | AdminUser) {
if (user instanceof AdminUser) {
console.log(`관리자 ${user.username}님, 환영합니다!`);
user.performAdminTask();
} else {
user.sayHello();
}
}
const normalUser = new NormalUser("김재능");
const adminUser = new AdminUser("박관리", 1);
greetUser(normalUser); // 안녕하세요, 김재능입니다!
greetUser(adminUser); // 관리자 박관리님, 환영합니다! 박관리 관리자가 관리 작업을 수행합니다.
이 예제에서 instanceof를 사용해 user가 AdminUser의 인스턴스인지 확인하고 있어. 이를 통해 각 사용자 타입에 맞는 동작을 수행할 수 있지.
3. in 연산자 사용하기
객체에 특정 프로퍼티가 있는지 확인하는 방법도 있어. 재능넷의 재능 시스템을 다시 예로 들어볼게.
type TechnicalSkill = { type: "technical", language: string };
type ArtisticSkill = { type: "artistic", medium: string };
function describeSkill(skill: TechnicalSkill | ArtisticSkill) {
if ("language" in skill) {
console.log(`기술 재능: ${skill.language}`);
} else {
console.log(`예술 재능: ${skill.medium}`);
}
}
describeSkill({ type: "technical", language: "TypeScript" }); // 기술 재능: TypeScript
describeSkill({ type: "artistic", medium: "수채화" }); // 예술 재능: 수채화
여기서는 'in' 연산자를 사용해 skill 객체에 'language' 프로퍼티가 있는지 확인하고 있어. 이 방법은 객체의 구조를 기반으로 타입을 구분할 때 유용해.
4. 사용자 정의 타입 가드
때로는 더 복잡한 로직이 필요할 수 있어. 이럴 때는 사용자 정의 타입 가드를 만들 수 있지.
function isTechnicalSkill(skill: TechnicalSkill | ArtisticSkill): skill is TechnicalSkill {
return (skill as TechnicalSkill).language !== undefined;
}
function enhanceSkill(skill: TechnicalSkill | ArtisticSkill) {
if (isTechnicalSkill(skill)) {
console.log(`${skill.language} 실력을 향상시킵니다.`);
} else {
console.log(`${skill.medium} 기법을 연마합니다.`);
}
}
enhanceSkill({ type: "technical", language: "Python" }); // Python 실력을 향상시킵니다.
enhanceSkill({ type: "artistic", medium: "조각" }); // 조각 기법을 연마합니다.
여기서 isTechnicalSkill 함수는 사용자 정의 타입 가드야. 이 함수는 skill이 TechnicalSkill 타입인지 확인하고, 그 결과를 TypeScript에게 알려줘. 이렇게 하면 더 복잡한 타입 체크도 가능해지지.
💡 팁: 타입 가드를 잘 사용하면 Union Types를 더욱 안전하고 효과적으로 다룰 수 있어. 코드의 가독성도 좋아지고, 버그도 줄일 수 있지!
자, 이제 타입 가드에 대해 알았으니 Union Types를 더 자신있게 사용할 수 있겠지? 하지만 아직 끝이 아니야. Union Types의 고급 기능들이 더 남아있어! 다음 섹션에서 계속 알아보자! 🚀
🎓 Union Types의 고급 기능
자, 이제 Union Types의 기본을 마스터했으니 좀 더 고급 기능으로 넘어가볼까? 여기서부터는 진짜 TypeScript의 강력함을 느낄 수 있을 거야! 😎
1. Discriminated Unions (판별 유니온)
이미 간단히 다뤘지만, 판별 유니온은 정말 유용해서 더 자세히 알아볼 가치가 있어. 재능넷의 결제 시스템을 예로 들어볼게.
type Cash = {
kind: "cash";
amount: number;
};
type CreditCard = {
kind: "credit";
cardNumber: string;
securityCode: string;
};
type BankTransfer = {
kind: "transfer";
accountNumber: string;
bankCode: string;
};
type Payment = Cash | CreditCard | BankTransfer;
function processPayment(payment: Payment) {
switch(payment.kind) {
case "cash":
console.log(`${payment.amount}원을 현금으로 받았습니다.`);
break;
case "credit":
console.log(`카드 번호 ${payment.cardNumber}로 결제를 진행합니다.`);
break;
case "transfer":
console.log(`${payment.bankCode} 은행의 계좌 ${payment.accountNumber}로 이체를 진행합니다.`);
break;
}
}
processPayment({ kind: "cash", amount: 50000 });
processPayment({ kind: "credit", cardNumber: "1234-5678-9012-3456", securityCode: "123" });
processPayment({ kind: "transfer", accountNumber: "987-65-43210", bankCode: "WB" });
여기서 'kind' 속성이 판별자(discriminator) 역할을 해. TypeScript는 이를 통해 각 case에서 정확한 타입을 추론할 수 있지. 이렇게 하면 타입 안전성을 유지하면서도 다양한 결제 방식을 쉽게 처리할 수 있어.
2. Exhaustiveness Checking (완전성 검사)
판별 유니온을 사용할 때, 모든 경우를 처리했는지 확인하는 것이 중요해. TypeScript는 이를 위한 트릭을 제공해.
function assertNever(x: never): never {
throw new Error("Unexpected object: " + x);
}
function processPayment(payment: Payment) {
switch(payment.kind) {
case "cash":
console.log(`${payment.amount}원을 현금으로 받았습니다.`);
break;
case "credit":
console.log(`카드 번호 ${payment.cardNumber}로 결제를 진행합니다.`);
break;
case "transfer":
console.log(`${payment.bankCode} 은행의 계좌 ${payment.accountNumber}로 이체를 진행합니다.`);
break;
default:
assertNever(payment); // 여기에 도달하면 컴파일 에러!
}
}
assertNever 함수를 사용하면, 모든 경우를 처리하지 않았을 때 컴파일 에러가 발생해. 이렇게 하면 나중에 Payment 타입에 새로운 결제 방식을 추가했을 때, 반드시 그 케이스를 처리하도록 강제할 수 있지.
3. Intersection Types와의 조합
Union Types는 Intersection Types와 함께 사용될 때 더욱 강력해져. 재능넷의 사용자 프로필 시스템을 예로 들어볼게.
type BasicProfile = {
name: string;
age: number;
};
type SkillProfile = {
skills: string[];
};
type SocialProfile = {
socialLinks: { [key: string]: string };
};
type UserProfile = BasicProfile & (SkillProfile | SocialProfile);
function displayProfile(profile: UserProfile) {
console.log(`이름: ${profile.name}, 나이: ${profile.age}`);
if ("skills" in profile) {
console.log(`보유 기술: ${profile.skills.join(", ")}`);
} else {
console.log(`소셜 링크: ${Object.keys(profile.socialLinks).join(", ")}`);
}
}
displayProfile({
name: "김재능",
age: 28,
skills: ["TypeScript", "React", "Node.js"]
});
displayProfile({
name: "이소셜",
age: 32,
socialLinks: {
twitter: "https://twitter.com/leesocial",
instagram: "https://instagram.com/leesocial"
}
});
여기서 UserProfile은 BasicProfile과 (SkillProfile 또는 SocialProfile)의 조합이야. 이렇게 하면 모든 사용자가 기본 정보를 가지면서, 추가로 기술 정보나 소셜 정보 중 하나를 가질 수 있게 돼. 이런 방식으로 복잡한 타입 구조를 유연하게 표현할 수 있지.
4. Conditional Types와의 활용
Union Types는 Conditional Types와 함께 사용될 때 더욱 강력한 타입 로직을 구현할 수 있어. 재능넷의 검색 시스템을 예로 들어볼게.
type SearchBy<T> = T extends "skill" ? { skill: string } :
T extends "name" ? { name: string } :
T extends "location" ? { city: string, country: string } :
never;
function search<T extends "skill" | "name" | "location">(searchType: T, query: SearchBy<T>) {
// 검색 로직
console.log(`Searching by ${searchType}:`, query);
}
search("skill", { skill: "TypeScript" });
search("name", { name: "김재능" });
search("location", { city: "서울", country: "대한민국" });
// search("age", { age: 30 }); // 컴파일 에러!
이 예제에서 SearchBy는 Conditional Type을 사용해 검색 타입에 따라 적절한 쿼리 객체 타입을 반환해. 이렇게 하면 타입 안전성을 유지하면서도 유연한 검색 기능을 구현할 수 있지.
🚀 고급 팁: Union Types, Intersection Types, Conditional Types를 조합하면 정말 복잡한 타입 로직도 표현할 수 있어. 하지만 너무 복잡해지면 가독성이 떨어질 수 있으니 적절한 균형을 찾는 게 중요해!
5. Mapped Types와 Union Types
Mapped Types를 Union Types와 함께 사용하면 더욱 강력한 타입 변환을 할 수 있어. 재능넷의 사용자 설정 시스템을 예로 들어볼게.
type UserPreferences = {
theme: "light" | "dark";
fontSize: "small" | "medium" | "large";
notifications: "all" | "important" | "none";
};
type PreferenceToggle<T> = {
[K in keyof T]: {
enable: boolean;
value: T[K];
}
};
type UserPreferencesToggle = PreferenceToggle<UserPreferences>;
const userSettings: UserPreferencesToggle = {
theme: { enable: true, value: "dark" },
fontSize: { enable: false, value: "medium" },
notifications: { enable: true, value: "important" }
};
function applySettings(settings: UserPreferencesToggle) {
for (const [key, { enable, value }] of Object.entries(settings)) {
if (enable) {
console.log(`Applying ${key}: ${value}`);
} else {
console.log(`Skipping ${key}`);
}
}
}
applySettings(userSettings);
이 예제에서 PreferenceToggle은 Mapped Type을 사용해 각 설정에 enable 속성을 추가해. 이렇게 하면 각 설정을 개별적으로 활성화하거나 비활성화할 수 있지.
마무리
자, 이제 Union Types의 고급 기능들까지 살펴봤어. 이런 기능들을 잘 활용하면 정말 강력하고 유연한 타입 시스템을 구축할 수 있지. TypeScript의 진정한 힘은 이런 고급 기능들을 조합해서 사용할 때 나타나!
Union Types는 단순히 여러 타입 중 하나를 선택하는 것 이상의 의미를 가져. 복잡한 비즈니스 로직, 다양한 사용자 시나리오, 그리고 유연한 API 설계 등 다양한 상황에서 활용될 수 있어. 특히 재능넷 같은 복잡한 플랫폼을 개발할 때 이런 기능들이 큰 도움이 될 거야.
계속 연습하고 실제 프로젝트에 적용해보면서 이 개념들을 마스터해나가길 바라! TypeScript의 세계는 정말 깊고 넓으니까, 항상 새로운 것을 배울 준비를 하고 있어야 해. 화이팅! 🚀🌟
🎉 마무리: Union Types 마스터하기
우와, 정말 긴 여정이었지만 드디어 Union Types의 세계를 탐험했어! 👏 이제 Union Types가 얼마나 강력하고 유연한 도구인지 알게 됐을 거야. 재능넷 같은 복잡한 플랫폼을 개발할 때 이런 지식은 정말 큰 도움이 될 거야.
우리가 배운 내용을 간단히 정리해볼까?
- Union Types의 기본 개념과 사용법
- 함수에서 Union Types 활용하기
- 리터럴 타입과 Union Types의 조합
- 객체 타입과 Union Types
- 타입 가드를 이용한 안전한 Union Types 사용
- Discriminated Unions (판별 유니온)
- Exhaustiveness Checking (완전성 검사)
- Intersection Types와의 조합
- Conditional Types와 Union Types
- Mapped Types와 Union Types
이 모든 개념들을 완전히 이해하고 활용하는 데는 시간이 걸릴 거야. 하지만 걱정하지 마! 프로그래밍은 계속 연습하고 경험을 쌓아가는 과정이니까. 😊
💡 앞으로의 학습 팁:
- 실제 프로젝트에 Union Types를 적용해보세요.
- 다른 개발자들의 코드를 읽고 Union Types가 어떻게 사용되는지 관찰하세요.
- 복잡한 비즈니스 로직을 Union Types로 모델링해보는 연습을 하세요.
- TypeScript의 공식 문서를 자주 참고하세요. 항상 새로운 기능이 추가되고 있어요!
Union Types는 TypeScript의 가장 강력한 기능 중 하나야. 이를 마스터하면 더 안전하고, 더 유연하며, 더 표현력 있는 코드를 작성할 수 있을 거야. 재능넷 같은 복잡한 시스템을 개발할 때, 이런 타입 시스템의 힘을 제대로 활용하면 정말 큰 차이를 만들 수 있지.
자, 이제 당신은 Union Types의 진정한 마스터로 거듭났어! 🎓 이 지식을 가지고 더 멋진 프로젝트를 만들어나가길 바라. 항상 새로운 것을 배우고, 도전하는 자세를 잃지 마세요. TypeScript의 세계는 아직 더 많은 흥미로운 기능들로 가득하니까요!
다음에 또 다른 흥미로운 TypeScript 주제로 만나길 바라요. 코딩 재능 가득한 여러분, 모두 화이팅! 🚀💻✨