[리팩토링] TS 로 변경하기

jeongjwon·2023년 9월 19일
0

Project

목록 보기
6/8

TypeScript 적용하기

기존 HTML, JavaScript, CSS 에서 CRA 로 새로 변경해서 작업을 했었다. 작업한 내용은 TypeScript 로 적용하는 것이다. 물론, 많이 해본 것도 아니고 메인 프로젝트때 3주간 했던 역량으로 리팩토링을 시도해보았다.



과정 1. TypeScript 설정하기

npx create-app [프로젝트명] --typescript 를 설치하여 새롭게 프로젝트를 생성하면 되지만,
기존의 프로젝트에서 변경해보고자 했다.

  1. 우전 기존 프로젝트에 TypeScript 패키지와 typescript 버전으로 추가한다.
    npm install typescript @types/node @types/react @types/react-dom

  2. tsconfig.json 설정

{
  "compilerOptions": {
    "target": "es6",
    "lib": ["dom", "dom.iterable", "esnext"],
    "noImplicitAny": true,
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "jsx": "react",
    "outDir": "./dist",
    "typeRoots": ["./node_modules/@types", "types"]
  },
  "include": ["./src/**/*"]
}


과정 2. 파일 변환하기

기존의 js 파일에서 tsx 로 변환하는 과정을 거쳤다.

hook 같은 경우는 다음과 같은 이유로 tsx 가 아닌 ts 로 변경하였다.

ts (TypeScript): JSX 없이 일반 TypeScript 코드만 있는 파일에 사용된다. 이런 파일에서는 JSX 문법을 사용하지 않으며 React 컴포넌트를 작성하지 않는다.
tsx (TypeScript with JSX) : React 컴포넌트와 JSX 문법이 포함된 파일에 사용된다. React 컴포넌트를 작성할 때 일반적으로 .tsx 를 사용한다. 이렇게 하면 TypeScript 코드 안에 JSX 요소를 포함할 수 있다.



과정 3. 에러 해결하기

1. Argument of type 'HTMLElement | null' is not assignable to parameter of type 'Element | DocumentFragment'

const root = ReactDOM.createRoot(document.getElementById("root"));

루트 엘리먼트를 만들 때 null 일 수 있어 발생하는 문제이다.
getElementById 를 통해 받아오는 객체의 Type 을 지정해주면 된다. TypeScript 가 데이터 타입을 알아볼 수 있도록 해주기 위해 as HTMLElement 로 type을 지정해준다.

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);

2. Binding element implicitly has an 'any' type

export type Record = {
  date: string;
  distance: number;
  hour: number;
  minute: number;
  second: number;
  perMin: number;
  perSec: number;
  id: number;
};

가장 많이쓰이는 type 으로 데이터가 담겨있는 객체로 Record 를 생성하였다.
다른 컴포넌트에서 import 할 수 있게 export 를 하였고, 다른 컴포넌트나 props 로 받은 데이터를 any 가 아닌 type 를 제대로 설정해주었다.


3. HTML DOM Event

  const handleInputChange = (
    e: React.ChangeEvent<HTMLInputElement>,
    fieldName: string
  ) => {
    const { value } = e.target;
    setFormData((prevData) => ({
      ...prevData,
      [fieldName]: fieldName === "date" ? value : parseFloat(value),
    }));
  };

폼 컴포넌트에서 자주 쓰이는 Input 요소의 Change 함수 핸들러의 인자인 이벤트 e 의 type 을 명확하게 정의한다.


4. undefined 형식은 할당할 수 없습니다.

해당 Item 컴포넌트에서 수정 아이콘을 클릭해야지만, 편집 폼 모달이 띄워지고 editedTask 라는 상태값을 지정할 수 있다.

//Board.tsx
  const [editedTask, setEditedTask] = useState<Record | undefined>(undefined);

//EditForm.tsx
 const { id, date, distance, hour, minute, second, perMin, perSec } =
    editedTask || {
      id: 0,
      date: "",
      distance: 0,
      hour: 0,
      minute: 0,
      second: 0,
      perMin: 0,
      perSec: 0,
    };

상위 컴포넌트인 Board 에서 초기값은 수정 아이콘을 클릭했을 경우에 명확히 정해지는 부분이라 Record 나 undefined 로 지정해야만 했다.
하위 컴포넌트인 EditForm 에서는 전달받은 props 인 editedTask 가 undefined 값이 아니라면 editedTask 로, undefined 일 경우에는 구조 분해 할당을 통해 초기값을 설정해주었다.


5. 'React'은(는) UMD 전역을 참조하지만 현재 파일은 모듈입니다.

기존 CRA에서 typescript 변환 과정에 있어서 에러가 발생했다.
CRA 는 버전 4에서 즉시 사용 가능한 새로운 JSX 변환을 지원하지만, 사용자 정의 설정을 사용하는 경우 작성 jsx 하지 않고 TypeScript 오류를 제거하려면 필요하다.

  • typescript 버전 4.1 이상
  • react, react-dom qjwjs 17 이상
  • tsconfig.json 파일에서 compilerOptions 안에 "jsx": "react-jsx" 확인 및 추가

6. 월별 기록 재계산

  • 기존에 총시간이나 평균 페이스가 한 자리 수라면 10:27:04 라면 위와 같이 10:27:4 처럼 포맷되지 않은 채 계산이 되었었다.
  • 총 거리 또한 정리되지 않은 채 모든 소숫점이 표현되었다.
//Summary.tsx
const calculateTotalDistanceAndTime = (monthlyRecord: Record[]) => {
    let totalDistance = 0;
    let totalHour = 0;
    let totalMinute = 0;
    let totalSecond = 0;
    let totalPaceMin = 0;
    let totalPaceSec = 0;
    let totalTime = 0;

    // monthlyRecord 배열을 순회하면서 거리(distance)와 시간(hour, minute, second)을 더합니다.
  
    monthlyRecord.forEach((record: Record) => {
      totalDistance += record.distance;
      totalHour += record.hour;
      totalMinute += record.minute;
      totalSecond += record.second;
    });

    totalTime =
      totalHour === 0
        ? totalMinute * 60 + totalSecond
        : totalHour * 60 * 60 + totalMinute * 60 + totalSecond;

    // 총 거리 계산 후 소숫점 첫번째까지만 표현되도록 합니다. 
    const formattedTotalDistance = totalDistance.toFixed(1);

    const totalPaceSeconds = totalHour * 3600 + totalMinute * 60 + totalSecond;
    const totalPacePerKilometer = totalPaceSeconds / totalDistance;
    totalPaceMin = Math.floor(totalPacePerKilometer / 60);
    totalPaceSec = Math.floor(totalPacePerKilometer % 60);

    // 초(second)를 분(minute)과 시간(hour)로 변환합니다.
    totalMinute += Math.floor(totalSecond / 60);
    totalSecond %= 60;
    totalHour += Math.floor(totalMinute / 60);
    totalMinute %= 60;

    // 평균 페이스의 초를 두 자릿수로 포맷팅과 NaN 제외시킴
    const formattedPaceMin = isNaN(totalPaceSec)
      ? "00"
      : String(totalPaceMin).padStart(2, "0");

    const formattedPaceSec = isNaN(totalPaceSec)
      ? "00"
      : String(totalPaceSec).padStart(2, "0");

    // 총 걸린 시간을 시, 분, 초로 나누고 각각을 두 자릿수로 포맷팅
    const formattedTotalHour = String(Math.floor(totalTime / 3600)).padStart(
      2,
      "0"
    );
    const formattedTotalMin = String(
      Math.floor((totalTime % 3600) / 60)
    ).padStart(2, "0");
    const formattedTotalSec = String(
      Math.floor((totalTime % 3600) % 60)
    ).padStart(2, "0");

    return {
      formattedTotalDistance,
      formattedTotalHour,
      formattedTotalMin,
      formattedTotalSec,
      formattedPaceMin,
      formattedPaceSec,
    };
  };

calculateTotalDistanceAndTime 를 통해 총 거리와 시간, 평균페이스를 재계산하여 { formattedTotalDistance, formattedTotalHour, formattedTotalMin, formattedTotalSec, formattedPaceMin, formattedPaceSec, } 하나의 객체로 리턴하도록 하여 각각의 값들을 사용하도록 하였다.



마치면서

타입스크립트로 변환하는 과정에서 단순히 타입을 지정하는 것에만 치중하는 것보다는 이에 발생하는 예외를 처리하는 것에 애를 많이 먹었다. 특히나 Element 나 Event 의 타입을 지정하는 것에 어려움이 있었다. 리팩토링하는 과정에서 지난 결과물에 비해 조금 더 성장할 수 있는 부분을 기여할 수 있어서 기분도 좋았다.



보완하고 싶은 점

현재 localStorage 를 통해 데이터를 관리하고 있다.
물론, 내 로컬에서 진행한 프로젝트이기에 아무런 영향이 없지만, 전역 관리 라이브러리 Recoil 로 리팩토링해보고 싶다.

0개의 댓글