프라이D
프라이Develog(❁´◡`❁)
프라이D
전체 방문자
오늘
어제
  • ALL (378)
    • TDD, Cleancode with JavaScr.. (5)
    • 프로젝트 (32)
      • work (3)
      • 직접 만드는 기술 블로그 (2)
      • 데일리 옥션 (19)
      • 모락모락 (8)
    • Computer Science (1)
    • Algorithm & 자료구조 (94)
      • 알고리즘 w.JavaScript (53)
      • 자료구조 (5)
      • (인프런) 자바스크립트 알고리즘 문제풀이 (34)
    • JavaScript (45)
      • JavaScript (41)
      • 모던 자바스크립트 Deep Dive (4)
    • WEB (13)
    • 회고 (12)
    • TIL (109)
    • WIL (7)
    • Stacks (20)
      • React.js (6)
      • Next.js (1)
      • Redux (3)
      • Node.js (2)
      • GIT (2)
      • SAP (1)
    • 15일 메이킹 프로젝트 (15)
    • 이전 기록 (14)
    • ETC. (5)
    • ---------------2021 (6)
      • 내일배움단-웹개발 5주 (2)
      • 정보처리기사 (4)

블로그 메뉴

  • 홈
  • 태그
  • 미디어로그
  • 위치로그
  • 방명록

공지사항

인기 글

태그

  • 자바스크립트비트마스크
  • 코드스테이츠
  • JavaScript
  • nomadcoder
  • nomadcoders
  • 2023 인프콘 후기
  • 모던자바스크립트딥다이브
  • 국비지원
  • 자바스크립트알고리즘
  • 내일배움단
  • Til
  • 비트마스크
  • vanilaJS
  • 자바스크립트
  • MySQL
  • 스파르타코딩클럽
  • 알고리즘
  • 코딩프로젝트
  • 내일배움카드
  • 투포인터알고리즘

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
프라이D

프라이Develog(❁´◡`❁)

[Toy] 리액트, 리덕스 툴킷, styled-components 로 만든 투두 리스트
회고

[Toy] 리액트, 리덕스 툴킷, styled-components 로 만든 투두 리스트

2022. 9. 12. 15:09

🐱개요

배운 내용을 연습하기 위한 토이 프로젝트 입니다. 리액트, 리덕스 툴킷, styled-components 를 활용하여 만들었습니다.

LINK

https://hyejj19.github.io/react-todo/

GITHUB

 

GitHub - hyejj19/react-todo: 리액트로 만든 간단한 Todo List

리액트로 만든 간단한 Todo List. Contribute to hyejj19/react-todo development by creating an account on GitHub.

github.com

🐱기능

  • 할 일 추가 및 삭제
  • 반응형 디자인 

🐱결과물

🐱프로젝트 구조

폴더 구조

컴포넌트 구조

FigJam 을 사용해 컴포넌트 구조와 각 컴포넌트별 필요한 props와 state를 간단하게 정리해보았다.

초반에는 useState를 사용해서 전체 할 일 목록을 todoList 라는 state로 관리하고, 함수를 props로 전달해 하위 컴포넌트에서 상태를 끌어올려서 변경하도록 만들었다. 이후에 리덕스 툴킷을 적용하면서 수정했다.

🐱상세 기능

GlobalStyle 적용

styled-componensts 로 글로벌 스타일을 적용해 전체 색상과 html 기본 효과를 제거했다.

좌 : Globalstyle.js / 우 : TodoApp.js

미디어 쿼리 반응형

최상위 컴포넌트 TodoApp 에서는 전체 컨테이너의 스타일을 아래와 같이 정의했다. 미디어쿼리로 반응형 디자인을 적용했다. 

//TodoApp.js

const TodoContainer = styled.div`
  width: 500px;
  height: 800px;
  border-radius: 20px;
  background-color: var(--main-color);
  padding: 50px;
  transition: all 0.2s ease-in-out;

  > h1 {
    margin-top: 0;
    color: white;
    font-size: 3rem;
    margin-bottom: 45px;
  }
  @media screen and (max-width: 992px) {
    width: 100vw;
    height: 100vh;
    border-radius: 0px;
    transition: all 0.2s ease-in-out;
    padding: 50px 30px;
  }
`;

//...생략

스크롤 바 디자인 변경

투두 목록을 출력하는 아이템 컨테이너에 아래와 같이 스타일을 적용하고, 스크롤바 스타일을 변경했다. 

// TodoItems.js

const TodoItemsContainer = styled.div`
  margin-top: 20px;
  max-height: 75%;
  overflow-y: scroll;
  overflow-x: hidden;

  &::-webkit-scrollbar {
    width: 8px;
    background-color: var(--main-color);
  }

  &::-webkit-scrollbar-thumb {
    background-color: var(--point-color);
    border-radius: 10px;
  }
`;

//...생략

스크롤 바 스타일 변경

React-icons 적용

할 일 목록에 hover 상태에서 나타나는 아이콘은 react-icons 를 적용해보았다. react-icons로 불러온 아이콘은 색상을 직접 변경할 수가 없어서 부득이하게 inline-style 을 사용했는데 다음에는 사용법을 더 숙지해서 보완해야겠다. 아래 코드에서는 Remove 라는 이름의 div에 아이콘을 넣은 뒤 ItemContainer 에서 hover 상태일 때 display 속성을 변경시켰다. 

//TodoItem.js

const TodoItemContainer = styled.div`
  padding: 20px 0px 20px 10px;
  display: flex;
  align-items: center;
  border-radius: 10px;

  &:hover {
    background-color: var(--hover-color);
    transition: all 0.2s ease-in-out;
    cursor: pointer;
    ${Remove} {
      display: initial;
    }
  }

  > .todo__checkbox {
    margin-right: 1rem;
    min-width: 1.1rem;
    min-height: 1.1rem;
  }

  > span {
    font-size: 1.1rem;
    width: 100%;
    &.checked {
      color: gray;
      text-decoration: line-through;
    }
  }
`;

function TodoItem({id}) {
//... 중략
return (
    <TodoItemContainer>
      <input className={'todo__checkbox'} type="checkbox" checked={isCheck} onChange={onChangeCheckbox} />
      <span className={classNameCheck}>{todoItem.contents}</span>
      <Remove onClick={deleteTodoHandler}>
        <FaTrash style={{fill: '#ff6b6b'}} />
      </Remove>
    </TodoItemContainer>
  );
}

Redux-toolkit 적용

초반 코드에서는 useState 훅을 사용했는데, 리덕스를 학습한 뒤 튜토리얼을 참고해 리덕스 툴킷을 적용해 보았다.

컴포넌트가 4개, 상태가 todoList와 체크 값을 확인하는 isCheck 밖에 없었는데도 입력된 내용으로 상태를 업데이트하고 또 체크 상태에 따라 todoList 를 업데이트 하기 위해서 todo 라는 상태를 추가로 만드는 등 ... 기능이 제대로 동작은 했지만 추후에 새로운 기능을 만들어 확장하려면 많은 수정이 필요할 것 같았다. 확장성과 유지보수성을 고려해 리덕스 툴킷을 적용해 코드를 리팩토링했다.

 

아래 코드는 대부분 리덕스 툴킷 튜토리얼을 참고하여 작성했다. 

 

리덕스 툴킷의 configureStore 메서드로 store를 생성했다. 일단 여러개의 reducer를 별도의 함수 없이 전달할 수 있어서 편리한 것 같고 reducer 이외에도 여러 설정을 한 번에 해줄 수 있다고 하는데 다른 기능은 아직은 잘 모르겠다. 

// store.js

import {configureStore} from '@reduxjs/toolkit';
import todoReducer from '../slice/todoSlice';

export const store = configureStore({
  reducer: {
    todoList: todoReducer,
  },
});

 

todoSlice는 create, remove, toggleCheck 라는 세가지 액션을 갖는다. todoList에 새 todo가 create 될 때마다 고유한 id를 부여하기 위해 react-uuid 를 사용했다. 이 아이디로 컴포넌트에 key를 부여하고 삭제와 체크값 변경이 가능해진다.

 

초기 상태값은 로컬스토리지에 저장된 데이터 혹은 이게 없을 때 빈 배열이 된다. 가장 상위 컴포넌트에서 todoList 가 업데이트 될 때마다 로컬 스토리지에 갱신하여 저장하도록 코드를 짰다.

//todoSlice.js

import {createSlice, current} from '@reduxjs/toolkit';
import uuid from 'react-uuid';

// todoList 초기값 : localStorage에 저장된 내용이 없을 경우 빈 배열을 초기값으로 한다.
const initialState = JSON.parse(localStorage.getItem('todoList')) || [];

export const todoSlice = createSlice({
  name: 'todoList',
  initialState,
  reducers: {
    create: (state, action) => {
      state.push({
        id: uuid(),
        contents: action.payload,
        isCheck: false,
      });
    },
    remove: (state, action) =>
      state.filter(todo => {
        return todo.id !== action.payload;
      }),
    toggleCheck: (state, action) =>
      state.map(todo => {
        return todo.id === action.payload.id ? {...todo, isCheck: action.payload.isCheck} : todo;
      }),
  },
});

export const {create, remove, toggleCheck} = todoSlice.actions;

export default todoSlice.reducer;

slice 를 작성하면서 state 를 콘솔로 확인하려고 했는데, Proxy 객체가 나와서 당황했다. 이유를 찾아보니 원인은 immer.js 였다. 여기서 콘솔에서 현재 값을 확인하려면 current 메서드를 추가로 불러와 current(state) 와 같이 사용해야한다.

그리고 filter나 map 메서드처럼 새 배열을 리턴하는 메서드를 사용할 때는 기존처럼 return을 해주어야하는데 화살표 함수에서 중괄호를 사용하면서 return을 안하는 실수를 해서 약간 헤맸다..; 

 

할 일 추가

TodoInput 컴포넌트에서 입력된 내용을 토대로 todoList 에 업데이트된다. 여기서 dispatch 함수를 가지고 create 액션을 불러와 내용을 전달하면, 이 내용을 토대로 todoSlice의 reducer 가 새 객체를 생성해 상태를 업데이트하여 화면에 출력한다.

//TodoInput.js

function TodoInput() {
  const dispatch = useDispatch();
  const [inputValue, setInputValue] = useState('');

  const onChangeInput = e => {
    setInputValue(e.target.value);
  };

  const onKeyUpEnter = e => {
    if (e.key === 'Enter' && inputValue) {
      dispatch(create(inputValue));
      setInputValue('');
    } else if (!inputValue) {
      alert('내용을 입력해 주세요!');
    }
  };

  return (
    <>
      <InputEl type="text" name="inputValue" onChange={onChangeInput} value={inputValue} onKeyUp={onKeyUpEnter} />
      <Label htmlFor="inputValue">Notes...</Label>
    </>
  );
}

할 일 화면 출력

먼저 최상위 컴포넌트에서 useEffect를 사용해 todoList가 업데이트 될 때마다 로컬스토리지에 해당 내용을 갱신하여 저장하는 함수를 작성했다. useState를 사용했을때는 상태가 끌어올려졌을 때 실행되는 다른 함수가 많이 있었는데 툴킷을 적용하면서 엄청나게 간소화되었다.

//TodoApp.js

function TodoApp() {
  // store에서 가져온 todoList state
  const todoList = useSelector(state => state.todoList);

  // 로컬스토리지 저장 함수
  const updateLocalStorage = () => {
    localStorage.setItem('todoList', JSON.stringify(todoList));
  };

  // 초기 마운트 될 때, todoList가 업데이트 될 때 로컬스토리지에 todoList 저장
  useEffect(() => {
    updateLocalStorage();
  }, [todoList]);

  return (
    <>
      <GlobalStyle />
      <TodoBackground>
        <TodoContainer>
          <h1>To Do</h1>
          <TodoInput />
          <TodoItems />
        </TodoContainer>
      </TodoBackground>
    </>
  );
}

TodoItem의 상위 컴포넌트인 TodoItems 에서 map으로 각 todo 데이터를 하나씩 출력한다. 코드는 아래와 같다.

이 컴포넌트에서는 isCheck 라는 값으로 체크 여부를 추적하고 클릭이 일어났을 때 변경된 체크값과 현재 id 를 담아 toggleCheck 액션을 수행한다. delete도 비슷하게 동작한다. 

//TodoItem.js

function TodoItem({id}) {
  const dispatch = useDispatch();

  const todoItem = useSelector(state => state.todoList).find(todo => todo.id === id);

  let isCheck = todoItem.isCheck;
  let classNameCheck = isCheck === true ? 'checked' : 'unchecked';

  function onChangeCheckbox() {
    isCheck = !isCheck;
    dispatch(toggleCheck({id, isCheck}));
  }

  const deleteTodoHandler = () => {
    dispatch(remove(id));
  };

  return (
    <TodoItemContainer>
      <input className={'todo__checkbox'} type="checkbox" checked={isCheck} onChange={onChangeCheckbox} />
      <span className={classNameCheck}>{todoItem.contents}</span>
      <Remove onClick={deleteTodoHandler}>
        <FaTrash style={{fill: '#ff6b6b'}} />
      </Remove>
    </TodoItemContainer>
  );
}

투두 리스트를 만들면서 가장 헷갈렸던 부분이 체크 값을 변경하는 부분이었다. 체크 값을 변경하고 전체 todoList 상태를 업데이트하면 로컬스토리지에 변경된 내용이 저장 되어야 하는데 그 부분이 잘 되지 않았다. 먼저 초반에는 useState로 복잡하게 하다보니 헷갈려서 힘들었다. 또 isCheck 도 state로 등록해서 useEffect로 업데이트되도록 코드를 짰기 때문에 불필요한 렌더링도 많이 일어났다. 툴킷으로 리팩토링하면서는 slice 에서 업데이트 하는 부분에서 return 문을 잘못써서... 시간을 많이 썼다. 

🐱배포

깃허브 페이지를 통해 배포를 했다. 배포 후 흰 페이지와 콘솔에 404 에러가 떴었는데, 빌드 후 생성된 index.html 내의 경로를 수정하고 manifest 관련 코드를 주석처리하니 해결되었다.

script 태그의 경로가 절대경로로 표시되어 있었는데 잘 찾지 못해서 static 부터 상대경로로 변경하여 작성했다. 

<!-- 주석처리 -->
<!-- <link rel="manifest" href="/react-todo/index.html/manifest.json" /> -->

<!-- 경로 수정 -->
<script defer="defer" src="./static/js/main.7a7a070b.js"></script>

참고링크 : https://stackoverflow.com/questions/61730152/manifest-json-404-not-found

🐱소감

처음 리액트를 배우고, 배운 내용을 적용해보려고 시작한 간단한 토이 프로젝트였다. 처음에는 styled-components 대신에 css를 import 하여 코드를 짰었는데 여러 툴을 배우게 되니 또 적용하고 싶어서 코드를 짜고 뜯어고치고 또 짜고 뜯어고치고를 반복하느라 거의 한 달 정도 걸렸다. 계속 뜯어 고치면서 기초적인 문법과 에러 핸들링에도 쬐금 익숙해진 것 같다.

 

여기에 추가하고 싶은 여러 기능들이 많이 있는데, 우선 다른 것들을 공부하여 그 내용을 토대로 새롭게 적용해보려고 한다. 끝 ~ 

저작자표시 (새창열림)

'회고' 카테고리의 다른 글

[Project] 🔥모락모락 프로젝트 최종 회고🔥  (2) 2022.12.07
[회고] 코드스테이츠 FE 섹션 4 회고  (0) 2022.10.19
[회고] 코드스테이츠 FE 섹션 3 회고  (0) 2022.09.19
[회고] 코드스테이츠 FE 섹션 2 회고  (0) 2022.08.18
[회고] 코드스테이츠 FE 섹션 1 회고  (0) 2022.07.20
    '회고' 카테고리의 다른 글
    • [회고] 코드스테이츠 FE 섹션 4 회고
    • [회고] 코드스테이츠 FE 섹션 3 회고
    • [회고] 코드스테이츠 FE 섹션 2 회고
    • [회고] 코드스테이츠 FE 섹션 1 회고
    프라이D
    프라이D
    틀린내용 정정 및 개선사항은 언제든지 댓글 달아주세요 :D

    티스토리툴바