[React] React Project 1 - ToDo List

React Project 1 - ToDo List

Intro

React를 이용해 간단한 Todo-List 페이지를 만들어 보았다.

결과 화면은 다음과 같다.

1

참고 : Velopert님의 React 기초 입문 프로젝트 – 흔하디 흔한 할 일 목록 만들기

필요한 컴포넌트

  • TodoListTemplate : 전체 템플릿을 담고 있는 컴포넌트
  • Form : input과 추가 버튼을 관리하는 컴포넌트
  • TodoItem : 하단의 각 todo 아이템을 관리하는 컴포넌트
  • TodoItemList : 여러 개의 TodoItem을 관리하는 컴포넌트
  • Palette : 글자 색을 지정할 수 있는 컴포넌트

상태 관리 방법

<컴포넌트 전체 구조>

2

하위 컴포넌트들끼리 직접 데이터를 주고받는 방식은 규모가 커졌을 때 유지보수 하기가 힘들다.

따라서, 모든 컴포넌트들은 부모를 통하여 데이터를 주고받아야 한다.

즉, 부모 컴포넌트에서 자식 컴포넌트에 state값과 함수들을 전달하면, 자식 컴포넌트에서는 props로 받아와 기능을 수행할 수 있다. (전달할 함수의 기능은 부모 컴포넌트에서 구현한다. )

App에서 정의할 변수

  • state에서 관리하는 변수

    1. input : Form의 input 상태를 변경하고, 제출한 경우 clear해야 하는 변수
    2. todos {id, text, checked} : 각 할일 목록이 담긴 배열. 고유 id와 text, 체크 여부를 결정하는 checked 변수가 포함된다.
    3. color : Palette에서 특정 color가 선택될 때마다 바뀌어야 하는 변수.
  • 일반 변수

    1. colors : 각 색깔이 담긴 배열. 값이 바뀔 필요가 없으므로 state에서 관리하지 않는다.

Form 컴포넌트에서 필요한 기능

  1. 텍스트 내용 바뀌면 state 업데이트 : handleChange(e)
  2. 버튼이 클릭되면 새로운 todo 생성 후 todos 업데이트 : handleCreate()

concat() 함수를 이용하여 원래 배열의 데이터들이 포함된 새로운 배열을 생성한다.

  1. 인풋에서 Enter 누르면 버튼을 클릭한것과 동일한 작업진행하기 : handleKeyPress(e)

Palette 컴포넌트에서 필요한 기능

  1. 특정 색이 클릭되면 state의 color값 업데이트 : handleSelectColor(color)

TodoItemList 컴포넌트에서 필요한 기능

  1. 아이템이 클릭될 때마다 상태값 반전 (toggle 기능) : handleToggle(id)

    ⇒ 선택된 id를 가진 아이템의 index와 해당 객체를 추출하고 (selected),

    const nextTodos = [...todos] 를 이용하여 원래 배열의 데이터가 담긴 새로운 배열(nextTodos)을 생성한다.

    선택된 객체(selected)의 나머지 데이터는 그대로 둔 채 checked 값만 반전시킨다.

  2. 아이템 제거 기능 : handleRemove(id)

    filter(todo=> [todo.id](http://todo.id/)!==id) 이용.

전체 Code

App.js

import React, { Component } from "react";
import TodoListTemplate from "./components/TodoListTemplate";
import Form from "./components/Form";
import TodoItemList from "./components/TodoItemList";
import Palette from "./components/Palette";

/* TODO: Form 컴포넌트에서 필요한 기능
1. 텍스트 내용 바뀌면 state 업데이트
2. 버튼이 클릭되면 새로운 todo 생성 후 todos 업데이트
3. 인풋에서 Enter 누르면 버튼을 클릭한것과 동일한 작업진행하기 */

const colors = ["#343a40", "#f03e3e", "#12b886", "#228ae6"];

class App extends Component {
    id = 3;
    state = {
        input: "",
        todos: [
            { id: 0, text: "hi", checked: false },
            { id: 1, text: "react", checked: true },
            { id: 2, text: "welcome", checked: false }
        ],
        color: "#343a40"
    };
    handleChange = e => {
        this.setState({
            input: e.target.value
        });
    };
    handleCreate = () => {
        const { input, todos, color } = this.state;
        this.setState({
            input: "",
            todos: todos.concat({
                id: this.id++,
                text: input,
                checked: false,
                color
            })
        });
    };
    handleKeyPress = e => {
        if (e.key === "Enter") {
            this.handleCreate();
        }
    };
    handleToggle = id => {
        const { todos } = this.state;
        const index = todos.findIndex(todo => todo.id === id);
        const selected = todos[index];
        const nextTodos = [...todos];
        nextTodos[index] = {
            ...selected,
            checked: !selected.checked
        };
        this.setState({
            todos: nextTodos
        });
    };
    handleRemove = id => {
        const { todos } = this.state;
        this.setState({
            todos: todos.filter(todo => todo.id !== id)
        });
    };
    handleSelectColor = color => {
        this.setState({
            color
        });
    };
    render() {
        const { input, todos, color } = this.state;
        const {
            handleChange,
            handleCreate,
            handleKeyPress,
            handleToggle,
            handleRemove,
            handleSelectColor
        } = this;
        return (
            <TodoListTemplate
                form={
                    <Form
                        value={input}
                        color={color}
                        onKeyPress={handleKeyPress}
                        onChange={handleChange}
                        onCreate={handleCreate}
                    />
                }
                palette={
                    <Palette
                        colors={colors}
                        selected={color}
                        onSelect={handleSelectColor}
                    />
                }
            >
                <TodoItemList
                    todos={todos}
                    onToggle={handleToggle}
                    onRemove={handleRemove}
                />
            </TodoListTemplate>
        );
    }
}
export default App;

TodoListTemplate.js

import React from "react";
import "./TodoListTemplate.css";

const TodoListTemplate = ({ form, palette, children }) => {
    return (
        <main className="todo-list-template">
            <div className="title">오늘 할 일</div>
            <section className="palette-wrapper">{palette}</section>
            <section className="form-wrapper">{form}</section>
            <section className="todos-wrapper">{children}</section>
        </main>
    );
};
export default TodoListTemplate;

Form.js

import React from "react";
import "./Form.css";

const Form = ({ value, color, onChange, onCreate, onKeyPress }) => {
    return (
        <div className="form">
            <input
                value={value}
                onChange={onChange}
                onKeyPress={onKeyPress}
                style={{ color }}
            />
            <div className="create-button" onClick={onCreate}>
                추가
            </div>
        </div>
    );
};
export default Form;

Palette.js

import React from "react";
import "./Palette.css";

const Color = ({ color, active, onClick }) => {
    /* 구현 */
    return (
        <div
            className={`color ${active && "active"}`}
            style={{ background: color }}
            onClick={onClick}
        ></div>
    );
};

const Palette = ({ colors, selected, onSelect }) => {
    const colorList = colors.map(color => (
        <Color
            color={color}
            active={selected === color}
            onClick={() => onSelect(color)}
            key={color}
        />
    ));
    return <div className="palette">{colorList}</div>;
};
export default Palette;

TodoItemList.js

import React, { Component } from "react";
import TodoItem from "./TodoItem";
import "./TodoItemList.css";

class TodoItemList extends Component {
    shouldComponentUpdate(nextProps, nextState) {
        return this.props.todos !== nextProps.todos;
    }
    render() {
        const { todos, onToggle, onRemove } = this.props;
        /*  const todoList = todos.map(
            ({id,text,checked})=> (
                <TodoItem   
                    id={id}
                    text={text}
                    checked={checked}
                    onToggle={onToggle}
                    onRemove={onRemove}
                    key={id}
                />
            )
        ) */
        const todoList = todos.map(todo => (
            <TodoItem
                {...todo}
                // 내부의 값들이 모두 자동으로 props로 설정됨
                onToggle={onToggle}
                onRemove={onRemove}
                key={todo.id}
            />
        ));
        return <div>{todoList}</div>;
    }
}
export default TodoItemList;

TodoItem.js

import React, { Component } from "react";
import "./TodoItem.css";

class TodoItem extends Component {
    shouldComponentUpdate(nextProps, nextState) {
        return this.props.checked !== nextProps.checked;
    }
    render() {
        const { text, checked, id, color, onToggle, onRemove } = this.props;
        console.log(id);

        return (
            <div className="todo-item" onClick={() => onToggle(id)}>
                <div
                    className="remove"
                    onClick={e => {
                        e.stopPropagation();
                        onRemove(id);
                    }}
                >
                    &times;
                </div>
                <div
                    className={`todo-text ${checked ? "checked" : ""}`}
                    style={{ color: color }}
                >
                    <div>{text}</div>
                </div>
                {checked && <div className="check-mark"></div>}
            </div>
        );
    }
}
export default TodoItem;