티스토리 뷰

WEB/React

[React] First Project: Todo List

춘햄 2023. 3. 28. 17:40

이제까지 리액트의 상태나 컴포넌트 등 기초적인 부분을 다뤄봤으니, 국민 첫 프로젝트인 Todo List를 한번 만들어보려고 한다. 

 

SPA(single page application)으로 만들기 때문에 그렇게 많은 컴포넌트나 코드는 없이, 정말 간단한 CRUD 기능을 가지고 있는 Todo List 이다. 

 

바로 한번 시작해보자.


우선, 총 App 컴포넌트를 제외하고 총 4개의 컴포넌트를 만들어 사용할 것인데, 각 컴포넌트를 용도별로 확인해보자.

 

1. TodoTemplate: 화면을 가운데에 정렬시켜 주며, 앱 타이틀을 보여준다.

2. TodoInsert: 새로운 항목을 입력하고 추가할 수 있는 컴포넌트이다. state를 통해 인풋의 상태를 관리한다.

3. TodoListItem: 각 To do 항목에 대한 정보를 보여주는 컴포넌트이다. todo 객체를 props로 받아 와서 상태에 따라 다른 스타일의 UI를 보여준다.

4. TodoList: todos 배열을 props로 받아 온 후, 이를 배열 내장 함수 map을 사용해서 여러 개의 TodoListItem 컴포넌트로 변환하여 보여준다.

 

추가로, 이번 프로젝트에서 사용한 아이콘은 react-icons 라이브러리를 사용했다. 

react-icons는 여기에서 확인할 수 있으며, 이 라이브러리는 SVG 형태로 이루어진 아이콘을 리액트 컴포넌트처럼 매우 쉽게 사용할 수 있다.


TodoTemplate

TodoTemplate 컴포넌트는 children props를 받아서 내부에 셋팅해놓는, Wrapper의 역할을 하고 있다고 보면 된다.

 

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

 

◎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;
  }
}

 


TodoInsert

TodoInsert 컴포넌트는 input 창에 할 일을 적고 추가 버튼을 눌러 리스트에 아이템을 추가할 컴포넌트이다.

대부분의 이벤트는 App 컴포넌트에서 선언하고, props로 사용할 컴포넌트에 보내는 방식이다.

 

◎TodoInsert.js

import {MdAdd} from 'react-icons/md';
import {useState, useCallback} from "react";
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;

 

◎TodoInsert.scss

.TodoInsert {
  display: flex;
  background: #495057;

  input {
    outline: none;
    border: none;
    padding: 0.5rem;
    font-size: 1.125rem;
    line-height: 1.5;
    color: black;

    &::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;
    }
  }
}

TodoList 

TodoList 컴포넌트는 상대적으로 매우 간단하다. 그냥 todos 를 props로 받아 map 함수로 반복문을 돌려 TodoListItem 컴포넌트에 각 todo를 넣어주는 역할을 한다.

 

◎TodoList.js

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;

 

◎TodoList.scss

.TodoList {
  min-height: 320px;
  max-height: 513px;
  overflow-y: auto;
}

TodoListItem

TodoListItem 컴포넌트는 todo props을 받아서 화면에 뿌려주고, 각 아이콘 버튼 아이콘에 클릭 이벤트를 걸어 이벤트를 핸들링할 수 있게 해준다.

 

◎TodoListItem.js

import {
    MdCheckBoxOutlineBlank,
    MdCheckBox,
    MdRemoveCircleOutline,
} from "react-icons/md";
import './TodoListItem.scss';
import cn from 'classnames';

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;

 

◎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 컴포넌트에 추가, 수정, 삭제 기능을 구현할 함수를 useCallback을 사용하여 선언해주고, TodoTemplate컴포넌트 내부에 TodoInsert와 TodoList를 넣어주기만 하면 끝난다.

 

◎App.js

import './App.css';
import TodoTemplate from "./components/TodoTemplate";
import TodoInsert from "./components/TodoInsert";
import TodoList from "./components/TodoList";
import {useState, useRef, useCallback} from "react";

const App = () => {
    const [todos, setTodos] = useState([
        {
            id: 1,
            text: '리액트의 기초 알아보기',
            checked: true
        },
        {
            id: 2,
            text: '컴포넌트 스타일링 해보기',
            checked: true
        },
        {
            id: 3,
            text: '일정 관리 앱 만들어보기',
            checked: false
        }
    ]);

    // ref 를 사용하여 변수 담기
    const nextId = useRef(4);

    const onInsert = useCallback(
        text => {
            const todo = {
                id: nextId.current,
                text,
                checked: false
            };
            setTodos(todos.concat(todo));
            nextId.current += 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;

모두 이전에 다뤘던 개념들이고 코드를 차근차근 데이터를 따라 확인해보면 크게 어려울 내용은 없기 때문에 일부러 따로 설명을 많이 적어두지는 않았다. 


이렇게 리액트 공부를 시작하고 처음으로 하나의 프로젝트를 만들어 보았다. 

 

하지만 이 프로젝트는 props를 가지고 다니면서 전달하기 때문에 todos의 크기가 커진다면 충분히 성능에 문제가 생길 것이 분명하다. 

 

이를 위해 다음 포스팅에서는 이 TodoList 프로젝트를 최적화하는 방법을 다뤄보려고 한다.

 

 

끝!

'WEB > React' 카테고리의 다른 글

[React] 불변성 유지: immer  (0) 2023.03.29
[React] Todo List: 컴포넌트 성능 최적화  (0) 2023.03.29
[React] 컴포넌트 스타일링  (0) 2023.03.27
[React] Hooks  (0) 2023.03.27
[React] 컴포넌트의 라이프사이클  (0) 2023.03.24
Comments