[React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: JavaScript → TypeScript 마이그레이션 2(대시보드 페이지, 대시보드 관련 컴포넌트: state 빈 배열 초기화 never type 에러 해결)

2024. 4. 3. 09:37·웹 프로젝트/👨‍👨‍👧‍👧소셜 가계부

TS 마이그레이션 1편

 

[React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: JavaScript → TypeScript 마이그레이션 1(세

마이그레이션 이유 기존에 프로젝트를 개발하면서 타입 때문에 발생한 에러를 잡느라 오랫동안 헤맸던 경험이 있다. (몽고DB의 ObjectId와 string으로 된 id를 비교해서, 게시글을 작성한 유저임에도

florescene.tistory.com

 

지난 마이그레이션 1편에 이어서 오늘은 대시보드 관련 컴포넌트를 마이그레이션 해보았다.

다른 컴포넌트는 이전에 했던 것처럼 useSelector의 타입만 지정해 주면 되는 거라 간단히 끝났다.

그런데 한 컴포넌트에서 기존 빈 배열([])로 state값을 초기화한 부분에서 never type 관련 오류가 발생했다.

오늘은 해당 내용을 포스팅해보려고 한다. 

 

 

YearlyExpenseChart.jsx

...
import { AreaChart, Area, Tooltip, ResponsiveContainer, XAxis } from "recharts";
import { fetchLatestExpensesAPI } from "../../utils/transactionAPI";
import { useSelector } from "react-redux";

const calculateExpenseChangeRate = (previousMonthExpense, currentMonthExpense) => {
  if (previousMonthExpense === 0) {
    if (currentMonthExpense === 0) {
      return 0;
    }
    return 100;
  }
  const changeRate = ((currentMonthExpense - previousMonthExpense) / previousMonthExpense) * 100;

  return changeRate.toFixed(2);
};

export default function YearlyExpenseChart() {
  const [data, setData] = useState([]);
  const [totalExpense, setTotalExpense] = useState(0);
  const [expenseChangeRate, setExpenseChangeRate] = useState(0);
  const uid = useSelector((state) => state.user.userInfo.userId);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const responseData = await fetchLatestExpensesAPI(uid);
        setData(responseData.monthlyExpenses);
        setTotalExpense(responseData.totalYearlyExpense);
      } catch (error) {
        console.log("최근 1년 지출금액 API 호출 도중 에러 발생:", error.message);
      }
    };
    fetchData();
  }, [uid]);

  useEffect(() => {
    if (data.length) {
      setExpenseChangeRate(calculateExpenseChangeRate(data[10].total, data[11].total));
    }
  }, [data]);

  return (
    <Section>
     ...
      <div className="chart">
        <ResponsiveContainer height="100%" width="100%">
          <AreaChart width={800} height={400} data={data} margin={{ top: 0, left: 0, right: 0, bottom: 0 }}>
            <Tooltip />
            <XAxis dataKey="month" />
            <Area
              animationBegin={800}
              animationDuration={2000}
              type="monotone"
              dataKey="total"
              stroke="#3c76e0"
              fill="purple"
              strokeWidth={0}
            />
          </AreaChart>
        </ResponsiveContainer>
      </div>
    </Section>
  );
}

 

문제가 되는 코드는 useEffect 부분이었다.

  useEffect(() => {
    if (data.length) {
      setExpenseChangeRate(calculateExpenseChangeRate(data[10].total, data[11].total));
    }
  }, [data]);

기존 자바스크립트 구현 시 data를 초기화 시킬 때 단순히 빈 배열을 넣어서 `const [data, setData] = useState([]);`으로 초기화를 시켰었는데, 여기서 `const data: never[]` 에러가 발생했다.

아직 타입스크립트 관련한 개념이 제대로 정립되지 않은 상태라 찾아보니 타입스크립트에서는 빈 배열을 never 타입으로 인식한다고 한다. 

그럼 이 data의 초기값을 명확히 지정해 줘야 한다는 뜻이기에 먼저 데이터가 형식부터 확인했다.

 

data에는 차트를 그릴 때 필요한 최근 12개월의 지출이 들어오고 있다. 12개의 객체가 배열 하나에 묶여 있는 형태이다.

데이터 형식은 

[{year: 2023, month: 12, total: 32000},

...,

{ year: 2024, month: 11, total: 322000 }]

이와 같다.

그런데 Array로 묶인 객체는 어떻게 받아와야 하는 건지 알 수가 없었다.

 

그렇게 배열과 관련한 type 지정에 대해 찾아보다가 아래 내용을 찾았다.

type Todo = { id: number; text: string; done: boolean };
const [todos, setTodos] = useState<Todo[]>([]);
추가적으로 상태의 타입이 까다로운 구조를 가진 객체이거나 배열일 때는 Generics를 명시하는 것이 좋습니다.
배열인 경우에는 위와 같이 빈 배열만 넣었을 때 해당 배열이 어떤 타입으로 이루어진 배열인지 추론할 수 없기 때문에 Generics를 명시하셔야 합니다. 

출처:
https://tinyurl.com/2xlm6ljf

 

이 부분에서 힌트를 얻어 제네릭에 대해 공부해 보았다.

 

 

❓ 제네릭

모든 타입의 값을 적용할 수 있는 범용적인 함수

인수로 Number 타입의 값을 전달하면 반환 타입이 Number가 되고, 인수로 String 타입의 값을 전달하면 반환값의 타입도 String 타입이 되도록 설정할 수 있다.

function func<T>(value: T): T {
  return value;
}

let num = func(10);
// number 타입
  • 함수 이름 뒤에 타입을 담는 변수인 타입 변수 T를 선언
  • 매개변수와 반환값의 타입을 이 타입변수 T로 설정

👉 T에 어떤 타입이 할당될 지는 함수 호출 시 결정된다.
👉  func(10) 처럼 Number 타입의 값을 인수로 전달하면, 매개변수 value에 Number 타입의 값이 저장되면서 T가 Number 타입으로 추론된다.
👉  따라서 이때의 func 함수의 반환값 타입 또한 Number 타입이 된다.
출처: https://ts.winterlood.com/0e41a293-21d9-419e-8e2a-57b5813e0582

 

 

 

공부한 내용을 바탕으로 기존 코드를 ts로 고쳐보았다.

 

YearlyExpenseChart.tsx

import ...;

interface MonthlyExpenses {
  year: number;
  month: number;
  total: number;
}

const calculateExpenseChangeRate = (previousMonthExpense: number, currentMonthExpense: number): string => {
  if (previousMonthExpense === 0) {
    if (currentMonthExpense === 0) {
      return "0";
    }
    return "100";
  }
  const changeRate = ((currentMonthExpense - previousMonthExpense) / previousMonthExpense) * 100;

  return changeRate.toFixed(2);
};

export default function YearlyExpenseChart() {
  const uid: string = useSelector((state: StoreData) => state.user.userInfo.userId);
  const [data, setData] = useState<MonthlyExpenses[]>([]);
  const [totalExpense, setTotalExpense] = useState(0);
  const [expenseChangeRate, setExpenseChangeRate] = useState("0");

  useEffect(() => {
    const fetchData = async () => {
      try {
        const responseData = await fetchLatestExpensesAPI(uid);
        setData(responseData.monthlyExpenses);
        setTotalExpense(responseData.totalYearlyExpense);
      } catch (error) {
        console.log("최근 1년 지출금액 API 호출 도중 에러 발생:", error.message);
      }
    };
    fetchData();
  }, [uid]);

  useEffect(() => {
    if (data.length) {
      setExpenseChangeRate(calculateExpenseChangeRate(data[10].total, data[11].total));
    }
  }, [data]);

  return (
    <Section>
      ...
    </Section>
  );
}

 

🔍

interface MonthlyExpenses {
  year: number;
  month: number;
  total: number;
}

const [data, setData] = useState<Array<MonthlyExpenses>>([]);
const [data, setData] = useState<MonthlyExpenses[]>([]);

두 개의 초기화 코드를 작성해 보았는데, 실제 두 코드가 수행하는 작업은 동일하다. 

첫 번째 코드 `<Array<MonthlyExpenses>>`는 배열을 나타내는 표현이며, 각 요소는 MonthlyExpenses 타입을 가지고 있다는 의미이다.

두 번째 코드 `<MonthlyExpenses[]>` 역시 배열 타입을 나타내며, 배열 내부의 요소들이 MonthlyExpenses 타입임을 나타내고 있다.

후자가 더 가독성이 좋아 나는 뒤의 코드를 채택해 프로젝트에 사용했다.

 

 

여기까지 수행했더니 오류가 사라졌다.

제네릭에 대해 공부할 수 있었던 좋은 기회였다.

 


참고

 

210427 React with TypeScript TIL - useState에서 빈 배열([])로 초기화시키는 경우, props 타입(함수형, 클래스

useState에서 빈 배열([])로 초기화시키는 경우typescript에서 useState hooks를 사용해서 초기 값을 빈 배열로 할 경우, 타입스크립트에서는 빈배열([])을 never type으로 인식하기 때문에 구체적인 타입을

leehyungi0622.github.io

 

제네릭 소개 - 제네릭

한 입 크기로 잘라먹는 타입스크립트

ts.winterlood.com

 

타입스크립트로 리액트 Hooks 사용하기 (useState, useReducer, useRef)

이번 섹션에서는 타입스크립트를 사용하는 리액트 컴포넌트에서 `useState` 및 `useReducer` 를 사용하여 컴포넌트의 상태를 관리하는 방법과 `useRef` 를 사용하여 컴포넌트 내부에서 관리하는 변수 및

velog.io

 

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

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

[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 마이그레이션 4(가계부 페이지 관련 컴포넌트: TransactionList, Modal)  (0) 2024.04.08
[React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: JavaScript → TypeScript 마이그레이션 3(가계부 페이지, Button 컴포넌트: onClick 함수 타입 지정, 자잘한 오류들 해결)  (0) 2024.04.04
[React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: 개인정보, 토큰 저장 방식 변경2 (redux-persist 보안: redux-persist-transform-encrypt)  (0) 2024.03.31
[React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: 개인정보, 토큰 저장 방식 변경1 (LocalStorage → redux-persist)  (0) 2024.03.31
[React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: JavaScript → TypeScript 마이그레이션 1(세팅, App, AppRouter)  (0) 2024.03.27
'웹 프로젝트/👨‍👨‍👧‍👧소셜 가계부' 카테고리의 다른 글
  • [React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: JavaScript → TypeScript 마이그레이션 4(가계부 페이지 관련 컴포넌트: TransactionList, Modal)
  • [React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: JavaScript → TypeScript 마이그레이션 3(가계부 페이지, Button 컴포넌트: onClick 함수 타입 지정, 자잘한 오류들 해결)
  • [React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: 개인정보, 토큰 저장 방식 변경2 (redux-persist 보안: redux-persist-transform-encrypt)
  • [React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: 개인정보, 토큰 저장 방식 변경1 (LocalStorage → redux-persist)
청량리 물냉면
청량리 물냉면
프로그래밍 공부를 하고 있습니다. 공부 내용 정리 겸 정보 공유를 목적으로 합니다.
  • 청량리 물냉면
    노력중인 블로그
    청량리 물냉면
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 프로그래밍
        • Programming
        • C | C++
        • Java
        • Python
      • 웹 프로그래밍
        • HTML | CSS
        • JavaScript | TypeScript
        • React
        • Vue.js
        • Next.js
        • Spring & Spring Boot
        • JSP & Servlet
        • DB
      • 웹 프로젝트
        • 웹 프로젝트
        • 🥨스낵몰
        • 👨‍👨‍👧‍👧소셜 가계부
        • 🌜꿈 일기장
        • 🔮포트폴리오 사이트
        • 🏃‍♂️팀 프로젝트: 일정관리 프로그램
        • 📈팀 프로젝트: AI기반 주식 분석 플랫폼
        • 😺Just Meow It: 고양이의 조언
      • 앱 프로그래밍
        • Flutter
        • Kotlin
      • Problem Solving
        • 백준
        • 프로그래머스
        • SWEA
      • Computer Science
        • 알고리즘
        • 컴퓨터 네트워크
        • 이산수학
      • Developer
        • 후기
        • 자료정리
        • 취업 | 취준
        • 웹개발 교육 프로그램
        • TIL
  • 블로그 메뉴

    • 홈
    • Github
  • 공지사항

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

  • 태그

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

  • hELLO· Designed By정상우.v4.10.3
청량리 물냉면
[React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: JavaScript → TypeScript 마이그레이션 2(대시보드 페이지, 대시보드 관련 컴포넌트: state 빈 배열 초기화 never type 에러 해결)
상단으로

티스토리툴바