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

2024. 3. 27. 01:35·웹 프로젝트/👨‍👨‍👧‍👧소셜 가계부
반응형

마이그레이션 이유

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

이때의 경험을 토대로 요즘 뜨고 있는 타입스크립트를 프로젝트에 적용해 보자는 생각을 하고, 본격적으로 JS -> TS 마이그레이션에 들어갔다.

 

 

필요한 환경설정 설치 (+ tsconfig.json 설정)

npm i -D typescript @types/node @types/react @types/react-dom @babel/preset-typescript
npm install typescript
tsc --init

여기까지 입력하면 `tsconfig.json` 파일이 생성된다.

 

 

TypeScript 한글 문서

TypeScript 한글 번역 문서입니다

typescript-kr.github.io

위의 문서(리액트 마이그레이션)에서 소개해 주는 대로,  tsconfig.json 내부 설정은 아래와 같이 고쳤다.

{
    "compilerOptions": {
        "outDir": "./dist/",        // path to output directory
        "sourceMap": true,          // allow sourcemap support
        "strictNullChecks": true,   // enable strict null checks as a best practice
        "module": "es6",            // specify module code generation
        "jsx": "react",             // use typescript to transpile jsx to js
        "target": "es5",            // specify ECMAScript target version
        "allowJs": true             // allow a partial TypeScript and JavaScript codebase
    },
    "include": [
        "./src/"
    ]
}

 

 

본격적인 마이그레이션 시작 (가장 상위 컴포넌트인 App 컴포넌트부터)

기존 App.js를 App.ts로 파일명 변경 후, 구글링한 대로 React:FC로 타입을 지정해 주었다.

React:FC가 무슨 의미인지 모르고 일단 따라쳐 보았다.

 

🚫 오류!

App.ts

`AppRouter` 컴포넌트를 가져오는 부분이 인식이 안 되는 현상이 발생했다.

어떻게 해결해야 하나 하다가, 다른 분들이 마이그레이션 하는 과정을 적어주신 포스팅이 많기에 참고해 보았다. 위의 현상은 아래 블로그를 통해 해결했다!

 

 

타입스크립트로 마이그레이션 여정기 2

1. 확장자명 js, ts, tsx 파일 파일 확장자명 js에서는 뭐가 js고 jsx인지 모릅니다. 그러나 ts 파일 내에서는 ts와 리액트 컴포넌트들을 구분해서 처리를 하기 때문에 react에 관련 import 즉 tsx를 알 수

kjhg478.tistory.com

ts는 순수한 ts로직만 담겨있는 파일이고, tsx는 jsx요소를 사용한 파일의 파일확장자이다.

ts로 바꾸었던 파일을 tsx 형식으로 고쳤다. 

App.tsx

`AppRouter`가 잘 인식된다! 

그런데 이제 return에 빨간줄... 이건 왜 이러지?

에러를 확인하니

Type 'Element' is not assignable to type 'FC<{}>'.
Type 'ReactElement<any, any>' provides no match for the signature '(props: {}, context?: any): ReactNode'

대충 이런 에러가 뜬다.

해결방법은 에러를 검색창에 그대로 입력해주는 것으로 간단히 찾을 수 있었다. 

import React from "react";
import AppRouter from "./routes/Router";

function App(): ReturnType<React.FC> {
  return <AppRouter />;
}

export default App;

위와 같이 리턴타입 부분을 바꿔주면 된다고 한다!

해결 방법을 따라 하니 에러 없이 코드가 잘 동작한다.

(참고: https://stackoverflow.com/questions/67213681/typescript-returns-error-when-a-function-type-is-functionalcomponent-but-not-fo)

 

Typescript returns error when a function type is FunctionalComponent, but not for arrow function

Below is the example: function RoundButton(): React.FC { return <div>"hello world"</div>; } gives an error: Type 'Element' is not assignable to type 'FC<{}>'. Type '

stackoverflow.com

 

❓❓ React.FC 사용을 지양하라

그런데 문제를 해결하고 난 뒤 `React.FC`에 대해 조금 더 자세히 알아봐야겠다는 생각에 검색해 보니, `React.FC` 사용을 지양하라는 말이 있었다. 관련한 포스팅을 가져와 보았다.

 

[React] React.FC 사용 지양하기

Function Component의 줄임말로 React + TypeScript 조합으로 개발할 때 사용하는 타입함수형 컴포넌트 사용 시 타입 선언에 쓸 수 있도록 React에서 제공하는 타입React.FC를 사용하는 경우 위와 같이 props의

velog.io

 

React.FC 사용 지양하기

FunctionComponent 타입의 줄임말React + Typescript 조합으로 개발 시 사용하는 타입 중 하나함수형 컴포넌트 사용 시 타입 선언에 쓸 수 있도록 React에서 제공하는 타입클래스형 컴포넌트에서 함수형 컴

velog.io

코드 길이도 길어지고, TS의 제네릭 문법도 지원하지 않는다고 한다...

 

 

💁‍♀️그렇다면 내 프로젝트는 어떻게 고쳐야 할까? 

App.tsx의 경우 최상위 코드이므로 props를 받지 않는다. 그러므로 간단하게 매개변수 부분과 React.FC 부분도 싹 지워주었다.

import React from "react";
import AppRouter from "./routes/Router";

function App() {
  return <AppRouter />;
}

export default App;

오류가 모두 사라졌다.

 

 

마이그레이션2 (AppRouter 컴포넌트)

App.tsx 파일이 정상적으로 동작하니, 이제 App.tsx에서 불러오고 있는 `AppRouter.jsx`를 고쳐보기로 했다.

아래와 같이 하나씩 차근차근 타입을 지정해 주었다.

...

interface StoreData {
  transactionAnalytics: Object;
  transactions: Object;
  user: UserData;
}

interface UserData {
  isLoggedIn: boolean;
  token: string;
  tokenExpiration: string;
  userInfo: Object;
}

function AppRouter() {
  const [isLoading, setIsLoading] = useState(true);
  const userData: UserData = useSelector((state: StoreData) => state.user);
  const dispatch = useDispatch();

  const logoutHandler = useCallback(() => {
    dispatch(logout());
    localStorage.removeItem("userData");
  }, [dispatch]);

  useEffect(() => {
    const storedData = JSON.parse(localStorage.getItem("userData") || "{}");
    if (storedData && storedData.token && new Date(storedData.tokenExpiration) > new Date()) {
      dispatch(loginSuccess());
      dispatch(setUserInfo(storedData.userInfo));
      dispatch(setToken(storedData.token));
      dispatch(setTokenExpiration(storedData.tokenExpiration));
    }

    setIsLoading(false);
  }, [logoutHandler, dispatch]);

  useEffect(() => {
    let logoutTimer: ReturnType<typeof setTimeout>;
    //토큰과 만료기간 둘 다 있으면 타이머 설정
    if (userData.token && userData.tokenExpiration) {
      const remainingTime = new Date(userData.tokenExpiration).getTime() - new Date().getTime(); //남은 만료기간
      logoutTimer = setTimeout(logoutHandler, remainingTime);
    }
    return () => {
      if (logoutTimer) {
        clearTimeout(logoutTimer); //진행 중인 타이머 모두 제거
      }
    };
  }, [logoutHandler, userData.token, userData.tokenExpiration]);
  
  
 ...

 

수정한 내용을 정리해 보면 다음과 같다.

 

1. StoreData 인터페이스를 이용해 전역에서 사용되는 리덕스 데이터 타입을 정의했다.

2. UserData 인터페이스는 StoreData 내의 `user` 속성에 대한 타입을 명시하고 있다.

3. `JSON.parse(localStorage.getItem("userData") || "{}")`에서는, localStorage에서 데이터를 가져올 때 null일 경우를 처리하기 위한 fallback 값을 추가했다.

4. `logoutTimer` 변수의 리턴 타입을 `ReturnType<typeof setTimeout>`으로 명시했다.

 

🚫 오류! ( clearTimeout(logoutTimer); Variable 'logoutTimer' is used before being assigned. )

useEffect(() => {
    let logoutTimer: ReturnType<typeof setTimeout>;
    //토큰과 만료기간 둘 다 있으면 타이머 설정
    if (userData.token && userData.tokenExpiration) {
      const remainingTime = new Date(userData.tokenExpiration).getTime() - new Date().getTime(); //남은 만료기간
      logoutTimer = setTimeout(logoutHandler, remainingTime);
    } else {
      clearTimeout(logoutTimer); //진행 중인 타이머 모두 제거
    }
  }, [logoutHandler, userData.token, userData.tokenExpiration]);

 

원인

타입스크립트는 변수가 할당되기 전에 사용되는 것을 엄격하게 검사한다.

위 코드에서 `logoutTimer` 변수는 조건문 밖에서 선언되었다.
`userData.token`과 `userData.tokenExpiration`가 존재한다면 상관이 없지만, 그 반대의 상황이라면 `logoutTimer` 변수가 초기화되지 않은 상태로 `clearTimeout(logoutTimer)`를 호출하게 된다. 이런 상황에 대비해 TypeScript는 경고를 발생시키고 있는 것이다.

 

해결방안

따라서 이러한 상황에서는 TypeScript 컴파일러가 `logoutTimer` 변수의 할당 여부를 인식하지 못하고, 변수가 할당되기 전에 `clearTimeout` 함수가 호출되는 것을 방지하기 위해서는 `logoutTimer` 변수가 초기화된 상태인지 추가적인 확인이 필요하다.

 

해결

이를 해결하기 위해 useEffect 내부에 `return` 함수를 사용하여 `clearTimeout` 함수를 호출하기 전에 변수가 할당되었는지 확인하는 방법을 사용했다.

 useEffect(() => {
    let logoutTimer: ReturnType<typeof setTimeout>;
    //토큰과 만료기간 둘 다 있으면 타이머 설정
    if (userData.token && userData.tokenExpiration) {
      const remainingTime = new Date(userData.tokenExpiration).getTime() - new Date().getTime(); //남은 만료기간
      logoutTimer = setTimeout(logoutHandler, remainingTime);
    }
    return () => {
      if (logoutTimer) {
        clearTimeout(logoutTimer); //진행 중인 타이머 모두 제거
      }
    };
  }, [logoutHandler, userData.token, userData.tokenExpiration]);

useEffect에서의 `return`은 해당 effect가 더 이상 실행할 필요가 없을 때 clean-up하기 위한 용도로 쓰인다.

`return` 함수가 클린업을 진행하는 경우는 아래와 같다.
1. dependancy(두 번째 인자인 의존 배열 내부 요소)가 바뀌어서 effect가 달라져야 할 때
2. 해당 컴포넌트가 unmount 될 때

즉, 컴포넌트의 unmount이전 / update직전에 어떠한 작업을 수행하고 싶다면, clean-up 함수를 반환해 주어야 한다.
clean-up 함수의 작동순서
re-render → 이전 effect의 clean-up → effect 실행

 

`return` 함수는 useEffect가 clean-up을 수행할 때 호출되는데, 여기서는 `logoutTimer`가 할당되었는지 확인하고 타이머를 제거하는 역할을 하고 있다. `return` 구문은 useEffect가 종료될 때 실행되며, 만약에 `logoutTimer`가 설정되지 않았다면 `clearTimeout` 함수를 호출하지 않는다.
즉 `return` 함수에서는 `logoutTimer`가 설정되었는지 확인한 후에 `clearTimeout` 함수를 호출하므로, 변수가 초기화되기 전에 `clearTimeout` 함수가 호출되는 문제가 발생하지 않아 오류가 사라진다.

 

 

 

 

➕ 추가로 vercel에서 커밋한 코드가 배포되지 않는 오류가 발생해서 ts 버전을 다운그레이드 시켜 주었다.

npm i typescript@^4 -S

참고: https://stackoverflow.com/questions/77621320/im-trying-to-deploy-my-react-app-with-vercel-i-did-every-step-in-my-terminal-b

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

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

[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] 소셜 가계부 프로젝트 구현 일지: 개인정보, 토큰 저장 방식 변경1 (LocalStorage → redux-persist)  (0) 2024.03.31
[React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: 배포 (서버: Koyeb, 프론트: vercel)  (0) 2024.03.07
[React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: 리덕스(Redux)로 가계부 데이터 상태 관리 1(리덕스 선택이유, 리덕스 정의 및 스토어, 액션, 리듀서 정의, 덕스패턴 적용까지)  (0) 2024.03.07
[React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: 몽고DB 연동 후 데이터 전송하기 (입출금 내역 POST, GET, PATCH, DELETE 요청)  (0) 2024.01.28
'웹 프로젝트/👨‍👨‍👧‍👧소셜 가계부' 카테고리의 다른 글
  • [React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: 개인정보, 토큰 저장 방식 변경2 (redux-persist 보안: redux-persist-transform-encrypt)
  • [React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: 개인정보, 토큰 저장 방식 변경1 (LocalStorage → redux-persist)
  • [React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: 배포 (서버: Koyeb, 프론트: vercel)
  • [React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: 리덕스(Redux)로 가계부 데이터 상태 관리 1(리덕스 선택이유, 리덕스 정의 및 스토어, 액션, 리듀서 정의, 덕스패턴 적용까지)
청량리 물냉면
청량리 물냉면
프로그래밍 공부를 하고 있습니다. 공부 내용 정리 겸 정보 공유를 목적으로 합니다.
    반응형
  • 청량리 물냉면
    노력중인 블로그
    청량리 물냉면
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 프로그래밍
        • 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
  • 공지사항

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

  • 태그

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

  • hELLO· Designed By정상우.v4.10.3
청량리 물냉면
[React/Node.js/Express/MongoDB] 소셜 가계부 프로젝트 구현 일지: JavaScript → TypeScript 마이그레이션 1(세팅, App, AppRouter)
상단으로

티스토리툴바