[#10] React로 Task Manager 만들기

오닐·2022년 6월 26일
0

React : Task Manager

목록 보기
10/11
post-thumbnail

🛼앱 최적화

🛼TodoList 컴포넌트 분리

  • 체크박스를 체크할 때 체크된 체크박스만 렌더링 되고 다른 체크박스는 렌더링될 필요 없으므로 CheckForm에 React.memo를 적용했다. 그러면 CheckForm이 받는 props(id, todo, checked)가 변경될 때만 렌더링되게 최적화를 할 수 있다. 체크박스가 체크되면 checked가 기본값인 false에서 true로 바뀌고, 이때 재렌더링이 일어나며, checked값이 바뀌지 않은 다른 항목들은 그대로 있게 된다.
  • 체크박스를 체크하고 ProgressBar가 업데이트될 때마다 둘과는 상관없잉 투두리스트 입력창까지 렌더링 되어서 이를 막기 위해 입력창과 입력 버튼을 AddForm이라는 컴포넌트로 분리해 주었다.
  • Diary랑 WishList Editor의 input창에 값을 입력할 때마다 다른 컴포넌트들도 리렌덩이 돼서 분리할까 했지만, input마다 컴포넌트로 분리하는 게 코드가 복잡해지는 데다 크게 무거운 컴포넌트가 아니기 때문에 최적화하지 않는 게 더 효율적이라고 판단해서 그대로 두었다. 페이지가 얼마 없는 작은 프로젝트에서도 최적화를 이렇게 고민하는데, 이것보다 몇 배는 더 큰 프로젝트나 사이트들은 어떻게 관리할지... 벌써부터 머리가 아프군.

🛼React.memo + useCallback

불필요하게 리렌더링되는 컴포넌트를 React.memo로 Memoization하고, 해당 컴포넌트가 props로 받는 함수들을 useCallback으로 최적화했다.

여기서 React.memo란 함수형 컴포넌트에 업데이트 조건을 걸어서 해당 조건을 충족하는 컴포넌트만 리렌더링하는 고차 컴포넌트를 말하는데, 쉽게 말해서 전달 받은 props가 바뀌지 않으면 컴포넌트가 반환하는 값도 바뀌지 않기 때문에 컴포넌트 자체가 리렌더링되지 않는다.

이때 props로 전달하는 함수에 useCallback을 적용하면 useCallback에 의존성 배열로 넘긴 값이 바뀔 경우를 제외하고는 항상 동일한 함수가 props로 전달되고, props인 함수가 바뀌지 않으면 React.memo가 적용된 컴포넌트도 재렌더링되지 않기 때문에 앱을 최적화 할 수 있게 된다.

Diary와 WishList의 submitHandler 같은 경우는 의존성 배열로 설정해야 할 값이 많아서 useCallback을 하든 안 하든 성능 차이가 없는 관계로 적용하지 않았다.

마찬가지로 SideBar도 메뉴 4개를 일일이 컴포넌트로 분리시키는 건 비효율적일 듯하여 SideBar를 열고 닫는 부분만 SideHeader로 분리시켰다.

몇 군데 안 되지만 직접 최적화를 해보니 왜 리액트 강의의 최적화 파트나 성능 개선 관련 글에서 작은 프로젝트는 굳이 최적화할 필요가 없고 때로는 불필요한 최적화가 프로젝트를 더 무겁게 만들 수 있다고 하는지 알 것 같다. 무한 렌더링이 일어나서 프로젝트가 다운되거나 무거운 컴포넌트들이 많아서 분리가 시급한 상황이 아니라면 이렇게 코드 여기저기에 훅을 적용하는 게 오히려 앱을 무겁게 만들 수 있을 것 같다. 기계적으로 여기는 재렌더링될 필요가 없으니까 React.memo로 감싸야지~ 하는 대신 컴포넌트를 분리하거나 코드 길이를 줄이는 방법을 사용하는 게 더 나을 수도.

어쨌든 결론은 생각하면서 코딩하자!

🛼Styled-components는 최적화가 좀 어려울지도...

개발 막바지인 지금까지 Styled-components를 굉장히 유용하게 사용했지만, 최적화 단계에서 돌아보니 이렇게 스타일마다 컴포넌트를 만드는 게 과연 최선의 선택이었나 하는 생각이 든다.

이제 와서 다른 스타일링 방법을 찾아서 적용하는 건 너무 늦어서 다른 방법은 없다만, 그래도 이렇게 직접 부딪혀서 장단점을 파악했으니 그걸로 된 거 아닐까? 나중에는 tailwind나 부트스트랩도 한 번 써보고 싶다.


🛼TypeScript 적용하기

개념을 제대로 알기도 전에 무작정 적용해야겠다 마음 먹었던 타입스크립트를 드디어 만났다. 솔직히 말해서 채용 공고를 보니 타입스크립트까지도 해야 되겠다는 생각이 들어서 가보자고~ 하기는 했는데, 후회를 하지 않은 건 아니지만 결론적으로는 만족스럽달까.

새로운 기술을 배울 때면 늘 공식 문서나 블로그를 참조해서 줄글로 먼저 핵심 개념을 익히고, 그 다음에 이것저것 만들어 보는 게 편해서 이번에도 그렇게 진행했다. 활용도가 높아지는 웹 프론트엔드 언어, 타입스크립트(TypeScript)와 공식 문서를 통해 개념을 정리했고, 이미 리액트로 완성한 프로젝트에 타입스크립트를 적용하는 것이기 때문에 이미 생성된 create-react-app에 typescript 적용하기를 참고해서 초기 세팅을 해주었다.

🛼초기 세팅

react-scripts를 최신 버전의 react-scripts-ts로 바꾸려고 했는데 미리 깔아둔 TypeScript랑 버전이 안 맞는다는 오류가 떴다. 그래서 react-sctiprs npm 사이트에 들어가 보니 최신 버전의 react-scripts에 타입스크립트가 포함되어 있다고 해서 그냥 두었다.

npm을 이용해서 TypeScript를 설치한 후, 앱을 만들면서 사용한 라이브러리들의 TypeScript 버전까지 설치해 주었다. 보통은 라이브러리 앞에 @type/을 붙이면 되는데, 몇몇 개는 라이브러리 자체가 TS를 포함하는 경우도 있었다.

라이브러리를 다 설치한 후에는 tsconfig.json 파일을 생성해서 기본 설정을 넣어 주었다. 지금 생각해 보니 이때 내가 따로 파일을 만들 게 아니라 tsc를 실행시키는 등 다른 작업을 했으면 좀 더 수월했을 것 같긴 한데... TS를 연습하고 싶다면 처음부터 TS로 프로젝트를 만들자...^^

여기서 tsconfig.json 파일을 만드는 이유는 여기에 컴파일과 관련된 설정을 해두어야 VSCode가 이를 참조해서 .ts 파일을 인식하기 때문이라고 한다.

tsconfig 파일까지 만들었으면 이제 남은 일은 파일 하나하나 ts와 tsx로 변환하는 것. strict 모드를 해제한 상태에서 차근차근 옵션을 하나씩 true로 바꿔가면서 변환하는 방식으로 진행했다.

🛼TypeScript 팁

  • 날짜를 나타내는 date 객체를 props로 전달할 때는 TypeScript 내장 객체인 Date 타입을 사용한다. 타입스크립트에는 흔히들 알고 있는 string, number, array 등 일반적인 타입 말고도 정말 매우 많은 타입이 있다! 참고로 일반 input(HTMLInputElement)과 textarea(HTMLTextAreaElement)도 타입이 다르다...!
  • useRef를 focus를 이용하는 등 변경 불가한 객체로 사용할 때는 인수로 null을 전달해서 RefObject로 만든 다음에 옵셔널 체이닝을 사용하자. useRef의 타입에는 변경 가능한 MutableRefObject와 ReadOnly인 RefObject가 있어서 필요에 따라 적절하게 지정하지 않으면 에러 메시지가 끊임없이 괴롭힌다...
  • 한데 묶인 채로 여러 곳에서 사용되는 데이터, 나의 경우에는 Diary 관련 페이지에서 사용되는 Diary 데이터 같은 건 미리 interface에 해당 데이터와 관련된 타입을 정의해 둔 후, 이 데이터를 props로 전달할 때마다 import해서 쓰면 편하다.

🛼Styled-components에 TypeSctipt 적용하기

먼저 공식 문서를 읽어 봤는데, 지금까지 한 방식과 조금 다르게 theme 파일에 type을 한꺼번에 만들어서 import하는 식으로 적용하는 듯했다. 내 Styled-components들은 파일별로 사혼의 조각이 되어 있는데...!

다행히 Using custom props 파트에 컴포넌트 파일 내에서 interface를 사용하는 방법이 안내가 되어 있었다. 이를 참고해서 BasicBox 파일의 스타일을

const StyledSection = styled.section<{ padding: string }>`
	padding: ${(props) => props.padding || "20px"};
`;

이렇게 적용해 주었고, 에러도 나지 않았다!

하지만 보통 이렇게 Styled-components에 전달해야 하는 props는 함수형 컴포넌트에도 똑같이 전달해야 하기 때문에 먼저 함수형 컴포넌트에서 사용하는 타입들을 interface로 정의한 뒤, Styled-components에 <{ padding }: Interface["padding"]>처럼 지정해 주는 게 더 직관적인 것 같다.

🛼Redux-toolkit에 TypeScript 적용하기

Redux나 Redux-toolkit에 TypeScript를 적용하는 건 크게 어렵지도 않고 복잡하지도 않았다. Reducer를 제대로 만들어 놓았다면 그와 관련된 initialState와 action에 타입만 제대로 지정하면 된다.

다만 initialState를 빈 배열 또는 빈 객체로 할당한 상태에서 타입을 정의하려니 조금 헷갈리기는 했다. 다행히 미리 안에 들어갈 데이터의 타입을 interface로 정의해 놔서 Array<DiaryProps> 등으로 지정하니 No error! Reducer의 action 타입도 동일하게 적용하고, Remove 액션처럼 payload 전부가 아니라 id 하나만 받아와야 하는 경우에는 그냥 number를 타입으로 전달했다. 코드가 어떻게 작동하는지 생각하면서 유용하게 적용하면 되는 듯.

  • useSelector와 useDispatch를 사용자 정의하면 보다 편하게 사용할 수 있다는 것을 알게 되었다. 하지만 useDispatch 같은 경우 middleware를 사용하지 않는다면 굳이 Hook으로 만들 필요는 없는 듯.

🛼TypeScript 에러

  1. 'React' refers to a UMD global, but the current file is a module. Consider adding an import instead.

파일 하나를 tsx로 변환하자마자 뜬, 나의 첫 번째 타입스크립트 에러이다. React 17부터 import React from 'react';를 파일마다 추가해 줄 필요가 없어서 18 버전을 사용하는 현재 React.memo가 필요한 곳 외에는 해당 코드를 삽입하지 않았는데, 그래서 발생하는 에러라고 한다. import 해 오면 되는 거 아니냐라고 할 수도 있지만, 이 파일뿐만 아니라 앞으로 ts로 변환할 모든 파일에 이 코드를 일일이 입력한다? 굉장한 노동이다!

늘 그렇듯 스택오버플로우에는 해답이 있었다. TypeScripts 4.1부터는 React 17의 이러한 상황을 반영한다고 하니 tsconfig.json 파일에 "jsx": "react-jsx"를 추가하는 것으로 해결!

  1. Cannot use JSX unless the '--jsx' flag is provided.ts

1번과 같이 설정하니 터미널과 localhost에서는 에러가 사라졌으나, VSCode 에디터 내에 위와 같은 에러가 떴다. 터미널에도 없고 플젝도 정상 작동되는 걸 보면 확실히 에디터 내부 문제 같아서 구글링해 보니 바로 해결책이 나왔다.

지금 사용하는 IDE의 TS 버전을 내가 프로젝트에 사용하고 있는 버전과 동일하게 설정해 주면 된다. 즉, VSCode의 명령 팔레트에 Select Type Script Version...을 입력하고 들어가서 설정을 Use Workspace Verision으로 바꿔주면 끝!

  1. 조건에 따라 함수의 파라미터인 item의 타입이 바뀌어야 하는 상황.

쉽게 말해서 type === "diary"일 때는 item이 DiaryProps를, 그 외의 상황에서는 WishListProps를 가져야 하는데, typeof를 써서 Narrowing을 하는 방법으로는 해결이 되지 않아 결국 item별로 함수를 분리시켰다.

    const emotionFilter = (item: DiaryProps) => {
      if (emotionType === "Perfect") {
        return item.emotion === 1;
      } else if (emotionType === "Happy") {
        return item.emotion === 2;
      } else if (emotionType === "Soso") {
        return item.emotion === 3;
      } else if (emotionType === "Unhappy") {
        return item.emotion === 4;
      } else {
        return item.emotion === 5;
      }
    };

    const wishFilter = (item: WishListProps) => {
      if (wishType === "Wish") {
        return item.icon === "Wish";
      } else {
        return item.icon === "Purchased";
      }
    };

코드가 길어지기는 했지만 타입을 제대로 지정할 수만 있다면야...

  1. createSlice의 initialState에 타입을 지정하지 않아 never 속성으로 떴다.

나는 Diary와 WishList, TodoList를 배열 안에 오브젝트 형식으로 저장해 두고 있기 때문에 일반 배열이나 오브젝트로 지정하면 계속 에러가 났다. 열심히 구글링을 해도 답이 나오지 않았는데, 해답은 기본 원리에 있었다. 배열 안에 들어갈 오브젝트에는 데이터들이 저장되니 이 데이터들의 타입을 interface로 만든 후, 이녀석들만 들어갈 수 있도록 Array<interface> 식으로 만들면 되지 않을까 했는데 됐다! 배열과 오브젝트에 타입 지정하는 연습을 더 많이 해봐야겠다.

  1. "strictNullChecks": true 코드를 추가하니 그간 숨겨져 있던 이세상 모든 Undefined가 나를 찾아왔다 (?

분명 명확하게 타입을 지정한 변수에도 암묵적으로 undefined가 있는 모양인지 자꾸만 불러대길래 해결책을 찾으려 공식 문서를 읽다가 Non-null assertion에 대해 알게 되었다. 느낌표를 사용해서 해당 피연산자에 null 또는 undefined 값이 들어가지 않을 거라 단언한다고 하는데, 동시에 eslint에서는 the strict null-checking mode의 이점을 활용하지 않아서 권장하지 않는다는 사실 또한 알게 되었다. 그래서 일단은 Non-null assertion보다 직관적이도록 as를 이용해서 필요한 부분에 타입 단언을 해주었다.

  1. 어떠한 컴포넌트에는 넘겨주고 어떠한 컴포넌트에는 넘겨주지 않는 옵셔널 props가 있어서 interface에 물음표를 사용해서 타입을 정의했다. 그랬더니 그걸 props로 받은 컴포넌트에서 undefined일 수 있다며 나를 괴롭혔다.

옵셔널 체이닝으로 해결되지 않아서 if문을 사용했다. 다행히 이미 if문이 사용된 코드라 조건을 하나 더 추가하는 방식으로 간단하게 해결했다.

이렇게 생겼던 코드가

이렇게 바뀌었다. undefined일 수도 있는 값이 확실히 truthy할 때만 해당 값을 활용하도록 if문에 조건을 하나 더 추가한 것이다.

다만 운 좋게도 if문을 사용했기에 망정이지 일반 코드였다면 if문을 쓰면서 코드가 길어질 수도 있는 노릇이니 더 나은 방법이 있는지 더 공부를 해야 할 듯하다.


🛼가벼운 회고

드디어 배포 직전까지 왔다!!!!!!!!
생각보다 오래 걸린 것 같기도 하고 아닌 것 같기도 한데, 그래도 원하는 대로 잘 동작하는 앱을 보니 마음은 뿌듯하다......... 제대로 만든 건지는 모르겠지만 그래도 어쨌든 거의 다 마무리됐으니 기뻐해도 되겠지.

이제 코드를 훑어 보면서 수정할 수 있는 곳은 수정하고, 주석을 한글로 통합하는 작업만 조금 더 한 다음에 배포해야겠다. 트이타에 링크 걸면 누가 좀 봐주려나 후후....... 딱히 코드 리뷰를 받을 만한 곳은 없으니 만든 거 토대로 이력서 쓰고 바로 지원해 봐야겠다.

정말 너무 형편없다고 해도 좋으니 누군가 보고서 이런저런 지적을 해줬으면 좋겠다. 이럴 때보면 네트워크가 없는 독학은 외로운 것 같기도.

하지만 뭐, 어쩌겠나. 지금 할 수 있는 한 최선을 다해봐야지.
아무튼 수고했고, 조금만 더 수고해서 취업까지 가보자고...~

0개의 댓글