[TypeScript] React에서 자주 사용되는 작업 패턴2

우지끈·2024년 12월 12일
post-thumbnail

타입 좁히기(Type Narrowing)와 타입 가드

타입스크립트는 정적으로 타입을 검사하지만, 코드 실행 중에 타입을 확인하고 그에 따라 다르게 동작해야 하는 경우가 많다. 그런 상황에서 타입 좁히기를 통해 타입 안정성을 유지하면서도 다양한 타입에 대한 처리를 할 수 있다.

💡 타입 좁히기 (Type Narrowing)란?
: 여러 가능한 타입 중 하나로 타입을 좁히는 과정이다. 즉, 코드 실행 중에 특정 값이 어떤 타입인지 확인하고, 그 타입에 맞는 로직을 적용하는 것

💡 타입 가드 (Type Guard)란?
: 런타임에 조건문을 사용하여 타입을 검사하고 타입 범위를 좁혀주는 기능을 뜻한다. (타입좁히기를 구현하기 위한 수단 정도로 생각해도 좋다)


단순 조건문을 사용한 타입 가드

type User = {
    name: string;
    age?: number;
}

function printUserAge(user: User) {
    if (user.age !== undefined) { 
        console.log(`User's age is ${user.age}`);
    } else {
        console.log("User's age is not provided");
    }
}

printUserAge({ name: "abc"})
printUserAge({ name: "abc", age: 30})

위 코드는 옵셔널 속성(age?)을 타입 가드로 확인하는 예제이다.
옵셔널 속성은 객체에 존재하지 않을 수 있으므로, 접근하기 전에 반드시 확인이 필요하다.

user.age !== undefined를 통해 user.ageundefined가 아니면 해당 속성이 존재한다고 간주하며, 타입을 number로 좁힌다.


typeof 타입가드

function printValue(value: string | number) {
    if (typeof value === 'string') {
        console.log(`String: ${value.toUpperCase()}`);
    } else {
        console.log(`Number: ${value.toFixed(2)}`);
    }
}

// 테스트 코드
printValue("Hello"); // String: HELLO
printValue(42);      // Number: 42.00

위 코드는 typeof 연산자를 사용하여 변수의 타입을 확인하고, 그에 따라 분기 처리를 하고 있다.

typeof value === "string"이면 문자열 관련 메서드인 toUpperCase()를 호출하고, 그렇지 않으면 숫자 관련 메서드인 toFixed()를 호출한다.


instanceof 타입 가드

import axios, { AxiosError } from 'axios';

async function fetchData(url: string) {
    try {
        const response = await axios.get(url);
        console.log('Data:', response.data);
    } catch (error) {
        if (error instanceof AxiosError) {
            // AxiosError인 경우
            console.log('Axios error occurred')
        } else {
            // 일반 Error인 경우
            console.log('General error occurred');
        }
    }
}

위 코드는 instanceof를 사용하여 특정 클래스의 인스턴스인지 확인하는 타입 가드 예제이다.

error instanceof AxiosErrorerror 객체가 AxiosError 클래스에서 파생된 인스턴스인지를 검사하여, AxiosError인 경우에는 Axios와 관련된 오류로 처리하고, 그렇지 않으면 일반적인 Error로 처리한다.


in 타입가드

type Dog = {
    bark: () => void;
}

type Cat = {
    meow: () => void;
}

function makeSound(animal: Dog | Cat) {
    if ('bark' in animal) {
        animal.bark();
    } else {
        animal.meow();
    }
}

const dog: Dog = { bark: () => console.log('Woof!') };
const cat: Cat = { meow: () => console.log('Meow!') };

makeSound(dog); // Woof!
makeSound(cat); // Meow!

위 코드는 객체에 특정 키가 존재하는지 확인하여 타입을 좁히는 타입 가드의 예제이다.

'bark' in animalanimal 객체에 'bark'라는 키가 존재하는지 검사하여 존재하면 animalDog 타입으로 간주한다.

따라서, bark 키가 있으면 Dog 타입으로 판단하여 bark 메서드를 호출하고 없으면 Cat 타입으로 판단하여 meow 메서드를 호출한다.


사용자정의 타입가드

사용자 정의 타입가드란 참, 거짓을 반환하는 함수를 정의하여 이 함수를 이용해 사용자가 원하는 타입가드를 만들 수 있도록 도와주는 타입스크립트 문법이다.

type Dog = {
    bark: () => void;
}

type Cat = {
    meow: () => void;
}

// isDog 함수의 리턴값이 true 라면, animal 은 Dog다.
function isDog(animal: Dog | Cat): animal is Dog {
    return "bark" in animal;
}

function makeSound(animal: Dog | Cat) {
    if (isDog(animal)) {
        animal.bark();
    } else {
        animal.meow();
    }
}

const dog: Dog = { bark: () => console.log('Woof!') };
const cat: Cat = { meow: () => console.log('Meow!') };

makeSound(dog); // Woof!
makeSound(cat); // Meow!

위 코드는 is 키워드를 사용하여 타입을 좁히는 사용자 정의 타입가드의 예제이다.

animal.bark가 정의되어 있으면 true를 리턴하여 animal is Dog 즉, Dog 타입으로 판별한다. 함수의 반환값이 true면 Dog 타입, 그렇지 않으면 Cat 타입으로 처리된다.


객체 타입을 정의하는 방법

Object Literal

const user: { id: number; name: string; email: string } = {
  id: 1,
  name: "Alice",
  email: "alice@example.com"
};

객체 리터럴 방식으로 타입을 정의한다. 객체 구조를 명시적으로 지정하여 id, name, email 필드가 각각 number, stirng, string 타입임을 정의한다.

장점

  • 간단한 객체의 타입을 정의할 때 유용하다.

단점

  • 동일한 타입을 반복적으로 정의해야 하는 경우 유지보수가 어렵다.
  • 재사용성 부족

Type Alias

type User = {
  id: number;
  name: string;
  email: string;
};

const user: User = {
  id: 1,
  name: "Alice",
  email: "alice@example.com"
};

type 키워드를 사용하여 User라는 별칭을 만들어 객체의 타입을 정의한다. 타입을 재사용하거나 확장하는 데 적합하다.

장점

  • 타입의 재사용성이 높아진다.
  • 코드의 가독성이 좋고, 유지보수가 용이하다.

단점

  • TypeScript에서 제공하는 일부 기능(ex:extends)는 type 보다는 interface에 더 적합하다.

Interface

interface User {
  id: number;
  name: string;
  email: string;
}

const user: User = {
  id: 1,
  name: "Alice",
  email: "alice@example.com"
};

interface를 사용하여 객체의 구조를 정의한다. 객체의 타입을 정의할 때 가장 일반적으로 사용되며, 클래스와도 호환된다.

장점

  • 객체 구조의 계층화를 지원하여 확장 가능(extends)
  • 클래스와의 호환성이 뛰어나다.

단점

  • 유니온 타입이나 교차 타입을 지정할 수 없다.

Index Signature

type User = {
  id: number;
  name: string;
  email: string;
};
type UserRecord = {
  [key: string]: User;
};

const users: UserRecord = {
  admin: { id: 1, name: "Alice", email: "alice@example.com" },
  user: { id: 2, name: "Bob", email: "bob@example.com" },
  guest: { id: 3, name: "James", email: "james@example.com" }
};

동적인 데이터 타입을 정의할 때 주로 사용되며, 객체의 특정 속성값을 미리 알 수 없을 때 유용하다. (그 외의 경우에는 최대한 사용을 지양하는 것이 좋음)

위 예시에서는 문자열 키(key: string)와 User 타입의 값으로 구성된 객체를 정의했다.

장점

  • 객체의 키와 값을 동적으로 정의할 수 있다.

단점

  • 키 타입 제한이 부족하면 예상치 못한 데이터가 들어갈 수 있다.
  • 값 타입에만 제약이 걸리므로, 타입 안정성이 떨어질 수 있다.

Mapped Type

type User = {
  id: number;
  name: string;
  email: string;
};
type UserRecord = {
  [key in 'admin' | 'user' | 'guest']: User;
};

// 기존 User의 속성들 각각에 readonly 제약 추가
type ReadOnlyUser = {
  readonly [K in keyof User]: User[K];
};

// 각 속성을 옵셔널로 설정
type PartialUser = {
  [K in keyof User]?: User[K];
};

기존 타입을 기반으로 새로운 타입을 생성할 때 사용된다. index signature와 달리, Mapped Type은 키의 집합을 제한할 수 있다.

위 예제에서는 'admin' | 'user' | 'guest' 를 키로, 각 키의 타입은 User 타입을 갖는 객체를 정의했다.

장점

  • 기존 타입을 변형하거나 확장하는 데 적합하다.
  • 각 속성을 readonly로 설정하거나 선택적(optional)로 변경하는 등의 작업을 간단히 수행할 수 있다.

단점

  • 복잡한 구조에서는 코드가 다소 읽기 어려울 수도 있다.

Record

// 예시 1
type User = {
  id: number;
  name: string;
  email: string;
};

type UserRecord = Record<'admin' | 'user' | 'guest', User>;

const users: UserRecord = {
  admin: { id: 1, name: "Alice", email: "alice@example.com" },
  user: { id: 2, name: "Bob", email: "bob@example.com" },
  guest: { id: 3, name: "James", email: "james@example.com" }
};
  
// 예시 2
  
// 객체의 키 타입 제한
type PermissionRecord = {
  read: boolean;
  write: boolean;
  execute: boolean;
}

// 위 객체 타입을 Record를 이용해서 정리
type Permissions = 'read' | 'write' | 'execute';
type PermissionRecord = Record<Permissions, boolean>;

const userPermissions: PermissionRecord = {
  read: true,
  write: false,
  execute: true
};

맵드 타입을 좀 더 간결한 형태로, 키와 값의 타입을 지정한다.
위 예제에서는 'admin' | 'user' | 'guest' 를 키로, 각 키의 값이 User 타입을 갖는 객체를 정의하고 있다.

장점

  • 가독성이 높고 반복적인 코드를 줄일 수 있다.
  • 동일한 값 타입을 가지는 여러 속성을 정의할 때 유용하다.

단점

  • 다소 제한적인 용도로 사용된다. (복잡한 키 구조에는 부적합)

0개의 댓글