TypeScript 유틸리티 타입: 알아두어야 할 5가지 핵심

반응형
반응형

TypeScript 유틸리티 타입
TypeScript 유틸리티 타입

TypeScript 유틸리티 타입: 개발자가 꼭 알아야 할 5가지

TypeScript는 정적 타입을 지원하여 코드의 안정성과 가독성을 높여주는 강력한 도구입니다. 그중에서도 유틸리티 타입은 기존 타입을 변형하거나 새로운 타입을 생성할 때 유용하게 활용됩니다. 이번 글에서는 개발자가 꼭 알아두어야 할 5가지 주요 유틸리티 타입을 소개하고, 각 타입의 특징과 활용 방법을 살펴보겠습니다.

1. Partial: 부분 타입 정의

 

Partial<T>는 TypeScript에서 타입의 모든 속성을 선택적으로 바꿀 수 있는 유틸리티 타입입니다. 이를 통해 기존 객체 타입을 부분적으로 사용할 수 있도록 지원하며, 유연하고 재사용 가능한 코드를 작성할 때 유용합니다.

Partial란 무엇인가?

Partial는 제네릭 타입 T의 모든 속성을 선택적으로 만들기 위해 사용됩니다. 다시 말해, T 타입의 속성들이 모두 optional 속성으로 변환됩니다. 기존 객체 타입을 재정의하지 않고도 더 유연하게 사용할 수 있는 장점이 있습니다.

Partial의 기본 문법

Partial<T>를 사용하면 간단히 모든 속성을 선택적으로 만들 수 있습니다. 아래는 기본 문법입니다:

type Partial = {
  [P in keyof T]?: T[P];
};

위 코드는 T의 모든 속성 P를 순회하면서 선택적 속성(?)으로 변환한 타입을 정의합니다.

Partial의 사용 예시

실제 예제를 통해 Partial가 어떻게 사용되는지 살펴보겠습니다:

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

function updateUser(user: User, updates: Partial): User {
  return { ...user, ...updates };
}

// 사용 예시
const user: User = { id: 1, name: 'John', email: 'john@example.com' };
const updatedUser = updateUser(user, { email: 'new_email@example.com' });
console.log(updatedUser);
// 출력: { id: 1, name: 'John', email: 'new_email@example.com' }

위 코드에서 updateUser 함수는 기존 사용자 객체와 업데이트할 데이터 객체를 병합합니다. Partial<User>를 사용함으로써 updates 객체가 선택적 속성만 포함해도 작동합니다.

Partial 활용 시 주의사항

  • 모든 속성이 선택적으로 변환: Partial를 사용할 경우 모든 속성이 선택적 속성이 됩니다. 이는 선택적 속성만 필요할 때 유용하지만, 필수 속성이 필요할 때는 다른 유틸리티 타입과 결합해 사용하는 것이 좋습니다.
  • 타입 안정성: Partial를 과도하게 사용하면 타입 안정성이 약화될 수 있습니다. 필요한 경우 특정 필드를 반드시 포함하도록 강제하는 추가적인 타입 정의가 필요합니다.

Partial를 다른 유틸리티 타입과 결합하기

Partial는 다른 유틸리티 타입과 결합하여 더욱 강력한 타입 시스템을 구현할 수 있습니다. 예를 들어, 특정 속성만 선택적으로 설정해야 할 경우 Pick과 함께 사용할 수 있습니다:

type PartialName = Partial<Pick<User, 'name' | 'email'>>;

const partialUser: PartialName = { name: 'Jane' };
// email 속성 없이도 타입 검사 통과

Partial의 주요 활용 사례

Partial는 아래와 같은 상황에서 유용합니다:

  • 부분 업데이트: 기존 객체를 부분적으로 업데이트해야 하는 경우.
  • 테스트 데이터 생성: 모든 필드가 필요하지 않은 더미 데이터를 생성할 때.
  • 초기화 상태 관리: 상태 객체를 생성할 때 기본값을 채워 넣기 전의 상태를 정의할 경우.

Partial를 적절히 활용하면 코드를 더욱 유연하게 만들 수 있습니다. 하지만, 사용 범위를 적절히 설정하여 타입 안정성을 유지하는 것이 중요합니다.

TypeScript로 고급 프로그래밍하기

2. Pick<T, K>: 특정 속성 선택

 

Pick<T, K>는 TypeScript에서 제공하는 유틸리티 타입 중 하나로, 기존 타입에서 특정 속성만 선택하여 새로운 타입을 정의할 때 유용합니다. 이 유틸리티를 활용하면, 복잡한 객체 타입에서도 필요한 부분만 추출해 가독성과 유지보수성을 높일 수 있습니다.

Pick<T, K>의 정의

Pick<T, K>는 두 가지 제네릭 파라미터를 사용합니다:

  • T: 원본 객체 타입
  • K: 원본 객체 타입에서 선택할 속성의 키(문자열 리터럴 또는 문자열 리터럴의 유니온 타입)

결과적으로 Pick<T, K>는 원본 타입 T에서 지정한 키 K만 포함하는 새로운 타입을 생성합니다.

// Pick<T, K>의 기본 정의
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

예제: Pick<T, K>의 기본 활용

다음은 Pick<T, K>를 사용하여 객체 타입의 일부 속성만 선택하는 간단한 예제입니다.

// 원본 타입
interface User {
  id: number;
  name: string;
  email: string;
  isAdmin: boolean;
}

// Pick을 사용하여 특정 속성만 선택
type UserPreview = Pick<User, 'id' | 'name'>;

// 새로운 타입을 가진 객체
const user: UserPreview = {
  id: 1,
  name: "John Doe"
};

위 예제에서, UserPreview 타입은 User 타입에서 idname 속성만 포함하도록 정의됩니다.

활용 사례 1: API 응답 데이터 처리

Pick<T, K>는 대규모 데이터 구조를 다룰 때 유용합니다. 예를 들어, API 호출 응답에서 전체 데이터를 사용하지 않고 필요한 부분만 선택하여 처리하고자 할 때 적합합니다.

// 서버에서 받은 전체 사용자 데이터
interface FullUser {
  id: number;
  name: string;
  email: string;
  phone: string;
  address: string;
}

// 필요한 데이터만 선택
type UserSummary = Pick<FullUser, 'id' | 'name' | 'email'>;

function processUser(data: FullUser): UserSummary {
  return {
    id: data.id,
    name: data.name,
    email: data.email,
  };
}

이처럼 필요한 데이터만 선택함으로써 코드의 간결성과 유지보수성을 동시에 확보할 수 있습니다.

활용 사례 2: UI 컴포넌트에 적합한 데이터 전달

React와 같은 프론트엔드 라이브러리에서는 컴포넌트에 데이터를 전달할 때 Pick<T, K>를 사용하여 불필요한 데이터를 걸러낼 수 있습니다.

// 원본 데이터 타입
interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
}

// UI에 필요한 데이터만 선택
type ProductCardProps = Pick<Product, 'id' | 'name' | 'price'>;

// React 컴포넌트에서 활용
const ProductCard: React.FC<ProductCardProps> = ({ id, name, price }) => (
  <div>
    <h3>{name}</h3>
    <p>Price: ${price}</p>
  </div>
);

이렇게 하면 컴포넌트의 props에 필요하지 않은 데이터가 포함되지 않아 성능과 보안 측면에서도 이점이 있습니다.

Pick<T, K> 사용 시 주의사항

  • K는 반드시 원본 타입 T의 키여야 합니다. 그렇지 않으면 컴파일 오류가 발생합니다.
  • 타입 안전성을 유지하려면 Pick<T, K>를 사용한 타입 정의가 프로젝트의 요구사항에 적합한지 확인하세요.

Pick<T, K>로 효율적인 타입 정의

Pick<T, K>는 필요한 부분만 추출하여 새로운 타입을 정의할 수 있는 강력한 도구입니다. 이를 통해 코드의 가독성과 유지보수성을 개선하고, 특정 요구에 맞춘 타입 정의가 가능해집니다. 다양한 실무 시나리오에서 Pick<T, K>를 적극 활용해 보세요.

3. Omit<T, K>: 특정 속성 제외

 

TypeScript 유틸리티 타입 중 Omit<T, K>는 매우 유용한 도구입니다. 이 타입은 객체 타입 T에서 특정 속성 K를 제외한 새로운 타입을 생성하는 데 사용됩니다. 코드의 유지보수성과 가독성을 높이는 데 도움을 주는 이 타입을 더 깊이 이해해 보겠습니다.

Omit<T, K>란 무엇인가요?

Omit은 TypeScript에서 기본적으로 제공되는 유틸리티 타입으로, 객체의 일부 속성을 제외하고 나머지 속성으로 새로운 타입을 생성합니다. 이때, T는 원본 객체 타입이고, K는 제외할 속성의 키를 지정합니다.

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;

위 정의에서 알 수 있듯, Omit은 내부적으로 PickExclude를 조합하여 구현됩니다. 이를 통해 TypeScript는 타입 레벨에서 필요한 속성을 손쉽게 제외할 수 있습니다.

사용 예시: 실무에서 Omit 적용하기

Omit의 유용성을 이해하려면 실무 사례를 살펴보겠습니다. 예를 들어, 사용자 정보를 관리하는 시스템에서 사용자 프로필 타입에서 민감한 정보를 제외한 타입을 생성할 수 있습니다.

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

type PublicUser = Omit<User, 'password'>;

// 결과 타입: { id: number; name: string; email: string; }
const user: PublicUser = {
  id: 1,
  name: 'John Doe',
  email: 'john@example.com'
};

이 코드는 데이터베이스에서 가져온 사용자 정보 중 password를 제외하고 공개 프로필 타입을 생성합니다. 이렇게 하면 중요한 정보가 노출되지 않도록 보장할 수 있습니다.

Omit의 장점

Omit은 다음과 같은 장점을 제공합니다.

  • 코드 재사용성 향상: 동일한 기본 타입을 기반으로 여러 변형된 타입을 정의할 수 있습니다.
  • 유지보수성 증가: 속성 제외 로직이 명확히 드러나기 때문에 코드가 더 읽기 쉽고 관리하기 쉬워집니다.
  • 타입 안정성 보장: 컴파일 타임에 타입 충돌을 방지할 수 있습니다.

Omit을 사용할 때 주의할 점

Omit은 편리하지만, 잘못 사용하면 타입 정의가 불필요하게 복잡해질 수 있습니다. 다음은 주의해야 할 몇 가지 사항입니다.

  • 너무 많은 속성을 제외하는 경우: 타입 정의가 직관적이지 않을 수 있습니다.
  • 중첩된 객체 타입에서 속성을 제외하는 경우: Omit은 단일 수준의 속성에만 적용되므로 중첩 속성을 처리하려면 커스텀 유틸리티 타입이 필요할 수 있습니다.

이 경우, 직접 커스텀 타입을 작성하거나 추가적인 유틸리티 타입을 활용해야 합니다.

커스텀 Omit 구현

Omit의 기본 동작 외에, 커스텀 구현으로 특정 요구를 충족할 수 있습니다. 예를 들어, 중첩된 속성을 제외하는 Omit을 작성할 수 있습니다.

type DeepOmit<T, K extends keyof any> = {
  [P in keyof T as P extends K ? never : P]: T[P] extends object ? DeepOmit<T[P], K> : T[P];
};

interface ComplexUser {
  id: number;
  profile: {
    name: string;
    email: string;
  };
  password: string;
}

type PublicComplexUser = DeepOmit<ComplexUser, 'password'>;

이 예시는 DeepOmit을 통해 중첩된 객체에서도 특정 속성을 제외할 수 있음을 보여줍니다.

Omit<T, K>는 간결하고 명확한 타입 정의를 가능하게 하여 TypeScript 개발에서 중요한 역할을 합니다. 이를 통해 민감한 정보를 보호하거나 재사용 가능한 타입을 정의할 수 있습니다. 실무에 적용할 때는 Omit의 기본 동작과 한계를 이해하고, 필요하다면 커스텀 타입을 구현하여 활용해 보세요.

TypeScript에서 Interface 확장하기: extends 키워드 활용법

4. Readonly: 읽기 전용 타입

반응형

Readonly는 TypeScript에서 매우 유용한 유틸리티 타입 중 하나로, 객체의 모든 속성을 읽기 전용으로 변환하는 데 사용됩니다. 이 기능은 코드에서 데이터를 보호하고 불필요한 수정으로 인한 오류를 방지할 수 있도록 설계되었습니다. 이번 섹션에서는 Readonly의 정의와 활용 사례를 중심으로 살펴보겠습니다.

Readonly의 기본 개념

Readonly는 타입에 포함된 모든 프로퍼티를 읽기 전용(read-only)으로 변경합니다. 이를 통해 객체의 속성을 수정할 수 없게 되어 데이터의 불변성을 유지할 수 있습니다. TypeScript로 선언된 객체에서 특정 데이터를 의도적으로 변경하지 않아야 할 경우 매우 유용합니다.

// Readonly 사용 예시
interface User {
  id: number;
  name: string;
}

const user: Readonly<User> = {
  id: 1,
  name: "John Doe",
};

// 다음 라인은 오류를 발생시킵니다.
// user.name = "Jane Doe"; // Error: Cannot assign to 'name' because it is a read-only property.

왜 Readonly를 사용할까?

코드에서 불변성(immutability)은 데이터를 안정적으로 유지하는 데 중요한 개념입니다. 특히 협업 프로젝트나 대규모 애플리케이션에서 예상치 못한 데이터 수정은 버그를 유발할 수 있습니다. Readonly는 다음과 같은 상황에서 유용합니다:

  • 상수와 같은 변경 불가능한 데이터 모델링
  • 함수에서 입력된 객체를 보호하기 위해
  • 불변성이 요구되는 Redux 상태 관리나 불변 데이터 구조 설계

실제 활용 사례

Readonly는 간단한 데이터 보호 이상으로 널리 사용됩니다. 특히 프런트엔드 개발과 관련된 다양한 작업에서 효율적으로 활용됩니다. 다음은 몇 가지 활용 사례입니다.

1. 컴포넌트 속성 보호

React와 같은 프레임워크에서 컴포넌트의 props는 일반적으로 읽기 전용입니다. 이를 명시적으로 TypeScript로 구현할 수 있습니다.

interface ComponentProps {
  title: string;
  description: string;
}

const componentProps: Readonly<ComponentProps> = {
  title: "Hello, World!",
  description: "This is a read-only description.",
};

// 수정 시 오류 발생
// componentProps.title = "New Title"; // Error!

2. 데이터베이스 모델 보호

데이터베이스에서 조회한 결과를 수정하지 않도록 Readonly를 사용할 수 있습니다. 이는 데이터 조회의 안전성을 보장하는 데 유용합니다.

interface Product {
  id: number;
  name: string;
  price: number;
}

const fetchedProduct: Readonly<Product> = {
  id: 101,
  name: "Laptop",
  price: 1500,
};

// 다음과 같은 수정은 오류를 유발합니다.
// fetchedProduct.price = 1200; // Error!

3. 함수 매개변수로의 사용

함수의 매개변수로 Readonly를 적용하면, 함수 내에서 원본 데이터가 수정되지 않음을 보장할 수 있습니다.

function printUserInfo(user: Readonly<User>) {
  console.log(`User ID: ${user.id}`);
  console.log(`User Name: ${user.name}`);
  
  // user.name = "Changed Name"; // Error!
}

Readonly를 사용할 때의 주의점

Readonly는 데이터의 속성을 읽기 전용으로 만들어주지만, 얕은 복사를 수행합니다. 즉, 중첩 객체의 내부 속성은 보호되지 않을 수 있습니다.

interface Nested {
  id: number;
  details: {
    name: string;
  };
}

const nestedObj: Readonly<Nested> = {
  id: 1,
  details: {
    name: "John Doe",
  },
};

// 다음은 동작하지만 내부 속성은 보호되지 않습니다.
nestedObj.details.name = "Jane Doe"; // No Error

이 문제를 해결하려면 중첩 객체에도 재귀적으로 Readonly를 적용하거나 더 깊은 수준의 데이터 불변성 라이브러리를 사용하는 것이 좋습니다.

Readonly는 TypeScript에서 데이터 불변성을 유지하고 코드의 안정성을 높이는 데 매우 유용한 유틸리티 타입입니다. 이를 통해 중요한 데이터 모델을 보호하고, 의도치 않은 수정으로부터 시스템을 안전하게 만들 수 있습니다. 그러나 얕은 복사의 한계를 인식하고 필요에 따라 추가적인 방어 로직을 구현하는 것이 중요합니다.

5. Record<K, T>: 키-값 매핑 타입

Record<K, T>는 TypeScript에서 매우 유용한 유틸리티 타입 중 하나로, 특정 키(Key)와 값(Value)의 매핑을 쉽게 정의할 수 있도록 도와줍니다. 이 기능은 특히 객체에서 키-값 구조를 명시적으로 표현하거나, 동적으로 생성된 데이터의 타입을 정의할 때 유용합니다. 이번 섹션에서는 Record<K, T>의 정의와 사용법을 다양한 사례와 함께 알아보겠습니다.

Record<K, T>란 무엇인가?

Record<K, T>는 두 개의 제네릭 타입 KT를 받습니다. 여기서 K는 키의 타입을, T는 값의 타입을 나타냅니다. 이 유틸리티 타입은 객체의 모든 키와 값이 특정 타입을 따르도록 강제할 수 있습니다.

type Record = {
  [P in K]: T;
};

위의 정의에서 볼 수 있듯이, RecordK로 전달된 키를 순회하며 모든 키가 T 타입의 값을 가지도록 설정합니다. keyof any는 문자열, 숫자, 또는 심볼 타입의 키를 허용합니다.

Record<K, T>의 활용 사례

1. 사용자 역할 매핑

Record를 활용하면 다양한 사용자 역할(Role)을 명확히 정의할 수 있습니다. 예를 들어, 각 역할에 따른 권한 정보를 매핑한다고 가정해 봅시다.

type UserRole = 'admin' | 'editor' | 'viewer';
type RolePermissions = Record<UserRole, string[]>;

const permissions: RolePermissions = {
  admin: ['read', 'write', 'delete'],
  editor: ['read', 'write'],
  viewer: ['read'],
};

위 예제에서 UserRole은 키의 타입이고, string[]는 값의 타입입니다. 이를 통해 각 역할에 대해 허용되는 권한 리스트를 명확히 정의할 수 있습니다.

2. 설정 객체 타입 정의

어플리케이션의 설정 값을 정의할 때도 Record를 사용할 수 있습니다. 예를 들어, 특정 키와 값 쌍으로 설정을 구성한다고 가정합시다.

type AppSettings = Record<'theme' | 'language' | 'layout', string>;

const settings: AppSettings = {
  theme: 'dark',
  language: 'en',
  layout: 'grid',
};

이 코드는 특정 설정 키에 대해서만 값을 허용하고, 다른 키를 추가하려 하면 컴파일 타임에 오류를 발생시킵니다.

3. API 응답 데이터 타입

REST API의 응답 데이터를 키-값 구조로 처리할 때 Record를 활용하면 더욱 명확한 타입 정의가 가능합니다. 아래는 특정 키와 데이터 배열을 매핑하는 예입니다.

type ApiResponse = Record<'status' | 'data', T>;

const response: ApiResponse<number[]> = {
  status: 'success',
  data: [1, 2, 3, 4, 5],
};

이렇게 하면 API 응답 데이터의 구조를 명확히 정의할 수 있습니다.

4. 다국어 지원 (i18n)

다국어 지원에서도 Record는 유용합니다. 특정 언어 키에 해당하는 번역 문자열을 매핑할 때 사용할 수 있습니다.

type Translations = Record<'en' | 'kr' | 'jp', string>;

const i18n: Translations = {
  en: 'Hello',
  kr: '안녕하세요',
  jp: 'こんにちは',
};

이 구조는 다양한 언어 키를 명확히 정의하고, 개발 중 실수로 잘못된 키나 값을 사용하는 것을 방지합니다.

Record<K, T>의 장점

  • 타입 안정성: 모든 키와 값의 타입을 강제할 수 있습니다.
  • 가독성 향상: 객체의 구조를 명확히 표현할 수 있습니다.
  • 유연한 제네릭 활용: 다양한 상황에 맞춰 키와 값을 설정할 수 있습니다.

Record<K, T>를 활용하면 더욱 안정적이고 가독성 높은 TypeScript 코드를 작성할 수 있습니다. 다양한 활용 사례를 직접 적용해 보며 익숙해지는 것을 추천합니다!

가장 많이 찾는 글

ReactJS와 TypeScript를 활용하여 안전하게 웹 개발 프로젝트를 시작하는 방법을 초보자도 따라 할 수

초보자도 쉽게 따라 할 수 있는 React + TypeScript 셋업ReactJS와 TypeScript는 빠르게 변화하는 웹 개발 환경에서 코드의 안정성과 가독성을 높여주는 강력한 조합입니다. 특히, TypeScript는 JavaScript에 엄

it.rushmac.net

TypeScript에서 Decorator 사용법 완벽 가이드: 코드 재사용과 효율성 극대화

TypeScript 데코레이터(Decorator)의 모든 것: 개념, 설정 및 활용법TypeScript의 데코레이터는 클래스와 메서드, 프로퍼티에 메타프로그래밍 기능을 부여하여 코드 재사용과 유지보수성을 크게 향상할

it.rushmac.net

TypeScript 제네릭 이해하기: 유연하고 안전한 코딩을 위한 실전 활용법

TypeScript 제네릭을 활용한 코드 품질 향상법TypeScript는 JavaScript의 강력한 확장 언어로, 정적 타입을 통해 개발자가 오류를 줄이고, 코드의 유지 보수를 용이하게 합니다. 특히, 제네릭(Generic) 기능

it.rushmac.net

결론

TypeScript의 유틸리티 타입은 코드의 재사용성과 가독성을 높여주는 강력한 도구입니다. Partial, Pick, Omit, Readonly, Record와 같은 유틸리티 타입을 적절히 활용하면 복잡한 타입 정의를 간결하게 만들 수 있습니다. 이러한 타입들을 숙지하고 활용하여 더욱 효율적인 TypeScript 개발을 경험해 보시기 바랍니다.

반응형

댓글