[React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: JavaScript → TypeScript 마이그레이션 4(가계부 페이지 관련 컴포넌트: TransactionList, Modal)

2024. 4. 8. 10:39·웹 프로젝트/👨‍👨‍👧‍👧소셜 가계부
반응형

이전 마이그레이션 관련 포스팅

  1. JavaScript → TypeScript 마이그레이션 1(세팅, App, AppRouter)
  2. JavaScript → TypeScript 마이그레이션 2(대시보드 페이지, 대시보드 관련 컴포넌트: state 빈 배열 초기화 never type 에러 해결)
  3. 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`(가계부의 모든 프로퍼티의 형식을 지정해 놓은 인터페이스)이다.

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의 타입은 어떻게 정하지...?

인터넷을 뒤져봤더니 아래 글이 나온다.

https://www.reddit.com/r/reactjs/comments/1034qsv/trying_to_figure_out_the_type_for_a_click_event/

저런 식으로 지정하는구나 싶어서 별 생각 없이 따라 해 보았다.

  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

 

반응형
저작자표시 비영리 변경금지 (새창열림)

'웹 프로젝트 > 👨‍👨‍👧‍👧소셜 가계부' 카테고리의 다른 글

[React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: JavaScript → TypeScript 마이그레이션 7(타입 폴더 구조 및 타입명 변경, 마이그레이션 완료 소감)  (0) 2024.04.17
[React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: JavaScript → TypeScript 마이그레이션 6(index, 리덕스 코드)  (0) 2024.04.17
[React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: JavaScript → TypeScript 마이그레이션 5(Auth: Test 계정 Auto Fill 구현, Community: pagination 마이그레이션)  (1) 2024.04.14
[React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: JavaScript → TypeScript 마이그레이션 3(가계부 페이지, Button 컴포넌트: onClick 함수 타입 지정, 자잘한 오류들 해결)  (0) 2024.04.04
[React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: JavaScript → TypeScript 마이그레이션 2(대시보드 페이지, 대시보드 관련 컴포넌트: state 빈 배열 초기화 never type 에러 해결)  (0) 2024.04.03
[React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: 개인정보, 토큰 저장 방식 변경2 (redux-persist 보안: redux-persist-transform-encrypt)  (0) 2024.03.31
'웹 프로젝트/👨‍👨‍👧‍👧소셜 가계부' 카테고리의 다른 글
  • [React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: JavaScript → TypeScript 마이그레이션 6(index, 리덕스 코드)
  • [React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: JavaScript → TypeScript 마이그레이션 5(Auth: Test 계정 Auto Fill 구현, Community: pagination 마이그레이션)
  • [React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: JavaScript → TypeScript 마이그레이션 3(가계부 페이지, Button 컴포넌트: onClick 함수 타입 지정, 자잘한 오류들 해결)
  • [React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: JavaScript → TypeScript 마이그레이션 2(대시보드 페이지, 대시보드 관련 컴포넌트: state 빈 배열 초기화 never type 에러 해결)
청량리 물냉면
청량리 물냉면
프로그래밍 공부를 하고 있습니다. 공부 내용 정리 겸 정보 공유를 목적으로 합니다.
    반응형
  • 청량리 물냉면
    노력중인 블로그
    청량리 물냉면
  • 전체
    오늘
    어제
    • 분류 전체보기 (505)
      • 프로그래밍 (41)
        • Programming (1)
        • C | C++ (6)
        • Java (28)
        • Python (5)
      • 웹 프로그래밍 (108)
        • HTML | CSS (5)
        • JavaScript | TypeScript (41)
        • React (25)
        • Vue.js (0)
        • Next.js (18)
        • Spring & Spring Boot (13)
        • JSP & Servlet (1)
        • DB (4)
      • 웹 프로젝트 (77)
        • 웹 프로젝트 (22)
        • 🥨스낵몰 (3)
        • 👨‍👨‍👧‍👧소셜 가계부 (26)
        • 🌜꿈 일기장 (11)
        • 🔮포트폴리오 사이트 (11)
        • 🏃‍♂️팀 프로젝트: 일정관리 프로그램 (0)
        • 📈팀 프로젝트: AI기반 주식 분석 플랫폼 (0)
        • 😺Just Meow It: 조언 사이트 (2)
        • 📕Workly: 교대근무 다이어리 (1)
      • 앱 프로그래밍 (26)
        • Flutter (24)
        • Kotlin (2)
      • Problem Solving (166)
        • 백준 (52)
        • 프로그래머스 (79)
        • SWEA (29)
      • Computer Science (40)
        • 알고리즘 (14)
        • 컴퓨터 네트워크 (18)
        • 이산수학 (8)
      • Developer (47)
        • 후기 (4)
        • 자료정리 (4)
        • 취업 | 취준 (9)
        • SSAFY (1)
        • 웹개발 교육 프로그램 (9)
        • TIL (20)
  • 블로그 메뉴

    • 홈
    • Github
  • 공지사항

    • 프로그래밍 공부 중😊
  • 인기 글

  • 태그

    프로젝트
    클론 프로젝트
    파이썬
    d3
    리액트
    React
    포트폴리오
    알고리즘
    자바
    웹사이트
    bfs
    강의내용정리
    SWEA
    백준
    Next.js
    Jiraynor Programming
    ZeroCho
    플러터
    spring boot
    AWS
    Til
    공식문서
    뉴렉처
    블로그 제작
    구현
    자바스크립트
    프로그래머스
    mysql
    타입스크립트
    컴퓨터네트워크
  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
청량리 물냉면
[React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: JavaScript → TypeScript 마이그레이션 4(가계부 페이지 관련 컴포넌트: TransactionList, Modal)
상단으로

티스토리툴바