관리 메뉴

거니의 velog

(3) 일정 관리 애플리케이션 만들기 3 본문

React/React_일정 관리 앱 만들기

(3) 일정 관리 애플리케이션 만들기 3

Unlimited00 2023. 12. 6. 09:41

3. 기능 구현하기

* 이제 일정 관리 애플리케이션이 실제로 동작할 수 있도록 기능을 구현해 보자.


(1) App에서 todos 상태 사용하기

* 나중에 추가할 일정 항목에 대한 상태들은 모두 App 컴포넌트에서 관리한다. App에서 useState를 사용하여 todos라는 상태를 정의하고, todos를 TodoList의 props로 전달해 보자.

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;

* todos 배열 안에 들어 있는 객체에는 각 항목의 고유 id, 내용, 완료 여부를 알려 주는 값이 포함되어 있다. 이 배열은 TodoList에 props로 전달되는데, TodoList에서 이 값을 받아 온 후 TodoItem으로 변환하여 렌더링하도록 설정해야 한다.

[ 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를 전달해 주어야 한다고 배웠다. 여기서 사용되는 key 값은 각 항목마다 가지고 있는 고윳값인 id를 넣어 줘야 한다. 그리고 todo 데이터는 통째로 props로 전달해 주자. 여러 종류의 값을 전달해야 하는 경우에는 객체로 통째로 전달하는 편이 나중에 성능 최적화를 할 때 편리하다.

* 이제 TodoListItem 컴포넌트에서 받아 온 todo 값에 따라 제대로 된 UI를 보여 줄 수 있도록 컴포넌트를 수정해 보자. 이 코드에서는 조건부 스타일링을 위해 classnames를 사용한다.

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

* 이제 TodoList 컴포넌트는 App에서 전달해 준 todos의 값에 따라 다른 내용을 제대로 보여 준다.

데이터 전달 완료


(2) 항목 추가 기능 구현하기

* 이번에는 일정 항목을 추가하는 기능을 구현해 보자. 이 기능을 구현하려면, TodoInsert 컴포넌트에서 인풋 상태를 관리하고 App 컴포넌트에서는 todos 배열에 새로운 객체를 추가하는 함수를 만들어 주어야 한다.

[1] TodoInsert value 상태 관리하기

* TodoInsert 컴포넌트에서 인풋에 입력하는 값을 관리할 수 있도록 useState를 사용하여 value라는 상태를 정의하겠다. 추가로 인풋에 넣어 줄 onChange 함수도 작성해 주어야 하는데, 이 과정에서 컴포넌트가 리렌더링될 때마다 함수를 새로 만드는 것이 아니라, 한 번 함수를 만들고 재사용할 수 있도록 useCallback Hook을 사용해 보겠다.

import React, { useCallback, useState } 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
        type="text"
        placeholder="할 일을 입력하세요."
        value={value}
        onChange={onChange}
      />
      <button type="submit">
        <MdAdd />
      </button>
    </form>
  );
};

export default TodoInsert;

* 이제 인풋에 텍스트를 입력해 보자. 오류가 발생하지 않고 텍스트가 잘 입력되나요?

* 사실 인풋은 value 값과 onChange를 설정하지 않더라도 입력할 수 있다. 그저 리액트 컴포넌트 쪽에서 해당 인풋에 무엇이 입력되어 있는지 추적하지 않을 뿐이다. 이런 경우 현재 state가 잘 업데이트되고 있는지 확인하려면, onChange 함수 안에서 console.log를 찍어 보는 것 외에 어떤 방법이 있을까? 바로 리액트 개발자 도구(React Development Tools)를 사용하는 방법이 있다.

[2] 리액트 개발자 도구

* 리액트 개발자 도구는 브라우저에 나타난 리액트 컴포넌트를 심층 분석할 수 있도록 리액트 개발팀이 만들었으며, 크롬 웹 스토어에서 React Developer Tools를 검색하여 설치할 수 있다. 크롬 웹 스토어 주소는 다음과 같다.

https://chromewebstore.google.com/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?utm_source=ext_app_menu

 

React Developer Tools

Adds React debugging tools to the Chrome Developer Tools. Created from revision ef8a840bd on 10/25/2023.

chrome.google.com

* 설치하고 나서 크롬 개발자 도구를 열면 개발자 도구 탭에 React가 나타난다. 이를 클릭해 보자. 그리고 하단에 나오는 Elements 탭에서 TodoInsert 컴포넌트를 검색한 후 선택하면, 다음과 같이 인풋을 수정했을 때 Hooks의 State 부분에도 똑같은 값이 잘 들어가는 것을 확인할 수 있다.

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

* 이번에는 App 컴포넌트에서 todos 배열에 새 객체를 추가하는 onInsert 함수를 만들어 보자. 이 함수에서는 새로운 객체를 만들 때마다 id 값에 1씩 더해 주어야 하는데, id 값은 useRef를 사용하여 관리할 것이다. 여기서 useState가 아닌 useRef를 사용하여 컴포넌트에 사용할 변수를 만드는 이유는 무엇일까? id 값은 렌더링되는 정보가 아니기 때문이다. 예를 들어 이 값은 화면에 보이지도 않고, 이 값이 바뀐다고 해서 컴포넌트가 리렌더링될 필요도 없다. 단순히 새로운 항목을 만들 때 참조되는 값일 뿐이다.

* 또한, onInsert 함수는 컴포넌트의 성능을 아낄 수 있도록 useCallback 으로 감싸 줄 것이다. props로 전달해야 할 함수를 만들 때는 useCallback 을 사용하여 함수를 감싸는 것을 습관화 하도록 하자.

* onInsert 함수를 만든 뒤에는 해당 함수를 TodoInsert 컴포넌트의 props로 설정해 주자.

[App.js]

import React, { useCallback, useRef, 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,
    },
  ]);

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

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

* 지금부터는 버튼을 클릭하면 발생할 이벤트를 설정해 보자. 방금 App에서 TodoInsert에 넣어 준 onInsert 함수에 현재 useState를 통해 관리하고 있는 value 값을 파라미터로 넣어서 호출한다.

import React, { useCallback, useState } 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
        type="text"
        placeholder="할 일을 입력하세요."
        value={value}
        onChange={onChange}
      />
      <button type="submit">
        <MdAdd />
      </button>
    </form>
  );
};

export default TodoInsert;

* onSubmit 이라는 함수를 만들고, 이를 form 의 onSubmit으로 설정했다. 이 함수가 호출되면 props로 받아 온 onInsert 함수에 현재 value 값을 파라미터로 넣어서 호출하고, 현재 value 값을 초기화한다.

* 추가로 onSubmit 이벤트는 브라우저를 새로고침시킨다. 이 때, e.preventDefault() 함수를 호출하면 새로고침을 방지할 수 있다.

* 물론 다음과 같이 onSubmit 대신에 버튼의 onClick 이벤트로도 충분히 처리할 수 있다.

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

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

* 이렇게 클릭 이벤트만으로도 할 수 있는데 굳이 form과 onSubmit 이벤트를 사용한 이유는 무엇일까? onSubmit 이벤트의 경우 인풋에서 [Enter]를 눌렀을 때도 발생하기 때문이다. 반면 버튼에서 onClick만 사용했다면, 인풋에서 onKeyPress 이벤트를 통해 [Enter]를 감지하는 로직을 따로 생성해야 한다. 그렇기 때문에 이번에는 onClick 이 아닌 onSubmit 으로 새 항목을 추가하도록 처리했다.

* 코드를 모두 입력했다면 브라우저에서 직접 새 일정 항목을 한 번 추가해 보자.

추가 기능 구현 완료

* 일정 항목 추가 기능이 모두 구현되었다!