개발일지

타입스크립트로 마이그레이션 여정기 2

FE 2022. 5. 5. 14:14

TypeScript, a Superset of JavaScript

1. 확장자명 js, ts, tsx 파일

파일 확장자명 js에서는 뭐가 js고 jsx인지 모릅니다. 그러나 ts 파일 내에서는 ts와 리액트 컴포넌트들을 구분해서 처리를 하기 때문에 react에 관련 import 즉 tsx를 알 수 없습니다. 정리해보면

  • .ts - 순수 Typescript 파일에 사용
  • .tsx - JSX를 포함하는 파일에 사용 (리액트 구성요소)

 

간혹 모든 파일 확장자명이 tsx로 되어있는 것을 보면 아 이래서 그랬구나라고 깨달았던 것 같습니다. 저희는 로직만 담은건 ts로 처리하고 React 구성요소가 포함되어 있으면 tsx로 처리하기로 했습니다.

 

.ts
.tsx

2. Typescript의 타입 자동추론은 정말 강력하다.

타입스크립트와 vscode 둘 다 마이크로소프트에서 만들었기 때문에 에디터간의 자동완성이나 기능들이 너무 좋습니다.

 

처음에 Typescript를 도입하기 전에 이 많은 파일에 타입들을 어떻게 다 지정해주지? 라는 생각을 했던적이 있습니다. 그러나 Typescript를 실제로 사용하고나면 타입스크립트의 자동추론에 감탄하게 될 거라고 생각합니다. 예를 들어, Api response type을 지정해주면 알아서 타입추론을 하기 때문에 다른 곳에서 쓸 때 타입을 지정해 줄 필요가 없습니다.

 

이렇게 Api에서 response Type을 지정해주면 쓰는쪽에서 바로 어떤 타입이다라고 추론되는 것을 볼 수 있습니다.

 

그리고 당연한 얘기지만 인수에서는 함수를 호출하는 곳이기 때문에 타입지정을 할 수 없습니다. 인수에서 타입을 지정하게 되면 그때 그때 타입이 지정되기 때문에 어떤 타입이 넘어올 지 알 수가 없게됩니다. parmeter로 받는 인자에서 타입을 지정할 수 있습니다.

3. Typescript의 오류 체커

react-query를 js에서 사용했을땐 data를 제대로 가져오기 전에 옵셔널 체이닝이나 별도의 처리 없이도 오류가 뜨지 않았습니다. null인지 undefined인지 신경을 안쓰기 때문에 당연한 일이기도 합니다. js에서는 그 값을 당연히 있을거야라고 생각하니까요 그러다가 작업을 할 때 즉, 렌더링이 될 때 오류를 알 수 있습니다. 즉, 런타임 환경에서만 알 수 있습니다.

 

그러나 타입스크립트에서는 data를 제대로 가져오기 전이거나 안에 있는 값들을 참조하려면 undefined 체크(옵셔널 체이닝 등)를 해줘야 합니다. 이렇게 컴파일 단계에서 오류를 알 수 있어 미연에 에러 방지를 할 수 있습니다.

 

그리고 간혹가다 ts가 있던 브런치에서 js만 있는 브런치로 왔다갔다 하면 에디터가 꼬이는 경우가 발생합니다. 이럴 땐 간단하게 껐다가 키면 해결됩니다.

4. Error 타입 지정

const onClickFindPasswordBtn = async () => {
    try {
      await postResetPassword({ email });
      setFindPasswordPopupVisible(true);
    } catch (error) {
      // error에 타입을 지정할 수 없고 any unknown만 지정이 되기 때문에 변수로 빼서 지정
      const { errors } = (error as AxiosError).response?.data || null;
      if (errors.response.data.errors[0])
        ...
    }
  };

이렇게 Error에 대한 타입을 지정할 때 catch (error: AxiosError) 이렇게 지정을 할 수 없습니다. 

이것을 Type Annotations라고 하는데 즉, catch절에는 Type Annotations을 허용하지 않습니다. 

 

참고 사이트 : https://github.com/Microsoft/TypeScript/issues/8677#issuecomment-220385124

예외의 유형을 알 수 있는 방법이 없기 때문에 catch 절에 허용을 안한다고 위 설명을 통해 알 수 있습니다.

5. Double Exclamation Mark 활용하기

!! 라고 하는 Double Exclamation Mark 입니다. 간단하게 설명하면 !!는 falsy 값은 false로, truthy 값은 true로 평가됩니다.

예를 들면 버튼 유효성 검증 값을 잘 채워졌는지 체크할 때 사용할 수 있습니다.

useEffect(() => {
    setFinish(investorGrade && identityFile && submitDate);
  }, [investorGrade, identityFile, submitDate]);

js에서는 이렇게 버튼 유효성 체크를 하고 있었지만 ts는 각 변수 타입의 따라서 boolean 형태로 타입을 지정해줄 수 없다는 오류가 나옵니다. 이럴 때 !!를 활용할 수 있습니다.

useEffect(() => {
    setFinish(!!investorGrade && !!identityFile && !!submitDate);
  }, [investorGrade, identityFile, submitDate]);

6. Typescript as 사용하기 (타입 단언문)

정말 다양한 경우에 as를 사용할 수 있지만 가장 많이 사용할 땐 외부에서 주는 값이 타입을 보장해줄 수 없을 때 사용할 수 있습니다.

await postEmail({ email: email.value, type: (type as string)?.toUpperCase() as UserType });

7. string 타입과 리터럴 타입

타입스크립트에 리터럴 타입은 특정 값을 나타내는 유형입니다. 

리터럴 유형은 문자열, 숫자형, 부울형 유형일 수 있습니다. 간단하게 요약하면 리터럴 유형은 변수의 가능한 값을 특정 값 집합으로 제한하려는 상황에서 유용하게 사용할 수 있습니다!

 

타입스크립트를 사용하며 기존 js에서 값을 잘 꺼내서 사용하던게 타입이 맞지 않다고 오류가 났던 경험이 있습니다. 이 부분이 딱 처음 접했을 땐 이해가 되지 않아서 헷갈렸던 기억이 나네요.

 

여기서 PRIVACY_REJECT_TYPE의 value는 string 타입이지만 rejectType은 RejectReasonType이라는 유니온(리터럴)타입 입니다. RejectReasonType은 type이 리터럴 타입으로 정의되어 있는데 PRIVACT_REJECT_TYPE에서는 string으로 너무 넓게 타입을 열어주니 이러한 오류가 나게 됩니다. RejectReasonType과 맞춰주기 위해 string 타입보다 더 구체적인 타입을 사용하면 됩니다!

// Types
export type PrivacyRejectReasonType =
  | "CANNOT_IDENTIFY"
  | "DISCORD_RESIDENT_NUMBER"
  | "DISCORD_IDENTITY_CARD"
  | "DISCORD_PUBLISH_DATE";

export type RejectReasonType =
  | "NONE"
  | "ETC"
  | "EXPIRED"
  | "INFORMATION"
  | "DOCUMENT"
  | "CANNOT_IDENTIFY"
  | "DISCORD_RESIDENT_NUMBER"
  | "DISCORD_IDENTITY_CARD"
  | "DISCORD_PUBLISH_DATE"
  | "NOT_MATCHED"
  | "INVALID_NUMBER"
  | "WARRANT_ERROR"
  | "REGISTERED_SEAL_ERROR";

// reject.ts
export const PRIVACY_REJECT_TYPE = {
  CANNOT_IDENTIFY: "식별 불가",
  DISCORD_RESIDENT_NUMBER: "정보 불일치(1)",
  DISCORD_IDENTITY_CARD: "정보 불일치(2)",
  DISCORD_PUBLISH_DATE: "정보 불일치(3)",
};

이렇게 지정을 해주고 사용할 땐 타입 단언인 as로 사용하여 해당하는 이 값만 올 수 있게 지정해주면 됩니다.

{PRIVACY_REJECT_TYPE[invest?.privacy?.rejectType as PrivacyRejectReasonType]}

이렇게 사용하는 방법이 있고, 두 번째로는 key in을 이용하는 방법이 있습니다!

// 리터럴 타입
export type EvaluationStatus = "NONE" | "WAIT" | "REJECT" | "ACCEPT";

export const EvaluationStatusLabel: {
  [key in EvaluationStatus]: string;
} = {
  NONE: "",
  REJECT: "반려",
  WAIT: "심사요청",
  ACCEPT: "승인",
};

// jsx
<span>
  {EvaluationStatusLabel[data.status]}
</span>

key는 EvaluationStatus 안에 있는 리터럴 타입으로 강제하고 그 타입에 따른 값을 의도한대로 보여주고 싶을 때 사용할 수 있습니다!

 

string 타입의 범위는 매우 넓기 때문에 이렇게 사용하면 가능한 변수의 값을 string보다 더 정확하게 사용할 수 있습니다. 물론 이렇게 하는 방법 말고도 keyof로 객체의 속성 체크 등 다양한 방식으로도 해결할 수 있지만 저는 주로 이렇게 사용했던 것 같습니다!

 

8. Api params에 다른 컬럼을 보내야 할 때

이 경험은 id 값이 있을 때와 없을 때 호출해야 하는 api가 달라서 보내야 하는 params 값이 달랐을 때 겪었던 경험인데요.

    if (dateValidate) {
      let params = {
        name: memberData?.name || "",
        identityType,
        identityFileId: identityFile.fileId,
      };

      if (invest.privacyMode === MODE_ADD) {
        mutatePostMyPrivacy(params);
      } else {
        params.id = invest.privacy.id; // 이렇게 자유롭게 추가가 됐다.
        mutatePutMyPrivacy(params);
      }
    }

js에서는 이렇게 put 요청을 보낼 때 id값을 같이 넘겨줘야 하는 상황에서 동적으로 추가를 할 수 있었습니다.

그러나 ts에서는 저렇게 자유롭게 추가하면 안되기 때문에(API Request Type을 정의해줬기 때문에) 구분을 해줘야 하는 상황이였는데요. 그냥 처음엔 저 경우일 때 params를 하나 더만들자라고 생각하고 코드를 작성했습니다.

    if (dateValidate) {
      let params = {
        name: memberData?.name || "",
        identityType,
        identityFileId: identityFile!.fileId,
      };

      if (!invest?.privacy.id) {
        mutatePostMyPrivacy(params);
      } else {
        let params = {
          id: invest.privacy.id,
          name: memberData?.name || "",
          identityType,
          identityFileId: identityFile!.fileId,
        };
        mutatePutMyPrivacy(params);
      }
    }

딱 봐도 별로지 않나요? 중복되는 코드가 너무 많고 가독성도 별로입니다. 이럴 땐 params 변수에 타입을 지정해줘서 추가할 수 있습니다.

    if (dateValidate) {
      let params: InvestorPrivacyRequest = { // 이렇게 타입을 지정해줘서 추가 가능
        name: memberData?.name || "",
        identityType,
        identityFileId: identityFile!.fileId,
      };

      if (!invest?.privacy.id) {
        mutatePostMyPrivacy(params);
      } else {
        params.id = invest.privacy.id;
        mutatePutMyPrivacy(params);
      }
    }

9. 배포 시 문제를 찾아주는 ts

저희는 CI/CD 도구로 젠킨스를 이용하고 있는데요. 기존 JS 였다면 이런 타입체커로 오류를 찾아주지 않고 배포가 되었을겁니다. 그리고 나중에 undefined로 페이지가 뜨지 않는 문제가 발견되면 Hotfix 건으로 올라오게 되겠죠?! 지금 이 경우에는 공통 컴포넌트에 보내줬던 props가 optional이 아니라 필수로 설정되어 있는데 하나의 컴포넌트에서만 props를 보내고 있어 발생했던 오류였습니다. 이렇게 타입스크립트는 배포시에도 한번 더 체크를 해주기 때문에 좀 더 안전하게 프로젝트를 관리할 수 있다는 장점이 있습니다. 물론 로컬에서 빌드 테스트를 하고 난 이후에 올리는 것이 더 안전하지만 놓칠 수 있으니까요! 

 

그리고 9가지의 대해서 다뤄봤지만 어떤 주제는 제가 사용한 방식이 아니여도 다양한 방식으로도 문제 해결을 할 수 있을거에요! 여러가지 방식중에서 제가 해결했던 방식에 대해서 정리하고자 했던거니 참고만 부탁드리면 감사하겠습니다!!!!

 

당연히 이거 말고도 정말 다양한 경우를 접하고 싶고 해결하고 싶습니다. 추가적으로 알게 된 경험이나 지식들에 대해선 추후에 포스팅 할 예정이고요. TS를 도입하면서 사소하지만 깨달았던 경험과 지식들.. 그리고 강력한 장점들을 많이 경험하고 있습니다. 가장 많이 와닿는 것은 자동타입추론과 자동완성기능입니다. 좀 더 얘기하면 컴파일 단계에서 에러체크를 좀 더 면밀히 할 수 있는점도 얘기할 수 있겠네요! 

 

JS를 쓰고 계시다면 TS 도입에 대한 고민을 적극적으로 권장하고 싶습니다!!