상세 컨텐츠

본문 제목

[리액트스타터2] 10장. 일정 관리 웹 애플리케이션 만들기

22-23/22-23 리액트 스타터 2

by mk020 2022. 12. 22. 10:00

본문

728x90

 

우리는 이전의 단원들을 통해 리액트의 기본기부터 컴포넌트를 스타일링하는 방법까지를 배웠습니다. 지금까지 배운 내용을 활용하여 프런트 엔드를 공부할 때 자주 구현하는 일정관리 애플리케이션을 만들어보겠습니다.

이번 실습은 다음과 같은 순서로 진행됩니다.

  1. 프로젝트 준비하기
  2. UI 구성하기
  3. 기능 구현하기

 


I. 프로젝트 준비하기

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

일정 관리 애플리케이션을 만들기위해 터미널에 create-react-app을 사용한 명령어를 입력하겠습니다. 또한 프로젝트가 생성되면 todo-app 디렉터리로 이동하여 애플리케이션을 만들 때 필요한 라이브러리를 설치하세요.

$ yarn create react-app todo-app
$ cd todo-app
$ yarn add node-sass classnames react-icons
  • sass : Sass 를 사용할 예정이므로 설치
  • classnames : 조건부 스타일링을 편하게 하기 위해 설치
  • react-icons : 리액트에서 다양하고 예쁜 아이콘을 사용하는 라이브러리, 아이콘을 컴포넌트처럼 다룰 수 있음

 

2. Prettier 설정 

  • Prettier : 코드가 제대로 정렬되고, 세미콜론(;)이 바진 곳에 자동 추가되는 등의 자동 코드 정리를 해줌, .prettierrc라는 파일에서 이러한 스타일링을 커스터마이징할 수 있음

Prettier을 설정하여 코드 스타일링을 깔끔하게 정리합시다. .prettierrc 파일을 다음과 같이 만드세요.

// .prettierrc

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

 

3. index.ccss 수정 

글로벌 스타일 파일이 들어있는 index.css를 수정하겠습니다.

// index.css

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

 

4. App 컴포넌트 초기화

컴포넌트를 초기화 한 뒤, 프로젝트 디렉터리에서 yarn start 명령어를 입력하여 개발 서버를 구동하세요.

// App.js

import React from 'react';
 
const App = () => {
  return <div>Todo App을 만들자!</div>;
};
 
export default App;

II. UI 구성하기

앞으로 만들 컴포넌트들은 다음과 같습니다. 이 컴포넌트들은 관습적으로 src 디렉터리에 components 디렉터리를 만들어 그 안에 저장합니다.

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

 

1.TodoTemplate 만들기

// TodoTemplate.js

import './TodoTemplate.scss';

const TodoTemplate = ({ children }) => {
    return (
        <div className="TodoTemplate">
            <div className="app-title">일정관리</div>
            <div className="content">{children}</div>
        </div>
    );
};

export default TodoTemplate;
  • 이 컴포넌트를 작성한 뒤 App.js 에서 import를 따로 해야합니다.
import React from 'react';
import TodoTemplate from './components/TodoTemplate';
( ... )

 

TodoTemplate의 css 스타일을 작성하기 위한 코드는 다음과 같습니다.

// TodoTemplate.scss

.TodoTemplate {
    width: 512px;
    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;
    }
}
  • flex는 grid 이전의 레이아웃 배치 전용 기능으로 고안되었습니다.
  • 가로 방향으로 아이템들이 배치되고 높이는 컨테이너 크기로 자동 조정됩니다.
  • flex라는 display 속성을 더 자세히 알고 싶다면 Flexbox Froggy(https://flexboxfroggy.com/#ko)에 방문하여 학습하는 것도 좋은 방법입니다.

 

2. 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;
import { 아이콘 이름 } from 'react-icons/md'

App.js에도 렌더링합니다.

// App.js

import React from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/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;
    }
  }
}
  • + 버튼에 마우스를 올리면 버튼의 배경색상이 바뀌는 것을 확인할 수 있습니다.

 

3.  TodoListItem 과 TodoList 만들기

일정 관리 항목을 보일 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;
  • 아직 사용하지 않은 아이콘인 MdCheckBox 아이콘 컴포넌트는 할 일이 완료되었을 때 체크된 상태를 보여줄 때 사용합니다.
// 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;
  • 이 컴포넌트에 TodoListItem을 불러와 별도의 props 전달 없이 내용을 보여줍니다.
  • 이후에 기능을 추가하여 다양한 데이터를 전달할 예정입니다.

두 컴포넌트의 스타일링은 다음과 같습니다.

// TodoList.scss

.TodoList {
  min-height: 320px;
  max-height: 513px;
  overflow-y: auto;
}
// 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;
  }
}

 

컴포넌트 스타일링을 마친 후에는 App.js에 만들어둔 컴포넌트를 모두 랜더링해봅시다.

// 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;

III. 기능 구현하기

1. 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;

 

  • 추가한 일정 항목에 대한 상태들은 App 컴포넌트에서 관리합니다. useState를 사용하여 todos라는 상태를 정의하고 TodoList의 props로 전달하겠습니다.
  • todos 배열 안에 들어있는 객체는 고유의 id, 내용, 완료 여부를 알려주는 값을 가집니다.
  • 이 배열은 TodoList 에 props 로 전달됩니다.

 

// 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;
  • props로 받아온 todos 배열은 map을 통해 TodoListItem 으로 이루어진 배열로 변환해 렌더링했습니다.
  • map 을 사용할 때는 key props를 전달해야하는데 여기서는 id 값을 사용했습니다.

TodoListItem 컴포넌트에서 받아온 todo 값에 따른 UI를 보여주도록 코드를 수정해봅시다.

 

// TodoListItem.js

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;

 

2. 항목 추가 기능 구현하기

일정항목을 추가하기 위해서는 TodoInsert 컴포넌트에서 인풋 상태를 관리하고 App 컴포넌트에서는 todos 배열에 새로운 객체를 추가하는 함수를 만들어야합니다.

 

(1) TodoInsert value 상태 관리하기

// TodoInsert.js

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

const TodoInsert = () => {
  const [value, setValue] = useState(“);

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

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

export default TodoInsert;

 

  • 인풋에 입력하는 값을 관리할 수 있도록 useState를 사용하여 value라는 상태를 정의합니다
  • 인풋에 넣어줄 onChange 함수도 작성하되, 함수를 재사용하도록 useCallback Hook을 사용합니다.

state가 잘 업데이트 되었는지를 확인하는 다른 방법은 리액트 개발자 도구를 사용하는 방법이 있습니다.

https://chrome.google.com/webstore/category/extensions

  • onChange 함수에서 console.log 함수를 찍어보는 방법을 이용하면 됩니다.
  • 위의 주소에서 리액트 개발자 도구를 설치후 열면 개발자 도구 탭에 Component가 나타납니다.
  • TodoInsert를 선택하면 인풋을 수정했을 때 Hooks의 State 부분이 변경되는 것을 확인할 수 있습니다.

 

(2) todos 배열에 새 객체 추가하기

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

const App = () => {
 ( ... )

  // 고윳값으로 사용될 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],
  );

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

export default App;
  • App 컴포넌트에서 todos 배열에 새 객체를 추가하는 onInsert 함수입니다.
  • 새로운 객체를 만들 때 마다 id 값에 1을 더해야하는데 useRef를 이용해 이를 관리합니다.
  • id 값은 렌더링되는 값이 아니기 때문에 useState가 아닌 useRef를 이용합니다.
  • 또한 onInsert 함수는 컴포넌트 성능을 위해 useCallback 으로 감쌉니다.

 

(3) TodoInsert에서 onSubmit 이벤트 설정하기

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(“); // value 값 초기화
 
      // submit 이벤트는 브라우저에서 새로고침을 발생시킵니다.
      // 이를 방지하기 위해 이 함수를 호출합니다.
      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;
  • App에서 TodoInsert에 넣어둔 onInsert 함수에 현재 useState로 관리하는 value 값을 파라미터로 넣어 호풀합니다.
  • onSubmit 함수가 호출되면 props로 받아온 onInsert 함수에 현재 value 값을 파라미터로 넣어 호출하고, 현재 value 값을 초기화합니다.
  • onSubmit 이벤트는 또한 브라우저를 새로 고침하기에 e.preventDefault() 함수를 이용해 새로고침을 방지합니다.

 

3. 지우기 기능 구현하기

리액트 컴포넌트에서는 배열의 불변성을 지키면서 배열 원소를 지워야하는 경우, 배열의 내장함수 filter를 사용합니다.

 

(1) 배열 내장 함수 filter

filter 함수는 기존의 배열을 그대로 둔 상태에서 특정 조건을 만족하는 원소들만 추출하여 새로운 배열을 만듭니다.

const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const biggerThanFive = array.filter(number => number > 5);
// result: [6, 7, 8, 9, 10]
  • 함수의 파라미터로 조건문을 넣어둡니다.
  • 파라미터로 넣는 함수는 ture 혹은 flase 값을 반환해야합니다.
  • true를 반환하는 원소만 새로운 배열에 포함됩니다.

 

(2) todos 배열에서 id로 항목 지우기

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 onRemove = useCallback(
    id => {
      setTodos(todos.filter(todo => todo.id != = id));
    },
    [todos],
  );
  
return (
    <TodoTemplate>
      <TodoInsert onInsert={onInsert} />
      <TodoList todos={todos} onRemove={onRemove} />
    </TodoTemplate>
  );
};

export default App;
  • App 컴포넌트에 id를 파라미터로 받아와서 같은 id를 가진 항목을 todos 배열에서 지우는 함수 onRemove
  • TodoList의 props를 설정해야함

 

(3) TodoListItem에서 삭제 함수 호출하기

// TodoList.js

import React from ‘react‘;
import TodoListItem from ‘./TodoListItem‘;
import ‘./TodoList.scss‘;

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

export default TodoList;
  • TodoListItem에서 onRemove 함수를 사용하기 위해 TodoList 컴포넌트를 거쳐야합니다.
  • props로 받아온 onRemove 함수를 TodoListItem에 전달하는 코드를 작성합시다.

 

// TodoListItem.js

import React from 'react';
import {
  MdCheckBoxOutlineBlank,
  MdCheckBox,
  MdRemoveCircleOutline,
} from 'react-icons/md';
import cn from 'classnames';
import './TodoListItem.scss';
 
const TodoListItem = ({ todo, onRemove }) => {
  const { id, text, checked } = todo;
 
  return (
    <div className="TodoListItem">
      <div className={cn('checkbox', { checked })}>
        {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
        <div className="text">{text}</div>
      </div>
      <div className="remove" onClick={() => onRemove(id)}>
        <MdRemoveCircleOutline />
      </div>
    </div>
  );
};
 
export default TodoListItem;
  • 삭제 버튼을 누르면 TodoListeItem에서 onRemove 함수에 현재 자신이 가진  id를 넣어서 삭제 함수를 호출합니다.

 

4. 수정 기능

수정 기능은 삭제 기능과 비슷합니다.

onToggle이라는 함수를 App에 만들고 해당 함수를 TodoList 컴포넌트에 props로 넣어줍니다. 이 함수는 TodoList를 통해 TodoListItem에게 전달합니다.

(1)  onToggle 구현하기

// 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 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;
  • 배열 내장 함수인 map을 이용하여 특정 id 객체의 checked 값을 반전시킵니다.
  • 불변성을 유지하면서 특정 배열 원소를 업데이트할 때는 map을 사용합니다.
  • map을 사용하여 만든 배열에서 변화가 필요한 원소만 업데이트 되고 나머지는 그대로 있습니다.

 

(2) TodoListItem 에서 토글 함수 호출하기

App에서 만든 onToggle 함수를 TodoListItem에서 호출하도록 TodoList를 거쳐 전달합시다.

// TodoList.js

import React from ‘react‘;
import TodoListItem from ‘./TodoListItem‘;
import ‘./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;

 

// TodoListItem.js

import React from ‘react‘;
import {
  MdCheckBoxOutlineBlank,
  MdCheckBox,
  MdRemoveCircleOutline,
} from ‘react-icons/md‘;
import cn from ‘classnames‘;
import ‘./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;

 

 


QUIZ

  1. react에서 아이콘을 컴포넌트처럼 사용할 수 있도록 하는 라이브러리의 이름은 (____) 입니다.
  2. (____) 는 코드를 작성할 때 코드 스타일을 깔끔하게 정리하는데 사용합니다.
  3. (____) 라는 display 속성은 배치에 사용됩니다.
  4. map 을 사용하여 컴포넌트로 변환할 때는 (____) 를 전달해야하기에 실습에서는 id 값을 넣어주었습니다.
  5. (____) 는 브라우저에 나온 리액트 컴포넌트를 심층 분석할 수 있도록 리액트 개발 팀이 만든 도구 입니다.
  6. 리액트 배열에서 특정 조건만을 만족하는 원소만을 추출하기 위한 함수는 (____) 입니다.
  7. (____) 함수를 이용하면 배열의 특정 원소만 업데이트하여 사용할 수 있습니다.
  8. react-icons의 아이콘 "MdAdd" 를 사용하기 위한 코드를 작성해보세요.
  9. 우리가 진행한 실습에서는 TodoInsert에서 onSubmit 이벤트 설정하였습니다. onClick 이벤트로 우리가 했던 이벤트 처리를 하는 코드를 작성해보세요.

 

  1. react-icons
  2. Prettier
  3. flex
  4. key props
  5. 리액트 개발자 도구
  6. filter
  7. map
  8.  
import { MdAdd } from 'react-icons/md'

    9. 

const onClick = useCallback(
  () => {
    onInsert(value);
    setValue(“); // value 값 초기화
  },
  [onInsert, value],
);

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

 

 

 

 

 

728x90

관련글 더보기