[Todo-List] 3. 기능 구현

🏃Dekay (JuniorDeveloper)·2021년 10월 5일
0

ToyProject

목록 보기
3/5
post-thumbnail

앞서 생성한 컴포넌트들이 실제로 작성할 수 있도록 기능을 구현해보자.

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

  • Todo-List 프로젝트는App 컴포넌트에서 나중에 추가할 일정 항목에 대한 모든 상태들을 관리한다.
  • src/App.js 파일을 아래의 코드로 수정하였다.
//App.js
import React, { useState } from 'react';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
import TodoTemplate from './components/TodoTemplate';

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, 내용, 완료 여부를 알려주는 값이 포함되어 있다.
  • 이 배열은 TodoListprops로 전달되며, 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를 보여주기 위해 TodoListItem.js를 다음과 같이 수정했다.
//TodoListItem.js
import React from "react";
import {
    MdCheckBox,
    MdRemoveCircleOutline,
    MdCheckBoxOutlineBlank,
} 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;
  • 위의 코드는 조건부 스타일링을 위해서 classnames를 사용했다.
  • 여기 까지 제대로 구현이 됐다면 TodoList 컴포넌트는 App에서 전달해 준 todos 값에 따라 내용을 보여주는 것을 확인할 수 있다.

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

  • 일정 항목에 추가하는 기능을 구현하기 위해서 TodoInsert 컴포넌트에서 인풋 상태를 관리하고 App 컴포넌트에 todos 배열에 새로운 객체를 추가했다.

2.1 TodoInsert value 상태 관리하기

  • TodoInsert 컴포넌트에서 인풋에 입력하는 값을 관리하기 위해 useState를 사용하여 value라는 상태를 정의했다.
  • 추가로, 인풋에 넣어 줄 onChange 함수를 생성하는데 컴포넌트가 리렌더링될 때마다 함수를 생성하지 않도록 주의❗❗
    👉 useCallback Hook을 사용하면 해결 가능.
//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);
    }, []);

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

export default TodoInsert;

2.2 todos 배열에 새로운 객체 추가하기

  • App 컴포넌트에서 todos 배열에 새로운 객체를 추가하는 onInsert 함수를 생성해야 한다.
  • 새로운 객체를 만들 때마다 고윳값인 id를 1씩 더해 주어야 하는데, id 값은 useRef를 사용하여 관리했다.
    👉 useState가 아닌 useRef를 사용하는 이유는 id 값은 렌더링되는 정보가 아니기 때문임❗❗
    👉 즉, 참조되는 값으로 생각하자✔
  • 그리고 onInsert 함수 또한 useCallback으로 감싸주었다.
    👉 props로 전달해야 할 함수는 useCallback을 사용하여 함수를 감싸는 것을 습관화 하자.❗❗
//App.js
import React, { useState, useRef, useCallback } from 'react';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
import TodoTemplate from './components/TodoTemplate';

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; // id 값 1씩 증가
  }, [todos]);

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

export default App;

2.3 TodoInsert에서 onSubmit 이벤트 설정하기

  • 버튼을 클릭하면 App에서 TodoInsert에 넣어 준 onInsert 함수에 현재 useState를 통해 관리하고 있는 value 값을 파라미터로 넣어서 호출하는 함수를 만들어 보자.
//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(''); // 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;
  • onSubmit 이라는 함수를 만들고, 이를 formonSubmit으로 설정했다.
    👉 그 이유는, onSubmit 이벤트의 경우 Enter를 눌렀을 때도 이벤트가 발생하기 때문이다.✔
  • onSubmit 함수가 호출되면 props로 받아 온 onInsert 함수에 현재 value 값을 파라미터로 넣어서 호출하고, 현재 value 값을 초기화한다.
  • 추가로, onSubmit 이벤트는 브라우저를 새로고침시키는데, 위의 코드와 같이 e.preventDefault() 함수를 호출하면 새로고침을 방지할 수 있다.

2.4 지우기 기능 구현하기

  • 앞서 리액트를 공부하면 배웠던 내용 중배열의 불변성을 지키면서 배열 원소를 제거해야 할 경우, 배열 내장 함수인 filter를 사용하면 된다.
    👉 filter 함수에는 조건을 확인해 주는 함수를 파라미터로 넣어주고, true를 반환하는 경우만 새로운 배열에 포함된다.
  • filter 함수를 사용하여 onRemove 함수를 생성했다.
//App.js
import React, { useState, useRef, useCallback } from 'react';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
import TodoTemplate from './components/TodoTemplate';

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;
  • 즉, onRemove 함수는 App 컴포넌트에 id를 파라미터로 받아 와서 같은 id를 가진 항목을 todos 배열에서 지우는 함수이다.
  • 이 함수를 생성하고나서 TodoListprops로 설정했다.

2.4.1 TodoListItem에서 onRemove 함수 호출하기

  • 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 {
    MdCheckBox,
    MdRemoveCircleOutline,
    MdCheckBoxOutlineBlank,
} 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;

2.5 Checkbox 수정하기

  • checkbox가 클릭될 때 상태를 수정하기 위해 onToggle 이라는 함수를 App에 만들고, 해당 함수를 TodoList 컴포넌트에 props로 넣어 사용해야 한다.
  • 그 후, 앞선 방법과 동일하게 TodoList를 통해 TodoListItem까지 전달해 주면 된다.
//App.js
import React, { useState, useRef, useCallback } from 'react';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
import TodoTemplate from './components/TodoTemplate';

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를 가지고 있는 객체check 값을 반전시켜 주고있다.
    👉 map 함수를 사용한 이유는 id 값을 비교하여 정해 준 규칙대로 새로운 객체를 생성 하지만, id 값이 다를 때는 변화를 주지 않고 처음 상태를 반환해야 하기 때문이다.

2.5.1 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.js
import React from "react";
import {
    MdCheckBox,
    MdRemoveCircleOutline,
    MdCheckBoxOutlineBlank,
} 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;

3. 결과


토이 프로젝트를 진행했는데, 정말 누가봐도 기본적이고 소규모 프로젝트라서 따로 컴포넌트 리렌더링 최적화 작업을 하지 않아도 잘 작동한다.

하지만 일정의 개수가 많이 생겼을 경우 기존 항목삭제토글할 때 지연될 수 있을 것 같다.❗❗

다음엔 Todo-List 프로젝트를 활용하여 클라이언트 자원을 더욱 효과적으로 사용하기 위해 불필요한 리렌더링을 방지하는 방법에 대해 살펴보고 실습해보고자 한다.😊😊

end

profile
Believe you can & you're half way there 🙏

0개의 댓글