한국어

TypeScript의 readonly 타입을 사용하여 불변 데이터 구조의 힘을 활용하세요. 의도치 않은 데이터 변경을 방지하여 더 예측 가능하고 유지보수하기 쉬운 견고한 애플리케이션을 만드는 방법을 배워보세요.

TypeScript Readonly 타입: 불변 데이터 구조 마스터하기

끊임없이 발전하는 소프트웨어 개발 환경에서 견고하고 예측 가능하며 유지보수하기 쉬운 코드를 추구하는 것은 끊임없는 노력입니다. TypeScript는 강력한 타이핑 시스템을 통해 이러한 목표를 달성하기 위한 강력한 도구를 제공합니다. 이 도구들 중에서 readonly 타입은 불변성을 강제하는 중요한 메커니즘으로 돋보이며, 이는 함수형 프로그래밍의 초석이자 더 신뢰할 수 있는 애플리케이션을 구축하는 열쇠입니다.

불변성이란 무엇이며 왜 중요한가?

불변성은 핵심적으로 객체가 생성된 후에는 그 상태를 변경할 수 없다는 것을 의미합니다. 이 간단한 개념은 코드 품질과 유지보수성에 심오한 영향을 미칩니다.

TypeScript의 Readonly 타입: 불변성을 위한 당신의 무기고

TypeScript는 readonly 키워드를 사용하여 불변성을 강제하는 여러 방법을 제공합니다. 다양한 기술과 이를 실제로 적용하는 방법을 살펴보겠습니다.

1. 인터페이스 및 타입의 Readonly 속성

속성을 읽기 전용으로 선언하는 가장 간단한 방법은 인터페이스나 타입 정의에서 readonly 키워드를 직접 사용하는 것입니다.


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

const person: Person = {
  id: "unique-id-123",
  name: "Alice",
  age: 30,
};

// person.id = "new-id"; // 오류: 'id'는 읽기 전용 속성이므로 할당할 수 없습니다.
person.name = "Bob"; // 이것은 허용됩니다

이 예제에서 id 속성은 readonly로 선언되었습니다. TypeScript는 객체가 생성된 후 이를 수정하려는 모든 시도를 막을 것입니다. readonly 수정자가 없는 nameage 속성은 자유롭게 수정할 수 있습니다.

2. Readonly 유틸리티 타입

TypeScript는 Readonly<T>라는 강력한 유틸리티 타입을 제공합니다. 이 제네릭 타입은 기존 타입 T를 받아 모든 속성을 readonly로 변환합니다.


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

const point: Readonly<Point> = {
  x: 10,
  y: 20,
};

// point.x = 30; // 오류: 'x'는 읽기 전용 속성이므로 할당할 수 없습니다.

Readonly<Point> 타입은 xy가 모두 readonly인 새로운 타입을 생성합니다. 이는 기존 타입을 빠르게 불변으로 만드는 편리한 방법입니다.

3. 읽기 전용 배열 (ReadonlyArray<T>) 및 readonly T[]

JavaScript의 배열은 본질적으로 변경 가능합니다. TypeScript는 ReadonlyArray<T> 타입이나 약식 표현인 readonly T[]를 사용하여 읽기 전용 배열을 만드는 방법을 제공합니다. 이는 배열 내용의 수정을 방지합니다.


const numbers: ReadonlyArray<number> = [1, 2, 3, 4, 5];
// numbers.push(6); // 오류: 'readonly number[]' 타입에 'push' 속성이 없습니다.
// numbers[0] = 10; // 오류: 'readonly number[]' 타입의 인덱스 시그니처는 읽기만 허용합니다.

const moreNumbers: readonly number[] = [6, 7, 8, 9, 10]; // ReadonlyArray와 동일
// moreNumbers.push(11); // 오류: 'readonly number[]' 타입에 'push' 속성이 없습니다.

push, pop, splice와 같이 배열을 수정하는 메서드를 사용하거나 인덱스에 직접 할당하려고 하면 TypeScript 오류가 발생합니다.

4. constreadonly: 차이점 이해하기

constreadonly를 구별하는 것이 중요합니다. const는 변수 자체의 재할당을 막는 반면, readonly는 객체 속성의 수정을 막습니다. 이들은 다른 목적을 가지며, 최대의 불변성을 위해 함께 사용될 수 있습니다.


const immutableNumber = 42;
// immutableNumber = 43; // 오류: const 변수 'immutableNumber'에 재할당할 수 없습니다.

const mutableObject = { value: 10 };
mutableObject.value = 20; // 변수만 const일 뿐 *객체*는 const가 아니므로 허용됩니다.

const readonlyObject: Readonly<{ value: number }> = { value: 30 };
// readonlyObject.value = 40; // 오류: 'value'는 읽기 전용 속성이므로 할당할 수 없습니다.

const constReadonlyObject: Readonly<{ value: number }> = { value: 50 };
// constReadonlyObject = { value: 60 }; // 오류: const 변수 'constReadonlyObject'에 재할당할 수 없습니다.
// constReadonlyObject.value = 60; // 오류: 'value'는 읽기 전용 속성이므로 할당할 수 없습니다.

위에서 설명했듯이, const는 변수가 항상 메모리의 동일한 객체를 가리키도록 보장하는 반면, readonly는 객체의 내부 상태가 변경되지 않도록 보장합니다.

실용적인 예제: 실제 시나리오에서 Readonly 타입 적용하기

다양한 시나리오에서 readonly 타입이 코드 품질과 유지보수성을 향상시키는 데 어떻게 사용될 수 있는지 몇 가지 실용적인 예제를 살펴보겠습니다.

1. 설정 데이터 관리

설정 데이터는 종종 애플리케이션 시작 시 한 번 로드되며 런타임 중에 수정되어서는 안 됩니다. readonly 타입을 사용하면 이 데이터가 일관되게 유지되고 우발적인 수정을 방지할 수 있습니다.


interface AppConfig {
  readonly apiUrl: string;
  readonly timeout: number;
  readonly features: readonly string[];
}

const config: AppConfig = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  features: ["featureA", "featureB"],
};

function fetchData(url: string, config: Readonly<AppConfig>) {
    // ... config.timeout과 config.apiUrl이 변경되지 않을 것임을 알고 안전하게 사용
}

fetchData("/data", config);

2. Redux와 같은 상태 관리 구현

Redux와 같은 상태 관리 라이브러리에서 불변성은 핵심 원칙입니다. readonly 타입을 사용하여 상태가 불변으로 유지되도록 하고, 리듀서가 기존 상태를 수정하는 대신 새로운 상태 객체만 반환하도록 보장할 수 있습니다.


interface State {
  readonly count: number;
  readonly items: readonly string[];
}

const initialState: State = {
  count: 0,
  items: [],
};

function reducer(state: Readonly<State>, action: { type: string; payload?: any }): State {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + 1 }; // 새로운 상태 객체 반환
    case "ADD_ITEM":
      return { ...state, items: [...state.items, action.payload] }; // 업데이트된 아이템으로 새로운 상태 객체 반환
    default:
      return state;
  }
}

3. API 응답 다루기

API에서 데이터를 가져올 때, 특히 UI 컴포넌트를 렌더링하는 데 사용하는 경우 응답 데이터를 불변으로 취급하는 것이 바람직합니다. readonly 타입은 API 데이터의 우발적인 변경을 방지하는 데 도움이 될 수 있습니다.


interface ApiResponse {
  readonly userId: number;
  readonly id: number;
  readonly title: string;
  readonly completed: boolean;
}

async function fetchTodo(id: number): Promise<Readonly<ApiResponse>> {
  const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
  const data: ApiResponse = await response.json();
  return data;
}

fetchTodo(1).then(todo => {
  console.log(todo.title);
  // todo.completed = true; // 오류: 'completed'는 읽기 전용 속성이므로 할당할 수 없습니다.
});

4. 지리 데이터 모델링 (국제 예제)

지리적 좌표를 표현하는 것을 고려해 보세요. 좌표가 한 번 설정되면 이상적으로는 일정하게 유지되어야 합니다. 이는 특히 북미, 유럽, 아시아에 걸친 배송 서비스의 GPS 좌표와 같이 여러 지리적 지역에서 작동하는 지도 제작이나 내비게이션 시스템과 같은 민감한 애플리케이션을 다룰 때 데이터 무결성을 보장합니다.


interface GeoCoordinates {
 readonly latitude: number;
 readonly longitude: number;
}

const tokyoCoordinates: GeoCoordinates = {
 latitude: 35.6895,
 longitude: 139.6917
};

const newYorkCoordinates: GeoCoordinates = {
 latitude: 40.7128,
 longitude: -74.0060
};


function calculateDistance(coord1: Readonly<GeoCoordinates>, coord2: Readonly<GeoCoordinates>): number {
 // 위도와 경도를 사용한 복잡한 계산을 상상해보세요
 // 간단함을 위해 플레이스홀더 값 반환
 return 1000; 
}

const distance = calculateDistance(tokyoCoordinates, newYorkCoordinates);
console.log("Distance between Tokyo and New York (placeholder):", distance);

// tokyoCoordinates.latitude = 36.0; // 오류: 'latitude'는 읽기 전용 속성이므로 할당할 수 없습니다.

깊은 Readonly 타입: 중첩된 객체 다루기

Readonly<T> 유틸리티 타입은 객체의 직접적인 속성만 readonly로 만듭니다. 객체에 중첩된 객체나 배열이 포함된 경우, 해당 중첩 구조는 변경 가능한 상태로 유지됩니다. 진정한 깊은 불변성을 달성하려면 모든 중첩 속성에 Readonly<T>를 재귀적으로 적용해야 합니다.

다음은 깊은 읽기 전용 타입을 만드는 방법의 예입니다:


type DeepReadonly<T> = T extends (infer R)[]
  ? DeepReadonlyArray<R>
  : T extends object
  ? DeepReadonlyObject<T>
  : T;

interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}

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

interface Company {
  name: string;
  address: {
    street: string;
    city: string;
    country: string;
  };
  employees: string[];
}

const company: DeepReadonly<Company> = {
  name: "Example Corp",
  address: {
    street: "123 Main St",
    city: "Anytown",
    country: "USA",
  },
  employees: ["Alice", "Bob"],
};

// company.name = "New Corp"; // 오류
// company.address.city = "New City"; // 오류
// company.employees.push("Charlie"); // 오류

DeepReadonly<T> 타입은 모든 중첩 속성에 Readonly<T>를 재귀적으로 적용하여 전체 객체 구조가 불변임을 보장합니다.

고려사항 및 장단점

불변성은 상당한 이점을 제공하지만, 잠재적인 장단점을 인지하는 것이 중요합니다.

불변 데이터 구조를 위한 라이브러리

여러 라이브러리가 TypeScript에서 불변 데이터 구조 작업을 단순화할 수 있습니다:

Readonly 타입 사용을 위한 모범 사례

TypeScript 프로젝트에서 readonly 타입을 효과적으로 활용하려면 다음 모범 사례를 따르십시오:

결론: TypeScript Readonly 타입으로 불변성 수용하기

TypeScript의 readonly 타입은 더 예측 가능하고, 유지보수하기 쉬우며, 견고한 애플리케이션을 구축하기 위한 강력한 도구입니다. 불변성을 수용함으로써 버그의 위험을 줄이고, 디버깅을 단순화하며, 코드의 전반적인 품질을 향상시킬 수 있습니다. 고려해야 할 몇 가지 장단점이 있지만, 불변성의 이점은 특히 복잡하고 오래 지속되는 프로젝트에서 비용을 능가하는 경우가 많습니다. TypeScript 여정을 계속하면서, readonly 타입을 개발 워크플로우의 핵심 부분으로 삼아 불변성의 모든 잠재력을 발휘하고 진정으로 신뢰할 수 있는 소프트웨어를 구축하세요.