[TS] 5장 타입 활용하기 - keyof typeof, Record 유틸리티 타입 개선

naini 🐰·2025년 2월 18일

FrontEnd

목록 보기
16/18

📘 학습 후기
타입스크립트를 학습하다보면 타입스크립트가 단순히 타입을 지정하는 언어가 아니라, 개발자가 실수할 가능성을 줄이고 안전한 코드를 작성하도록 도와주는 강력한 도구라는 걸 확실히 깨닫게 된다.
알고 쓰면 타입스크립트의 장점을 확실히 느끼며 개발할 수 있을 것 같다.

우아한 타입 스크립트 with 리액트 학습 내용을 정리했다.

4. 불변 객체 타입으로 활용하기


프로젝트에서 상숫값을 관리할 때 객체를 사용한다.
컴포넌트나 함수에서 이런 객체를 사용할 때 열린 타입으로 설정할 수 있다.
키 타입을 해당 객체에 존재하는 키값으로 설정하는 것이 아니라 string으로 설정하면 colors에 어떤 값이 추가될지 모르기 때문에 getColorHex 함수의 반환 값은 any가 된다.

const colors = {
  red: "#F45452",
  green: "#0C952A",
};

const getColorHex = (key: string) => colors[key];

객체 타입을 저확하고 안정하게 설정하는 방법을 알아보자.

[1] theme style 객체 활용

Atom 단위의 작은 컴포넌트는 다양한 환경에서 유연하게 사용될 수 있도록 구현되어야 하는데 이러한 설정값은 props로 넘겨주도록 설계한다.

(1) props로 직접 값을 넘겨주면 문제가 발생한다

  • 사용자가 모든 색상 값을 인지해야 한다.
  • 변경 사항이 생길 때 직접 값을 넣은 모든 곳을 찾아 수정해야 하는 번거로움이 생기기 때문에 변경에 취약한 상태가 된다.

(2) theme 객체에서 스타일 값을 관리한다

Atom 컴포넌트에서는 theme 객체의 색상, 폰트 사이즈의 키값을 props로 받은 뒤 theme 객체에서 값을 받아오도록 설계하여 문제를 해결한다.

어떻게하면 정확하고 안전한 값을 받아오도록 설계할 수 있을지 알아보자.

[2] keyof, typeof 연산자

(1) string으로 설정시 문제점

  • Button 컴포넌트의 props로 color, backgroundColor를 넘겨줄 때 키값이 자동 완성되지 않는다.
  • 잘못된 키값을 넣어도 에러가 발생하지 않게된다.
// 1. 타입
interface Props {
  color?: string;
  backgroundColor?: string;
}

// 2. 스타일
const Button = styled.button<Props>`
  color: ${({ color })=>theme.colors[color]}; 
  backgroundColor: ${({ backgroundColor })=>theme.colors[backgroundColor]};  
`;

// 3. 버튼 컴포넌트
<Button backgroundColor="red" />; // "blue"를 넣어도 에러가 발생하지 않는다.

해당 문제는 theme 객체로 타입을 구체화해서 해결할 수 있다.

(2) keyof: 객체의 키값을 타입으로 추출하기

keyof 연산자는 객체 타입을 받아 해당 객체의 키값을 string 또는 number의 리터럴 유니온 타입을 반환한다.

interface ColorType {
  red: string;
  green: string;
}

type ColorKeyType = Keyof ColorType; // 'red'| 'green'

(3) typeof: 값을 타입으로 다루기

type ColorsType = typeof colors;
/*
{
  red: string;
  green: string;
}
*/

(4) keyof typeof 적용하기

// 1. keyof typeof 타입 정의
interface Props {
  color: keyof typeof theme.colors;
  backgroundColor: keyof typeof theme.colors;
}

// 2. 스타일
const Button = styled.button<Props>`
  color: ${({ color })=>theme.colors[color]}; 
  backgroundColor: ${({ backgroundColor })=>theme.colors[backgroundColor]};
`;

// 3. 사용
const App = () => {
  return (
    <>
      <Button color="red" backgroundColor="green">button1</Button>
      <Button color="red" backgroundColor="blue">Error</Button>
    <>
  );
};

(5) 장점

  • 다른 값을 넣으면 타입 오류가 발생하여 실수를 방지할 수 있다.
  • vscode 자동완성을 제공한다.

5. Record 원시 타입 키 개선하기


[1] Record 타입

Record<K, V>는 타입스크립트의 유틸리티 타입 중 하나로, 특정 키와 값을 가지는 객체 타입을 정의할 때 사용한다.

[2] 문제

객체 선언 시 키가 어떤 값인지 명확하지 않다면 string이나 number 같은 원시 타입으로 명시하곤 한다.

타입스크립트는 키가 유효하지 않더라도 타입상으로는 문제가 없기 때문에 오류를 표시하지 않기 때문에 예상치 못한 런타임 에러를 야기할 수 있다.

type Category = string;

interface Food {
  name: string;
}

// Category를 키로 사용하는 객체
const foodByCategory: Record<Category, Food[]> = {
  한식: [{name: "떡볶이"}, {name: "삼겹살"}];
  일식: [{name: "초밥"}],
}

foodByCategory["양식"]; // Food[]로 추론
foodByCategory["양식"].map((food) => console.log(food.name)); // 오류 발생하지 않는다.
  • Category의 타입은 string이고 Category를 Record 키로 사용하는 foodByCategory 객체는 무한한 키 값을 가지게 된다.
  • foodByCategory 객체에 없는 키값을 사용하더라도 타입스크립트는 오류를 표시하지 않는다.
  • foodByCategory["양식"]은 런타임에 undefined가 되어 오류를 반환한다.

Record를 명시적으로 사용하는 방법을 알아보자.

[3] 옵셔널 체이닝

옵셔널 체이닝은 객체의 속성을 찾을 때 중간에 null 또는 undefined가 있어도 오류 없이 안전하게 접근하는 방법이다.
?. 문법으로 표현되며 옵셔널 체이닝을 사용할 때 중간에 null 또는 undefined인 속성이 있는지 검사한다.
속성이 존재하면 해당 값을 반환하고, 존재하지 않으면 undefined를 반환한다.

(1) 사용법

foodByCategory["양식"]?.map((food) => console.log(food.name));

(2) 문제점

  • 어떤 값이 undefined인지 매번 판단해야 한다는 번거로움이 생긴다.
  • 실수로 undefined일 수 있는 값을 인지하지 못하고 코드를 작성하면 예상치 못한 런타임 에러가 발생할 수 있다.

타입스크립트의 기능을 활용하여 개발 중에 유효하지 않은 키가 사용되었는지 또는 undefined일 수 있는 값이 있는지 등을 사전에 판단해보자.

[4] 유닛 타입으로 변경해보자

키가 유한한 집합이라면 유닛 타입을 사용할 수 있다.
유닛 타입은 다른 타입으로 쪼개지지 않고 오직 하나의 정확한 값을 가지는 타입을 말한다.

type Category = "한식"|"일식"

interface Food {
  name: string;
}

// Category를 키로 사용하는 객체
const foodByCategory: Record<Category, Food[]> = {
  한식: [{name: "떡볶이"}, {name: "삼겹살"}];
  일식: [{name: "초밥"}],
}

// Property '양식' does not exist on type 'Record<Category, Food[]>'
foodByCategory["양식"];
  • Category로 한식 또는 일식만 올 수 있기 때문에 양식을 키로 사용하면 에러가 발생한다.
  • 개발 중에 유효하지 않은 키가 사용되었는지를 확인할 수 있다.

그러나 키가 무한해야 하는 상황에서는 적합하지 않다.

[5] Partial을 활용하여 정확한 타입을 표현하자

키가 무한한 상황에서는 Partial을 사용하여 해당 값이 undefined일 수 있는 상태임을 표현할 수 있다.

// 1. PartialRecord 타입 선언
type PartialRecord<K extends string, T> = Partial<Record<K, T>>;
type Category = string;

interface Food {
  name: string;
}

// Category를 키로 사용하는 객체
const foodByCategory: PartialRecord<Category, Food[]> = {
  한식: [{name: "떡볶이"}, {name: "삼겹살"}];
  일식: [{name: "초밥"}],
}

foodByCategory["양식"]; // Food[] 또는 undefined 타입으로 추론
foodByCategory["양식"].map((food) => console.log(food.name)); // Object is possibly 'undefined'
foodByCategory["양식"]?.map((food) => console.log(food.name)); // OK
  • 타입스크립트는 foodByCategory[key]를 Food[] 또는 undefined로 추론하고, 개발자에게 이 값은 undefined일 수 있으니 해당 값에 대한 처리가 필요하다고 표시해준다.
  • 개발자는 안내를 보고 옵셔널 체이닝을 사용하거나 조건물을 사용하는 등 사전에 조치할 수 있다.
  • 예상치 못한 런타임 오류를 줄일 수 있다.

0개의 댓글