이전 마이그레이션 관련 포스팅
- JavaScript → TypeScript 마이그레이션 1(세팅, App, AppRouter)
- JavaScript → TypeScript 마이그레이션 2(대시보드 페이지, 대시보드 관련 컴포넌트: state 빈 배열 초기화 never type 에러 해결)
이전에 이어서 이번에는 Transactions 페이지와 관련한 컴포넌트 마이그레이션을 진행해 볼 예정이다.
Transactions 페이지
파일 형식을 jsx에서 tsx로 바꾼 뒤에 에러가 뜨는 코드를 확인하고 수정해 주었다.
// Transactions.tsx
import React, { useEffect, useState } from "react";
...
import { UserInfo } from "../interfaces/UserData";
import { StoreData } from "../interfaces/StoreData";
import { TransactionData } from "../interfaces/TransactionData";
export default function Transactions() {
const transactionList: TransactionData[] = useSelector((state: StoreData) => state.transactions.transactions);
const userInfo: UserInfo = useSelector((state: StoreData) => state.user.userInfo);
...
const [data, setData] = useState<TransactionData[]>([]);
...
return (
<Section>
<div className="container">
<Header
text={dateText}
leftChild={<Button text="◀" onClick={decreaseMonth} color="grey" />}
rightChild={<Button text="▶" onClick={increaseMonth} color="grey" />}
/>
...
</Section>
);
}
이전 마이그레이션 파트에서 자주 등장하던 `userInfo` 이외에,
리덕스 저장소에서 가계부 리스트를 가져와 저장하는 `transactionList`라는 변수가 새로 생겼다.
가계부 리스트 타입을 정의하기 위해 새로운 인터페이스를 생성했다.
// TransactionData.ts
export interface TransactionList {
transactions: TransactionData[];
}
export interface TransactionData {
uid: string;
transaction_type: boolean;
date: number;
category: string;
title: string;
amount: number;
memo: string;
}
어제 배웠던 대로 배열 내부에 객체가 들어가는 데이터 타입을 지정하기 위해 `TransactionData[]`를 사용했다.
`TransactionData` 내부에는 가계부에 사용되는 데이터의 모든 속성과 타입이 들어가 있다.
가계부 코드 짠 지 워낙 오래돼서 속성명이나 데이터 타입을 뭘로 정했는지 생각이 안 났다; 몽고DB에 데이터 저장할 때 작성해 둔 스키마를 참고해 타입 지정을 해주었다.
이제 `Transactions` 내부에서 해결할 수 있는 에러는 모두 해결되었다.
대신 `Transactions` 컴포넌트에서 불러와 사용하고 있는 공용 컴포넌트 `Button`에서 에러가 발생하고 있어서, 오류 수정을 위해 `Button.jsx` 파일로 이동해 보겠다.
Button 컴포넌트
// Button.jsx
import React from 'react';
import { MouseEventHandler } from "react";
import styled from "styled-components";
export default function Button({ text, type, onClick, color }) {
return (
<CustomButton type={type} onClick={onClick} color={color}>
{text}
</CustomButton>
);
}
const CustomButton = styled.button`
...
`;
컴포넌트에 있어도 되고 없어도 되는 props 가 있다면 ? 문자를 사용
// Transactions.tsx
...
leftChild={<Button text="◀" onClick={decreaseMonth} color="grey" />}
rightChild={<Button text="▶" onClick={increaseMonth} color="grey" />}
...
`Transactions` 컴포넌트에서는 `Button` 컴포넌트에 text, onClick, color 총 3개의 props를 전달하고 있다.
그러나 `Button` 컴포넌트에서는 3개의 props에 type까지 총 4개의 props를 받는다.
자바스크립트로 코드를 작성할 때는 필요 없는 props는 굳이 전달하지 않아도 되었는데, 타입스크립트에서는 정의된 그대로 props를 모두 전달해 주어야 하고 이 때문에 에러가 발생하고 있었다.
😲 해결
전에 TS 강의에서 타입스크립트의 선택적 프로퍼티에 대해 배운 적이 있다.
변수를 선택적 프로퍼티로 설정하기 위해서는 단순히 변수 뒤에 ? 기호를 붙여주기만 하면 된다.
이러한 선택적 프로퍼티 기능을 컴포넌트에서 props를 받을 때도 동일하게 사용할 수 없나 하고 찾아봤다.
import React from 'react';
type GreetingsProps = {
name: string;
mark: string;
optional?: string;
};
function Greetings({ name, mark, optional }: GreetingsProps) {
return (
<div>
Hello, {name} {mark}
{optional && <p>{optional}</p>}
</div>
);
}
위 예시처럼 컴포넌트에서 props를 받을 때도 optional 기호를 사용할 수 있다고 한다.
(출처: https://react.vlpt.us/using-typescript/02-ts-react-basic.html)
위 코드대로 진행하기 위해 `Button.jsx`를 `Button.tsx`로 바꾸고 프롭스를 optional하게 지정해 주었다.
다른 속성은 쉽게 타입 지정을 했는데, onClick 함수는 어떻게 초기값 지정을 해야 하는지 헷갈렸다.
props로 전달하는 onClick 함수의 타입 지정하기
서치 해 보니 리액트 마우스 이벤트 핸들러 타입은 다음과 같이 저장되어 있다고 한다.
type MouseEventHandler<T = Element> = EventHandler<MouseEvent<T>>;
interface Props {
onClick: React.MouseEventHandler<HTMLButtonElement>;
}
따라서 해당 `MouseEventHandler<HTMLButtonElement>` 코드를 그대로 타입으로 지정해 주었다.
(참고: https://velog.io/@eenaree/props-onClick-type)
type ButtonProps = {
text: string;
type?: string;
onClick: MouseEventHandler<HTMLButtonElement>;
color?: string;
};
export default function Button({ text, type, onClick, color }: ButtonProps) {
return (
<CustomButton type={type} onClick={onClick} color={color}>
{text}
</CustomButton>
);
}
위와 같이 타입을 optional하게 지정해 주니 에러가 사라졌다!
🚫 에러: styled-components Could not find declaration file
jsx를 tsx로 바꾸면서부터 `styled-components Could not find declaration file`라는 에러가 뜨고, styled-component를 정상적으로 불러오지 못하는 에러가 발생했다.
이는 TypeScript가 styled-components와 연관된 모듈에 대한 선언 파일을 가져오지 못했을 때 발생하는 문제였다.
에러를 해결하기 위해 간단하게 @type이 추가된 styled-components 모듈을 재설치해주었다.
npm i --save-dev @types/styled-components
😲 TypeScript를 사용하는 프로젝트에 모듈을 설치할 때 @type을 붙이는 이유
👉 @type 없이 설치할 경우 모듈 내부 코드에 대한 type이 정의되어 있지 않아 TypeScript 파일에서 type추론이 불가능하기 때문
이렇게 styled-components를 불러오지 못하는 문제는 해결 완료 하였다.
🚫 에러: 'React' refers to a UMD global, but the current file is a module
이건 또 뭔지...?
`'React' refers to a UMD global, but the current file is a module` 에러 발생.
해결 방법은 간단했다. `import React from 'react'` 코드를 추가하면 된다.
(참고: https://student513.tistory.com/82)
이렇게 하니 에러는 사라졌다. 이제 남은 건 옆의 type오류...
🚫 에러: 이 호출과 일치하는 오버로드가 없습니다. 2의 오버로드 1, '(프롭스: {type?: "button" | submit" | "reset" | undefined; on Click?: MouseEventHandler <HTMLButtonElement> | undefined; color?: string | undefined; ... 271 more...; onTransitionEndCapture?: TransitionEventHandler <...> | undefined; } & {...; } & {...} & {...}; }): ReactElement <...>에서 다음 오류가 발생했습니다.
`<Button>` 컴포넌트 호출 시 인자로 전달된 값들이 해당 컴포넌트의 기대 props 값과 다르기 때문에 발생하는 오류였다.
해당 오류를 해결하기 위해서는 `<Button>` 컴포넌트를 호출할 때 필요한 props를 정확히 전달해 주어야 한다.
오류 메시지를 잘 확인해 누락된 프로퍼티를 확인하고, 필요한 경우 optional로 설정해 주는 식으로 타입 오류를 해결했다.
이렇게 완성된 최종 `<Button>` 컴포넌트는 아래와 같다.
// Button.tsx
import React from "react";
import { MouseEventHandler } from "react";
import styled from "styled-components";
type ButtonProps = {
text?: string;
type?: "button" | "submit" | "reset";
onClick: MouseEventHandler<HTMLButtonElement>;
color?: string;
};
export default function Button({ text, type, onClick, color }: ButtonProps) {
return (
<CustomButton type={type} onClick={onClick} color={color}>
{text}
</CustomButton>
);
}
const CustomButton = styled.button`
...
`;
이제 모든 코드가 에러 없이 잘 동작한다.