인덱스 시그니쳐 어떻게 사용해야 할까?
## 학습 계기
우테코 프론트엔드 리액트 페이먼츠 미션을 진행 중,
인덱스 시그니쳐에 대한 피드백을 받아 ‘이펙티브 타입스크립트’를 통해 인덱스 시그니쳐에 대해 학습하게 되었습니다.
<br>
## 인덱스 시그니쳐란?
```tsx
export interface CardNumbers {
first: string;
second: string;
third: string;
fourth: string;
[key: string]: string; // 이 부분이 인덱스 시그니쳐!
}
```
제가 작성한 타입 선언입니다. 맨 아래에 보면 `[key: string]: string` 과 같은 문장이 있는데 이러한 모양이 인덱스 시그니쳐입니다.
<br>
자바스크립트의 장점 중 하나는 바로 객체를 생성하는 문법이 간단하다는 것입니다.
```jsx
const CardNumbers = {
first: '1111',
second: '2222',
third: '3333',
fourth: '4444',
}
```
자바스크립트 객체는 문자열 키를 타입의 값에 관계없이 매핑합니다.
<br>
타입스크립트에서는 타입에 **‘인덱스 시그니처’**를 명시하여 유연하게 매핑을 표현할 수 있습니다.
```tsx
type IndexSignature = {[property: string]: string};
const CardNumbers: IndexSignature {
first: '1111',
second: '2222',
third: '3333',
fourth: '4444',
} // 정상적으로 동작
```
`[property: string]: string` 이 인덱스 시그니쳐이며, 다음 세 가지 의미를 담고 있습니다.
1. 키의 이름 `(property)` : 키의 위치만 표시하는 용도입니다. 타입 체커에서는 사용하지 않기 때문에 무시할 수 있는 참고 정보라고 생각해도 됩니다.
2. 키의 타입 `(string)` : string이나 number 또는 symbol 조합이어야 하지만, 보통은 string을 사용합니다.
3. 값의 타입 `(string)` : 어떤 것이든 될 수 있습니다.
<br>
이렇게 타입 체크가 수행되면 네 가지 단점이 드러납니다.
1. 잘못된 키를 포함해 모든 키를 허용합니다. `first` 대신 `First` 혹은 `FI~R@S%TㅋㅋT` 로 작성해도 유효한 타입이 됩니다.
2. 특정 키가 필요하지 않습니다. `{}` 도 유효한 타입입니다.
3. 키마다 다른 타입을 가질 수 없습니다. 예를 들어, `fourth` 는 `string` 이 아니라 `number` 여야 할 수도 있습니다.
4. 타입스크립트 언어 서비스는 다음과 같은 경우에 도움이 되지 못합니다. 예를 들어 `first:` 를 입력할 때, 키는 무엇이든 가능하기 때문에 자동 완성 기능이 동작하지 않습니다.
<br>
결론은 인덱스 시그니쳐는 부정확하므로 더 나은 방법을 찾아야 합니다.
보다 더 나은 방법을 이용하면 타입스크립트에서 제공하는 언어 서비스를 모두 사용할 수 있습니다. (자동완성, 정의로 이동, 이름 바꾸기 등)
<br>
## 잠깐! 그러면 인덱스 시그니처는 언제 사용하나요?
이펙티브 타입스크립트에서는 런타임 때까지 객체의 속성을 알 수 없을 경우에만 인덱스 시그니쳐를 사용하라고 권장합니다.
<br>
즉, 인덱스 시그니쳐는 동적 데이터를 표현할 때 사용합니다. 예를 들어 CSV파일처럼 헤더 행(row)에 열(column) 이름이 있고, 데이터 행을 열 이름과 값으로 매핑하는 객체로 나타내고 싶은 경우입니다.
<br>
일반적인 상황에서 열 이름이 무엇인지 미리 알 방법은 없습니다. 이럴 때 인덱스 시그니쳐를 사용합니다. 반면에 열 이름을 알고 있다면, 미리 선언해 둔 타입으로 단언문을 사용하면 됩니다. (물론 선언해 둔 열들이 런타임에 실제로 일치한다는 보장은 없습니다. 이 부분이 걱정된다면 undefined를 추가하면 됩니다.)
<br>
## 인덱스 시그니쳐 보다 더 나은 방법
첫 번째는 인터페이스를 사용하는 방법입니다.
```tsx
export interface CardNumbers {
first: string;
second: string;
third: string;
fourth: string;
}
```
<br>
두 번째는 Record를 사용하는 방법입니다.
Record는 키 타입에 유연성을 제공하는 제너릭 타입입니다. 특히, string의 부분 집합을 사용할 수 있습니다.
```tsx
type CardNumbers = Record<'first' | 'second' | 'third' | 'fourth', string>;
// Type CardNumbers = {
// first: string;
// second: string;
// third: string;
// fourth: string;
// }
```
<br>
세 번째는 매핑된 타입을 사용하는 방법입니다.
매핑된 타입은 키마다 별도의 타입을 사용하게 해 줍니다.
```tsx
type CardNumbers = {[k in 'first' | 'second' | 'third' | 'fourth']: string};
// Type CardNumbers = {
// first: string;
// second: string;
// third: string;
// fourth: string;
// }
```
<br>
이번 기회에 인덱스 시그니쳐를 학습하게 되었는데, 정말 잘했다는 생각이 들었다.
타입스크립트를 잘 사용하면 본인의 실수도 줄일 수 있지만, 함께 코드를 작성하는 동료의 실수도 잡을 수 있는 장점이 있다.
그러나 인덱스 시그니쳐를 사용하게 되면 이러한 장점들을 누리지 못할 것같다고 느낀다. (뭔가 as와 같은 타입 단언 느낌)
타입스크립트는 학습이 어렵지만, 점차 익숙해지면 매우 유용하고 재미있게 느껴진다.