본문 바로가기
JavaScript,TypeScript/TypeScript

TypeScript 100% 활용하기

by 그냥하는거지뭐~ 2024. 4. 12.
목차
- 프롤로그
- 1. 조건부 타입으로 타입 좁혀나가기
- 2. 매핑된 타입으로 깔꼼하게 
- 3. 객체를 타입화할때 Record? Index Signature? 
- 4. 타입 가드
- 5. Redux + TS: configureStore.hooks.ts 

 

프롤로그: 리액트와 타입스크립트의 케미

 리액트의 렌더링 과정에서 데이터는 핵심적인 역할을 한다. 리액트의 동작 원리는 모두 데이터의 흐름을 기반으로 한다고 해도 과언이 아니다. 하지만 동적 타입 언어인 자바스크립트에서는 변수의 타입이 실행 시점에 결정되기 때문에, 개발자의 의도와 다르게 타입이 변환되어 예상치 못한 오류가 발생할 수 있다. 예를 들어, 숫자를 예상했지만 문자열이 전달될 경우, 불필요한 타입 변환으로 인한 성능 저하 또는 런타임 오류가 발생할 수 있다.

 

알수록 뇌절인 자바스크립트

const add = (a, b) => a + b;

add(true, 1); // 2
add(true, 'abc'); // 'trueabc'
add([], []); // ''
add([], {}); // '[object object]'
add({}, {}); // NaN

 

JS로 바나나만들기!

 

 타입스크립트는 이러한 문제를 해결해줄 수 있는 강력한 도구다. 정적 타입 시스템을 제공함으로써, 개발 과정에서 데이터의 타입을 명시적으로 선언하고 검사할 수 있다. 데이터의 흐름을 안정적으로 관리할 수 있게 된 것이다. TS의 장점이라 하면 오류를 감소해주고 가독성을 높여주고 너무 많은데, 나는 자동 완성 기능이 진짜 너무 좋다. 

 

 아무튼! 타입스크립트를 리액트와 함께 사용하면 개발 초기 단계에서 더 많은 오류를 잡아내고, 프로젝트의 안정성을 높일 수 있다. 따라서 이제는 타입스크립트의 적용이 선택이 아닌 필수다. 이번 시간에는 어떻게 하면 TS를 고급지고 우아하게 사용할 수 있는지, 그 방법을 연구해보도록 하겠다. 

 

c.f) 문법이 기억이 안난다면? 

 

Cheat Sheets

To read later or print

www.typescriptlang.org

 


1. 조건부 타입(Conditional Types)으로 타입 좁혀나가기

if문처럼 동작하는 타입

T extends U ? X : Y

 

조건부 타입을 이용해서 데이터 페칭 결과 상태에 따라 다르게 처리하는 예시를 보자. 

우선, 타입을 정의하자

type ApiResponse<T> = {
  isLoading: true;
} | {
  isLoading: false;
  error: Error;
} | {
  isLoading: false;
  error: null;
  data: T;
};

ApiResponse는 다음 세 가지 상태 중 하나를 가진다. 

1. 데이터 로딩 중 (isLoading: true)
2. 로딩 완료, 에러 발생 (error: Error)
3. 로딩 완료, 데이터 성공적으로 로드 (data: T)

 

function DataComponent<T>({ response }: { response: ApiResponse<T> }) {
  if (response.isLoading) {
    return <div>Loading...</div>;
  }

  if (response.error) {
    return <div>Error: {response.error.message}</div>;
  }

  return <div>Data: {JSON.stringify(response.data)}</div>;
}

- isLoading: true이면 Loading...을 보여준다.

- isLoading: false이고 error가 존재하면, 에러 메세지를 띄운다.

- isLoading: false이고, error: null이며, data가 존재하는 경우, 로드된 데이터를 보여준다. 

 

이런식으로 하면 사용자에게 정확한 기능을 제공할 수 있다!


2. 매핑된 타입 (Mapped Types)으로 깔꼼하게 

이미 존재하는 타입을 재사용해서 타입 생성

type MappedType<T> = {
  [P in keyof T]: T[P];
};

 

매핑된 타입은 객체의 모든 프로퍼티에 대해 타입 변환을 적용하고 싶을 때 사용한다. 기존 타입을 재사용하면서, 쉽게 변형을 생성할 수 있다. 예를 들어, 어떤 객체의 모든 프로퍼티를 선택적으로 만들거나, 모든 프로퍼티의 타입을 변경하고 싶을 때 사용할 수 있을 것이다. 

 

Example 1: 모든 프로퍼티 선택적으로 만들기

type Optional<T> = {
  [P in keyof T]?: T[P];
};

interface Props {
  name: string;
  age: number;
}

type OptionalProps = Optional<Props>;

 

 

Example 2: 타입 바꾸기

type Options<T> = {
  [P in keyof T]: boolean;
};

interface State {
  user: string;
  id: number;
}

type ChangedState = Options<State>;

이 경우 type ChangedState는 {user: boolean; id: boolean;}이 된다.

 

Example 3: optional 속성 제거해버리기

type Concrete<T> = {
  [P in keyof T]-?: T[P];
};
 
type MaybeUser = {
  id: string;
  name?: string;
  age?: number;
};
 
type User = Concrete<MaybeUser>;

- 또는 +를 접두사로 붙여서 이런 수정자를 추가하거나 제거할 수 있는데, 위의 예시에서는 -?로 제거했다. 


3. 객체를 타입화할때 Record? Index Signature? 

문법: Record<Keys, Type>의 정의
Record 타입은 두 개의 타입 매개변수를 받는다
- Keys: 객체의 키가 될 수 있는 문자열 또는 숫자의 union 타입
- Type: 객체의 각 키에 할당될 값의 타입

 

그냥 Index signature를 쓰면 되지 않나? 라고 생각할 수 있다. 어떤 상황에서 Record를 사용하면 좋은지 알아보자. 

 

우선, index signature을 쓴 경우는 다음과 같다. 

interface RoleAccess {
  [key: string]: number;
}

const roleAccess: RoleAccess = {
  admin: 3,
  user: 2,
  guest: 1
};

이 경우, RoleAccess에 어떤 문자열 키도 사용할 수 있다. 여기서 새로운 역할을 추가하면, 다음과 같이 오류가 나지 않는다. 

roleAccess.vip = 4;  // 오류 없음

 

이를 막고싶다면 Record 타입을 쓰면 된다. 

type UserRoles = 'admin' | 'user' | 'guest';

const roleAccess: Record<UserRoles, number> = {
  admin: 3,
  user: 2,
  guest: 1
};

Record 타입은 해당 키들에 대한 값을 정의할 때 모든 키가 포함되어야 한다. 새로운 역할을 추가하려고 한다면, 오류가 발생한다. 그리고 roleAccess에 예를 들어 guest를 명시하지 않았다면, 없다고 오류가 난다. 

 

즉 Record가 더 엄격하다. 

 


4. 특정 역할에 따라 다른 속성을 가지는 경우 - 타입 가드

일반 유저와 관리자 유저가 있다. id, name은 공통으로 가지고, 일반 유저는 paid 속성을, 관리자는 permissions 속성을 가지고 싶을 때 어떻게 해야할까

 

Solution 1 : type

type User = {
  id: number;
  name: string;
  role: 'user' | 'admin';
  paid?: boolean;
  permissions?: string[];
};

이렇게..? 

 

문법: type을 이용해서 상속을 구현하는 방법
type User = {
  name: string;
};

// type ExtendedUser은 User, 그리고 {}이다.
type ExtendedUser = User & {};

 

이걸 이용해서 요렇게 구현할 수 있다!

type User = {
  id: number;
  name: string;
} & ({ 
    role: 'user';
    paid: boolean;
    } | {
    role: 'admin';
    permissions: string[];
});

 

 

Solution 2 : interface

interface로도 구현할 수 있다. 타입의 종류가 많아지면 type 대신 interface를 사용하여 가독성을 높이자. 각 인터페이스를 구분하는 공통 속성(type)을 추가해서 value.type으로 구분했다. 

interface Person {
  type: "Person";
  name: string;
  age: number;
}

interface Product {
  type: "Product";
  name: string;
  price: number;
}

interface Building {
  type: "Building";
  name: string;
  location: string;
}

function toString(value: Person | Product | Building) {
  switch (value.type) {
    case "Person":
      return `${value.name} ${value.age}`;
    case "Product":
      return `${value.name} ${value.price}`;
    case "Building":
      return `${value.name} ${value.location}`;
  }
}

 


5. [Redux+TS] configureStore.hooks.ts 

redux를 쓸 때 useSelector나 useDispatch를 쓸 때마다 다음과 같이 타입을 지정해줘야하는 번거로움이 있다. 

 

const {accessToken} = useSelector((state: RootState) => state.auth);
const dispatch = useDispatch<AppDispatch>();

 

반복작업을 피하는 방법으로, configureStore.hooks.ts 파일을 만들어 해결할 수 있다. 

 

Step 1: redux store 설정

// configureStore.ts
import { configureStore } from '@reduxjs/toolkit';
import authReducer from './modules/auth'; 

const store = configureStore({
  auth: authReducer
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export default store;

 

Step 2: configureStore.hooks.ts 파일에서 useSelectoruseDispatch에 대한 타입 정의를 추가하여 커스텀 훅 생성

// configureStore.hooks.ts
import { TypedUseSelectorHook, useSelector, useDispatch } from 'react-redux';
import { RootState, AppDispatch } from './store';

export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
export const useTypedDispatch = () => useDispatch<AppDispatch>();

 

Step 3: 사용

const {accessToken} = useTypedSelector(state => state.auth);
const dispatch = useTypedDispatch();

 

 


추천하는 vscode extension 

오류를 예쁘게 보고싶다면? 

 

GitHub - yoavbls/pretty-ts-errors: 🔵 Make TypeScript errors prettier and human-readable in VSCode 🎀

🔵 Make TypeScript errors prettier and human-readable in VSCode 🎀 - yoavbls/pretty-ts-errors

github.com

 


Reference

- 조건부 타입 https://www.typescriptlang.org/ko/docs/handbook/2/conditional-types.html

- 매핑된 타입 https://www.typescriptlang.org/ko/docs/handbook/2/mapped-types.html

- Record https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type

- https://fe-developers.kakaoent.com/2021/211012-typescript-tip/