공부블로그

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

리액트/리액트를 다루는 기술

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

떠어영 2021. 1. 21. 01:29

일정관리 웹 애플리케이션을 만들어 보자. 

10. 1 ) 프로젝트 준비하기 

1. 프로젝트 생성 및 필요한 라이브러리 설치

 

$ yarn create-app todo-app 으로 새로운 프로젝트를 생성하고

$ yarn add node-sass classnames react-icons 로 필요한 라이브러리를 설치한다. 

//react-icons는 리액트에서 아이콘을 사용할 수 있는 라이브러리로, SVG형태로 이루어진 아이콘을 리액트 컴포넌트처럼 쉽게 사용할 수 있다.

 

2. Prettier 설정

 

2장에서 배웠던 Prettier를 설정하여 코드를 작성할 때 코드 스타일을 깔끔하게 정리해보자. 

프로젝트의 최상위 디렉터리에 .prettierrc를 작성

 

3. index.css 수정

 

4. App컴포넌트 초기화

 

( 프로젝트 준비가 완료된 모습 )

10. 2 ) UI 구성하기

src디렉터리에 components라는 디렉터리를 생성하여 총 4개의 컴포넌트를 만들어 폴더에 넣어준다.

  • TodoTemplate 컴포넌트 : 화면을 가운데에 정렬, 앱 타이틀( 일정관리 )을 보여준다. children으로 내부 JSX를 props으로 받아 와서 렌더링
  • TodoInsert 컴포넌트 :  새로운 항목을 입력하고 추가할 수 있다. state를 통해 인풋의 상태를 관리
  • TodoListItem 컴포넌트 : 각 할 일 항목에 대한 정보를 보여준다. todo객체를 props로 받아와서 상태에 따라 다른 스타일의 UI를 보여준다.
  • TodoList 컴포넌트 : todos배열을 prop로 받아온 후 map함수를 사용해서 여러개의 TodoListItem 컴포넌트로 변환하여 보여준다. 

 

10. 2. 1 ) TodoTemplate 만들기

 

TodoTemplate. js 

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

const TodoTemplate = ({ children }) => { //children props받아와서 사용
  return (
    <div className="TodoTemplate">
      <div className="app-title">일정 관리</div>
      <div className="content">{children}</div>
    </div>
  );
};
export default TodoTemplate;

App. js

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

const App = () => {
  return <TodoTemplate>Todo앱을 만들자!</TodoTemplate>;
};
export default App;

닫혀 있는 파일에도 자동 완성이 제대로 작동하려면 프로젝트 최상위 디렉터리에 jsconfig.json파일을 만들어 주어야 한다. 

jsconfig.json 파일을 만들고 열어서 ctrl + Space를 눌러주면 자동 완성 박스에 의해 코드가 작성된다.

 

여기까지 작성하면 아래와 같이 나온다.

이제 꾸며보자!  TodoTemplate. scss

.TodoTemplate { /*TodoTemplate클래스에 적용할 스타일*/
  width: 512px;
  /*넓이가 주어진 상태에서 좌우 중앙 정렬*/
  margin-left: auto;
  margin-right: auto;
  margin-top: 6rem;
  border-radius: 4px;
  overflow: hidden;

  .app-title { /*app-title클래스에 적용할 스타일*/
    background: #22b8cf;
    color: white;
    height: 4rem;
    font-size: 1.5rem;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .content { /*content에 적용할 스타일*/
    background: white;
  }
}

( 실행 화면 )

 

10. 2. 2 ) TodoInsert 만들기

 

components 디렉터리에 TodoInsert.js와 TodoInsert.scss파일을 생성해서 작성해보자.

 

TodoInsert. js

import React from 'react';
import { MdAdd } from 'react-icons/md'; // { 아이콘 이름 }
import './TodoInsert.scss';

const TodoInsert = () => {
  return (
    <form className="TodoInsert">
      <input placeholder="할 일을 입력하세요" />
      <button type="submit">
        <MdAdd />
      </button>
    </form>
  );
};
export default TodoInsert;

 

App. js에서 렌더링 해주면 아래와 같은 화면이 나온다.

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 {
      //input의 문구에 적용됨
      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. 2. 3 ) TodoListItem과 TodoList 만들기

 

TodoListItem. js

import React from 'react';
import {
  MdCheckBoxOutlineBlank,
  MdCheckBox,
  MdRemoveCircleOutline,
} from 'react-icons/md'; //아이콘 여러개 사용
import './TodoListItem.scss'; //스타일 받아오기

const TodoListItem = () => {
  return (
    <div className="TodoListItem">
      <div className="checkbox">
        <MdCheckBoxOutlineBlank />
        <div className="text">할 일</div>
      </div>
      <div className="remove">
        <MdRemoveCircleOutline />
      </div>
    </div>
  );
};
export default TodoListItem;

 

TodoList. js

import React from 'react';
import TodoListItem from './TodoListItem'; //블러오기
import './TodoList.scss';

const TodoList = () => {
  return (
    <div className="TodoList">
      <TodoListItem />
      <TodoListItem />
      <TodoListItem />
    </div>
  );
};
export default TodoList;

 

App. js에서 렌더링

import React from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';

const App = () => {
  return (
    <TodoTemplate>
      <TodoInsert />
      <TodoList />
    </TodoTemplate>
  );
};
export default App;

( 실행 화면 )

TodoLsit. scss

.TodoList {
  /*전체 틀 크기 조절*/
  min-height: 320px;
  max-height: 513px;
  overflow: auto;
}

 

TodoLsitItem. 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;
  }
}

스타일링까지 마치면 아래와 같이 나온다.

 

10. 3 ) 기능 구현하기

 

10. 3. 1 ) App에서 todos 상태 사용하기

 

나중에 추가할 일정 항목에 대한 상태들은 모두 App컴포넌트에서 관리한다.

App에서 useState를 사용하여 todos라는 상태를 정의하고, todos를 TodoList의 props로 전달한다.

 

App. js

import React, { useState } from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';

const App = () => {
  const [todos, setTodos] = useState([
    //초기값으로 객체 배열이 들어감
    {
      id: 1,
      text: '리액트의 기초 알아보기', //내용
      checked: true, //완료여부
    },
    {
      id: 2,
      text: '컴포넌트 스타일링 해보기',
      checked: true,
    },
    {
      id: 3,
      text: '일정관리 앱 만들어 보기',
      checked: false,
    },
  ]);
  return (
    <TodoTemplate>
      <TodoInsert />
      <TodoList todos={todos} /> {/* props 넣어주기 */}
    </TodoTemplate>
  );
};
export default App;

TodoList. js

import React from 'react';
import TodoListItem from './TodoListItem'; //블러오기
import './TodoList.scss';

const TodoList = ({ todos }) => {
  //받아온 props
  return (
    //map함수로 TodoListItem으로 이루어진 배열로 변환
    <div className="TodoList">
      {todos.map((todo) => (
        <TodoListItem todo={todo} key={todo.id} /> //todo데이터는 통째로 props에 전달, key는 id
      ))}
    </div>
  );
};
export default TodoList;

TodoListItem. js

import React from 'react';
import {
  MdCheckBoxOutlineBlank,
  MdCheckBox,
  MdRemoveCircleOutline,
} from 'react-icons/md'; //아이콘 여러개 사용
import cn from 'classnames'; //조건부 스타일링을 위해 classnames사용
import './TodoListItem.scss'; //스타일 받아오기

const TodoListItem = ({ todo }) => {
  const { text, checked } = todo; //객체
  return (
    <div className="TodoListItem">
      <div className={cn('checkbox', { checked })}>
        {' '}
        {/* checked가 참이어야 checked클래스가 적용된다. */}
        {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
        <div className="text">{text}</div>
      </div>
      <div className="remove">
        <MdRemoveCircleOutline />
      </div>
    </div>
  );
};
export default TodoListItem;

TodoList에서 props으로 받아온 값을 TodoListItem으로 변환하여 렌더링하도록 설정해준다.

 

최종적으로 TodoList컴포넌트는 App에서 전달해 준 todos값에 따라 다른 내용을 보여준다

 

 

10. 3. 2 ) 항목 추가 기능 구현하기

 

TodoInsert컴포넌트에서 인풋상태를 관리하고 App컴포넌트에는 todos배열에 새로운 객체를 추가하는 함수를 만들어야 한다.

 

1. TodoInsert value 상태 관리하기

TodoInsert 컴포넌트에서 input에 입력하는 값을 관리할 수 있도록 useState를 사용하여 value라는 상태를 정의, 

input의 이벤트를 핸들링할 onChange 함수도 작성해야 한다. ( 이때 useCallBack Hook을 사용해 함수를 재사용할 수 있도록 해준다. )

 

ToInsert. js

import React from 'react';
import { MdAdd } from 'react-icons/md'; // {아이콘 이름}
import { useState } from '../../node_modules/react/cjs/react.development';
import { useCallback } from '../../node_modules/react/index';
import './TodoInsert.scss';

const TodoInsert = () => {
  const [value, setValue] = useState(''); //useState로 상태 정의

  //onChange함수를 콜백함수로 정의
  const onChange = useCallback((e) => {
    setValue(e.target.value); //상태를 인풋에 입력받은 값으로 업데이트 해준다.
  }, []);
  return (
    <form className="TodoInsert">
      <input
        placeholder="할 일을 입력하세요"
        value={value} //초기값
        onChange={onChange} //onChange함수로 핸들링
      />
      <button type="submit">
        <MdAdd />
      </button>
    </form>
  );
};
export default TodoInsert;

 

2. 리액트 개발자 도구 

사실 인풋은 value와 onChange를 설정하지 않아도 입력할 수 있다. 그저 추적하지 않을 뿐이다. 이런 경우에 state가 잘 업데이트되고 있는지 확인하려면 리액트 개발자 도구를 사용하면 된다.

크롬 웹 스토어에서 React Developer Tools를 검색하여 설치해보자. ( 설치했는데 나는 "⚛️ Components" and "⚛️ Profiler" 이게 뜬다... 왜이러는지는 모르게따

 

3. todos 배열에 새 객체 추가하기

App컴포넌트에서 todos 배열에 새 객체를 추가하는 onInsert함수를 만들어보자.

새로운 객체를 만들 때마다 id에는 1씩 더해주고 id는 useRef를 통해 관리한다. ( 렌더링해도 바뀌지 않으므로 )

그리고 콜백함수로 onInsert를 감싸서 재사용해준다. ( 콜백함수 사용을 습관화하는 것이 좋다 )

 

App. js

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

const App = () => {
  const [todos, setTodos] = useState([
    //초기값으로 객체 배열이 들어감, todos는 객체 배열!!
    {
      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)); //todos배열에 위에서 만들어준 todo객체를 추가해준다
      nextId.current += 1;//id+1
    },
    [todos], //todos가 바뀔때만 렌더링
  );

  return (
    <TodoTemplate>
      <TodoInsert onInsert={onInsert}/>
      <TodoList todos={todos} /> {/* props 넣어주기 */}
    </TodoTemplate>
  );
};
export default App;

 

 

4. TodoInsert에서 onSubmit 이벤트 설정하기

버튼을 클릭하면 발생할 이벤트를 설정, App에서 TodoInsert에 넣어준 onInsert함수에 value값을 파라미터로 넣어서 호출한다. 

 

TodoInsert. js

import React from 'react';
import { MdAdd } from 'react-icons/md'; // {아이콘 이름}
import { useState } from '../../node_modules/react/cjs/react.development';
import { useCallback } from '../../node_modules/react/index';
import './TodoInsert.scss';

const TodoInsert = ({ onInsert }) => {
  //App.js 에서 <TodoInsert onInsert={onInsert}라고 작성되어 있음>, props로 함수를 받아옴
  const [value, setValue] = useState(''); //useState로 상태 정의

  //콜백함수로 정의
  const onChange = useCallback((e) => {
    setValue(e.target.value); //상태를 인풋에 입력받은 값으로 업데이트 해준다.
  }, []);

  const onSubmit = useCallback(
    //onSubmit이벤트를 핸들링할 때 넣어줄 함수
    (e) => {
      onInsert(value); //업데이트된value값을 onInsert함수에 넣어서 호출해준다. ( value == text )
      setValue(''); //value값 초기화
      //submit 이벤트는 새로고침을 발생시킨다. 이를 방지하기 위해 아래 함수 호출
      e.preventDefault();
    },
    [onInsert, value],
  );

  return (
    <form className="TodoInsert" onSubmit={onSubmit}>
      <input
        placeholder="할 일을 입력하세요"
        value={value} //초기값
        onChange={onChange} //onChange함수로 핸들링
      />
      <button
        type="submit" //submit타입
      >
        <MdAdd />
      </button>
    </form>
  );
};
export default TodoInsert;

onSubmit 대신에 onClick 이벤트로도 충분히 처리할 수 있지만, onSubmit은 인풋에서 엔터키를 눌러도 발생하기 때문에 더욱 유용하다. 

 

 

10. 3. 3 ) 지우기 기능 구현하기

 

1. 배열 내장 함수 filter로 todos배열에서 id로 항목 지우기

App컴포넌트에 id를 파라미터로 받아와서 해당id의 항목을 지우는 함수인 onRemove를 작성해 보자.

 

App. js

import React, { useState, useRef, useCallback } from 'react';
...

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

  return (
    <TodoTemplate>
      <TodoInsert onInsert={onInsert} />
      <TodoList todos={todos} onRemove={onRemove} /> {/* props 넣어주기 */}
    </TodoTemplate>
  );
};
export default App;

 

2. TodoListItem에서 삭제함수 호출하기

TodoList. js에서는 props로 받아온 onRemove 함수를 그대로 props로 TodoListItem으로 전달해준다. 

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

TodoListItem. js 에서는 onClick이벤트를 핸들링 하는 함수로 받아온 onRemove함수를 사용한다.

const TodoListItem = ({ todo, onRemove }) => {
  const { id, text, checked } = todo; //객체
  return (
    . . .
      <div
       className="remove"onClick={( ) => onRemove( id )} //아이콘을 누르면 onRemove함수 호출>
        <MdRemoveCircleOutline />
      </div>
    </div>
  );
};

 

10. 3. 4 ) 수정 기능

 

삭제 기능과 비슷하게 onToggle함수를 App에 만들고, 해당 함수를 TodoList 컴포넌트에 props로 넣어준다. 그다음 TodoList를 통해 TodoListItem까지 전달한다.

 

App. js

import React, { useState, useRef, useCallback } from 'react';
...
const App = () => {
 ...
  const onToggle = useCallback(
    (id) => {
      setTodos(
        todos.map(( //특정 id를 가지고 있는 객체의 checked값을 반전시켜줌
          todo,) => (todo.id === id ? { ...todo, checked: !todo.checked } : todo)),
      );
    },
    [todos],
  );

  return (
    <TodoTemplate>
      <TodoInsert onInsert={onInsert} />
      <TodoList todos={todos} onRemove={onRemove} onToggle={onToggle} />{' '}
      {/* props 넣어주기 */}
    </TodoTemplate>
  );
};
export default App;

여기에서 map을 쓰는 이유..?

 

TodoList. js 에서 onToggle을 props로 받아와 그대로 props으로 전달

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

 

TodoListItem. js

const TodoListItem = ({ todo, onRemove, onToggle }) => {
  const { id, text, checked } = todo; //객체
  return (
    <div className="TodoListItem">
      <div className={cn('checkbox', { checked })} onClick={() => onToggle(id)}>
        {/* checked가 참이어야 checked클래스가 적용된다. */}
        {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
        <div className="text">{text}</div>
      </div>
      ...
      </div>
    </div>
  );
};

 

끝!