제네릭 프로그래밍: 재사용 가능한 코드 작성 🚀

콘텐츠 대표 이미지 - 제네릭 프로그래밍: 재사용 가능한 코드 작성 🚀

 

 

안녕하세요, 코딩 마법사 여러분! 오늘은 아주 특별한 여행을 떠나볼 거예요. 바로 제네릭 프로그래밍의 세계로요! 🧙‍♂️✨ 이 여행은 마치 재능넷에서 새로운 재능을 발견하는 것처럼 흥미진진할 거예요. 자, 이제 TypeScript의 마법 지팡이를 들고 출발해볼까요?

🎭 제네릭 프로그래밍이란? 타입을 마치 변수처럼 사용하여, 다양한 데이터 타입에 대해 재사용 가능한 컴포넌트를 만드는 프로그래밍 패러다임입니다.

여러분, 제네릭은 마치 재능넷에서 다재다능한 전문가를 찾는 것과 비슷해요. 한 가지 재능만 있는 게 아니라, 상황에 따라 다양한 재능을 발휘할 수 있는 그런 느낌이죠! 😉

제네릭의 기본: 타입 변수 소개 🎨

제네릭의 핵심은 바로 타입 변수예요. 이것은 마치 화가의 팔레트와 같아요. 다양한 색(타입)을 담을 수 있는 그릇이죠!

function identity<T>(arg: T): T {
    return arg;
}

let output = identity<string>("Hello, Generics!");
console.log(output);  // "Hello, Generics!"

여기서 <T>는 우리의 타입 변수예요. 마치 재능넷에서 다양한 재능을 찾을 수 있듯이, 이 T는 어떤 타입이든 받아들일 준비가 되어 있답니다! 👐

제네릭 타입 변수 설명 T string number boolean

이 그림에서 보시는 것처럼, 우리의 T는 마치 만능 주머니와 같아요. string, number, boolean 등 어떤 타입이든 받아들일 수 있죠. 이게 바로 제네릭의 매력이에요! 🌈

제네릭 함수 만들기: 코드의 재사용성을 높이자! 🔄

자, 이제 본격적으로 제네릭 함수를 만들어볼까요? 이건 마치 재능넷에서 다재다능한 프리랜서를 찾는 것과 같아요. 어떤 프로젝트가 와도 척척 해낼 수 있는 그런 느낌이죠!

function swap<T, U>(tuple: [T, U]): [U, T] {
    return [tuple[1], tuple[0]];
}

let result = swap<string, number>(["hello", 42]);
console.log(result);  // [42, "hello"]

이 swap 함수는 정말 대단해요! 어떤 타입의 쌍이 와도 서로 위치를 바꿔줄 수 있어요. 마치 재능넷에서 다양한 재능을 가진 사람들이 서로 협력하는 것처럼요. 👥

🌟 제네릭의 장점:

  • 코드 재사용성 증가
  • 타입 안정성 보장
  • 더 깔끔하고 읽기 쉬운 코드

이런 장점들 덕분에 제네릭은 현대 프로그래밍에서 없어서는 안 될 중요한 개념이 되었어요. 마치 재능넷이 다양한 재능을 연결해주는 플랫폼이 된 것처럼 말이죠! 😊

제네릭 인터페이스와 클래스: 구조에 유연성을 더하다 🏗️

제네릭은 함수뿐만 아니라 인터페이스와 클래스에서도 사용할 수 있어요. 이건 마치 재능넷에서 다양한 카테고리의 서비스를 제공하는 것과 비슷해요. 각 카테고리마다 특별한 규칙이 있지만, 기본 구조는 같죠!

interface Box<T> {
    contents: T;
}

let stringBox: Box<string> = { contents: "Hello, TypeScript!" };
let numberBox: Box<number> = { contents: 42 };

console.log(stringBox.contents);  // "Hello, TypeScript!"
console.log(numberBox.contents);  // 42

이 Box 인터페이스는 정말 유연해요! 문자열도 담을 수 있고, 숫자도 담을 수 있어요. 마치 재능넷에서 다양한 재능을 가진 사람들이 각자의 특별한 서비스를 제공하는 것처럼요. 🎁

제네릭 Box 인터페이스 설명 Box<string> "Hello, TypeScript!" Box<number> 42

이 그림을 보세요. 같은 Box 구조지만, 안에 담긴 내용물은 완전히 다르죠? 이게 바로 제네릭의 힘이에요! 🦸‍♂️

제네릭 제약조건: 타입에 규칙을 정하자 📏

때로는 제네릭 타입에 어떤 조건을 걸고 싶을 때가 있어요. 이건 마치 재능넷에서 특정 자격을 갖춘 전문가만 찾는 것과 비슷해요. 모든 재능이 아니라, 특정 조건을 만족하는 재능만 필요할 때 말이죠!

interface Lengthwise {
    length: number;
}

function logLength<T extends Lengthwise>(arg: T): void {
    console.log(arg.length);
}

logLength("Hello");  // 5
logLength([1, 2, 3]);  // 3
logLength({ length: 10 });  // 10
// logLength(3);  // Error: number doesn't have a .length property

여기서 T extends Lengthwise는 아주 중요한 의미를 가져요. "T는 반드시 length 속성을 가져야 해!"라고 말하는 거죠. 마치 재능넷에서 "이 프로젝트는 3년 이상의 경력자만 지원 가능합니다"라고 명시하는 것과 비슷해요. 🏅

🔍 제네릭 제약조건의 이점:

  • 타입 안정성 강화
  • 의도하지 않은 사용 방지
  • 코드의 명확성 증가

이렇게 제약조건을 사용하면, 우리의 코드는 더욱 안전하고 예측 가능해져요. 마치 재능넷에서 검증된 전문가들만 만날 수 있는 것처럼 말이죠! 👨‍🏫

제네릭과 유니언 타입: 더 큰 유연성을 향해 🌊

제네릭과 유니언 타입을 함께 사용하면 정말 놀라운 일이 벌어져요! 이건 마치 재능넷에서 여러 가지 재능을 동시에 찾을 수 있는 것과 같아요. 한 사람이 여러 재능을 가질 수 있듯이, 하나의 함수가 여러 타입을 다룰 수 있게 되는 거죠!

function processValue<T extends string | number>(value: T): void {
    if (typeof value === "string") {
        console.log(value.toUpperCase());
    } else {
        console.log(value.toFixed(2));
    }
}

processValue("hello");  // "HELLO"
processValue(3.14159);  // "3.14"

processValue 함수는 정말 똑똑해요! 문자열이 오면 대문자로 바꾸고, 숫자가 오면 소수점 둘째 자리까지 표시해줘요. 마치 재능넷에서 "디자인과 코딩, 둘 다 할 수 있는 프리랜서"를 찾는 것과 비슷하죠? 🎨💻

제네릭과 유니언 타입 설명 T string number

이 그림에서 보듯이, 우리의 T는 이제 string과 number 두 가지 얼굴을 가지고 있어요. 상황에 따라 적절한 모습으로 변신하는 거죠! 🦹‍♀️

제네릭 타입 추론: TypeScript의 똑똑함을 믿어보자 🧠

TypeScript는 정말 똑똑해서, 때로는 우리가 제네릭 타입을 명시적으로 지정하지 않아도 알아서 추론해줘요. 이건 마치 재능넷에서 여러분의 포트폴리오를 보고 "아, 이 분은 이런 재능이 있구나!"라고 알아채는 것과 비슷해요.

function identity<T>(arg: T): T {
    return arg;
}

let output = identity("Hello, TypeScript!");  // TypeScript가 T를 string으로 추론해요
console.log(output);  // "Hello, TypeScript!"

여기서 우리는 <string>을 명시하지 않았어요. 하지만 TypeScript는 전달된 인자를 보고 T가 string이라는 걸 스스로 알아냈죠. 정말 똑똑하지 않나요? 🤓

🎓 타입 추론의 이점:

  • 코드 간결성 향상
  • 개발 생산성 증가
  • 실수 가능성 감소

이렇게 타입 추론을 활용하면, 우리는 더 적은 코드로 더 많은 것을 표현할 수 있어요. 마치 재능넷에서 여러분의 숨겨진 재능까지 발견해주는 것처럼 말이죠! 🕵️‍♀️

제네릭 기본값: 안전망을 깔아두자 🥅

때로는 제네릭 타입에 기본값을 설정하고 싶을 때가 있어요. 이건 마치 재능넷에서 "특별한 요구사항이 없으면 이 정도 수준의 전문가를 연결해드릴게요"라고 하는 것과 비슷해요.

interface ApiResponse<T = any> {
    data: T;
    status: number;
}

function fetchData<T = string>(url: string): Promise<ApiResponse<T>> {
    // 실제 API 호출 로직
    return Promise.resolve({ data: "Some data" as any, status: 200 });
}

let response = fetchData("https://api.example.com");
console.log(response);  // Promise<ApiResponse<string>>

여기서 <T = any><T = string>은 정말 중요해요! 만약 타입을 지정하지 않으면, ApiResponse는 any를, fetchData는 string을 기본값으로 사용한다는 뜻이에요. 이렇게 하면 코드를 더 안전하게 만들 수 있죠. 마치 재능넷에서 기본 옵션을 제공하는 것과 같아요! 🛡️

제네릭 기본값 설명 T = string 기본값

이 그림에서 보듯이, 우리의 T는 이제 기본값이라는 안전망을 가지고 있어요. 무언가 잘못되더라도 string으로 돌아갈 수 있는 거죠! 🦺

제네릭 조건부 타입: 타입의 마법사가 되어보자 🧙‍♂️

제네릭 조건부 타입은 정말 강력해요! 이건 마치 재능넷에서 "만약 이런 조건이라면 이 전문가를, 아니라면 저 전문가를 연결해드릴게요"라고 하는 것과 비슷해요. 상황에 따라 다른 타입을 선택할 수 있는 거죠!

type CheckNumber<T> = T extends number ? "Yes, it's a number" : "No, it's not a number";

type Result1 = CheckNumber<42>;  // "Yes, it's a number"
type Result2 = CheckNumber<"Hello">;  // "No, it's not a number"

function checkType<T>(value: T): CheckNumber<T> {
    return (typeof value === "number" ? "Yes, it's a number" : "No, it's not a number") as CheckNumber<T>;
}

console.log(checkType(42));  // "Yes, it's a number"
console.log(checkType("Hello"));  // "No, it's not a number"

CheckNumber 타입은 정말 똑똑해요! 주어진 타입이 number인지 아닌지를 검사하고, 그에 따라 다른 문자열을 반환하죠. 이런 식으로 타입 수준에서 조건문을 사용할 수 있어요. 마치 재능넷에서 여러분의 요구사항에 따라 맞춤형 전문가를 찾아주는 것과 같아요! 🎯

🔮 제네릭 조건부 타입의 활용:

  • 타입 기반의 로직 구현
  • 복잡한 타입 관계 표현
  • 유연한 API 설계

이렇게 조건부 타입을 사용하면, 우리의 타입 시스템은 더욱 똑똑해지고 유연해져요. 마치 재능넷이 여러분의 복잡한 요구사항도 정확히 이해하고 최적의 해결책을 제시하는 것처럼 말이죠! 🧠💡

제네릭 매핑된 타입: 타입 변환의 마법 🔮

제네릭 매핑된 타입은 기존 타입을 기반으로 새로운 타입을 만들어내는 강력한 도구예요. 이건 마치 재능넷에서 "기존 서비스를 약간 수정해서 새로운 서비스를 만들어내는" 것과 비슷해요. 기존의 것을 활용하면서도 새로운 가치를 창출하는 거죠!

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

interface Person {
    name: string;
    age: number;
}

type ReadonlyPerson = Readonly<Person>;

let john: ReadonlyPerson = { name: "John", age: 30 };
// john.name = "Jane";  // Error: Cannot assign to 'name' because it is a read-only property.

Readonly 타입은 정말 마법 같아요! 어떤 타입이 주어지든, 그 타입의 모든 속성을 읽기 전용으로 만들어버리죠. 이렇게 하면 객체의 불변성을 보장할 수 있어요. 마치 재능넷에서 "이 서비스는 한 번 계약하면 변경할 수 없어요"라고 명시하는 것과 비슷해요! 🔒

제네릭 매핑된 타입 설명 Person name: string age: number ReadonlyPerson readonly name: string readonly age: number

이 그림에서 보듯이, Person 타입이 ReadonlyPerson 타입으로 변환되면서 모든 속성 앞에 readonly가 붙었어요. 이제 이 객체의 속성은 변경할 수 없게 되었죠! 🛡️

제네릭 타입 추출과 제외: 타입 조각내기 ✂️

때로는 기존 타입에서 특정 부분만 추출하거나 제외하고 싶을 때가 있어요. 이건 마치 재능넷에서 "이 서비스 패키지에서 이 부분만 따로 구매하고 싶어요" 또는 "이 부분만 빼고 구매할게요"라고 하는 것과 비슷해요.

type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T;

type T0 = Extract<"a" | "b" | "c", "a" | "f">;  // "a"
type T1 = Exclude<"a" | "b" | "c", "a" | "f">;  // "b" | "c"

type Shape = { kind: "circle"; radius: number } | { kind: "square"; sideLength: number };
type CircleShape = Extract<Shape, { kind: "circle" }>;
type NonCircleShape = Exclude<Shape, { kind: "circle" }>;

ExtractExclude 타입은 정말 유용해요! Extract는 두 타입 중 공통된 부분만 추출하고, Exclude는 첫 번째 타입에서 두 번째 타입을 제외한 나머지를 남겨요. 이렇게 하면 복잡한 타입에서 필요한 부분만 정확하게 골라낼 수 있죠. 마치 재능넷에서 여러분의 요구사항에 딱 맞는 서비스만 골라내는 것과 같아요! 🎯

제네릭 타입 추출과 제외 설명 T U Extract<T, U> Exclude<T, U>

이 그림에서 보듯이, Extract는 두 원의 교집합 부분을, Exclude는 T에서 U를 뺀 나머지 부분을 나타내요. 이렇게 타입을 조작하면 정말 정교한 타입 설계가 가능해져요! 🧩

🔧 타입 조작의 이점: