투두리스트 뚝딱뚝딱

seul_velog·2023년 1월 25일
0
post-thumbnail

✍️ 독학하면서 투두리스트는 항상 JS강의 커리큘럼에 있어서 따라 제작해 보았던 것 같다.
하지만 이후 이제까지 팀프로젝트나 자체프로젝트에서는 어떤 리스트를 추가하거나 삭제, 수정, 필터링하는 아주 기본적인 기능은 쏙 빼고(?) 다른 기능들만 담당했는데..😅
오랜만에 React복습할겸 (내가 재미있고 싶어서) 기록 하게 되었다.✨

웹소켓(socket.IO) 채팅 통신도 구현했는데 투두리스트쯤이야! (하고 생각했다가 필터링에서 약간의 삽질을 했다..)
😌 하고나니 바닐라JS보다 엄청 간단하구나 느꼈다..


기본셋팅하기

  • create react app
  • yarn add react-icons
  • yarn add styled-components
  • style > GlobalStyles 세팅

😅 시작하기에 앞서 대략적으로 짠(휘갈긴) 구조...💦🔥
얼른 현업경험을 많이 쌓아서 효율적인 구조를 더 많이 접하고 싶다..




UI 작업

1) 마우스호버

  • 다크모드 아이콘에 호버 달기
  • 선에 닿을때만 호버가 될 것 같아서 그위에 Wrapper 스타일 컴포넌트를 만든 후 호버를 적용했다.
<Wrapper>
  <RiMoonClearLine
    size={23}
    />
</Wrapper>

const Wrapper = styled.div`
	&:hover {
		cursor: pointer;
		}
	`;

❓ 풀어야 할 것

<RiMoonClearLine
    size={23}
    onMouseOver={({ target }) => (target.style.color = 'white')}
    onMouseOut={({ target }) => (target.style.color = 'black')}
  />

🤔 이렇게 할 수는 있지만 선위에 있을때만 작동한다.
찾아보니 svg형식이어서 그런 것 같은데, 이전 프로젝트를 할때는 따로 svg 코드를 만져서 해결했는데, 혹시 다른 방식으로 해결해 볼 순 없을지 고민중! 🧐


Filter

1) 호버 애니메이션

const FilterBtn = styled.button`
  margin: 1.2rem 0rem;
  height: 2.2rem;
  font-size: 0.9rem;
  color: #999999;
  background: none;
  font-weight: 400;
  transition: color 250ms ease-in-out;
  &:first-child {
   ...
  }
  &:after {
    display: block;
    content: '';
    transform: translate(5rem, 0);
    border-bottom: solid 2px #fff;
    transform: scaleX(0);
    transition: transform 250ms ease-in-out;
  }
  &:hover {
    color: white;
    cursor: pointer;
  }
  &:hover:after {
    transform: scaleX(1);
  }
`;

List

1) 체크박스 커스텀
참고한 블로그

const CheckBox = styled.input`
  margin: 1rem 0.8rem 0 0;
  appearance: none;
  width: 1.5rem;
  height: 1.5rem;
  border: 1.5px solid gainsboro;
  border-radius: 0.35rem;
  &:checked {
    border-color: transparent;
    background-image: url("...");
    background-size: 100% 100%;
    background-position: 50%;
    background-repeat: no-repeat;
    background-color: #9eb3da;
  }
`;

AddForm

1) Form과 input

  • form 태그 내부에 input 태그와 button 태그를 넣고 추후 함수를 만들면서 onChangeonSubmit, 버튼의 onClick 을 컨트롤 할 수 있도록 만들었다.
    UI는 이전 투두리스트들과 동일하게 작성😀




✨ 기능작업하기

전체 아이템들 보여주기 + 아이템 추가

1) 구조 생각해보기
AddForm에서 input값과 Submit 값을 App컴포넌트로 올린 후 list컴포넌트에 내려주는 것으로 작성

  • ❗️이 부분이 과연 효율적인지에 대해서는 강의를 보고 복습하기!!

2) useRef 사용하기
돔 요소를 조작하기 위함이지만 우리는 그저 아이디값을 부여하므로 input 태그에 굳이 넣지 않아도 된다는 점!
→ 그래서 그냥 ref를 사용해야하는 App 컴포넌트에 정의하고 사용했다.

3) 에러 해결과정
오랜만이라서 map을 어디에 넣어서 돌려줘야하는지 헷갈렸다. 😂
→ 결과적으로 List 컴포넌트에서 map을 돌린 후 ListBox로 taskList 정보를 넘겨주고 ListBox컴포넌트에서 그것을 렌더링 하도록 작성했다. ▼

taskList.map(()=>{})   (x)
taskList.map(()=>())   (o)

//
const List = ({ taskList }) => {
  return (
    <ListContainer>
      {taskList.map((task) => (
        <ListBox task={task} key={task.id} />
      ))}
    </ListContainer>
  );
};

4) 고유 아이디 부여하기

  • 나는 useRef() 를 활용했다.

📌 다른방법
1. new Data()
2. uuid 라이브러리


아이템 삭제

const deleteTask = (e) => { // 1)
    const deleteId = e.currentTarget.id;
    setTaskList(
      taskList.filter((task) => {
        return task.id !== parseInt(deleteId);
      })
    );
  };

1) 에러 해결하기
currentTarget 은 내가 그 버튼을 눌렀을 때, svg나 path에 따라서 target이 설정되는 것이 아니고, 그 evnet가 달린 컴포넌트가 선택되기 때문이다.
→ 따라서 e.target.id (x) / e.currentTarget.id (o)

이전 비슷한 경험을 한 적이 있어서 자문을 구한적이 있었는데 그때 기억을 떠올렸다.😄
👉 내가 해결했던 이전 경험


아이템 체크박스

1) 아이템 체크박스 속성 이용하기

// List > ListBox
<CheckBox
  type='checkbox'
  checked={taskList.isChecked}
  onChange={(e) => {
    handleChecked(id, e); // taskList.map을 돌려서 얻어낸 각각의 task별 id
  }}
></CheckBox>


// App
const handleChecked = (id, e) => {
    setTaskList(
      taskList.map((task) => {
        if (task.id === id) {
          return { ...task, isChecked: e.target.checked };
        } else {
          return task;
        }
      })
    );
  };
  • 여기서 받아온 각각 task의 id와 onChange 이벤트를 통해서 쉽게 해결할 수 있다.
    (이 방벙을 몰라 조금 헤맸다..)
  • 받아온 id와 e를 이용해서 setState 함수를 실행시켜준다. 여기서 filter가 아니고 map을 통해서 새롭게 재구성 해준다.!
  • ❗️ 중요❗️
    → return 해주는 구문 잘보자. { ...task, isChecked: e.target.checked } 맵을 통해서 받아오고있는 각각의 task 객체를 스프레드 연산자로 복사해준뒤, 맵을 통해 해체된 각각의 task객체 내의 isChecked라는 프로퍼티만 이벤트에서 제공하는 checked(true,false)를 이용해서 새로 갈아끼워 넣어주면 된다 🥲.. ❗️
    (그 외에 id값이 같지 않는 task 객체는 ‘맵에서 돌려온 객체 그대로’ 리턴해준다.)

아이템 필터링 📌

✔ ❓어떻게 작성하면 좋을까? 🤔 먼저 구상해보기 ▼

1) All, Active, Completed 가 담길 State를 추가한다.

const filtering = (filter) => {
    setFilterType(filter);
  };
...

return ( ...
	<Filter filtering={filtering} />

이때 기본값은 ‘All’ 탭이 되도록 지정한다.

const [filterType, setFilterType] = useState('All');

2) 각 task의 객체 속성에 type 을 추가하여 All, Active, Completed가 담길 수 있도록 한다.
이때 기본값은 ‘Active’로 지정한다.

const addTask = (e) => {
    e.preventDefault();
    if (task === '') return;
    setTaskList([
      ...taskList,
      { id: taskId.current, task, isChecked: false, type: 'Active' },
    ]);
    taskId.current++;
    setTask('');
  };

2-1) ❗️ 여기서 중요한 점은 각 task.type 이 체크된 유무 맞는 필터값을 가지고 있어야한다는 것이다.

✍️ 이 부분의 코드가 잘 작성되어야 3번 UI렌더링까지 가능하다.

const handleChecked = (id, e) => {
    setTaskList(
      taskList.map((task) => {
        // 내가 선택한 task의 id가 일치해야하고 && 체크가 true일때 'Completed'로 설정
        if (task.id === id && e.target.checked) {
          return { ...task, isChecked: e.target.checked, type: 'Completed' };
        }
        // 체크가 true가 아닌 false일때 이 문으로 넘어옴 && 내가 선택한 task의 id가 일치할때
        else if (task.id === id) {
          return {
            ...task,
            isChecked: e.target.checked,
            type: 'Active',
          };
        }
        // 체크가 true든 false든 내가 선택한 값이 아닐때 변동주지 않기
        else return task;
      })
    );
  };
  • 초기값은 Active이다. All이 아님을 주의하자.

  • 체크를 할 경우 해당 체크 task만 Completed로 변경됨에 집중하자.


3) 현재 state의 filterType 과 task의 필터타입 task.type 을 비교해서 All일 경우와 Active, Completed일 경우를 구분하여 UI렌더링을 위한 return 을 한다.

const List = ({ taskList, deleteTask, handleChecked, filterType }) => {
  return (
    <ListContainer>
      {taskList.map((task) => {  // 이때 filter가 아닌 map을 써야 오류x
        if (filterType === 'All') {  // 초기값도 All이며, 내가 클릭한 필터타입이 All
          return (
            <ListBox
              task={task}
              deleteTask={deleteTask}
              id={task.id}
              handleChecked={handleChecked}
              key={task.id}
            />
          ); // 내가 클릭한 필터타입 === task가 가지고 있는 type의 이름이 일치할 경우
        } else if (filterType === task.type) { 

          return (
            <ListBox
              task={task}
              deleteTask={deleteTask}
              id={task.id}
              handleChecked={handleChecked}
              key={task.id}
            />
          );
        }
      })}
    </ListContainer>
  );
};

+) 추가 UI 수정

const Task = styled.div`
  margin-top: 0.2rem;
  font-size: 1rem;
  line-height: 3rem;
  text-decoration: ${(props) =>
    props.checked ? 'line-through rgba(187, 200, 222, 0.7) 2px' : 'none'};
  color: ${(props) => (props.checked ? '#aaadb1' : 'none')};
`;

+) 체크된 Task UI 적용하기

  • props를 받아와서 checked가 되었을 경우 효과를 적용한다.
    → 이때 App컴포넌트의 상태에서부터 끌어온다. checked는 taskList의 각 task에서의 isChecked 프로퍼티 값을 가져온다.
const Task = styled.div`
  margin-top: 0.2rem;
  font-size: 1rem;
  line-height: 3rem;
  text-decoration: ${(props) =>
    props.checked ? 'line-through rgba(187, 200, 222, 0.7) 2px' : 'none'};
  color: ${(props) => (props.checked ? '#aaadb1' : 'none')};
`;

다크모드 지원

1) useContext 사용

  • 이전 강의 react-basic에서 useContext를 사용한 것을 참고했다.
    → 전체 컴포넌트에서 사용될 경우 최상단에 프로바이더를 감싸서 사용하기!
  • props로 전달하여 스타일 컴포넌트에서 활용했다.
<Task checked={task.isChecked} darkMode={isDarkMode}> ... 

color: ${(props) => {
    if (props.checked && props.darkMode) {
      return '#aaadb1';
    } else if (props.darkMode) {
      return 'white';
    }
  }};

로컬스토리지 활용

✍️ 이전 바닐라JS 에서 활용했던 방법을 적용해 보았다. 다 하고나니 이부분이 특히 바닐라JS와 리액트의 차이가 더 느껴졌다..🤔

1) 먼저 localStorage에 아이템을 저장하자

  • JSON.stringify
useEffect(() => {
    localStorage.setItem('tasks', JSON.stringify(taskList));
  }, [taskList]);

2) localStorage에서 아이템을 가져오자

  • JSON.parse
function taskListFromLocalStorage() {
    const taskLists = localStorage.getItem('tasks');
    return taskLists ? JSON.parse(taskLists) : [];
  }

3) State 초기 상태 설정하기

  • ❗️ 새로고침 했을때 로컬스토리지와 taskList의 초기값이 빈배열 [] 이므로 빈배열을 setStorage 해서 스토리지에도 []이 담기고 UI도 아무것도 남지 않게 된다.
  • 이때 setTaskList 에서 useState의 초기값을 함수로 지정해준다.
const [taskList, setTaskList] = useState(() => taskListFromLocalStorage());

function taskListFromLocalStorage() {
    const taskLists = localStorage.getItem('tasks');
    return taskLists ? JSON.parse(taskLists) : [];
  }
// 함수 선언식으로 해야 위치가 아래여도 함수호이스팅에 의해서 제대로 작동한다. (호이스팅이론은 내추측)
  • 만약 taskLists가 있으면 JSON.parse() 실행, 아니라면 빈배열을 리턴한다.

❗️ 이때 주의할점

  • 만약 일반 함수호출로 전달하면 계속해서 해당 함수가 호출된다!!

(블로그참고)

✍️ 내가 이해한 것
1) 어짜피 task를 추가할때마다 useEffect에 의해 계속해서 정상적인 렌더링을 하게되고, 로컬스토리지에도 동기적으로 잘 저장이 되므로 UI단에서도 문제가 없다! (이부분이 JS로 투두를 만들때보다 간단해서 헷갈렸다.)

2) 새로고침시 날라가지 않도록 설정하기위해 초기값으로 getStorage를 설정한건 이해가 된다. 여기서 그냥 함수를 호출할 경우 매 렌더링이 이루어지므로 성능이 좋지 않다.

→ 따라서 콜백형태로 State의 초기값을 넣어줌으로써 초기 딱 한번만 실행되도록 한다. 이때 초기라는것은 새로고침을 눌렀을 때에만 스토리지에서 데이터를 불러와 UI렌더링을 해준다는 것 !! 🧐


추가

1) trim() 활용하여 빈문자열 제어하기

const addTask = (e) => {
    e.preventDefault();
    if (task.trim().length === 0) return; // trim() 메서드 활용 
    setTaskList([
      ...taskList,
      { id: taskId.current, task, isChecked: false, type: 'Active' },
    ]);
    taskId.current++;
    setTask('');
  };

2) label 적용하기

<Task
  checked={task.isChecked}
  darkMode={isDarkMode}
  htmlFor={task.id}
  >
  
const Task = styled.label`
...
`

📌 추가 CSS tip!
1. Css파트 보더값 두군데만 변경하고싶으면 단축속성명이있다.
ex) border-bottom-right-radius: 8px;
2. filter: brightness(130%) -> 기존색에서 명도가 밝아진다.



이제 가장 궁금하고 하고싶었던 과정만 남았다.✨
Solution과 비교하며 내가짠 코드와 같이 구조 비교하며 분석해보기 😆✍️✨

✨ 구조 비교해보기

profile
기억보단 기록을 ✨

0개의 댓글