TIL | #14 React | TodoList 만들기 #1

50109 단어 ReactTILReact

2021-04-05(월)

리액트 생성 및 모듈 설치

yarn react create-app todoList
node-sass, classnames, react-icons 설치
yarn add node-sass classnames react-icons

  • TodoTemplate
    앱 타이틀을 보여준다. children으로 내부 jsx를 props 받아서 렌더링
  • TodoInsert
    새로운 항목 입력 및 추가 state를 통해 인풋 상태 관리
  • TodoListItem
    항목에 대한 정보를 보여준다. 객체를 props로 받아와서 상태에 따라 다른 스타일을 보여준다.
  • TodoList
    할 일 배열을 props로 받아 온 후 map을 이용, 여러개의 TodoListItem 컴포넌트 변환

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

    // useRef로 컴포넌트에서 사용할 변수를 만드는 이유는 
    // id값은 렌더링되는 정보가 아니기 때문이다.
    // 즉 화면에 보이지도 않고 이 값이 바뀐다고해서 
    // 컴포넌트가 다시 렌더링 될 이유가 없기 때문이다.
    // 단순히 새로운 항목을 만들때 참조되는 값이다.
    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 onDelete = useCallback(
        (id) => {
            const nextTodos = todos.filter((todo) => todo.id !== id);
            setTodos(nextTodos);
        },
        [todos],
    );
    const onChecked = useCallback(
        (id) => {
            const nextTodos = todos.filter((todo) => {
                if (todo.id === id) todo.checked = !todo.checked;
                return todo;
            });
            setTodos(nextTodos);
        },
        [todos],
    );

    return (
        <TodoTemplate>
            <TodoInsert onInsert={onInsert} />
            {/* TodoList props로 전달 -> TodoList에서 이 값을 받아온 후
            TodoListItem으로 변환하여 렌더링 하도록 설정한다.*/}
            <TodoList todos={todos} onDelete={onDelete} onChecked={onChecked} />
        </TodoTemplate>
    );
};

export default App;

TodoTemplate

// TodoTemplate.js

import React from 'react';
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.js

import React, { useCallback, useState } 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); //현재 useState를 통해 관리하고 있는 value값을 인자로 전달
            setValue(''); //value값 초기화

            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 {
    display: flex;
    background: #495057;
    input {
        // 기본 스타일 초기화
        background: none;
        outline: none;
        border: none;
        padding: 0.5rem;
        font-size: 1.2rem;
        line-height: 1.5;
        color: white;
        &::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: #adb5db;
        }
    }
}

TodoList

// TodoList.js

import React from 'react';
import TodoListItem from './TodoListItem';
import './TodoList.scss';

const TodoList = ({ todos, onDelete, onChecked }) => {
    return (
        <div className="TodoList">
            {todos.map((todo) => (
                <TodoListItem
                    todo={todo}
                    key={todo.id}
                    onDelete={onDelete}
                    onChecked={onChecked}
                />
            ))}
        </div>
    );
};

export default TodoList;
// TodoList.scss

.TodoInsert {
    display: flex;
    background: #495057;
    input {
        // 기본 스타일 초기화
        background: none;
        outline: none;
        border: none;
        padding: 0.5rem;
        font-size: 1.2rem;
        line-height: 1.5;
        color: white;
        &::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: #adb5db;
        }
    }
}

TodoListItem.js

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

const TodoListItem = ({ todo, onDelete, onChecked }) => {
    const { id, text, checked } = todo;
    const remove = useCallback(
        (e) => {
            onDelete(id);
        },
        [onDelete, id],
    );
    const check = useCallback(
        (e) => {
            onChecked(id);
        },
        [onChecked, checked],
    );
    return (
        <div className="TodoListItem">
            <div className={cn('checkbox', { checked })} onClick={check}>
                {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
                <div className="text">{text}</div>
            </div>
            <div className="remove" onClick={remove}>
                <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;
    &:hover {
        color: #ff8787;
    }
    & + & {
        border-top: 1px solid #dee2e6;
    }
}

좋은 웹페이지 즐겨찾기