[React] 리액트를 다루는 기술 - 10장 일정 관리 웹 애플리케이션 만들기

Lynn·2021년 9월 7일
0

React

목록 보기
13/17
post-thumbnail

프로젝트 준비하기

  • CRA로 todo-app 프로젝트 생성
npx create-react-app todo-app
  • 필요한 라이브러리 설치
npm add node-sass classnames react-icons
  • Prettier 설정
/* 최상위 디렉토리에 .prettierrc */

{
    "singleQuote": true,
    "semi": true,
    "useTabs": false,
    "tabWidth": 2,
    "trailingComma": "all",
    "printWidth": 80
}
  • index.css 수정
body {
  margin: 0;
  padding: 0;
  background: #e9ecef;
}
  • App.js 컴포넌트 초기화
import React from 'react';
 
const App = () => {
  return <div>Todo App을 만들자!</div>;
};
 
export default App;


만들자!


UI 구성하기

이번 프로젝트에서 쓰일 컴포넌트는 아래와 같다.

  • TodoTemplate: 화면을 가운데에 정렬 / children으로 내부 JSX를 props로 받아 와서 렌더링
  • TodoInsert: 새로운 항목을 입력하고 추가 / state를 통해 인풋의 상태 관리
  • TodoListItem: 각 할 일 항목 / todo 객체를 props로 받아 와서 상태에 따라 다른 스타일의 UI를 보여줌
  • TodoList: todos 배열을 props로 받아 map으로 여러 개의 TodoListItem 컴포넌트로 변환해서 보여줌

TodoTemplate

  • TodoTemplate.js
import React from 'react';
import './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;
    margin-left: auto;
    margin-right: auto;
    margin-top: 6rem;
    border-radius: 4px;
    overflow: hidden;

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

overflow: hidden; 넘치는 부분은 잘려서 보여지지 않음

TodoInsert

  • 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;
  • TodoInsert.scss
.TodoInsert {
    display: flex;
    background: rgb(179, 179, 179);
    input {
        background: none;
        outline: none;
        border: none;
        padding: 0.5rem;
        font-size: 1.125rem;
        line-height: 1.5;
        color: white;
        &::placeholder {
            color: rgb(224, 224, 224);
        }
        flex: 1;
    }
    button {
        background: none;
        outline: none;
        border: none;
        background: rgb(141, 141, 141);
        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: rgb(247, 169, 160);
        }
    }
}

transition: 0.1s background ease-in;

  • background: (property) 애니메이션 시킬 속성
  • 0.1s: (duration) 시작해서 끝날 때까지의 시간
  • ease-in: (timingfunction) 속도 변화

TodoListItem

  • 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;
  • 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: salmon;
            }
            .text {
                color: #adb5bd;
                text-decoration: line-through;
            }
        }
    }

    .remove {
        cursor: pointer;
        display: flex;
        align-items: center;
        font-size: 1.5rem;
        color: rgb(141, 141, 141);
        &:hover {
            color: salmon;
        }
    }

    & + & {
        border-top: 1px solid #dee2e6;
    }
}

&는 중첩된 규칙의 부모 선택자를 참조; & + & = .TodoListItem + .TodoListItem

TodoList

  • 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;
  • TodoList.scss
.TodoList {
    min-height: 320px;
    max-height: 513px;
    overflow-y: auto; //리스트 안은 스크롤 가능하게 함
}

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;

컴포넌트 스타일링 완료


기능 구현하기

App에서 todos 상태 사용하기

  • 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}/>
    </TodoTemplate>
  );
};

export default App;
  • TodoList.js
import React from 'react';
import TodoListItem from './TodoListItem';
import './TodoList.scss';

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

export default TodoList;
  • TodoListItem.js
    조건부 스타일링을 위해 classnames 사용
import React from 'react';
import {
    MdCheckBoxOutlineBlank,
    MdCheckBox,
    MdRemoveCircleOutline,
} from 'react-icons/md';
import cn from 'classnames';
import './TodoListItem.scss';

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

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

export default TodoListItem;

항목 추가 기능 구현하기

useState -> value
useCallback -> onChange, onSubmit
(컴포넌트가 리렌더링될 때마다 함수를 새로 만드는 것이 아니라, 한 번 함수를 만들고 재사용할 수 있도록 Hook 사용)
onClick 대신에 onSubmit 쓰는 이유 -> Enter 감지

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

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

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

    const onSubmit = useCallback(
        e => {
            onInsert(value);
            setValue('');
            e.preventDefault();
        },
        [onInsert, value],
    );

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

export default TodoInsert;

onInsert
-> id 값은 렌더링되는 정보가 아니기 때문에 useRef를 사용하여 관리
-> props로 전달해야 할 함수이므로 useCallback을 사용

  • 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([
    {
      id: 1,
      text: '리액트의 기초 알아보기',
      checked: true,
    },
    {
      id: 2,
      text: '컴포넌트 스타일링해 보기',
      checked: true,
    },
    {
      id: 3,
      text: '일정 관리 앱 만들어 보기',
      checked: false,
    },
  ]);

  const nextId = useRef(4);

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

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

export default App;

지우기 기능 구현하기

filter 함수로 todos를 걸러 준다. App.js에 아래 onRemove 함수를 정의해 주고 TodoList, TodoListItem에 props로 전달 전달

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

현재 onRemove를 호출한 id를 가진 todo만 남기고 새로운 todos 배열로 바뀜

체크 기능 구현하기

위와 비슷하게 filter 함수로 todos를 걸러 준다. App.js에 아래 onToggle 함수를 정의해 주고 TodoList, TodoListItem에 props로 전달 전달

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

현재 onToggle을 호출한 id를 가진 todochecked 값을 반전시켰다(나머지는 그대로 todo 반환). 불변성을 유지하면서 특정 배열 원소를 업데이트해야 할 때 map을 사용하면 짧은 코드로 쉽게 작성할 수 있다.

완성!

profile
wanderlust

0개의 댓글