TodoList 만들기

go·2021년 6월 30일
0

React

목록 보기
6/15
post-thumbnail

기본 세팅하기

$ yarn create react-app todo-app

$ cd todo-app
$ yarn add node-sass classnames react-icons 

1. UI 구성하기

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

2. 기능 구현하기

2.1 App에서 todos 상태 사용하기

App에서 useState를 사용하여 todos라는 상태를 정의하고, todos를 TodoList의 props로 전달합니다.

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;

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를 보여 줄 수 있도록 컴포넌트를 수정합니다.

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})} onClick={() => onToggle(id)}>
                {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
                <div className="text">{text}</div>
            </div>
            <div className="remove">
                <MdRemoveCircleOutline />
            </div>
        </div>
    );
};

export default TodoListItem;

2.2 TodoList 항목 추가 기능 구현하기

TodoList 항목을 추가하는 기능을 구현하려면, TodoInsert 컴포넌트에서 Input 상태를 관리하고 App 컴포넌트에는 todos 배열에 새로운 객체를 추가하는 함수를 만들어 주어야 합니다.

TodoInsert value 상태 관리하기

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

import React, {useState, useCallback } from 'react';
import { MdControlPoint } 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" onSubmit={onSubmit}>
            <input 
                placeholder="할 일을 입력하세요"
                value={value}
                onChange={onChange}
            />
            <button type="submit">
                <MdControlPoint />
            </button>
        </form>
    );
};

export default TodoInsert;

todos 배열에 새 객체 추가하기

App 컴포넌트에서 todos 배열에 새 객체를 추가하는 onInsert 함수는 새로운 객체를 만들 때마다 id 값에 1씩 더해 주어야 합니다. id 값은 useRef를 사용하여 관리합니다. 여기서 useState가 아닌 useRef를 사용하는 이유는 id 값은 렌더링되는 정보가 아니기 때문입니다. (값이 바뀐다고 해서 컴포넌트가 리렌더링 될 필요가 없기 때문) 또한, onInsert 함수는 컴포넌트의 성능을 아낄 수 있도록 useCallback으로 감싸 줍니다.

props로 전달해야 할 함수를 만들 때는 useCallback을 사용하여 함수를 감싸는 것을 습관화해라.

onInsert 함수를 만든 뒤에는 해당 함수를 TodoInsert 컴포넌트의 props로 설정해줍니다.

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;  // nextId 1씩 더하기
    },
    [todos],
  );

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

export default App;

TodoInsert에서 onSubmit 이벤트 설정하기

버튼을 클릭하면 발생할 이벤트입니다. TodoInsert에 넣어준 onInsert 함수에 현재 useState를 통해 관리하고 있는 value 값을 파라미터로 넣어서 호출합니다.

import React, {useState, useCallback } from 'react';
import { MdControlPoint } 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">
                <MdControlPoint />
            </button>
        </form>
    );
};

export default TodoInsert;

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

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

버튼의 onClick 이벤트로도 구현 가능하지만 onSubmit 를 사용한 이유는 onSubmit 이벤트의 경우 input에서 enter를 눌렀을 때도 발생하기 때문입니다. onClick만 사용할 경우 onKeyPress 이벤트를 통해 enter를 감지하는 로직을 따로 작성해야 합니다.

2.3 지우기 기능 구현하기

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

배열 내장 함수 filter

filter 함수는 기존의 배열은 그대로 둔 상태에서 특정 조건을 만족하는 원소들만 따로 추출하여 새로운 배열을 만들어 줍니다. filter 함수에는 조건을 확인해 주는 함수를 파라미터로 넣어 주어야 합니다. 파라미터로 넣는 함수는 true / false 값을 반환해야 하며, true를 반환하는 경우만 새로운 배열에 포함됩니다.

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

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

onRemove는 App 컴포넌트에 id를 파라미터로 받아 와서 같은 id를 가진 항목을 todos 배열에서 지우는 함수입니다.

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;

TodoListItem에서 삭제 함수 호출하기

props로 받아온 onRemove 함수를 TodoListItem에 전달해줍니다.

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를 넣어서 삭제 함수를 호출하도록 설정합니다.

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})}>
                {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
                <div className="text">{text}</div>
            </div>
            <div className="remove" onClick={() => onRemove(id)}>
                <MdRemoveCircleOutline />
            </div>
        </div>
    );
};

export default TodoListItem;

2.4 할일 완료 체크 기능 구현하기

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

onToggle 구현하기

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;  // 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을 사용하면 짧은 코드로 쉽게 작성할 수 있습니다.


TodoListItem에서 토글 함수 호출하기

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

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;

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. 폰트 적용하기

구글 폰트를 다운받고 src 폴더에 font 폴더를 추가해줍니다.

적용하고 싶은 scss파일에 다음 코드를 추가해줍니다.

@font-face {
    font-family: "FjallaOne";
    src: url("../font/FjallaOne.ttf");
}

.TodoTemplate {
    .app-title {
        font-family: "FjallaOne";
    }
}

0개의 댓글