이전 마이그레이션 관련 포스팅
- 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