관리 메뉴

거니의 velog

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

React/React_일정 관리 앱 만들기

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

Unlimited00 2023. 12. 6. 13:36

(3) 지우기 기능 구현하기

* 이번에는 지우기 기능을 구현해 보겠다. 리액트 컴포넌트에서 배열의 불변성을 지키면서 배열 원소를 제거해야 할 경우, 배열 내장 함수인 filter를 사용하면 매우 간편하다.

[1] 배열 내장 함수 filter

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

* 다음 코드 예제를 한번 확인해 보자.

const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const biggerThanFive = array.filter(number => number > 5);
console.log(biggerThanFive); // 결과 : [6, 7, 8, 9, 10]

* filter 함수에는 조건을 확인해 주는 함수를 파라미터로 넣어 주어야 한다. 파라미터로 넣는 함수는 true 혹은 false 값을 반환해야 하며, 여기서 true를 반환하는 경우만 새로운 배열에 포함된다.

[2] todos 배열에서 id로 항목 지우기

* 방금 배운 filter 함수를 사용하여 onRemove 함수를 작성해 보겠다. App 컴포넌트에 id를 파라미터로 받아 와서 같은 id를 가진 항목을 todos 배열에서 지우는 함수이다. 이 함수를 만들고 나서 TodoList의 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],
  );

  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;

[3] TodoListItem에서 삭제 함수 호출하기

* TodoListItem 에서 방금 만든 onRemove 함수를 사용하려면 우선 TodoList 컴포넌트를 거쳐야 한다. 다음과 같이 props로 받아 온 onRemove 함수를 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 함수에 현재 자신이 가진 id를 넣어서 삭제 함수를 호출하도록 설정해 보자.

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

* 다 작성했으면, 브라우저를 열어 일정 항목의 우측에 나타나는 빨간색 아이콘 버튼을 눌러 보자. 항목이 제대로 삭제되었는가?

잘 삭제 되었다.


(4) 수정 기능

* 수정 기능도 방금 만든 삭제 기능과 꽤 비슷하다. onToggle이라는 함수를 App에 만들고, 해당 함수를 TodoList 컴포넌트에게 props로 넣어 주자. 그 다음에는 TodoList를 통해 TodoListItem까지 전달해 주면 된다.

[1] onToggle 구현하기

[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],
  );

  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;

* 위 코드에서는 배열 내장 함수 map을 사용하여 특정 id를 가지고 있는 객체의 checked 값을 반전시켜 주었다. 불변성을 유지하면서 특정 배열 원소를 업데이트 해야 할 때 이렇게 map을 사용하면 짧은 코드로 쉽게 작성할 수 있다.

* 자, 여기서 갑자기 왜 map이 사용된 것인지 이해하기 힘들 수 있다. map 함수는 배열을 전체적으로 새로운 형태로 변환하여 새로운 배열을 생성해야 할 때 사용한다고 배웠다. 지금은 딱 하나의 원소만 수정하는데 왜 map을 사용할까?

* onToggle 함수를 보면 todo.id === id ? ... : ... 이라는 삼항 연산자가 사용되었다. 여기서 사용한 코드에 대해 좀 더 자세히 알아보자. todo.id와 현재 파라미터로 사용된 id 값이 같을 때는 우리가 정해 준 규칙대로 새로운 객체를 생성하지만, id 값이 다를 때는 변화를 주지 않고 처음 받아 왔던 상태 그대로 반환한다. 그렇기 때문에 map을 사용하여 만든 배열에서 변화가 필요한 원소만 업데이트 되고 나머지는 그대로 남아 있게 되는 것이다.

[2] TodoListItem에서 토글 함수 호출하기

* 이제 App에서 만든 onToggle 함수를 TodoListItem에서도 호출할 수 있도록 TodoList를 거쳐 TodoListItem에게 전달하겠다.

[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도 수정해 보자. 이전에 onRemove를 사용했던 것과 비슷하게 구현하면 된다.

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

* 이제 마지막 기능까지 모두 구현하였다! 체크 박스를 눌러 보면, 상태가 잘 업데이트 되는 모습을 볼 수 있다.

체크 박스 토글 기능 구현 완료


4. 정리

* 축하한다! 첫 프로젝트를 완성했다. 이번에 만든 프로젝트는 소규모이기 때문에 따로 컴포넌트 리렌더링 최적화 작업을 하지 않아도 정상적으로 작동한다. 하지만 일정 항목이 몇 만개씩 생긴다면 새로운 항목을 추가하거나 기존 항목을 삭제 및 토글할 때 지연이 발생할 수 있다. 클라이언트 자원을 더욱 효율적으로 사용하려면 불필요한 리렌더링을 방지해야 하는데, 이에 관한 내용은 뒤에서 자세히 다루어 보도록 하겠다.