이전 마이그레이션 관련 포스팅
- JavaScript → TypeScript 마이그레이션 1(세팅, App, AppRouter)
- JavaScript → TypeScript 마이그레이션 2(대시보드 페이지, 대시보드 관련 컴포넌트: state 빈 배열 초기화 never type 에러 해결)
- JavaScript → TypeScript 마이그레이션 3(가계부 페이지, Button 컴포넌트: onClick 함수 타입 지정, 자잘한 오류들 해결)
저번 포스팅에 이어 이번에도 가계부 관련 페이지 코드를 TypeScript로 변경해 볼 예정이다.
기존 코드 확장자를 jsx → tsx로 변경한 뒤 오류가 생기는 부분이나 흰 점선이 나타나며 타입 지정하라고 지정해 주는 부분 위주로 변경해 보겠다.
변경 전 코드는 아래와 같다. 날짜별로 필터링된 데이터를 부모 컴포넌트에서 props로 받아 Table 형식으로 화면에 뿌려주는 컴포넌트이다.
// TransactionList.jsx
import React, { useState } from "react";
import styled from "styled-components";
...
export default function TransactionList({ data }) {
const dispatch = useDispatch();
const token = useSelector((state) => state.user.token);
const [selectedData, setSelectedData] = useState("");
const [openEditor, setOpenEditor] = useState(false);
const [isEdit, setIsEdit] = useState(false);
const [sortType, setSortType] = useState("latest");
const handleEdit = (id) => {
setIsEdit(true);
setOpenEditor(true);
const item = data.find((transaction) => transaction._id === id);
const selected = {
id: item._id,
transaction_type: item.transaction_type,
date: item.date,
category: item.category,
title: item.title,
amount: item.amount,
memo: item.memo,
};
setSelectedData(selected);
};
function handleCancelEditor() {
setOpenEditor(false);
setIsEdit(false);
}
const handleRemove = async (id) => {
setOpenEditor(false);
if (window.confirm("내역을 삭제하시겠습니까?")) {
try {
await deleteTransactionAPI(id, token);
dispatch(removeTransaction(id));
alert("삭제 완료!");
} catch (err) {
alert("가계부 내역 삭제 도중 오류가 발생했습니다.", err.message);
}
}
};
const getSortedTransactionList = () => {
const compare = (a, b) => {
if (sortType === "latest") {
return parseInt(b.date) - parseInt(a.date);
} else {
return parseInt(a.date) - parseInt(b.date);
}
};
const copyList = JSON.parse(JSON.stringify(data));
const sortedList = copyList.sort(compare);
return sortedList;
};
return (
<Section>
<div className="main-title">
<h2>입출금 내역</h2>
<CiSquarePlus onClick={() => setOpenEditor(true)} />
{openEditor && (
<Modal visible={openEditor} closable={true} maskClosable={false} onClose={handleCancelEditor}>
<TransactionEditor isEdit={isEdit} selectedData={selectedData} closeEditor={handleCancelEditor} />
</Modal>
)}
</div>
<ControlOption value={sortType} chooseOption={setSortType} optionList={sortOption} />
<div className="history">
{getSortedTransactionList().map((item) => (
<div className="card" key={item._id}>
<div className="content">
<div className="tr-data" onClick={() => handleEdit(item._id)}>
<div className="cell date">
<b>{`${new Date(item.date).getDate()}일 (${day[new Date(item.date).getDay()]})`}</b>
</div>
<div className="cell category-title">
<div className="category">{item.category}</div>
<div className="title">{item.title}</div>
</div>
<div className="cell memo">{item.memo}</div>
<div className="cell amount" style={{ color: item.transaction_type === false ? "#ec444c" : "green" }}>
{item.transaction_type === false
? "-" + item.amount.toLocaleString("ko-KR")
: item.amount.toLocaleString("ko-KR")}
</div>
</div>
<div className="cell action">
<FaTrashAlt onClick={() => handleRemove(item._id)} />
</div>
</div>
</div>
))}
</div>
</Section>
);
}
const Section = styled.section`
...
`;
🚫 Expected 0-1 arguments, but got 2.ts(2554)
alert("가계부 내역 삭제 도중 오류가 발생했습니다.", err.message);
위 코드에서 발생한 오류이다.
인수가 0-1개 들어가야 하는데 2개 들어갔다고 한다.
✅ 해결
alert("가계부 내역 삭제 도중 오류가 발생했습니다." + err.message);
`+` 기호를 사용해 문자열 2개를 하나로 만들어 alert 내부에 전달해 주었다.
🚫 Property 'date' does not exist on type 'number'
const getSortedTransactionList = () => {
// const compare = (a, b) => { 타입 명시 전
const compare = (a: number, b: number) => {
if (sortType === "latest") {
return b.date - a.date;
} else {
return a.date - b.date;
}
};
const copyList = JSON.parse(JSON.stringify(data));
const sortedList = copyList.sort(compare);
return sortedList;
};
date를 기준으로 데이터를 오름차순/내림차순 정렬해 주는 함수이다.
이때 date로는 `getTime()`을 이용해 반환한 숫자 값을 사용하므로 지정해 줄 타입은 number이다.
따라서 매개변수 a, b에 각각 number 타입을 지정해 주었는데, number 타입에 date 속성이 존재하지 않는다는 에러가 떴다.
✅ 해결
자세히 보니 `a.date`, `b.date`로 a와 b의 값에서 date를 가져와 값을 정렬하고 있는 상황이었다.
따라서 리턴 타입을 number로 지정해 준 뒤 함수 호출부를 살펴보았다. (리턴 타입은 따로 명시해 주지 않아도 TypeScript가 알아서 타입 추론해서 결정하지만 리턴 타입을 조금 더 명확하게 하기 위해 적어주었다)
getSortedTransactionList().map((item: TransactionData) => (...))
item의 타입은 `TransactionData`(가계부의 모든 프로퍼티의 형식을 지정해 놓은 인터페이스)이다.
`getSortedTransactionList()` 함수 호출을 통해 내림차순/오름차순 정렬된 가계부 리스트가 반환되고, map을 이용해 리스트를 순회하며 데이터를 Table에 넣어주는 방식으로 코드가 실행된다.
`getSortedTransactionList()` 함수 내부에서는 `const sortedList = copyList.sort(compare);` 코드를 통해 실질적인 정렬이 이루어지고 있다.
이때 정렬할 리스트의 데이터 타입은 당연히 `TransactionData` 타입이다. 따라서 `compare()` 함수에서 받는 데이터는 `TransactionData` 타입이라는 것을 알 수 있다.
따라서 아래와 같이 타입을 지정해주니 오류가 사라졌다.
const compare = (a: TransactionData, b: TransactionData): number => {
if (sortType === "latest") {
return b.date - a.date;
} else {
return a.date - b.date;
}
};
TransactionData 인터페이스에서 date는 number 타입으로 지정되어 있기 때문에 더 이상 `parseInt()`를 사용할 수 없고 사용할 필요도 없다. `parseInt()` 사용 코드까지 삭제하니 모든 오류가 해결되었다.
🚫 Argument of type 'string | undefined' is not assignable to parameter of type 'string'. Type 'undefined' is not assignable to type 'string'
아래 코드에서 에러가 발생하고 있었다.
<FaTrashAlt onClick={() => handleRemove(item._id)} />
`handleRemove` 함수
const handleRemove = async (id: string) => {
...
};
✅ 해결
에러 메시지를 해석하면
'string | undefined' 형식의 인수를 'string' 형식의 매개 변수에 할당할 수 없습니다.
'정의되지 않음' 유형을 'string' 유형에 할당할 수 없습니다
이다.
즉, `handleRemove`함수는 string 타입의 id를 받는데 실제 호출부인 `handleRemove(item._id)`에서는 undefined 값을 전달하고 있다는 뜻이다. 정말로 그런지 `TransactionData` 인터페이스 내부를 살펴보았다.
`_id` 위에 마우스를 올리니 저렇게 위에 창이 하나 뜨며 프로퍼티의 타입을 알려준다.
프로퍼티를 선택적 프로퍼티로 지정 시 string 외에도 undefined 타입이 지정될 수 있다는 걸 처음 알았다; 사용자가 프로퍼티 타입을 지정하지 않으면 타입스크립트가 개발자 대신 어떤 타입으로든 그 프로퍼티의 값을 지정할 텐데, 그걸 간과하고 있었다. 그리고 지정되는 값이 undefined라는 것도 오늘 처음 알았다.
위의 오류 해결 방법은 간단하다. undefined 값이 들어갈 수 없도록 `_id`의 타입을 string으로 못 박아 두었다.
export interface TransactionData {
id?: string;
// _id?: string; 선택적 프로퍼티 => 필수로 변경
_id: string;
...
}
해결 완료.
🚫 Property 'className' is missing in type '{ children: Element; visible: true; maskClosable: boolean; onClose: () => void; }' but required in type '{ className: any; onClose: any; maskClosable: any; visible: any; children: any; }'
<Modal visible={openEditor} maskClosable={false} onClose={handleCancelEditor}>
<TransactionEditor isEdit={isEdit} selectedData={selectedData} closeEditor={handleCancelEditor} />
</Modal>
모달 호출 부분에서 오류가 발생하고 있다.
바로 ` Modal` 컴포넌트로 달려가서 Modal 내부 모든 프로퍼티의 타입을 지정해 준다.
// type
type ModalProps = {
className?: string;
onClose: MouseEventHandler<HTMLDivElement>;
maskClosable?: boolean;
visible: boolean;
children: ReactNode;
};
// interface, 객체는 interface 권장
interface ModalProps {
className?: string;
onClose: MouseEventHandler<HTMLDivElement>;
maskClosable?: boolean;
visible: boolean;
children: ReactNode;
};
선택적 프로퍼티로 지정할 부분은 `?` 기호로 지정해 준다.
원래 interface로 객체 타입을 정해 주었는데 이번에는 type 키워드도 사용해 보았다.
type vs interface
type은 모든 타입을 선언할 때 사용 가능하며, interface는 객체에 대한 타입을 선언할 때만 사용할 수 있다.
또한 확장 불가능한 타입을 선언하고 싶을 때는 type을, 확장 가능한 타입을 선언하고 싶을 때는 interface를 사용하면 된다.
타입 객체의 확장성을 위해서 객체 선언 시에는 interface를 사용하는 것이 더 좋다. 반면 단순한 원시값(Primitive Type)이나 튜플(Tuple), 유니언(Union) 타입을 선언할 때 type을 사용하는 것이 좋다.
참고
마지막으로 아래와 같이 props 옆에 `:`을 사용해 타입을 명시해 주면 된다.
// Modal.tsx
type ModalProps = {
className?: string;
onClose: MouseEventHandler<HTMLDivElement>;
maskClosable?: boolean;
visible: boolean;
children: ReactNode;
};
function Modal({ className, onClose, maskClosable, visible, children }: ModalProps) {
const onMaskClick = (e) => {
if (maskClosable && e.target === e.currentTarget) {
onClose(e);
}
};
return (...)
}
👇 참고한 글
[TypeScript] 리액트 children 타입 지정해주기 - 타입별 특징
Children Props? 리액트 모든 컴포넌트에서 children props를 사용할 수 있습니다. children props란 컴포넌트의 여는 태그와 닫는 태그 사이의 내용입니다. 예를 들면, function App() { return Hello world! } 이 태그
shape-coding.tistory.com
What is the type of the 'children' prop?
I have a very simple functional component as follows: import * as React from 'react'; export interface AuxProps { children: React.ReactNode } const aux = (props: AuxProps) => props.chi...
stackoverflow.com
🚫 Parameter 'e' implicitly has an 'any' type React TypeScript
const onMaskClick = (e) => {
if (maskClosable && e.target === e.currentTarget) {
onClose(e);
}
e: 의 타입을 정의하라는 말인 것 같다.
e의 타입은 어떻게 정하지...?
인터넷을 뒤져봤더니 아래 글이 나온다.
저런 식으로 지정하는구나 싶어서 별 생각 없이 따라 해 보았다.
const onMaskClick = (e: MouseEvent<HTMLButtonElement, MouseEvent>): void => {
if (maskClosable && e.target === e.currentTarget) {
onClose(e);
}
};
당연히 발생하는 에러😂
Argument of type 'MouseEvent<HTMLButtonElement, MouseEvent<Element, MouseEvent>>' is not assignable to parameter of type 'MouseEvent<HTMLDivElement, MouseEvent>'.
Property 'align' is missing in type 'HTMLButtonElement' but required in type 'HTMLDivElement'.
const onMaskClick = (e: MouseEvent<HTMLElement, MouseEvent>): void => {
if (maskClosable && e.target === e.currentTarget) {
onClose(e);
}
};
Argument of type 'MouseEvent<HTMLElement, MouseEvent<Element, MouseEvent>>' is not assignable to parameter of type 'MouseEvent<HTMLDivElement, MouseEvent>'.
Property 'align' is missing in type 'HTMLElement' but required in type 'HTMLDivElement'.ts(2345)
IDE가 추천해 준 대로 타입을 지정해 주었는데도 위와 같이 에러가 발생한다.
지금 보니 에러 메시지가 살짝 안 맞는 것 같다. 코드를 수정 후 save 했는데도 반영이 느린 건지 IDE에서 보여주는 에러가 이전과 변함없이 그대로인 적이 종종 있는데 그래서가아닌지...
어쨌든 열심히 서치 해서 아래와 같이 코드를 고쳤다.
type ModalProps = {
className?: string;
onClose: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void;
maskClosable?: boolean;
visible: boolean;
children: ReactNode;
};
function Modal({ className, onClose, maskClosable, visible, children }: ModalProps) {
const onMaskClick = (e: MouseEvent<HTMLElement, MouseEvent>): void => {
if (maskClosable && e.target === e.currentTarget) {
onClose(e);
}
};
return (
...
);
}
무슨 원리인지는 모르겠지만 `onClose`의 타입을 변경해 줌으로써 해결완료.
이 부분에서 시간을 너무 많이 소요해서 우선 적용부터 해보고 그 후에 원리를 파악하자@ 하는 생각이었는데...
또다시 오류가 발생했다.
🚫 No overload matches this call. Overload 1 of 2, '(props: { slot?: string | undefined; style?: CSSProperties | undefined; title?: string | undefined; className?: string | undefined; onClose: (e: MouseEvent<HTMLElement, MouseEvent<...>>) => void | null; ... 262 more ...; onTransitionEndCapture?: TransitionEventHandler<...> | undefined; } & { ...; } & { ...; }): ReactElement<...>', gave the follow
{
...
return (
<>
<ModalOverlay visible={visible} />
<ModalWrapper className={className} onClick={maskClosable ? onMaskClick : null} tabIndex="-1" visible={visible}>
<ModalInner tabIndex="0" className="modal-inner">
{children}
</ModalInner>
</ModalWrapper>
</>
);
}
const ModalWrapper = styled.div`
box-sizing: border-box;
display: ${(props) => (props.visible ? "block" : "none")};
...
`;
const ModalOverlay = styled.div`
box-sizing: border-box;
display: ${(props) => (props.visible ? "block" : "none")};
...
`;
const ModalInner = styled.div`
..
`
이번 에러는 `styled-components`에 사용되는 프로퍼티(`props.visible`)의 타입 지정이 되어 있지 않아 생긴 문제였다.
처음에는 위에 이미 타입 지정해 놓은 `ModalProps`를 사용하려고 했는데 이 경우 onClose, children까지 필수 프로퍼티라 `styled-components` 내부에서도 반드시 값을 사용해야 했다.
type ModalProps = {
className?: string;
onClose: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void;
maskClosable?: boolean;
visible: boolean;
children: ReactNode;
};
아예 전부 선택적 프로퍼티로 변경할까도 했는데 그렇게 하니 `TransactionList` 컴포넌트에서 오류가 났던 것으로 기억한다.(코드 작성한 지 꽤 시간이 흘러서 기억...이...)
어떻게 문제를 해결해야 할지 고민하다가 자세히 살펴보니 현재 `Modal` 컴포넌트에서 쓰이는 값은 `visible`, `children` 뿐이라는 걸 확인했다. 어차피 필요도 없는 프로퍼티 때문에 타입 지정하고 오류 해결하느라 괜히 고생을 한 느낌... 나중에 사용될 프로퍼티일 수도 있지만 우선 지금은 일절 안 쓰는 속성이므로 그냥 지워주기로 했다.
추후 필요하면 다시 추가 예정이다.
이후 `styled-components` 코드도 살짝 수정했다. props를 사용할 때 필요한 값의 타입을 아래와 같이 지정해 부었다.
const ModalWrapper = styled.div<{ visible: boolean }>`
...
`;
이렇게 고쳐 본 최종 `Modal`은 다음과 같다.
import React, { ReactNode } from "react";
import styled from "styled-components";
type ModalProps = {
visible: boolean;
children: ReactNode;
};
function Modal({ visible, children }: ModalProps) {
return (
<>
<ModalOverlay visible={visible} />
<ModalWrapper visible={visible}>
<ModalInner className="modal-inner">{children}</ModalInner>
</ModalWrapper>
</>
);
}
const ModalWrapper = styled.div<{ visible: boolean }>`
box-sizing: border-box;
display: ${(props) => (props.visible ? "block" : "none")};
...
`;
const ModalOverlay = styled.div<{ visible: boolean }>`
box-sizing: border-box;
display: ${(props) => (props.visible ? "block" : "none")};
...
`;
const ModalInner = styled.div`
...
`;
export default Modal;
👉 이렇게 TransactionList, Modal 컴포넌트의 모든 에러 수정 완료!
참고
[TS] TypeScript에서 Styled-component 사용하기
TypeScript에서 styled-component를 사용해보자.
velog.io