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