기존 HTML, JavaScript, CSS 에서 CRA
로 새로 변경해서 작업을 했었다. 작업한 내용은 TypeScript
로 적용하는 것이다. 물론, 많이 해본 것도 아니고 메인 프로젝트때 3주간 했던 역량으로 리팩토링을 시도해보았다.
npx create-app [프로젝트명] --typescript
를 설치하여 새롭게 프로젝트를 생성하면 되지만,
기존의 프로젝트에서 변경해보고자 했다.
우전 기존 프로젝트에 TypeScript 패키지와 typescript 버전으로 추가한다.
npm install typescript @types/node @types/react @types/react-dom
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/**/*"]
}
기존의 js 파일에서 tsx 로 변환하는 과정을 거쳤다.
hook 같은 경우는 다음과 같은 이유로 tsx 가 아닌 ts 로 변경하였다.
ts (TypeScript): JSX 없이 일반 TypeScript 코드만 있는 파일에 사용된다. 이런 파일에서는 JSX 문법을 사용하지 않으며 React 컴포넌트를 작성하지 않는다.
tsx (TypeScript with JSX) : React 컴포넌트와 JSX 문법이 포함된 파일에 사용된다. React 컴포넌트를 작성할 때 일반적으로.tsx
를 사용한다. 이렇게 하면 TypeScript 코드 안에 JSX 요소를 포함할 수 있다.
const root = ReactDOM.createRoot(document.getElementById("root"));
루트 엘리먼트를 만들 때 null 일 수 있어 발생하는 문제이다.
getElementById
를 통해 받아오는 객체의 Type 을 지정해주면 된다. TypeScript 가 데이터 타입을 알아볼 수 있도록 해주기 위해 as HTMLElement
로 type을 지정해준다.
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
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 를 제대로 설정해주었다.
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 을 명확하게 정의한다.
해당 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 일 경우에는 구조 분해 할당을 통해 초기값을 설정해주었다.
기존 CRA에서 typescript 변환 과정에 있어서 에러가 발생했다.
CRA 는 버전 4에서 즉시 사용 가능한 새로운 JSX 변환을 지원하지만, 사용자 정의 설정을 사용하는 경우 작성 jsx 하지 않고 TypeScript 오류를 제거하려면 필요하다.
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 로 리팩토링해보고 싶다.