[#9] React로 Task Manager 만들기

오닐·2022년 6월 19일
0

React : Task Manager

목록 보기
9/11

🍢1. 새로 알게 된 것과 소소한 수정 사항

🍢1-1. Redirect? Navigate!

스택오버플로우에 따르면 React-router-dom 6버전에서는 Redirect가 사라지고 그 자리를 Navigate가 대신한다고 한다! 이 사실을 한참 뒤에 알아서 왜 나만 Redirect가 안 먹나 한참 찾았다 ㅋㅋㅋㅋ 이래서 강의나 글을 볼 때 버전 체크를 잘 하라고 하는 거로군.

🍢1-2. Hook에 대해 너무 모르는 거 아니에요?

Firebase와 연결하려 아등바등하던 때, Invalid Hook Call Warning 메시지가 떠서 보니 React Hook을 사용할 때 지켜야 할 규칙이 있다는 걸 알게 되었다. 이 에러 메시지가 뜨는 이유로는 React와 React Dom의 버전 불일치, Hooks 규칙 위반, 같은 앱에 React가 두 개 이상 존재 이렇게 세 가지 있는데, 이리저리 살펴 보니 나는 두 번째 경우였다. 공식 문서를 보면 Hook은 항상 최상위 레벨에서만 호출해야 하고, React 함수 내에서만 호출해야 한다고 한다. 후자를 좀 더 자세히 하면 일반적인 JS 함수나 이벤트 핸들러 등에서는 Hook을 호출하면 안 된다. 그것도 모르고 이벤트 핸들러에서 Hook을 호출했으니 당연히 오류가...~ 단순히 Hook을 호출하는 위치가 잘못된 것이라 이벤트 핸들러 바깥으로 이동시키니 오류가 해결되었다. 정리해 두었으니 앞으로 같은 에러는 보지 않았으면 좋겠다!

🍢1-3. props를 여러 군데에서 사용하는 Styled-components에 CSS 함수 적용

말 그대로 하나의 props로 여러 스타일을 바꾸거나, 한 번에 여러 props를 받는 Styled-components에 CSS 함수를 적용했다. 이를테면

${(props) =>
    props.checked === true &&
  css`
	span {
        text-decoration: line-through;
        color: gray;
    }
`}

이런 식으로! 원래는 text-decoration: ${(props) => props.checked === true && "line-through"}처럼 따로따로 썼었는데, 이렇게 하니 보기도 좋고 나중에 유지보수하기에도 좋을 듯하다.

🍢1-4. BasicButton와 BlueButton으로 분리

기존에는 Button 컴포넌트를 하나만 만든 다음 메인 컬러를 props로 받아서 원하는 색상으로 바꾸는 식으로 렌더링했었다. 그런데 코딩을 하다 보니 Defualt 값으로 지정해 둔 회색 버튼보다 메인 컬러를 직접 전달해야 하는 파란 버튼이 더 많이 쓰이는데다, 결정적으로 파란 버튼을 쓸 때마다 색상 코드 여섯 자리를 입력하는 게 너무나도 비효율적이었다.

그래서 회색 버튼 BasicButton으로, 파란 버튼은 BlueButton으로 분리시켰다. 이렇게 하면 매번 props로 메인 컬러를 전달할 필요도 없고, 색상 코드를 복사해서 붙여넣을 필요도 없어서 코드의 가독성도, 재사용성도 높일 수 있다.

🍢1-5. Context API야 다시 만나서 반가워

App.js에 만들어 놓은 함수를 Dropdown에 전달하기 위해 App.js -> Header -> Dropdown으로 props drilling을 했더니 옳지 않은 방법이라는 에러가 떴다. 그래서 Redux를 도입하면서 제거했던 Context API를 다시 쓰기로 했다. Context 만들어서 export하고, Provider로 함수 전달할 컴포넌트들 감싸주고, 함수를 사용하고자 하는 컴포넌트에서 useContext로 불러온 다음, 사용하면 끝!

🍢1-6. Node.js, MongoDB, Firebase DB 사용 포기

썸네일을 직접 업로드하면 해당 이미지 주소를 store에 저장해야 하는데, 이 주소가 너어어무 길어서 사진 첨부된 다이어리를 세 개 이상 쓰면 앱이 다운되는 문제가 생겼다. 이를 해결하기 위해 서버와 외부 DB, 혹은 Firebase처럼 서버리스한 DB를 연결해서 쓰려고 했는데, 처음부터 로컬스토리지에 담을 생각으로 만들다 보니 적용이 쉽지 않았다ㅠㅠ

state에 저장하지 않았더라면 코딩하기 수월할 듯한데, state에 들어있는 데이터를 서버를 거쳐서 DB에 저장하고, 또 꺼내와서 다시 state에 저장해서 사용하려니 머리가 터질 것 같았다.

이뿐만이 아니라 내가 배포를 위해 이용해 온 Github page, Netlify 등은 서버 배포는 지원하지 않아서 서버는 따로 배포해야 한다는 사실을 뒤늦게 알았다. 그래서 냉정하게 말해서 이 프로젝트를 시작할 때의 목표에 DB 연결은 없었고 플젝 진행 중에 추가하고자 하는 기능이었기 때문에
과감하게 포기하기로 했다. 나중에 현업에서는 DB 사용을 염두에 두지 않았던 데이터도 DB에 연결하는 방법을 알아야겠지만, 그건 실무를 하면서 차차 배우도록 하고...^^ 지금은 프론트단에 집중하는 게 맞는 것 같다. 그리고 결정적으로 타입스크립트도 적용하고 싶은데 아직 공부를 시작도 안 해서 이거부터 해야 할 듯하다!ㅋㅋㅋㅋㅋ

이번 플젝 끝내고 Firebase를 이용하는 작은 플젝을 하나 더 하든지, 아니면 MERN 스택을 사용하는 플젝을 하나 더 하든지 해서 이 부분은 따로 공부를 해야 할 것 같다. 이번 플젝은 React, Redux, TypeScript 선에서 완성시키는 걸로...!


🍢2. Redux-persist 도입

🍢2-1. 로그인 페이지 vs 컴포넌트 딜레마

앱에 처음 접속했을 때 로그인이 안 되어 있으면 로그인 페이지를 보여주고, 이름 입력창에 이름을 입력해서 로그인을 하면 해당 이름을 store에 저장한 뒤, 저장된 이름이 있을 경우에는 앱의 홈페이지로 넘어가게 하는 로직을 짜고 싶었다.

그런데 Router와 충돌이 생기는 건지

{isLoggedIn ? (
  <Router>
    <Route path="/" element={<Home />} />
    <Route path="*" element={<Navigate to="/" />} />
  </Router>
 ) : (
  <Router>
    <Route path="/login" element={<Login />} />
    <Route path="*" element={<Navigate to="/login" />} />
  </Router>
 )}

이런 식으로 코드를 짜면 여태까지 짜두었던 Reducer와 dispatch, localStorage 저장 등이 먹히지 않았다.

처음 생각했던 원인은, store의 state를 init 해오는 로직을 App.js에서 Diary, WishList, Todolist 각 컴포넌트로 분리했더니 이런 일이 생긴 줄 알았다. 그래서 원래대로 App.js로 복구시켰지만...

아니었다! 그래서 혹시나 로그인 페이지를 따로 만들어서 라우팅하지 않고 Home 페이지에서 state로 로그인 여부를 판별한 뒤 삼항 연산자를 사용해서 로그인 되었을 때는 Home 컴포넌트를, 로그아웃 상태일 때는 Login 컴포넌트를 렌더링하면 괜찮을까 싶어서 해보았다.

그리고 그것도 아니었다! 그냥 새로고침 없이 페이지를 왔다갔다 할 때는 데이터 유지가 되는데 어느 한 군데서라도 새로고침하면... 개망한다!

이런 치명적인 오류를 왜 몰랐나 싶었는데, 그간 GUI로 페이지를 이동하는 데에만 전념하느라 이런저런 로직을 시험해 보는 과정에서 새로고침을 할 필요가 없기 때문이었다. 지금이라도 알아서 다행...이겠지?

DB에 데이터를 저장하면 이런 문제도 없을 테지만, 상술한대로 DB와의 연결 대신 localStorage를 유지하기로 했기 때문에 다른 방법을 찾아야했다.
그렇게 나의 구원자... 구세주... Redux-persist를 도입하게 되었다.

🍢2-2. 나의 구원자, 구세주

공식 문서와 이 글을 참고해서 다음과 같이 적용했다.

//store.js
//import
import { persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage"; //localStorage 저장

//persist Store 정의
const persistConfig = {
  key: "task-manager",
  storage,
};

//Reducer 하나로 합쳐서 새로운 Reducer 만들기
const rootReducer = combineReducers({
  diary: diaryReducer.reducer,
  wish: wishReducer.reducer,
  todo: todoReducer.reducer,
  user: userReducer.reducer,
});

const persistedReducer = persistReducer(persistConfig, rootReducer);

//하나로 결합된 Reducer Store에 전달
export const store = configureStore({
  reducer: persistedReducer,
  middleware: getDefaultMiddleware({
    serializableCheck: {
      ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
    },
  }),
});

여기서 직렬화된 데이터가 전달되지 않았다는 오류가 떠서 middleware를 추가했다. 미들웨어는 Action과 Reducer 사이에서 사전에 지정된 작업을 하는 친구로, 비동기 작업을 할 때 유용하다고 한다. Redux를 사용할 때 직렬화할 수 없는 값을 action이나 state에 넣으면 안 되기 때문에 그러한 값과 관련해서 오류가 발생할 때가 있으나, 위 코드에 있는 REGISTER처럼 내가 만들지 않은 action에서 이런 오류가 발생할 때는 직렬화 여부의 체크를 무시하도록 함으로써 해결할 수 있다고 한다. 상술한 것처럼 ignoredAction을 직접 지정할 수도 있고 그냥 하나로 퉁쳐서 직렬화 체크 자체를 false로 둘 수도 있는데, 전자가 공식 문서에서 소개하는 방법이라고 하니 적용해 보았다.

Redux-persist가 자동으로 데이터를 직렬화해서 storage에 저장했다가 역질렬화해서 꺼내온다는 것을 알았을 때, 일일이 JSON.parse와 JSON.stringify하지 않아도 돼서 편하다고만 생각했었다. 하지만 직렬화된 데이터는 맨눈으로 보기에는 영 불편하고, 또 데이터를 가져와서 쓸 때마다 역직렬화를 해주어야 된다는 것을 알고 또 시작인가 싶었다 ㅋㅋㅋㅋ

하지만 다시 생각해 보니 어차피 state가 새로고침해도 남아있기 때문에 useSelector로 State에 있는 데이터를 가져오는 식으로 로직을 고치면 그만이었다.

예시로 dataId.current 값을 하나씩 증가시키는 코드는 이렇게 변경했다.

//변경 전
const dataId = useRef(0);

useEffect(() => {
  const localData = localStorage.getItem("data");
  
  if (localData) {
    const localDiaryList = JSON.parse(localData).diary;
    
    if (localDiaryList && localDiaryList.length > 0) {
      dataId.current = parseInt(localDiaryList[0].id) + 1;
    }
  }
}, []);

//변경 후
const diary = useSelector((state) => state.diary);

useEffect(() => {
  if (diary && diary.length > 0) {
    dataId.current = parseInt(diary[0].id) + 1;
  }
}, []);

이 같은 원리로 wishList의 dataId도 고쳐주었다. 그리고 이렇게 localStorage에 데이터가 잘 남아 있기 때문에 컴포넌트가 마운트될 때마다 useEffect로 일일이 state를 init해 줄 필요도 없어졌다!
아직 배우는 단계라 라이브러리에 너무 의존하면 안 되지만, 아마 Redux-persist를 사용하지 않았더라면 지금 내 수준에서는 절대 이 문제를 해결하지 못했을 것 같다. 이렇게 불가피하고 또 이유가 분명할 때는 라이브러리를 쓰는 것도 괜찮...지 않을까? ㅎㅎ...

🍢2-3. FileReader 가고 Mood Diary 오고

아무튼 위와 같은 이유로 DB 연결을 포기했기 때문에 FileReader를 통해 썸네일 파일을 직접 올리던 기능을 없애기로 했다. 대신 기존 목표였으나 제외하기로 했던 무드 트래커 기능을 겸하기 위해 일반 일기장이었던 Diary를 그날의 무드를 기록할 수 있는 무드 다이어리로 바꾸기로 했다.

이 과정에서는 제일 어려운 일이 감정을 나타내는 이미지 파일을 찾는 것일 정도로 크게 어려운 일이 없었다. 결국에는 마음에 드는 프리소스를 찾지 못해서 그냥 React-icon에서 적당한 표정 이모지를 불러왔다...^^

기존에 Study와 daily로 이루어졌던 category를 없애고 다섯 가지로 나뉜 emotion을 새로 만들었다. Diary를 필터링하는 select의 option도 다섯 가지로 고쳤고, 리스트에서 그날그날 어떤 감정이었는지 한눈에 볼 수 있도록 썸네일이 있떤 자리에 해당 emotion을 넣어주었다.

이 과정에서 조건부 렌더링을 위해 기존에 애용했던 삼항 조건식 대신 변수 + if문 방법을 사용했다. 웬만해서는 렌더링하는 방식을 통일하고 싶었으나, 조건이 두 개뿐이던 기존 로직들과는 달리 emotion 개수대로 조건이 다섯 개나 되었기 때문이다.

 let emotionIcon = [];

  if (emotion === 1) {
    emotionIcon = <RiEmotionLaughFill />;
  } else if (emotion === 2) {
    emotionIcon = <RiEmotionHappyFill />;
  } else if (emotion === 3) {
    emotionIcon = <RiEmotionNormalFill />;
  } else if (emotion === 4) {
    emotionIcon = <RiEmotionUnhappyFill />;
  } else {
    emotionIcon = <RiEmotionSadFill />;
  }

이렇게 emotion 숫자별로 아이콘을 따로 적용해야 하는 부분이 두세 군데나 있어서 아예 컴포넌트로 따로 분리해서 사용했다. 처음에 강의 듣고 정리할 때 이 방법이 가장 깔끔하고 쉬워서 자주 써야겠다고 생각했었는데, 이렇게 조건이 많은 경우가 아닐 때는 그냥 삼항 연산자를 쓰는 게 개발하는 입장에서는 편한 것 같다. 실무에서는 조건이 많은 경우가 대다수일 테니 이 방법도 일단은 잘 익혀두기!


🍢3. 대강 완성된 프로젝트

🍢4. 가벼운 회고

  • Router 전환 시 애니메이션 효과를 주고 싶은데 라이브러리밖에는 답이 없을까? 겨우 이거 때문에 라이브러리를 남용하고 싶지는 않은데!
  • 로딩 시에 띄워 줄 페이지도 하나 만들어 봐야겠다.
  • 없는 경로로 이동하지 못하도록 <Router path="*" element={<Navigate to="/"}을 작성해 놓았는데, 이렇게 하면 홈이 아닌 다른 페이지에서 새로고침할 때마다 홈으로 이동하는 문제가 있다. 404 페이지나 없는 페이지라는 걸 알려주는 컴포넌트도 만들어 봐야겠다.
  • 위에서 말한 것들만 해결하면 이제 TypeScript를 배워서 적용만 하면 플젝도 끝이다...! 처음 생각했던 것보다 많은 기능이 들어간 것 같기도 하고 아닌 것 같기도 하지만, 어쨌거나 저쨌거나 목표는 완성이니까! 늦어도 7월부터는 이력서 넣을 수 있겠지? 후후... 열심히 해보자고...

0개의 댓글