4.Node.js/JavaScript&TypeScript

[TypeScript] 8편. 개발자용 핵심 문법 & 실전 타입 설계 핸드북

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

 

8편. 개발자용 핵심 문법 & 실전 타입 설계 핸드북

 

📚 목차
1. 기본 타입 시스템 (Core Type System)
2. 함수 (Functions)
3. 클래스와 객체지향 프로그래밍 (Classes and OOP)
4. 제네릭 (Generics) : 타입 변수
5. 고급 타입 조작 (Advanced Type Manipulation)
6. 비동기 프로그래밍 (Asynchronous Programming)
7. 모듈 시스템 (ESM for Node.js) : export, import

 

개발자용 핵심 문법 삽화 이미지
개발자용 핵심 문법 삽화 이미지

 

1. 기본 타입 시스템 (Core Type System)

🔷 1. 원시 타입 (Primitive Types)

타입 설명 예시
string 문자열 const userName: string = "TypeScript Learner";
const greeting: string = `Hello, ${userName}!`;
number 숫자(정수/실수) const decimal: number = 25.5;
const hex: number = 0xf00d;
const binary: number = 0b1010;
boolean 논리값( true/false) let isActive: boolean = true;
null 의도적인 값 없음
strictNullChecks일 때 보통 유니온으로 사용
let data: string | null = null;
undefined 값이 할당되지 않음
strictNullChecks: true면 단일 타입에 바로 대입 불가
let result: number | undefined = undefined;
symbol 고유하고 불변한 값
같은 설명 문자열이어도 매번 고유
const uniqueId: symbol = Symbol("id");
console.log(uniqueId === anotherId); // 출력: false
bigint 매우 큰 정수, n 접미사 사용
Number 안전 범위 초과 정수 처리
const bigNumber: bigint = 9007199254740991n + 1n;

 

🔷 2. 특수 타입 (Special Types)

타입 설명 예시
any 모든 타입을 허용(사용 지양) let v: any = 1;
v.toUpperCase(); // 컴파일 OK, 런타임 에러 가능
void 함수가 값을 반환하지 않음 function log(msg: string): void {
console.log(msg);
}
never 절대 도달할 수 없는 코드 경로
무한 루프, 항상 예외를 던지는 함수,
철저한 switch exhaustiveness에 사용
function fail(): never {
throw new Error("x");
}
unknown 사용 전 반드시 타입 검사/좁히기 필요 let x: unknown = "hi";
if (typeof x === "string") x.toUpperCase();

 

▸ unknown의 안전한 사용법(Type Narrowing)

let userInput: unknown = "Hello TS";
userInput = 42;
userInput = { message: "Safe Data" };

// 1. unknown 타입의 값은 바로 사용하거나 속성에 접근할 수 없습니다.
// console.log(userInput.message); // 오류: 'userInput'의 형식이 'unknown'입니다.

// 2. 값을 사용하려면 반드시 타입 가드(Type Guard)를 사용하여 타입을 좁혀야 합니다.

// A. typeof 가드
if (typeof userInput === 'string') {
    // 이 블록 안에서 userInput은 string 타입으로 안전하게 취급됩니다.
    console.log(userInput.toUpperCase());
}

// B. instanceOf 가드 (객체 타입을 좁힐 때)
class CustomError extends Error {}
let errorInput: unknown = new CustomError("Failed");

if (errorInput instanceof CustomError) {
    // 이 블록 안에서 errorInput은 CustomError 타입입니다.
    console.log(`Error Name: ${errorInput.name}`);
}

 

🔷 3. 컬렉션 타입 (Collection Types)

▸ 배열 (Array)

두 가지 방식으로 정의할 수 있으며, 배열에 저장될 항목의 타입이 통일되어야 합니다.

// 1. 타입 뒤에 [] 사용 (가장 일반적인 형태)
const fruits: string[] = ["apple", "banana", "cherry"];

// 2. 제네릭 Array<T> 사용
const scores: Array<number> = [90, 85, 95];

// 배열에 다른 타입이 할당되면 오류 발생
// fruits.push(123); // 🚨 오류: number 타입은 string[] 배열에 할당될 수 없습니다.

 

▸ 튜플 (Tuple)
정해진 순서와 정해진 개수의 항목 타입을 미리 선언하는 배열의 특별한 형태입니다.

// 튜플 정의: [string, number, boolean] 순서를 지켜야 합니다.
let userProfile: [string, number, boolean];

// 할당: 순서와 개수가 일치해야 함
userProfile = ["Alice", 30, true];

// userProfile = [30, "Alice", true]; // 🚨 오류: 타입 순서가 다름

// userProfile[0]은 string 타입입니다.
console.log(`Name: ${userProfile[0]}`);

// userProfile[1]은 number 타입입니다.
const age: number = userProfile[1];

// 튜플은 배열 메서드를 허용하지만, 새 요소를 추가할 때는 유니온 타입으로 취급됩니다.
userProfile.push("Manager"); // ⚠️ push는 허용되나, 타입 안전성이 약해질 수 있음

 

🔷 4. 객체 타입 정의: 인터페이스 vs 타입 별칭

구분 interface type alias
확장 (Extension) extends 키워드 사용 &(인터섹션) 연산자 사용
선언적 확장 (Merging) 가능 (동일 이름으로 선언 시 자동 병합) 불가능 (동일 이름 선언 시 오류)
활용 범위 객체 타입, 클래스 구현 모든 타입 (원시, 유니온, 튜플, 객체 등)
주요 시나리오 객체 구조 정의,
클래스 타입 계약,
라이브러리/프레임워크의 타입 정의
유니온/튜플 등 복잡한 타입에 새 이름 부여,
타입 매핑

 

▸ 인터페이스 (interface)
주로 객체의 모양을 정의하고 클래스를 구현하거나 확장할 때 사용됩니다.

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

interface Point3D extends Point { // 인터페이스 확장 (extends)
    z: number;
}

const p: Point3D = { x: 10, y: 20, z: 30 };

// *특징: 선언적 확장 (Declaration Merging)
// 동일한 이름의 인터페이스를 여러 번 선언하면 자동으로 합쳐집니다.
interface User {
    id: number;
}
interface User {
    name: string;
}

const mergedUser: User = { id: 1, name: "Charlie" }; // id와 name을 모두 가짐

 

▸ 타입 별칭 (type alias)

새로운 타입 이름을 생성하며, 객체 외에도 원시 타입, 유니온, 튜플, 함수 시그니처 등 거의 모든 타입에 사용될 수 있습니다.

type ID = string | number; // 유니온 타입에 별칭 부여
type Coordinate = [number, number]; // 튜플 타입에 별칭 부여

type Employee = {
    id: ID;
    department: string;
};

// 타입 별칭은 인터섹션(&)을 사용하여 결합할 수 있습니다.
type Manager = Employee & {
    reportsTo: ID;
};

const manager: Manager = {
    id: 101,
    department: "Sales",
    reportsTo: 100
};

// *특징: 확장 불가 및 선언적 확장 불가
// type Manager extends Employee ... (불가)
// 동일 이름으로 type 선언 시 오류 발생 (Declaration Merging 불가)

 

🔷 5. 조합 타입 (Combination Types)

▸ 유니온 (Union Type, |) : 두 개 이상의 타입 중 하나일 수 있음을 나타냅니다.

// 값은 string이거나 number여야 합니다.
type Numeric = string | number;

function printID(id: Numeric) {
    console.log(`ID: ${id}`);
    
    // 유니온 타입의 값은 공통된 속성(예: toString())만 접근 가능합니다.
    // 타입 가드를 통해 타입을 좁혀야 합니다.
    if (typeof id === 'string') {
        console.log(id.toUpperCase()); // string 타입일 때만 가능
    }
}

printID(108);
printID("TS_101");

 

▸ 인터섹션 (Intersection Type, &) :

두 개 이상의 타입을 모두 만족하는 타입을 만듭니다.

interface HasName {
    name: string;
}

interface HasAge {
    age: number;
}

// IntersectedPerson은 name 속성과 age 속성을 모두 가져야 합니다.
type IntersectedPerson = HasName & HasAge;

const person: IntersectedPerson = {
    name: "David",
    age: 45
    // height: 180 // 인터섹션에 없는 속성을 추가하면 오류
};

 

🔷 6. 리터럴 타입 (Literal Types) :

특정 원시 값 자체를 타입으로 지정합니다. 값 자체가 타입이 되므로, 변수에 오직 그 리터럴 값만 할당할 수 있습니다.

// 1. 문자열 리터럴 타입
type Direction = "up" | "down" | "left" | "right";

let move: Direction;
move = "up";
// move = "forward"; // 🚨 오류: 'forward' 타입은 'Direction' 타입에 할당될 수 없습니다.

// 2. 숫자 리터럴 타입
type StatusCode = 200 | 404 | 500;

let status: StatusCode = 200;
// status = 201; // 🚨 오류

// 3. boolean 리터럴 타입 (사용 빈도는 낮음)
let isTrue: true = true;
// isTrue = false; // 🚨 오류

// 리터럴 타입을 함수 매개변수에 사용하면 인자로 허용되는 값이 제한됩니다.
function setDirection(dir: Direction): void {
    console.log(`Setting direction to: ${dir}`);
}

setDirection("left");

 

2. 함수 (Functions)

🔷 1. 함수 선언과 타입

// 1. 함수 선언식 (Function Declaration)
// 매개변수 a와 b는 number 타입, 반환 값은 number 타입임을 명시
function add(a: number, b: number): number {
    return a + b;
}

// 2. 함수 표현식 (Function Expression)
// 변수 자체에 함수 타입 시그니처를 지정할 수도 있습니다.
type Calculator = (x: number, y: number) => number;

const subtract: Calculator = function (a, b) {
    // 반환 값이 number 타입 시그니처와 일치하지 않으면 오류 발생
    return a - b;
};

// 3. 반환 타입 생략 (타입 추론)
// TypeScript는 return 값을 보고 반환 타입을 자동으로 추론합니다 (여기서는 string).
function createMessage(text: string) {
    return `Message: ${text}`;
}

// 명시적으로 void 타입을 사용하여 반환 값이 없음을 나타냅니다.
function logResult(result: number): void {
    console.log(`The result is: ${result}`);
    // return "Error"; // 🚨 오류: void 타입은 값을 반환할 수 없습니다.
}

logResult(add(5, 3)); // 8이 콘솔에 출력되고, 반환 값은 없음

 

🔷 2. 화살표 함수 (Arrow Functions)

▸ 기본 구조 및 문법 간소화

// 기본 형태 (명시적 반환)
const add = (a: number, b: number): number => {
    return a + b;
};

// 암시적 반환 (가장 흔한 형태)
const subtract = (a: number, b: number): number => a - b;
console.log(subtract(10, 4)); // 출력: 6

// 괄호 사용
const logValueWithBrackets = (value: string): void => {
    console.log(`Value: ${value}`);
};

// 괄호 생략
const logValue = (value: string): void => console.log(value);
logValue("Hello, TypeScript"); // 출력: Hello, TypeScript

// 매개변수 없이 현재 시간을 문자열로 반환
const getTimeString = (): string => new Date().toLocaleTimeString();
console.log(`Current Time: ${getTimeString()}`);

//객체 리터럴 반환 시
type Coordinates = { x: number, y: number };
// 못된 사용 (함수 본문으로 해석되어 undefined 반환)
// const createCoordBroken = (x: number, y: number) => { x: x, y: y }; 

// 올바른 사용: 객체 리터럴을 괄호로 감쌈
const createCoord = (x: number, y: number): Coordinates => ({ x: x, y: y });
// 속성 이름 축약 (Shorthand) 사용
const createPoint = (x: number, y: number): Coordinates => ({ x, y });
console.log(createPoint(1, 5)); // 출력: { x: 1, y: 5 }

 

▸ 고차 함수 (Higher-Order Functions) 콜백

const numbers = [1, 2, 3, 4, 5];

// 2.1. filter: 짝수만 걸러내기
const evens: number[] = numbers.filter(n => n % 2 === 0);
console.log(`Evens: ${evens}`); // 출력: [2, 4]

// 2.2. map: 각 항목을 제곱하여 새로운 배열 생성
const squares: number[] = numbers.map(n => n * n);
console.log(`Squares: ${squares}`); // 출력: [1, 4, 9, 16, 25]

// 2.3. reduce: 배열의 모든 값을 합산
const sum: number = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
console.log(`Sum: ${sum}`); // 출력: 15

 

🔷 3. 선택적 매개변수와 기본값

// lastName 매개변수는 선택적입니다.
// lastName은 string 또는 undefined 타입으로 추론됩니다.
function greet(firstName: string, lastName?: string): string {
    if (lastName) {
        return `Hello, ${firstName} ${lastName}!`;
    }
    return `Hello, ${firstName}!`;
}

console.log(greet("Minho"));      // 출력: Hello, Minho!
console.log(greet("Minho", "Kim")); // 출력: Hello, Minho Kim!


// count에 기본값 10을 설정
function getArray(item: string, count: number = 10): string[] {
    return new Array(count).fill(item);
}

// count를 생략하면 기본값 10이 사용됨
console.log(getArray("Default", 5).length); // 출력: 5
console.log(getArray("Default").length);    // 출력: 10

 

🔷 4. Rest 파라미터 (나머지 매개변수)

// items: string[] 타입으로 추론됩니다.
function concatenateStrings(separator: string, ...items: string[]): string {
    // items는 항상 string 배열입니다.
    console.log(`Items count: ${items.length}`);
    return items.join(separator);
}

const names = concatenateStrings(", ", "Alice", "Bob", "Charlie", "David");
// 출력: Items count: 4
// 반환: Alice, Bob, Charlie, David

// 숫자 타입의 Rest 파라미터 예시
function sumAll(...numbers: number[]): number {
    return numbers.reduce((total, num) => total + num, 0);
}

console.log(sumAll(1, 2, 3, 4, 5)); // 출력: 15

 

🔷 5. 함수 오버로딩 (Function Overloading) : 함수 시그니처 정의

// 1. 오버로드 시그니처 (호출 방식만 정의)
// string 또는 number를 받아 string을 반환
function convert(input: string): string;
function convert(input: number): number; 
// Date 타입을 받아 string을 반환
function convert(input: Date): string;


// 2. 구현 시그니처 (실제 로직을 담는 함수 본문)
// 구현 시그니처는 가장 포괄적인 유니온 타입을 사용해야 합니다.
function convert(input: string | number | Date): string | number {
    if (typeof input === 'string') {
        return input.toUpperCase(); // string -> string
    } else if (typeof input === 'number') {
        return input * 2; // number -> number
    } else if (input instanceof Date) {
        return input.toISOString(); // Date -> string
    }
    // never 타입으로 떨어지는 예외 처리 (선택적)
    throw new Error("Unsupported input type."); 
}

// 타입 안전성 검증:
const resultStr = convert("hello"); // resultStr은 string 타입으로 추론
const resultNum = convert(100);    // resultNum은 number 타입으로 추론
const resultDate = convert(new Date()); // resultDate는 string 타입으로 추론

// const resultInvalid = convert(true); // 🚨 오류: 오버로드 시그니처에 true를 받는 정의가 없음

 

🔷 6. 제네릭 함수 (Generic Functions)

// <T>는 타입 변수(Type Variable)입니다. 
// T는 함수가 호출될 때 전달된 인수의 타입으로 결정됩니다.
function identity<T>(arg: T): T {
    // T가 어떤 타입이든, 입력 타입과 출력 타입이 동일함을 보장합니다.
    return arg;
}

// 1. 명시적 타입 지정 (선택적)
let output1 = identity<string>("my string"); // output1은 string 타입
let output2 = identity<number>(12345);      // output2는 number 타입

// 2. 타입 추론 (가장 일반적인 사용법)
// TypeScript가 인수를 보고 자동으로 타입을 추론합니다.
let output3 = identity(true);   // output3은 boolean 타입
let output4 = identity([1, 2]); // output4는 number[] 타입

// 제네릭과 유니온 타입 활용 예시
// 두 값을 교환하는 함수: 입력 A, B의 타입이 다를 수 있으며, 반환되는 튜플의 타입은 [B, A]입니다.
function swapValues<A, B>(a: A, b: B): [B, A] {
    return [b, a];
}

const swapped = swapValues(10, "Text"); // swapped는 [string, number] 타입
console.log(swapped); // 출력: ["Text", 10]

 

3. 클래스와 객체지향 프로그래밍 (Classes and OOP)

🔷 1. 기본 문법, 접근제어자(Access Modifiers), 속성 접근자(get, set)

// Employee 클래스 정의
class Employee {
    // 1. 클래스 필드 정의 및 접근 제어자 활용

    // public: 어디서든 접근 가능 (기본값, 명시적으로 붙여줌)
    public name: string;
    
    // private: 오직 이 클래스 내부에서만 접근 가능
    private _salary: number; 
    
    // protected: 이 클래스와 상속받는 자식 클래스 내부에서만 접근 가능
    protected hireYear: number;
    
    // readonly: 초기화 후 수정 불가 (급여 등급은 변하지 않음)
    readonly salaryGrade: "A" | "B" | "C";

    // 2. 생성자 (Constructor)
    // 생성자 매개변수에 접근 제어자를 붙여 필드 정의와 초기화를 동시에 수행할 수 있습니다.
    constructor(
        name: string, 
        initialSalary: number, 
        hireYear: number, 
        grade: "A" | "B" | "C" = "B"
    ) {
        this.name = name;
        this._salary = initialSalary;
        this.hireYear = hireYear;
        this.salaryGrade = grade;
    }

    // 3. 속성 접근자 (Getter/Setter)

    // 3.1. Getter: _salary 값을 읽을 때 호출
    // 외부에서는 'employee.salary'처럼 속성처럼 접근합니다.
    get salary(): number {
        // 급여를 반환하기 전에 계산된 보너스 정보를 포함할 수 있습니다.
        const bonus = this.getBonusRate();
        return this._salary * (1 + bonus);
    }

    // 3.2. Setter: _salary 값을 할당할 때 호출
    // 외부에서는 'employee.salary = 60000'처럼 속성처럼 할당합니다.
    set salary(newSalary: number) {
        // 유효성 검사: 급여가 음수가 되는 것을 방지
        if (newSalary < 0) {
            console.error("🚨 오류: 급여는 음수가 될 수 없습니다.");
            return;
        }
        this._salary = newSalary;
    }

    // 3.3. Getter (계산된 속성): 서비스 연한
    // private 속성인 hireYear를 사용하여 현재 연도 기준으로 서비스 연한을 계산합니다.
    get yearsOfService(): number {
        const currentYear = new Date().getFullYear();
        // protected 속성인 this.hireYear에 접근 가능
        return currentYear - this.hireYear;
    }

    // private 메서드: 오직 내부 로직에만 사용
    private getBonusRate(): number {
        if (this.salaryGrade === "A") {
            return 0.15; // 15% 보너스
        } else if (this.salaryGrade === "B" && this.yearsOfService >= 5) {
            return 0.10; // 10% 보너스
        }
        return 0; // 보너스 없음
    }

    // public 메서드
    public introduce(): string {
        return `${this.name} (${this.salaryGrade} Grade). Service: ${this.yearsOfService} years.`;
    }
}

// ----------------------------------------------------
// 클래스 사용 예시
// ----------------------------------------------------

// 2018년 입사, 급여 50000, A등급의 직원 생성 (2025년 기준 7년차)
const manager = new Employee("Olivia", 50000, 2018, "A");

console.log(manager.introduce()); 
// 출력: Olivia (A Grade). Service: 7 years.

// 1. public 속성 접근
console.log(`Employee Name: ${manager.name}`); // 출력: Olivia

// 2. Getter 호출 (계산된 값 반환)
// A 등급 (15% 보너스): 50000 * 1.15 = 57500
console.log(`Annual Salary (with bonus): $${manager.salary}`); // 출력: $57500

// 3. Setter 호출 (유효성 검사 적용)
manager.salary = 60000; // 새로운 급여 설정 (Setter 호출)
console.log(`Updated Salary: $${manager.salary}`); // 출력: $69000 (60000 * 1.15)

// 4. private 속성 접근 시도
// console.log(manager._salary);   // 🚨 오류: '_salary'는 private입니다.

// 5. protected 속성 접근 시도
// console.log(manager.hireYear);  // 🚨 오류: 'hireYear'는 protected입니다.

// 6. readonly 속성 수정 시도
// manager.salaryGrade = "C"; // 🚨 오류: 읽기 전용 속성입니다.

 

🔷 2. 상속: 클래스 확장 및 super 키워드

// Account 클래스를 상속받는 SavingsAccount 클래스
class SavingsAccount extends Account {
    private interestRate: number;

    constructor(name: string, initialBalance: number, accNum: string, rate: number) {
        // 1. super() 호출: 부모 클래스(Account)의 생성자를 호출하여 부모 속성을 초기화해야 합니다.
        super(name, initialBalance, accNum);
        
        this.interestRate = rate;
    }

    // 부모 클래스의 메서드 재정의 (오버라이딩)
    public introduce(): string {
        // 2. super 키워드를 사용하여 부모 클래스의 메서드/속성에 접근할 수 있습니다.
        const parentIntro = super.getMaskedNumber(); // 부모의 protected 속성을 사용
        return `Savings account for ${this.ownerName}. Rate: ${this.interestRate}%. Acc: ${parentIntro}`;
    }

    public calculateInterest(): void {
        // protected 속성은 자식 클래스 내부에서 접근 가능
        // console.log(`Processing interest for ${this.accountNumber}`); // 접근 가능
    }
}

const savings = new SavingsAccount("Brian", 5000, "0987654321", 0.05);
console.log(savings.introduce());
// 출력: Savings account for Brian. Rate: 0.05%. Acc: XXXX-4321

// savings.deposit(50); // 부모 클래스의 public 메서드 사용 가능
// console.log(savings.accountNumber); // 🚨 오류 (protected)

 

🔷 3. 추상 클래스 (Abstract Classes)

// 'abstract' 키워드로 정의
abstract class Shape {
    // 공통 속성 정의
    abstract name: string; // 추상 속성: 자식 클래스에서 반드시 구현해야 함

    // 공통 메서드 (일반 메서드도 가질 수 있음)
    getInfo(): string {
        return `This is a ${this.name} shape.`;
    }

    // 추상 메서드: 본문이 없고, 자식 클래스에서 반드시 오버라이딩(구현)해야 합니다.
    abstract calculateArea(): number;
}

// const shape = new Shape(); // 🚨 오류: 추상 클래스는 인스턴스화할 수 없습니다.

class Circle extends Shape {
    radius: number;
    name: string = "Circle"; // 추상 속성 구현

    constructor(radius: number) {
        super();
        this.radius = radius;
    }

    // 추상 메서드 구현 강제
    calculateArea(): number {
        // Math.PI * r^2
        return Math.PI * this.radius ** 2;
    }
}

class Rectangle extends Shape {
    width: number;
    height: number;
    name: string = "Rectangle"; // 추상 속성 구현

    constructor(width: number, height: number) {
        super();
        this.width = width;
        this.height = height;
    }

    calculateArea(): number {
        return this.width * this.height;
    }
}

const myCircle: Shape = new Circle(5);
const myRectangle: Shape = new Rectangle(4, 6);

console.log(myCircle.getInfo());      // 출력: This is a Circle shape.
console.log(myCircle.calculateArea()); // 출력: 78.53...

 

🔷 4. 인터페이스 구현 (Interface Implementation)

// 1. 인터페이스 정의 (계약서)
interface Logger {
    log(message: string): void;
    error(message: string): void;
    level: 'info' | 'warn' | 'error';
}

// 2. 클래스에서 인터페이스 구현 (계약 이행)
class ConsoleLogger implements Logger {
    level: 'info' | 'warn' | 'error' = 'info'; // 속성 구현

    log(message: string): void { // 메서드 구현
        if (this.level === 'info') {
            console.log(`[INFO]: ${message}`);
        }
    }

    error(message: string): void { // 메서드 구현
        console.error(`[ERROR]: ${message}`);
    }
    
    // 인터페이스에 없는 메서드도 추가 가능
    setLevel(newLevel: 'info' | 'warn' | 'error') {
        this.level = newLevel;
    }
}

// 타입 계약에 따라 Logger 타입으로 활용 가능
const myLogger: Logger = new ConsoleLogger();

myLogger.log("Application started.");
myLogger.error("A critical error occurred.");

// myLogger.setLevel('warn'); // 🚨 오류: Logger 인터페이스에 setLevel 메서드가 정의되어 있지 않음
반응형

 

4. 제네릭 (Generics) : 타입 변수

function identity<T>(arg: T): T {
    // T는 입력 인수의 타입이며, 
    // 여기서 T는 string, number, boolean 등 어떤 타입이든 될 수 있습니다.
    return arg;
}

 

🔷 1. 제네릭 함수: 다양한 타입에서 동작하는 함수 정의

// 예시 1: 배열의 첫 번째 요소 반환
// <T>를 사용하여, 입력 배열의 요소 타입 T를 그대로 반환 타입으로 사용합니다.
function getFirstElement<T>(arr: T[]): T {
    if (arr.length === 0) {
        // 배열이 비어있는 경우, T 타입으로 간주되는 undefined를 반환합니다.
        // strictNullChecks가 true일 경우 T | undefined를 반환하는 것이 더 안전합니다.
        // 여기서는 예시의 간결함을 위해 T를 사용합니다.
        // 실제 코드에서는 T | undefined를 사용하는 것이 좋습니다.
    }
    return arr[0];
}

const numList = [10, 20, 30];
const firstNum = getFirstElement(numList); // firstNum은 number 타입

const strList = ["A", "B", "C"];
const firstStr = getFirstElement(strList); // firstStr은 string 타입
// firstNum.toUpperCase(); // 🚨 오류: number 타입에는 toUpperCase가 없습니다. (타입 안정성 보장)

// 예시 2: 여러 타입 변수 사용
// <T, U> 두 개의 타입 변수를 사용하여 서로 다른 타입의 두 인수를 받습니다.
function createPair<T, U>(v1: T, v2: U): [T, U] {
    // 반환 타입은 첫 번째는 T, 두 번째는 U인 튜플입니다.
    return [v1, v2];
}

// v1: string ("Key"), v2: number (123)
const mixedPair = createPair("Key", 123); 
// mixedPair의 타입은 [string, number]

// v1: boolean (true), v2: string ("Status")
const statusPair = createPair(true, "Status");
// statusPair의 타입은 [boolean, string]

console.log(`Key: ${mixedPair[0]}, Value: ${mixedPair[1]}`);

 

🔷 2. 제네릭 인터페이스: 데이터 구조의 타입 유연성 확보

// <T>는 이 인터페이스를 구현하거나 사용할 때 결정됩니다.
interface Box<T> {
    value: T;
    // T 타입의 값을 처리하는 메서드도 정의 가능
    read(): T; 
}

// Box 인터페이스를 string 타입으로 사용하여 구현
const stringBox: Box<string> = {
    value: "Generics",
    read: function() {
        return this.value;
    }
};

// Box 인터페이스를 number 타입으로 사용하여 구현
const numberBox: Box<number> = {
    value: 42,
    read: function() {
        return this.value;
    }
};

stringBox.read().toUpperCase(); // string 타입 메서드 사용 가능
// numberBox.read().toUpperCase(); // 🚨 오류: number 타입 메서드 사용 불가

 

🔷 3. 제네릭 클래스 : 데이터 구조의 타입 유연성 확보

// Queue 클래스는 어떤 타입 T의 항목이든 저장할 수 있습니다.
class Queue<T> {
    private items: T[] = []; // 내부 저장소는 T 타입의 배열입니다.

    enqueue(item: T): void {
        this.items.push(item);
    }

    dequeue(): T | undefined {
        return this.items.shift();
    }
}

// string 타입을 다루는 큐 인스턴스
const stringQueue = new Queue<string>();
stringQueue.enqueue("Task 1");
stringQueue.enqueue("Task 2");
// stringQueue.enqueue(123); // 🚨 오류: number는 string 타입에 할당될 수 없음

const dequeuedItem = stringQueue.dequeue(); // dequeuedItem은 string 타입
console.log(dequeuedItem!.toUpperCase()); 

// 객체 타입을 다루는 큐 인스턴스
type Task = { id: number; description: string };
const taskQueue = new Queue<Task>();
taskQueue.enqueue({ id: 1, description: "Fix bug" });

const nextTask = taskQueue.dequeue(); // nextTask는 Task 타입
console.log(nextTask!.description);

 

🔷 4. 제네릭 제약조건 (Constraints) : extends 키워드

제네릭 타입 T가 특정 속성을 가지고 있어야만 함수나 클래스 내부에서 해당 속성을 안전하게 사용할 수 있습니다

 

▸ 예시: length 속성 제약

// T extends { length: number }
// T는 반드시 length라는 number 타입의 속성을 가지고 있어야 합니다.
interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    // 이제 T는 length 속성을 가지고 있음이 보장되므로, 안전하게 접근 가능합니다.
    console.log(arg.length); 
    return arg;
}

// 1. 제약조건을 만족하는 경우 (string, Array는 length를 가짐)
loggingIdentity("Hello");   // string은 length 속성을 가짐
loggingIdentity([1, 2, 3]); // Array는 length 속성을 가짐

// 2. 제약조건을 만족하지 않는 경우
// loggingIdentity(10); // 🚨 오류: number 타입에는 length 속성이 없습니다.

// 3. 커스텀 객체로 제약조건 만족
const myObject = { value: 5, length: 100 };
loggingIdentity(myObject); // 객체가 length를 가지므로 허용됨 (출력: 100)

 

▸ 예시: 객체 키에 대한 제약

객체의 속성 이름을 안전하게 가져오는 함수를 만들 때 사용됩니다

// <T>는 객체 타입, <K>는 T의 키 타입입니다.
// K extends keyof T: K는 반드시 T 객체가 가지는 속성 이름 중 하나여야 합니다.
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    // key가 T에 존재함이 보장되므로 안전하게 접근 가능
    return obj[key];
}

const userProfile = { id: 1, username: "dev_user", isActive: true };

// 'username'은 userProfile의 키이므로 허용
const userName = getProperty(userProfile, "username"); // userName은 string 타입

// 'age'는 userProfile의 키가 아니므로 컴파일 오류 발생
// 🚨 오류: 'age' 형식은 'keyof T' 형식에 할당될 수 없습니다.
// const userAge = getProperty(userProfile, "age");

 

5. 고급 타입 조작 (Advanced Type Manipulation)

🔷 1. 타입 가드 (Type Guards)와 타입 좁히기 : typeof, instanceof, is

타입 좁히기(Type Narrowing)는 유니온 타입처럼 여러 후보 타입을 가진 변수의 타입을 특정 코드 블록 내에서 하나의 타입으로 확정하는 과정입니다.

 

▸ typeof 및 instanceof 가드 : 원시 타입 및 클래스 인스턴스 검사에 사용됩니다.

// 유니온 타입 정의
type Data = string | Date;

function processData(data: Data): void {
    if (typeof data === 'string') {
        // typeof 가드: data는 string 타입으로 좁혀집니다.
        console.log(`String length: ${data.length}`);
    } else if (data instanceof Date) {
        // instanceof 가드: data는 Date 타입으로 좁혀집니다.
        console.log(`Year: ${data.getFullYear()}`);
    } else {
        // never 타입 영역 (모든 경우가 커버되지 않으면 오류)
    }
}

processData("hello");       // 출력: String length: 5
processData(new Date());    // 출력: Year: 2025 (현재 연도)

 

▸ 사용자 정의 타입 가드 (is) : 개발자가 직접 런타임 검사 함수를 만들어 타입을 좁히는 규칙을 정의합니다.

interface ApiResponse { success: true; data: string; }
interface ApiError { success: false; error: string; }
type Result = ApiResponse | ApiError;

// 반환 타입: result is ApiResponse -> true일 경우 result는 ApiResponse로 좁혀짐
function isSuccess(result: Result): result is ApiResponse {
    return result.success === true;
}

function handleResult(result: Result) {
    if (isSuccess(result)) {
        // isSuccess가 true이므로 result는 ApiResponse 타입입니다.
        console.log(`Success data: ${result.data.toUpperCase()}`);
    } else {
        // result는 ApiError 타입입니다.
        console.error(`Error details: ${result.error}`);
    }
}

handleResult({ success: false, error: "Not Found" });

 

🔷 2. 타입 단언 (as Type)과 안정성 개선 (satisfies)

▸ 타입 단언 (Type Assertions, as Type)

const apiData: unknown = { id: 101, username: "tester" };

// 컴파일러에게 apiData가 User 객체 형태임을 강제로 확신시킵니다.
interface User { id: number; username: string; }
const user = apiData as User; 

// 실제 런타임 객체 구조와 단언 타입이 다를 경우 런타임 오류가 발생할 수 있습니다.
console.log(user.username); // 안전한 접근

 

▸ 타입 안정성 및 추론 개선: satisfies 키워드

type AllowedKeys = 'config' | 'style';
type SettingValues = Record<AllowedKeys, string>;

// 🚨 문제: 'as'를 사용하면 "config"의 구체적인 타입 ("large")을 잃고 string 타입으로 추론됩니다.
const settingsAs = {
    config: "large",
    style: "dark"
} as SettingValues;
// settingsAs.config의 타입은 string. 리터럴 메소드 (slice 등) 사용 시 오류 방지 불가.

// ✅ 해결책: satisfies 키워드
// SettingValues를 만족하는지 검사하면서, 각 속성의 리터럴 타입을 보존합니다.
const settingsSatisfies = {
    config: "large",
    style: "dark"
} satisfies SettingValues;

// settingsSatisfies.config의 타입은 리터럴 타입 "large"입니다.
// 리터럴 타입 메서드를 안전하게 사용 가능 (출력: lar)
console.log(settingsSatisfies.config.slice(0, 3)); 

// 🚨 satisfies는 타입 안정성도 보장합니다.
/*
const invalidSettings = {
// 🚨 오류: 'unknownKey'는 AllowedKeys 타입에 속하지 않아 컴파일 오류 발생
    unknownKey: "value" 
} satisfies SettingValues;
*/

 

🔷 3. 조건부 타입 (Conditional Types)

타입 시스템 내에서 삼항 연산자(T extends U ? X : Y)를 사용하여 타입을 조건적으로 결정할 수 있게 해 줍니다

// T가 number[]를 상속(확장)하는지 확인하고 타입을 결정합니다.
type IsNumberArray<T> = T extends number[] ? true : false;

type Res1 = IsNumberArray<number[]>; // Res1은 true
type Res2 = IsNumberArray<string[]>; // Res2은 false

// T가 유니온 타입일 경우, 각 멤버에 대해 조건이 개별적으로 적용됩니다. (분배 법칙)
type NonString<T> = T extends string ? never : T;

type Mixed = NonString<string | number | boolean>; // Mixed는 number | boolean
// (string -> never) | (number -> number) | (boolean -> boolean)

 

▸ infer 키워드의 활용

infer 키워드는 조건부 타입 내에서 타입 변수를 추론하거나 추출할 수 있게 해 줍니다.

// GetElementType<T>는 T가 배열 타입인 경우, 배열의 요소 타입(E)을 추론하여 반환합니다.
type GetElementType<T> = T extends Array<infer E> ? E : T;

type ItemType1 = GetElementType<string[]>; // ItemType1은 string
type ItemType2 = GetElementType<number>;   // ItemType2은 number (배열이 아니므로 T 반환)

// Promise의 해결된 타입을 추출하는 Awaited<T>의 핵심 원리
type ExtractPromiseResult<T> = T extends Promise<infer R> ? R : T;

type PromiseResult = ExtractPromiseResult<Promise<{ data: string }>>; 
// PromiseResult는 { data: string } 타입

 

🔷 4. Mapped Types (매핑된 타입)

Mapped Types는 TypeScript의 문법적 기능 그 자체이며, 기존 타입의 속성들을 순회하면서 새로운 타입을 생성하는 변환 로직을 정의하는 데 사용됩니다.

type NewType<T> = {
    [K in keyof T]: /* K와 T[K]를 활용한 새로운 타입 표현식 */;
};

//1. keyof T: 기존 타입 T의 모든 속성 이름(리터럴 유니온 타입)을 가져옵니다.
//2. K in ...: 이 속성 이름들(K)을 반복(순회)합니다.
//3. T[K]: 반복되는 현재 속성 이름(K)에 해당하는 기존 속성의 타입입니다.
interface APIResponse {
    id: number;
    name: string;
    isActive: boolean;
}

// 1. Partial<T> 구현 원리: 모든 속성을 선택적(`?`)으로 변환
type Optional<T> = {
    [K in keyof T]?: T[K]; // keyof T는 'id' | 'name' | 'isActive'
};

type PartialAPIResponse = Optional<APIResponse>;
/*
{
    id?: number;
    name?: string;
    isActive?: boolean;
}
*/

// 2. Flags 타입: 모든 속성의 값을 boolean으로 변환
type Flags<T> = {
    [K in keyof T]: boolean;
};

type APIFlags = Flags<APIResponse>;
/*
{
    id: boolean;
    name: boolean;
    isActive: boolean;
}
*/

 

🔷 5. 유틸리티 타입 (Utility Types)

유틸리티 타입은 TypeScript 컴파일러에 내장되어 있거나 혹은 외부 라이브러리에서 미리 정의되어 제공되는 제네릭 타입 별칭입니다.

// --- 1. 기본 타입 정의 ---
interface UserProfile {
    // 필수 속성
    id: number;
    username: string;
    // 선택적 속성 (원본에 이미 ?가 있음)
    email?: string; 
    // 읽기 전용 속성
    readonly createdAt: Date; 
    // 함수 속성
    // Promise를 반환하는 비동기 함수
    fetchActivity: (userId: number) => Promise<string[]>; 
}

type UserStatus = 'active' | 'pending' | 'deleted' | null | undefined;
type ValidId = string | number;
type MixedUnion = string | number | boolean | null | undefined;

// --- 2. 주요 유틸리티 타입 적용 예시 ---
console.log("--- 2.1. 속성 조작 유틸리티 (Property Manipulation) ---");
// 2.1.1. Partial<T>
// 모든 속성을 선택적(? 없어도 되게)으로 만듭니다.
type PartialUser = Partial<UserProfile>; 
const updateProfile: PartialUser = { 
    // id, username이 필수가 아니게 됩니다.
    username: "new_name", 
    email: "new@example.com" 
};
console.log("// Partial<T>: 모든 속성을 선택적으로 만듦");

// 2.1.2. Readonly<T>
// 모든 속성을 읽기 전용(readonly)으로 만듭니다.
type ReadonlyUser = Readonly<UserProfile>;
const immutableUser: ReadonlyUser = { 
    id: 1, username: "read_only", createdAt: new Date(), 
    fetchActivity: () => Promise.resolve([])
};
// immutableUser.username = "new"; // 🚨 오류: 읽기 전용 속성입니다.
console.log("// Readonly<T>: 모든 속성을 읽기 전용으로 만듦");

// 2.1.3. Pick<T, K>
// T 타입에서 K에 지정된 속성들만 선택합니다.
type UserSummary = Pick<UserProfile, 'id' | 'username'>; 
const summary: UserSummary = { id: 2, username: "summary_user" };
console.log("// Pick<T, K>: 지정된 속성만 선택");

// 2.1.4. Omit<T, K>
// T 타입에서 K에 지정된 속성들을 제외합니다.
type UserWithoutSensitive = Omit<UserProfile, 'id' | 'createdAt'>;
const safeUser: UserWithoutSensitive = {
    username: "safe_user",
    email: "safe@mail.com", // email은 원본에서 선택적이었으므로 그대로 선택적
    fetchActivity: () => Promise.resolve([])
};
console.log("// Omit<T, K>: 지정된 속성만 제외");

console.log("\n--- 2.2. 유니온 타입 조작 유틸리티 (Union Manipulation) ---");
// 2.2.1. Exclude<T, U>
// T 유니온 타입에서 U와 호환되는 타입(또는 멤버)을 제외합니다.
type OnlyStringOrNumber = Exclude<MixedUnion, boolean | null | undefined>; 
// OnlyStringOrNumber는 string | number 타입
console.log(`// Exclude<T, U>: ${'A'|'B'|'C'|'D'}에서 ${'A'|'C'}를 제외 -> ${'B'|'D'}`);

// 2.2.2. Extract<T, U>
// T 유니온 타입에서 U와 호환되는 타입(또는 멤버)만 추출합니다.
type OnlyNullish = Extract<UserStatus, null | undefined>; 
// OnlyNullish는 null | undefined 타입
console.log(`// Extract<T, U>: ${'A'|'B'|'C'|'D'}에서 ${'A'|'C'}만 추출 -> ${'A'|'C'}`);

// 2.2.3. NonNullable<T>
// T 유니온 타입에서 null과 undefined를 제거합니다.
type DefinedStatus = NonNullable<UserStatus>; 
// DefinedStatus는 'active' | 'pending' | 'deleted' 타입
console.log("// NonNullable<T>: null과 undefined를 제거");

console.log("\n--- 2.3. 함수 및 비동기 타입 조작 유틸리티 ---");
// 2.3.1. Parameters<T>
// 함수 타입 T의 매개변수 타입을 튜플로 추출합니다.
type FetchParams = Parameters<UserProfile['fetchActivity']>; 
// FetchParams는 [userId: number] 타입
const userIdTuple: FetchParams = [5];
console.log("// Parameters<T>: 함수의 매개변수를 튜플로 추출");

// 2.3.2. ReturnType<T>
// 함수 타입 T의 반환 타입을 추출합니다. (Promise<T> 자체를 반환)
type FetchReturn = ReturnType<UserProfile['fetchActivity']>; 
// FetchReturn은 Promise<string[]> 타입
console.log("// ReturnType<T>: 함수의 반환 타입 추출 (Promise 포함)");

// 2.3.3. Awaited<T> (Node.js 비동기 컨텍스트에서 중요)
// Promise T가 해결된 후의 최종 결과 타입(Promise 내부 타입)을 추출합니다.
type ActivityArray = Awaited<FetchReturn>; 
// ActivityArray는 string[] 타입 (Promise가 제거됨)
console.log("// Awaited<T>: Promise가 해결된 후의 타입 추출");

console.log("\n--- 2.4. 기타 유틸리티 ---");
// 2.4.1. Record<K, T>
// K 타입의 키와 T 타입의 값으로 구성된 객체 타입을 생성합니다.
type RolePermissions = 'admin' | 'user' | 'guest';
type PermissionsMap = Record<RolePermissions, boolean>; 
const permissions: PermissionsMap = {
    admin: true,
    user: true,
    guest: false
};
console.log("// Record<K, T>: 특정 키-값 쌍을 가진 객체 타입 생성");

 

6. 비동기 프로그래밍 (Asynchronous Programming)

🔷 1. Promise와 타입: Promise<T>의 타입 정의 및 활용

Promise는 비동기 작업의 최종 성공 값(결과) 또는 실패 이유(오류)를 나타내는 객체입니다.

// Promise<T> 정의: T는 성공 시 반환될 데이터의 타입입니다.
type FetchResult = { data: string };

function fetchData(id: number): Promise<FetchResult> {
    return new Promise((resolve, reject) => {
        // 실제 비동기 I/O 작업 (예: 네트워크 요청) 시뮬레이션
        setTimeout(() => {
            if (id > 0) {
                // resolve 함수는 성공 시 반환될 FetchResult 타입의 객체를 인수로 받습니다.
                resolve({ data: `User data for ID ${id}` });
            } else {
                // reject 함수는 실패 이유(Error 객체 또는 string 등)를 인수로 받습니다.
                reject(new Error("Invalid ID provided."));
            }
        }, 100);
    });
}

// Promise 사용 (then/catch 체이닝)
fetchData(5)
    .then(result => {
        // result는 FetchResult 타입으로 추론됩니다.
        console.log(`Success: ${result.data}`);
        
        // Promise 체이닝: 다음 .then()으로 string 타입의 새 Promise를 반환
        return result.data.toUpperCase(); 
    })
    .then(upperData => {
        // upperData는 string 타입으로 추론됩니다.
        console.log(`Uppercase: ${upperData}`);
    })
    .catch(error => {
        // error는 Error 타입으로 추론됩니다.
        console.error(`Error: ${error.message}`);
    });

 

🔷 2. async/await: 비동기 코드의 동기적 표현 및 타입 유추

async/await는 Promise 기반 코드를 마치 동기 코드처럼 읽기 쉽고 간결하게 작성할 수 있도록 하는 문법적 설탕(Syntactic Sugar)입니다.
▸ async 함수: 항상 Promise를 반환하도록 함수를 선언합니다. 함수의 반환 타입 T는 자동으로 Promise<T>가 됩니다.

▸ await 키워드: async 함수 내에서만 사용 가능하며, Promise가 해결(resolve)될 때까지 함수의 실행을 일시 중지하고, Promise의 최종 결과 값을 반환합니다.

// async 함수는 반환 타입이 명시되지 않아도 Promise<{ data: string }>로 추론됩니다.
async function processUserData(id: number): Promise<string> {
    try {
        // 1. await: fetchData Promise가 완료될 때까지 대기합니다.
        // result는 Promise<FetchResult>가 아닌, 해결된 타입인 FetchResult 타입입니다.
        const result = await fetchData(id); 

        // 2. 다른 비동기 함수 호출 (예시를 위해 단순화)
        const additionalInfo = await simulateDelay(50); 
        
        // 3. 최종적으로 string 값을 반환하지만, async 함수이므로 Promise<string>으로 래핑됩니다.
        return `${result.data}. Info: ${additionalInfo}`;

    } catch (error) {
        // 4. 에러 발생 시 try...catch로 잡을 수 있으며, Promise는 reject 상태가 됩니다.
        // Node.js 환경에서 error는 기본적으로 unknown 타입으로 추론될 수 있습니다.
        if (error instanceof Error) {
            throw new Error(`Processing failed: ${error.message}`);
        }
        throw new Error("An unknown error occurred.");
    }
}

// Promise<T>를 반환하는 Helper 함수
function simulateDelay(ms: number): Promise<string> {
    return new Promise(resolve => setTimeout(() => resolve("Loaded"), ms));
}

// 호출 및 결과 확인
processUserData(10)
    .then(finalString => {
        // finalString은 string 타입입니다.
        console.log(`\nFinal Output: ${finalString}`);
    })
    .catch(err => {
        console.error(`\nError caught by caller: ${err.message}`);
    });

processUserData(-1) // ID가 음수이므로 에러 발생
    .catch(err => {
        console.error(`\nError caught for invalid ID: ${err.message}`);
    });

 

🔷 3. 비동기 에러 핸들링: try...catch 및 .catch()의 적절한 사용

비동기 에러는 Promise의 reject 상태로 전달됩니다. 이를 포착하고 처리하는 방법은 async/await를 사용하느냐 then() 체이닝을 사용하느냐에 따라 달라집니다.

 

▸ async/await 에러 핸들링

async function safeApiCall(id: number) {
    try {
        const result = await fetchData(id); // Promise가 reject되면 여기서 catch로 점프
        console.log(`Success: ${result.data}`);
    } catch (error) {
        // TypeScript 4.4+부터 catch 블록의 변수(error)는 기본적으로 unknown 타입입니다.
        // 따라서 에러 객체의 속성(message 등)에 접근하려면 반드시 타입 가드가 필요합니다.
        if (error instanceof Error) {
            console.error(`[HANDLED ERROR]: ${error.message}`);
        } else {
            console.error("[UNKNOWN ERROR TYPE]");
        }
        // 에러를 재전파하지 않으면 Promise는 resolve 상태가 됩니다.
        // 재전파하려면 `throw error;`를 사용해야 합니다.
    }
}

safeApiCall(-5); // 에러 발생 및 [HANDLED ERROR] 출력

 

▸ Promise 체이닝 에러 핸들링

fetchData(0) // ID가 0이므로 reject 발생 가정
    .then(result => {
        console.log(`Should not reach here: ${result.data}`);
    })
    .catch(error => {
        // 체인의 어느 단계에서든 발생한 reject를 여기서 포착합니다.
        if (error instanceof Error) {
            console.error(`[CHAIN CATCH]: ${error.message}`);
        }
    })
    // 최종적으로 .finally()를 사용하여 성공 또는 실패에 관계없이 실행할 코드를 정의할 수 있습니다.
    .finally(() => {
        console.log("Async operation finished.");
    });

 

🔷 4. Promise 유틸리티: Promise.all(), Promise.race(), Promise.allSettled()

여러 개의 비동기 작업을 동시에 실행하고 결과를 취합할 때 사용됩니다.

 

▸ Promise.all(iterable)

모든 Promise가 성공해야 성공합니다. 하나라도 실패하면 전체가 실패합니다.

// 타입: Promise<[string, number]> (인수의 순서와 타입이 유지됨)
async function runAll() {
    const promise1 = Promise.resolve("Hello");
    const promise2 = Promise.resolve(42);
    const promise3 = fetchData(1);

    try {
        // results의 타입은 Awaited된 타입들의 튜플 [string, number, FetchResult]
        const results = await Promise.all([promise1, promise2, promise3]);
        
        // 타입 안전하게 접근 가능
        console.log("\n--- Promise.all() Success ---");
        console.log(`Type of 1st: ${typeof results[0]}`); 
        console.log(`Type of 3rd data: ${results[2].data}`);

    } catch (error) {
        // 하나라도 reject되면 이 블록이 실행됩니다.
        console.error("Promise.all() failed:", error);
    }
}
runAll();

 

▸ Promise.race(iterable)

가장 먼저 완료(성공 또는 실패)되는 Promise의 결과를 반환합니다.

async function runRace() {
    const slow = new Promise<string>(resolve => setTimeout(() => resolve("Slow done"), 500));
    const fast = new Promise<string>(resolve => setTimeout(() => resolve("Fast done"), 100));

    // result는 string 타입으로 추론됩니다.
    const result = await Promise.race([slow, fast]);

    console.log("\n--- Promise.race() ---");
    console.log(`Winner: ${result}`); // 출력: Fast done
}
runRace();

 

▸ Promise.allSettled(iterable)

모든 Promise가 완료(성공 또는 실패 무관)될 때까지 기다리며, 각 Promise의 결과 상태를 포함하는 객체의 배열을 반환합니다.

// Promise.allSettled의 반환 타입은 Promise<Array<PromiseSettledResult<T>>> 입니다.
async function runAllSettled() {
    const successPromise = Promise.resolve(100);
    const failurePromise = Promise.reject(new Error("Network fail"));

    // results 타입: Array<{ status: 'fulfilled', value: number } | { status: 'rejected', reason: Error }>
    const results = await Promise.allSettled([successPromise, failurePromise]);

    console.log("\n--- Promise.allSettled() ---");
    results.forEach(result => {
        if (result.status === 'fulfilled') {
            // result.value는 number 타입입니다.
            console.log(`Fulfilled with value: ${result.value}`); 
        } else {
            // result.reason은 unknown 타입이며, 타입 가드로 Error 객체임을 확인합니다.
            console.error(`Rejected with reason: ${result.reason instanceof Error 
            ? result.reason.message : result.reason}`);
        }
    });
}
runAllSettled();

 

7. 모듈 시스템 (ESM for Node.js) : export, import

🔷 Named Export (이름 지정 내보내기)

여러 기능을 내보낼 때 사용되며, 가져올 때도 동일한 이름을 사용해야 합니다.

// --- math.ts ---
// 1. 함수와 변수를 선언과 동시에 내보내기
export const PI: number = 3.14159;

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

// 2. 선언 후 별도로 내보내기
function subtract(a: number, b: number): number {
    return a - b;
}

export { subtract };
// --- app.ts ---
// 1. 필요한 항목만 구조 분해 할당 형태로 가져오기
import { PI, add, subtract } from './math.js'; 
// Note: Node.js ESM은 .js 또는 .mjs 확장자를 명시해야 합니다. 
// (TS 파일도 컴파일된 JS 파일을 가리킵니다.)

console.log(add(10, 5));          // 출력: 15
console.log(subtract(10, 5));     // 출력: 5

// 2. 전체 모듈을 하나의 객체로 가져오기 (Namespace Import)
import * as MathUtils from './math.js';

console.log(MathUtils.PI);        // 출력: 3.14159

 

🔷 Default Export (기본 내보내기)

모듈당 하나의 주요 항목만 내보낼 때 사용되며, 가져올 때 {} 없이 원하는 이름으로 자유롭게 가져올 수 있습니다.

// --- user.ts ---
interface User {
    name: string;
    id: number;
}

class UserService {
    getUser(id: number): User {
        return { name: `User_${id}`, id };
    }
}

// 모듈당 하나만 허용
export default UserService;
// --- app.ts ---
// 가져올 때 {} 없이 원하는 이름(UserService는 MyService로)으로 가져옵니다.
import MyService from './user.js'; 

const service = new MyService();
const user = service.getUser(10);
console.log(`Default Export User: ${user.name}`); // 출력: Default Export User: User_10

 

🔷 동적 Import: import()

동적 import()는 런타임에 필요할 때만 모듈을 비동기적으로 로드할 수 있게 해 줍니다.

이는 코드 스플리팅(Code Splitting)이나 조건부 로딩에 유용합니다. import()는 Promise를 반환합니다.

// 조건에 따라 모듈을 비동기로 로드
async function loadHeavyModule(condition: boolean) {
    if (condition) {
        // import('./heavy-calc.js')는 
        // Promise<{ add: Function, default: Class }> 형태를 반환합니다.
        const module = await import('./heavy-calc.js');
        
        // Named Export 접근
        const result = module.add(5, 5); 
        console.log(`Dynamic Load Result: ${result}`);

        // Default Export 접근
        const HeavyService = module.default;
        // ...
    }
}

// 모듈 로드 함수 호출
loadHeavyModule(true);

 

🔷 타입 전용 Import: import type ...

TypeScript에서만 사용되는 구문으로, 가져오는 내용이 타입에만 관련되어 런타임에 JavaScript 코드로 컴파일되지 않도록 보장합니다.

이는 모듈이 실제로 존재하지 않아도 되므로 빌드 성능과 안전성을 향상시킵니다.

// --- types/config.ts ---
export interface Config {
    timeout: number;
    retries: number;
}

// --- main.ts ---

// 1. 타입만 가져오는 경우 (런타임 코드 생성 X)
// Config는 타입 정의로만 사용됩니다.
import type { Config } from './types/config.js'; 

// 2. 값과 타입을 동시에 가져오는 경우
// import { SomeValue, type SomeType } from './some-module.js';

class App {
    private settings: Config; // Config는 타입으로만 사용

    constructor(config: Config) {
        this.settings = config;
    }
}

// 런타임에 사용되는 값은 일반 import를 사용합니다.
import { fetchConfig } from './config-loader.js';

 

 


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

반응형

 

반응형