본문 바로가기

React

[React] 투두리스트 웹 만들기3

에러가 난 원인이 components에 있는 TodoContext함수를 인식을 못해서 였는데

 

폴더 이름과 함수 이름을 바꿔 보아도 해결되지 않아서

 

TodoContext함수만 따로 빼주었습니다.

 

TodoCreate

 

+ 버튼을 누르면 45도 회전시켜서 X버튼으로 보이게 하고

 

할일을 입력할 수 있는 인풋 창이 생성됩니다.

 

여기서 처음 알게 된 것은

 

 box-sizing: border-box;

 

라는 css 속성인데  border-box로 설정하면 요소의 width 값은 

 

'콘텐츠 크기 + 좌우 padding 값 + 좌우 border 값'으로 계산되는 것입니다.

 

 width: 100%;

 

즉 width값이 100%이기 때문에 요소의 크기와 width 값이 동일하게 적용됩니다.

 

이제 UI 부분은 모두 구현된 것 같고

 

기능을 구현해 보도록 하겠습니다.

 

import React from 'react';
import styled from 'styled-components';
import { useTodoState } from '../TodoContext';

const TodoHeadBlock = styled.div`
  padding-top: 48px;
  padding-left: 32px;
  padding-right: 32px;
  padding-bottom: 24px;
  border-bottom: 1px solid #e9ecef;
  h1 {
    margin: 0;
    font-size: 36px;
    color: #343a40;
  }
  .day {
    margin-top: 4px;
    color: #868e96;
    font-size: 21px;
  }
  .tasks-left {
    color: #20c997;
    font-size: 18px;
    margin-top: 40px;
    font-weight: bold;
  }
`;

function TodoHead() {
  const todos = useTodoState();
  const undoneTasks = todos.filter(todo => !todo.done);

  return (
    <TodoHeadBlock>
      <h1>2019년 7월 10일</h1>
      <div className="day">수요일</div>
      <div className="tasks-left">할 일 {undoneTasks.length}개 남음</div>
    </TodoHeadBlock>
  );
}

export default TodoHead;

 

먼저 TodoHead.js에서 할 일의 개수가 몇 개 남았는지 구현합니다.

 

 const undoneTasks = todos.filter(todo => !todo.done);

 

여기서 filter는 괄호 안에 해당되는 요소들로 새로운 배열을 만들어 주는데

 

괄호 안에 내용은 todo.done상태가 아닌 것을 의미하고

 

완료되지 않은 할 일들로 이루어진 배열을 만들어 줄 것입니다.

 

이 배열의 길이를 출력하면 완료되지 않은 할 일의 개수를 알려줄 것입니다.

 

할 일의 개수 표시

 

 

할 일의 상태는 TodoContext에서 관리합니다.

 

const initialTodos = [
  {
    id: 1,
    text: '프로젝트 생성하기',
    done: true
  },
  {
    id: 2,
    text: '컴포넌트 스타일링하기',
    done: true
  },
  {
    id: 3,
    text: 'Context 만들기',
    done: false
  },
  {
    id: 4,
    text: '기능 구현하기',
    done: false
  }
];

 

done 상태가 false인 할 일이 2개가 있기 때문에 

 

할 일이 2개 남았다고 표시해 주는 것입니다.

 

다음은 날짜를 오늘의 날짜로 변경해 주는 기능을 구현합니다.

 

import React from 'react';
import styled from 'styled-components';
import { useTodoState } from '../TodoContext';

const TodoHeadBlock = styled.div`
  padding-top: 48px;
  padding-left: 32px;
  padding-right: 32px;
  padding-bottom: 24px;
  border-bottom: 1px solid #e9ecef;
  h1 {
    margin: 0;
    font-size: 36px;
    color: #343a40;
  }
  .day {
    margin-top: 4px;
    color: #868e96;
    font-size: 21px;
  }
  .tasks-left {
    color: #20c997;
    font-size: 18px;
    margin-top: 40px;
    font-weight: bold;
  }
`;

function TodoHead() {
  const todos = useTodoState();
  const undoneTasks = todos.filter(todo => !todo.done);

  const today = new Date();
  const dateString = today.toLocaleDateString('ko-KR', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  });
  const dayName = today.toLocaleDateString('ko-KR', { weekday: 'long' });

  return (
    <TodoHeadBlock>
      <h1>{dateString}</h1>
      <div className="day">{dayName}</div>
      <div className="tasks-left">할 일 {undoneTasks.length}개 남음</div>
    </TodoHeadBlock>
  );
}

export default TodoHead;

 

먼저 new Date()로 오늘의 날짜를 불러오고

 

toLocaleDateString이라는 함수를 써서 날짜를 상세히 표시할 수 있습니다.

 

'ko-KR'은 한국어로 표시해달라는 의미이고

 

year, month, day, minute등의 단위를 타입을 주어 표시할 수 있습니다.

 

보통은 month를 영어로 표시할 때만 'long'타입을 쓰고

 

나머지는 숫자로 표시하기 위해 'numeric'을 씁니다.

 

New Date

 

날짜가 최신화 된 모습!

 

다음은 TodoList에서 state를 조회하고 렌더링 해줍니다.

 

import React from 'react';
import styled from 'styled-components';
import TodoItem from './TodoItem';
import { useTodoState } from '../TodoContext';

const TodoListBlock = styled.div`
  flex: 1;
  padding: 20px 32px;
  padding-bottom: 48px;
  overflow-y: auto;
`;

function TodoList() {
  const todos = useTodoState();

  return (
    <TodoListBlock>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          id={todo.id}
          text={todo.text}
          done={todo.done}
        />
      ))}
    </TodoListBlock>
  );
}

export default TodoList;

 

map함수를 이용해서 TodoItem의 요소들을 렌더링 해줍니다.

 

onToggle, onRemove와 같은 세부 기능은 TodoItem에서 관리합니다.

 

import React from 'react';
import styled, { css } from 'styled-components';
import { MdDone, MdDelete } from 'react-icons/md';
import { useTodoDispatch } from '../TodoContext';

const Remove = styled.div`
  display: flex;
  align-items: center;
  justify-content: center;
  color: #dee2e6;
  font-size: 24px;
  cursor: pointer;
  opacity: 0;
  &:hover {
    color: #ff6b6b;
  }
`;

const TodoItemBlock = styled.div`
  display: flex;
  align-items: center;
  padding-top: 12px;
  padding-bottom: 12px;
  &:hover {
    ${Remove} {
      opacity: 1;
    }
  }
`;

const CheckCircle = styled.div`
  width: 32px;
  height: 32px;
  border-radius: 16px;
  border: 1px solid #ced4da;
  font-size: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 20px;
  cursor: pointer;
  ${props =>
    props.done &&
    css`
      border: 1px solid #38d9a9;
      color: #38d9a9;
    `}
`;

const Text = styled.div`
  flex: 1;
  font-size: 21px;
  color: #495057;
  ${props =>
    props.done &&
    css`
      color: #ced4da;
    `}
`;

function TodoItem({ id, done, text }) {
  const dispatch = useTodoDispatch();
  const onToggle = () => dispatch({ type: 'TOGGLE', id });
  const onRemove = () => dispatch({ type: 'REMOVE', id });
  return (
    <TodoItemBlock>
      <CheckCircle done={done} onClick={onToggle}>
        {done && <MdDone />}
      </CheckCircle>
      <Text done={done}>{text}</Text>
      <Remove onClick={onRemove}>
        <MdDelete />
      </Remove>
    </TodoItemBlock>
  );
}

export default TodoItem;

 

TodoItem에서는 상태가 'done'인지 아닌지에 따라서 시각적으로 보이는 부분을 

 

css에 변화를 주어 변경하고

 

onToggle, onRemover를 dispatch를 통해서 관리합니다.

 

function todoReducer(state, action) {
  switch (action.type) {
    case 'CREATE':
      return state.concat(action.todo);
    case 'TOGGLE':
      return state.map(todo =>
        todo.id === action.id ? { ...todo, done: !todo.done } : todo
      );
    case 'REMOVE':
      return state.filter(todo => todo.id !== action.id);
    default:
      throw new Error(`Unhandled action type: ${action.type}`);
  }
}

 

Dispatch는 TodoContext에서 리듀서를 통해 이미 만들어 놨습니다.

 

마지막으로는 TodoCreate에서 작업합니다.

 

import React, { useState } from 'react';
import styled, { css } from 'styled-components';
import { MdAdd } from 'react-icons/md';
import { useTodoDispatch, useTodoNextId } from '../TodoContext';

const CircleButton = styled.button`
  background: #38d9a9;
  &:hover {
    background: #63e6be;
  }
  &:active {
    background: #20c997;
  }

  z-index: 5;
  cursor: pointer;
  width: 80px;
  height: 80px;
  align-items: center;
  justify-content: center;
  font-size: 60px;
  position: absolute;
  left: 50%;
  bottom: 0px;
  transform: translate(-50%, 50%);
  color: white;
  border-radius: 50%;
  border: none;
  outline: none;


  transition: 0.125s all ease-in;
  ${props =>
    props.open &&
    css`
      background: #ff6b6b;
      &:hover {
        background: #ff8787;
      }
      &:active {
        background: #fa5252;
      }
      transform: translate(-50%, 50%) rotate(45deg);
    `}
`;

const InsertFormPositioner = styled.div`
  width: 100%;
  bottom: 0;
  left: 0;
  position: absolute;
`;

const InsertForm = styled.form`
  background: #f8f9fa;
  padding-left: 32px;
  padding-top: 32px;
  padding-right: 32px;
  padding-bottom: 72px;

  border-bottom-left-radius: 16px;
  border-bottom-right-radius: 16px;
  border-top: 1px solid #e9ecef;
`;

const Input = styled.input`
  padding: 12px;
  border-radius: 4px;
  border: 1px solid #dee2e6;
  width: 100%;
  outline: none;
  font-size: 18px;
  box-sizing: border-box;
`;

function TodoCreate() {
  const [open, setOpen] = useState(false);
  const [value, setValue] = useState('');

  const dispatch = useTodoDispatch();
  const nextId = useTodoNextId();

  const onToggle = () => setOpen(!open);
  const onChange = e => setValue(e.target.value);
  const onSubmit = e => {
    e.preventDefault(); // 새로고침 방지
    dispatch({
      type: 'CREATE',
      todo: {
        id: nextId.current,
        text: value,
        done: false
      }
    });
    setValue('');
    setOpen(false);
    nextId.current += 1;
  };

  return (
    <>
      {open && (
        <InsertFormPositioner>
          <InsertForm onSubmit={onSubmit}>
            <Input
              autoFocus
              placeholder="할 일을 입력 후, Enter 를 누르세요"
              onChange={onChange}
              value={value}
            />
          </InsertForm>
        </InsertFormPositioner>
      )}
      <CircleButton onClick={onToggle} open={open}>
        <MdAdd />
      </CircleButton>
    </>
  );
}

export default React.memo(TodoCreate);

 

TodoCreate에서는 인풋창에 대한 css를 변경하고

 

onSubmit에서는 open이라는 초기의 상태를 만들어서

 

생성했을 때의 done 상태를 'false'로 두고 value를 초기화합니다.

 

마지막 줄에는 React.memo로 감싸주면 불필요한 렌더링을 방지할 수 있다고 합니다.

 

 

 

할 일을 추가하면 변경점들이 적용되는지 볼까요?

 

 

 

 

이렇게 할 일이 추가되고 할 일의 개수도 늘어나는 것을 볼 수 있습니다.

 

 

그리고 우측의 쓰레기통 아이콘을 누르면 삭제되는 것이 신기하네요.

 

코드를 따라하는 것에 불과하지만 변경점들이 시각적으로 보이는 것이

 

즐거웠고 여러가지 리액트의 라이브러리를 사용해보면서

 

공부할 수 있는 시간을 가질 수 있어서 좋았던 것 같습니다.

 

다음 시간에는 db를 공부해서 로그인 창을 구현해 보도록 하겠습니다.

 

다른 추가할 사항이 있으면 그 쪽을 공부할 수도 있을 것 같습니다.

 

읽어주셔서 감사합니다!