본문 바로가기
Project/[클론 코딩] TODO LIST

[클론 코딩] TODO LIST 기능 구현

by Bam_t 2021. 12. 21.
728x90

지난 포스트에서 기본세팅과 UI를 구성했습니다. 이제 껍질의 내용물들을 구현해보도록 하겠습니다.


1. todos

우리가 만드는 TODO 앱은 App.js에서 배열의 형태로 관리됩니다. 그러기 위해서 useState Hook를 이용해서 todo를 관리하고 이 todos배열을 TodoList 컴포넌트의 props로 전달해서 보여주게 됩니다.

const App = () => {
    const [todos, setTodos] = useState([
        {
            id: 1,
            text: 'TODO LIST 앱 완성하기',
            checked: true,
        },
        {
            id: 2,
            text: '블로그 포스팅하기',
            checked: false,
        },
    ]);
    return (
        <TodoTemplate>
            <TodoInsert/>
            <TodoList todos={todos}/>
        </TodoTemplate>
    );
};

todos 배열을 뜯어보고 지나가자면, 배열안에는 각 항목들이 객체형태로 저장되어 있습니다. 각 항목 객체는 todo 번호, todo의 내용(List로 보여질 텍스트), 체크박스가 체크 되었는지에 대한 상태가 저장되어있습니다. 이 배열을 TodoList 컴포넌트로 전달하고 TodoList는 TodoListItem 컴포넌트로 변환해서 우리 눈에 각 항목별로 나타나듯이 보이게 됩니다.

그러면 TodoList에서 전달받은 todos 배열을 가공해보겠습니다.

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

전달받은 todos의 요소들을 map함수로 가공하고 Item으로 렌더링을 해줍니다. 각 항목인 todo는 TodoListItem 컴포넌트의 props로 전달이 됩니다. 또한 key로 todo 항목의 id값을 전달해주었습니다.

그럼 todo를 전달받은 TodoListItem 동작도 구현해보겠습니다.

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

export default TodoListItem;

조건부 스타일링을 위해 classnames를 import 해주었습니다.

 const {text, checked} = todo;

todo의 text와 checked 정보를 받아왔습니다. 구조분해할당 아시죠?

2021.05.07 - [Programming/Javascript] - [Javascript] 구조 분해 할당

 

[Javascript] 구조 분해 할당

ES2015에서 등장한 구조 분해 할당에 대해 다룹니다. 구조 분해는 배열이나 객체의 특정한 자료를 이용할 때 사용하는 방식입니다. 1. 배열의 구조 분해 할당 1-1. 변수에 배열의 값 할당 기존에 배

bamtory29.tistory.com

<div className={cn('checkbox', {checked})}>
  {checked ? <MdCheckBox/> : <MdCheckBoxOutlineBlank/>}
  <div className={"text"}>{text}</div>
</div>

조건부 스타일링을 위해 cn()을 이용해서 checkbox가 체크되어 있는지 확인합니다. todo객체의 checked값으로요. 그래서 체크되었다(true)라면 체크된 아이콘을, 체크되지 않았다(false)라면 체크되지 않은 아이콘을 렌더링합니다.

그리고 todo객체의 text를 나타냅니다.

여기까지 따라온 후 실행해보면 객체의 체크 상태에 따라 다른 모습들이 나타날 것입니다. 하지만 아직 다른 동작들은 구현이 되지 않아서 여전히 동작은 하지 않습니다.

 

 

2. 항목 추가하기

처음으로 추가할 기능은 todo 입력란에 항목을 입력하면 List에 추가되도록 하는 기능입니다. TodoInsert에서 input값을 관리하고 그 값을 App.js의 todos 배열에 추가시키는 작업을 하는 것이 이번의 목표입니다.

우선 TodoInsert 컴포넌트를 수정하겠습니다.

인풋 값을 관리하기 위해 setState를 통해 관리합니다. 그리고 <input>태그의 값 변경 감지를 위해 onChange 함수를 만들어서 사용합니다. 이때 재사용성을 위해 useCallback Hook을 통해 함수를 재활용하도록 합니다. 이렇게 작성하는 순간부터 input에 입력된 코드들이 추적이 됩니다.

const TodoInsert = () => {
    const [value, setValue] = useState('');
    const onChange = useCallback(e=> {
        setValue(e.target.value);
    }, []);
    
    return (
        <form className={"TodoInsert"}>
            <input placeholder={"todo 입력"} value={value} onChange={onChange}/>
            <button type={"submit"}>
                <MdAdd/>
            </button>
        </form>
    );
};

 

그럼 이제 App.js의 todos 배열에 input을 통해 추가된 항목을 더하는 기능을 구현해보겠습니다.

const App = () => {
    const [todos, setTodos] = useState([
        {
            id: 1,
            text: 'TODO LIST 앱 완성하기',
            checked: true,
        },
        {
            id: 2,
            text: '블로그 포스팅하기',
            checked: false,
        },
    ]);
    const nextId = useRef(3);
    const onInsert = useCallback(text => {
        const todo = {
            id: nextId.current,
            text,
            checked: false,
        };
        setTodos(todos.concat(todo));
        nextId.current++;
    }, [todos]);


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

 

const nextId = useRef(3);

todo의 id는 Ref를 통해서 관리합니다. useState를 이용하지 않는 이유는 id는 내부적으로만 사용되는 값이고 우리한테는 렌더링 될 일이 없기 때문에 useRef Hook으로 관리하고 있습니다. 또한 위에서 todos의 기본값으로 2를 만들어 놓았으므로 기본값으로 3을 주게 되었습니다.

    const onInsert = useCallback(text => {
        const todo = {
            id: nextId.current,
            text,
            checked: false,
        };
        setTodos(todos.concat(todo));
        nextId.current++;
    }, [todos]);

onInsert함수는 입력이 되면 todos 배열에 todo를 추가해주는 역할을 합니다. 입력된 정보에서 text는 그대로 이용하고 id는 useRef에서 받아온 정보를 이용합니다. checked는 기본값으로 false를 줍니다. 그 다음으로 setTodos를 통해 todos 배열에 방금 추가한 todo 요소를 추가하고 Ref nextId값을 1 늘려줍니다. 이렇게 작성된 onInsert 함수는 TodoInsert 컴포넌트의 props로 전달합니다.

 

뭔가 빠진것이 있습니다. 바로 TodoInsert는 input란과 submit 버튼으로 구성되어있는데 이 submit 버튼에 대한 동작을 정의하지 않았습니다. 그래서 다시 TodoInsert 컴포넌트로 돌아와서 submit 버튼의 이벤트를 정의해보겠습니다.

const TodoInsert = ({onInsert}) => {
    const [value, setValue] = useState('');
    const onChange = useCallback(e => {
        setValue(e.target.value);
    }, []);
    const onSubmit = useCallback(e => {
        onInsert(value);
        setValue('');
        e.preventDefault();
    }, [onInsert, value]);

    return (
        <form className={"TodoInsert"} onSubmit={onSubmit}>
            <input placeholder={"todo 입력"} value={value} onChange={onChange}/>
            <button type={"submit"}>
                <MdAdd/>
            </button>
        </form>
    );
};

onSubmit 함수를 정의했습니다. 현재 관리되는 State인 value를 onInsert 함수의 인자로 호출합니다.

동작은 form이 버튼을 통해 submit이 되면 onSubmit이 호출되며 이에 따른 여러 동작들을 수행하는 방식입니다. 그리고 onSubmit 이벤트는 새로고침을 발생시키므로 새로고침을 없애기 위해서 e.preventDefault로 새로고침을 방지했습니다.

추가적으로 지금 onSubmit을 통해서 이벤트를 처리했는데 버튼에 onClick이벤트를 달아서 처리하는 방식을 이용할 수도 있습니다.

여기 까지 하고 다시 실행시킨다음, todo를 입력하고 버튼을 클릭하면 제대로 추가가 되었나요?

 

 

 

3. 항목 삭제하기

이번에는 삭제기능을 한 번 구현해보도록 하겠습니다. 구현방식은 todo의 삭제 버튼을 누르면 해당 todo의 id를 가지고 App.js의 todos 배열에서 id를 비교한 다음 해당하는 id를 가진 todo를 삭제하는 방식으로 구현합니다.

App.js에서 onRemove 함수부터 정의합니다.

const onRemove = useCallback(id => {
        setTodos(todos.filter(
            todo => todo.id !== id
        ));
    }, [todos]);

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

filter 함수를 통해 삭제 대상의 id와 일치하지 않는 todo들은 todos 배열에 남기는 방식으로 삭제 대상 todo를 제거합니다. 그리고 TodoList의 props로 onRemove함수를 전달합니다.

전달받은 TodoList에서 다시 TodoListItem 컴포넌트로 onRemove함수를 props로 전달합니다.

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

이제 remove에 onClick 이벤트를 달고 id를 인자로 넘겨서 onRemove를 통해 삭제가 진행되도록합니다.

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

 

 

 

4. 항목 체크하기

마지막 기능은 항목 체크입니다. 항목 체크도 조금 전에 만든 REMOVE 기능과 유사하게 만들수 있습니다.

onRemove 처럼 id를 통해 토글이 될 대상을 찾습니다. 그리고 todos.map을 통해 배열을 만들어주는데, 이 내용이 조금 난해할 수도 있습니다.

todo.id === id 일때 true라면, 해당 id를 가진 todo의 checked 상태가 반대로 변한(토글된) 새로운 배열을 만들어내게 되고 아니라면 그대로 이용하게 됩니다.

그리고 TodoList의 props로 onToggle 함수를 전달해주고 TodoListItem으로 다시 전달해서 사용하게 됩니다.

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>
    );
//App.js
const TodoList = ({todos, onRemove, onToggle}) => {
    return (
        <div className={"TodoList"}>
            {todos.map(todo => (
                <TodoListItem todo={todo} key={todo.id} onRemove={onRemove} onToggle={onToggle}/>
            ))}
        </div>
    );
};
//TodoList.js

체크박스에 onClick이벤트로 onToggle함수를 호출되게 만들면 완성입니다.

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>
    );
};
//TodoListItem.js


이렇게 해서 TODO LIST앱을 만들어보고 기능까지 구현했습니다. 하지만 다 만들었다고 해서 끝은 아닙니다. 사용자 경험은 다양한 기능보다 얼마나 빠르고 강력하게 쓸 수 있는 최적화 문제에 달려있기 때문입니다. 그래서 다음 포스트에서 이 프로젝트를 최적화하는 방법들을 소개해드리도록 하겠습니다.

728x90

댓글