본문 바로가기
웹 프로그래밍/👨‍👨‍👧‍👧소셜 가계부

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

by 청량리 물냉면 2024. 4. 3.
반응형

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

 

반응형