10장 일정 관리 웹 애플리케이션 만들기

Posted by : at

Category : React


2021-07-05

10장 일정 관리 웹 애플리케이션 만들기


1장부터 9장까이의 개념을 통해서 일정 관리 애플리케이션을 구현하도록 한다.

프로젝트 준비 -> UI 구성하기 -> 구현하기 순서대로 진행된다.



10.1 프로젝트 준비하기

$ yarn create react-app todo-app

CRA(create-react-app)을 통해서 프로젝트를 생성하도록 한다.


$ cd todo-app
$ yarn add node-sass classnames react-icons

Sass를 통해서 디자인할 예정이므로 node-sass를 통해서 설치를 해주도록 한다. classnames는 조건부 스타일링을 더 편하게 하기 위해서 설치하고, react-icons는 리액테르에서 다양하고 예쁜 아이콘을 사용할 수 있도록 하는 라이브러리이다.

### [`node-sass npm 페이지 바로가기`](https://www.npmjs.com/package/node-sass) ### [`classnames npm 페이지 바로가기`](https://www.npmjs.com/package/classnames) ### [`react-icons npm 페이지 바로가기`](https://www.npmjs.com/package/react-icons)


해당 프로젝트를 진행하면서 node-sass 모듈을 설치하면 오류가 발생하는데 오류 내용은 다음과 같다.

Failed to compile.

./src/App.scss (./node_modules/css-loader/dist/cjs.js??ref--5-oneOf-6-1!./node_modules/postcss-loader/src??postcss!./node_modules/resolve-url-loader??ref--5-oneOf-6-3!./node_modules/s
ass-loader/dist/cjs.js??ref--5-oneOf-6-4!./src/App.scss)
Error: Node Sass version 5.0.0 is incompatible with ^4.0.0.

CRA로 만들어진 프로젝트는 5.0 버전이랑 충돌이 나니 4.xx 버전으로 다시 깔아야 한다.

//node-sass 삭제
$ yarn remove node-sass
//node-sass 4.14.0 버전 설치
$ yarn add node-sass@4.14.0

따라서 위와 같이 node-sass를 삭제하고 4.14.0 버전을 다시 깔도록 한다.



다음은 Prettier 설정이다.

.prettierrc

{
  "singleQuote": true,
  "semi": true,
  "useTabs": false,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 80
}

최상위 디렉터리에 다음과 같이 입력한 파일을 생성하도록한다.


다음은 index.css를 초기화하고 App 컴포넌트를 초기화하여 상관없는 파일들을 지우도록한다.

index.css

body {
  margin: 0;
  padding: 0;
  background: #e9ecef;
}


index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

src 디렉터리의 App.js, index.css, index.js 를 제외한 나머지 파일들을 지우도록 한다.


10.2 UI 구성하기


1. TodoTemplete

  • 화면을 가운데에 정렬시켜 주며, 앱 타이틀(일정 관리)을 보여준다. children으로 내부 JSX를 props로 받아 와서 렌더링 해준다.


2. TodoInsert

  • 새로운 항목을 입력하고 추가할 수 있는 컴포넌트이다. state를 통해 인풋의 상태를 관리한다.


3. TodoListitem

  • 각 할 일 항목에 대한 정보를 보여 주는 컴포넌트이다. todo 객체를 props로 받아 와서 상태에 따라 다른 스타일의 UI를 보여준다.


4. TodoList

  • todos 배열을 props로 받아 온 후, 이를 배열 내장 함수 map을 사용해서 여러 개의 TodoListItem 컴포넌트로 반환하여 보여준다.


TodoTemplate > TodoList > TodoListItem, TodoInsert 와같은 구조를 가진다.



jsconfig 파일 수정하기

{
  "rootDir": "scr",
  "compilerOptions": {
    "target": "es6",
    "baseUrl": "src",
    "paths": {
      "*": ["*"]
    }
  }
}

자동 import와, import 경로 해결을 위해서 다음과 같이 최상위 폴더에 jsconfig 파일을 생성하도록 한다.


10.3 TodoTemplate 만들기


TodoTemplate.js

import React from 'react';
import 'components/Scss/TodoTemplate.scss';

const TodoTemplate = ({ children }) => {
  return (
    <div className="TodoTemplate">
      // 일정관리 타이틀
      <div className="app-title">일정 관리</div>
      // 일정관리 타이틀 밑에 내용 (흰색 배경)
      <div className="content">{children}</div>
    </div>
  );
};

export default TodoTemplate;


TodoTemplate.scss

.TodoTemplate {
  width: 512px;
  // width가 주어진 상태에서 좌우 정렬
  margin-left: auto;
  margin-right: auto;
  margin-top: 6rem;
  border-radius: 4px;
  overflow: hidden;
}

.app-title {
  background: #22b8cf;
  color: white;
  height: 4rem;
  font-size: 1.5rem;
  display: flex;
  align-items: center;
  justify-content: center;
}

.content {
  background: white;
}

Flexbox Froggy 를 통해서 flex 에 대해서 자세히 학습할 수 있다.



10.4 TodoInsert 만들기


TodoInsert.js

import React, { useCallback, useState } from 'react';
import { MdAdd } from 'react-icons/md';
import 'components/Scss/TodoInsert.scss';

const TodoInsert = ({ onInsert }) => {
  const [value, setValue] = useState('');

  const onChange = useCallback((e) => {
    setValue(e.target.value);
  }, []);

  const onSubmit = useCallback(
    (e) => {
      if (value !== '') {
        onInsert(value);
        setValue(''); // value 값 초기화
      } else {
        alert('할 일을 입력하세요');
        setValue(''); // value 값 초기화
      }
      // submit 이벤트는 브라우저에서 새로고침을 발생시킨다.
      // 이를 방지하기 위해 이 함수를 호출한다.
      e.preventDefault();
    },
    [onInsert, value]
  );

  return (
    // onSubmit은 input안에 내용을 넣고 엔터만 치더라도 발생되기 때문에
    // 버튼에 onClick을 넣지 않고 form에 onSubmit 해준다.
    <form className="TodoInsert" onSubmit={onSubmit}>
      <input
        placeholder="할 일을 입력하세요"
        value={value}
        onChange={onChange}
      />
      <button type="submit">
        <MdAdd />
      </button>
    </form>
  );
};

export default TodoInsert;


TodoInsert.scss

.TodoInsert {
  display: flex;
  background: #495057;
  input {
    background: none;
    outline: none;
    border: none;
    padding: 0.5rem;
    font-size: 1.125rem;
    line-height: 1.5;
    color: white;
    &::placeholder {
      color: #dee2e6;
    }
    // 버튼을 제외한 영역을 모두 차지하기
    flex: 1;
  }
  button {
    background: none;
    outline: none;
    border: none;
    background: #868e96;
    color: white;
    padding-left: 1rem;
    padding-right: 1rem;
    font-size: 1.5rem;
    display: flex;
    align-items: center;
    cursor: pointer;
    transition: 0.1s background ease-in;
    &:hover {
      background: #adb5bd;
    }
  }
}



10.5 TodoListItem & TodoList 만들기


TodoListItem.js

import React from 'react';
import {
  MdCheckBoxOutlineBlank,
  MdCheckBox,
  MdRemoveCircleOutline,
} from 'react-icons/md';

import cn from 'classnames';
import 'components/Scss/TodoListItem.scss';

const TodoListItem = ({ todo, onRemove, onToggle }) => {
  const { id, text, checked } = todo;

  return (
    <div className="TodoListItem">
      <div className={cn('checkbox', { checked })} onClick={() => onToggle(id)}>
        {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
        <div className="text">{text}</div>
      </div>
      <div className="remove" onClick={() => onRemove(id)}>
        <MdRemoveCircleOutline />
      </div>
    </div>
  );
};

export default TodoListItem;


TodoListItem.scss

.TodoListItem {
  padding: 1rem;
  display: flex;
  align-items: center; // 세로 중앙 정렬
  &:nth-child(even) {
    background: #f8f9fa;
  }

  .checkbox {
    cursor: pointer;
    flex: 1; // 차지할 수 있는 영역 모두 차지
    display: flex;
    align-items: center; // 세로 중앙 정렬
    svg {
      // 아이콘
      font-size: 1.5rem;
    }

    .text {
      margin-left: 0.5rem;
      flex: 1; // 차지 할 수 있는 영역 모두 차지
    }

    // 체크되었을 때 보여 줄 스타일
    &.checked {
      svg {
        color: #22b8cf;
      }
      .text {
        color: #adb5bd;
        text-decoration: line-through;
      }
    }
  }

  .remove {
    display: flex;
    align-items: center;
    font-size: 1.5rem;
    color: #ff6b6b;
    cursor: pointer;
    &:hover {
      color: #ff8787;
    }
  }

  // 엘리먼트 사이사이에 테두리를 넣어줌
  & + & {
    border-top: 1px solid #dee2e6;
  }
}



TodoList.js

import React from 'react';
import TodoListItem from './TodoListItem';
import 'components/Scss/TodoList.scss';

const TodoList = ({ todos, onRemove, onToggle }) => {
  return (
    <div className="TodoList">
      {todos.map((todo) => (
        <TodoListItem
          todo={todo}
          key={todo.id}
          onRemove={onRemove}
          onToggle={onToggle}
        />
      ))}
    </div>
  );
};

export default TodoList;


TodoList.scss

.TodoList {
  min-height: 320px;
  max-height: 513px;
  overflow-y: auto;
}



10.6 App.js


App.js

import React, { useCallback, useRef, useState } from 'react';
import TodoInsert from 'components/Todo/TodoInsert';
import TodoList from 'components/Todo/TodoList';
import TodoTemplate from 'components/Todo/TodoTemplate';

const App = () => {
  const [todos, setTodos] = useState([
    {
      id: 1,
      text: '리액트 기초 알아보기',
      checked: true,
    },

    {
      id: 2,
      text: '컴포넌트 스타일링해 보기',
      checked: true,
    },

    {
      id: 3,
      text: '일정 관리 앱 만들어 보기',
      checked: false,
    },
  ]);

  // 고윳값으로 사용될 id
  // ref를 사용하여 변수 담기
  const nextId = useRef(4);

  const onInsert = useCallback(
    (text) => {
      const todo = {
        id: nextId.current,
        text,
        checked: false,
      };
      setTodos(todos.concat(todo));
      nextId.current += 1; // nextId 1씩 더하기
    },
    [todos]
  );

  const onRemove = useCallback(
    (id) => {
      setTodos(todos.filter((todo) => todo.id !== id));
    },
    [todos]
  );

  const onToggle = useCallback(
    (id) => {
      setTodos(
        todos.map((todo) =>
          todo.id === id ? { ...todo, checked: !todo.checked } : todo
        )
      );
    },
    [todos]
  );

  return (
    <TodoTemplate>
      <TodoInsert onInsert={onInsert} />
      <TodoList todos={todos} onRemove={onRemove} onToggle={onToggle} />
    </TodoTemplate>
  );
};

export default App;



10.7 결과 화면



About YeongJun
YeongJun

Hi I am YeongJun, a Web Developer and Designer.

Email : dudwns1045@naver.com

Website : http://gitbub.com/dudwns9331

About yeongjun

강원대학교 컴퓨터공학과 컴퓨터과학전공 R.O.T.C 60th 127 학군단, 프론트엔드 개발 준비중 :D

Categories
Useful Links