리액트 투두리스트 만들기

hhkim·2021년 7월 31일
4
post-thumbnail

🔗 react-todolist 완성본

리액트 필수예제 투두리스트 내맘대로 만들어보기


구조

  • TodoList 컴포넌트
    • AddForm 컴포넌트
      • input
      • 추가 버튼
    • Todo 컴포넌트
      • todo 출력
      • 체크박스
      • 수정, 삭제 버튼

프로젝트 세팅

  • create-react-app 프로젝트 생성
    npx create-react-app react-todolist

사용한 개념 정리

자식 컴포넌트에서 부모 컴포넌트 state 수정하기

  • 자식 컴포넌트에서 부모 컴포넌트의 state를 수정할 수 없으므로, 부모 컴포넌트에서 이벤트 핸들러를 props로 넘겨준다.

배열 요소의 삭제와 수정

  • 배열의 요소를 삭제할 때는 불변성 유지를 위해 filter()를 사용한다.
const newArray = array.filter((value) => [남길 요소들이 true를 리턴할 조건]);
  • 배열의 요소를 수정할 때 splice()을 사용할 수 있다.
    • splice()는 원본 배열을 수정하므로 주의!
array.splice(시작 인덱스, 삭제할 개수, 삭제할 위치에 넣을 데이터1, 데이터2, ...);

조건부 렌더링

DOM 요소를 조건부 렌더링하려면 bool 값을 갖는 ref를 만들고 &&|| 연산자로 처리해준다. (JSX 조건문)

// isUpdate가 거짓일 때 렌더링
{isUpdate || <span onClick={onClickTodo}>{todo}</span>}
// isUpdate가 참일 때 렌더링
{isUpdate && <input ref={input} value={value} onChange={onChangeInput} />}

useRef()useEffect() 활용

ref는 렌더링 되면서 연결된다.

예를 들어 위의 코드(조건부 렌더링)를 보면,
처음 렌더링에서 isUpdate의 기본값은 false
👉 span만 렌더링이 되고 input은 렌더링이 되지 않음
👉 span을 클릭하면 onClickTodo(isUpdatetrue로 변경해서 input을 렌더링하게 하는 함수) 실행
여기서 input.current.focus()는 동작하지 않는다!
input이 렌더링되지 않았으므로 ref가 연결되지 않아 input.currentundefined이다.
👉 useEffect()를 활용하여 렌더링 후에 isUpdate가 변경되었으면 input.current.focus()를 실행한다!

최적화

왜 input에 텍스트를 입력할 때마다 전체가 다 리렌더링..?
또는 자식 하나 변경했는데 왜 모든 자식이 리렌더링..?
👉 이런 걸 막기 위해서 자식 컴포넌트는 memo로 감싸서 기억해두고 props가 변경되지 않으면 리렌더링이 되지 않도록 한다.

localStorage에 데이터 저장하기

  • 페이지를 리로드해도 투두리스트가 남아있게 하려면 localStorage를 활용한다.
  • localStorage.setItem(key, value)로 저장하고, localStorage.getItem(key)로 불러온다.
  • 객체는 JSON.stringfy(value)로 JSON 형태의 문자열로 변환하여 저장하고, 불러온 후에는 JSON.parse(value)를 통해 다시 객체로 변환한다.
  • useEffect()를 사용하여 마운트되었을 때 불러오고, todoListid가 업데이트 되었을 때 저장한다.
  • localStorage의 값들은 문자열로 저장이 되기 때문에 숫자인 id 값을 불러올 때 parseInt()로 변환해서 사용한다.
// 마운트되었을 때 localStorage 데이터 불러오기
  useEffect(() => {
    const localTodoList = localStorage.getItem('todoList');
    console.log(localTodoList, JSON.parse(localTodoList));
    if (localTodoList) {
      setTodoList(JSON.parse(localTodoList));
    }
    const localId = localStorage.getItem('id');
    if (localId) {
      setId(parseInt(localId));
    }
  }, []);

// todoList나 id가 업데이트되면 localStorage에 데이터 저장하기
  useEffect(() => {
    localStorage.setItem('todoList', JSON.stringify(todoList));
    localStorage.setItem('id', id);
  }, [todoList, id]);

onBlur, onKeyUp 이벤트

  • input에서 포커스가 벗어나거나 esc 키를 누르면 업데이트를 취소(isUpdatefalse로 수정)하고 싶어서 onBluronKeyUp 이벤트를 활용했다.
  • onBlur는 포커스가 벗어났을 때 발생하는 이벤트
  • onKeyUp은 키보드로 입력되고 나서 발생하는 이벤트
    • 내가 누른 키의 정보는 event 객체의 key에 담겨있다.
    • esc키는 Escape, enterEnter 등으로 조건을 검사한다.
    const onBlurInput = () => {
      setIsUpdate(false);
    };

    const onKeyUpInput = (e) => {
      if (e.key === 'Escape') {
        setIsUpdate(false);
      }
    };

코드 작성

TodoList.js

import React, { useState, useCallback, useEffect, useRef } from 'react';
import AddForm from './AddForm';
import Todo from './Todo';
import './TodoList.css';

const TodoList = () => {
  const [todoList, setTodoList] = useState([]);
  const [id, setId] = useState(0);
  const isMount = useRef(true);

  useEffect(() => {
    if (!isMount.current) {
      localStorage.setItem('todoList', JSON.stringify(todoList));
      localStorage.setItem('id', id);
    }
  }, [todoList, id]);

  useEffect(() => {
    const localTodoList = localStorage.getItem('todoList');
    if (localTodoList) {
      setTodoList(JSON.parse(localTodoList));
    }
    const localId = localStorage.getItem('id');
    if (localId) {
      setId(parseInt(localId));
    }
    isMount.current = false;
  }, []);

  const addTodo = useCallback(
    (todo) => (e) => {
      console.log('add');
      e.preventDefault();
      if (todo) {
        setTodoList((prevTodoList) => [
          ...prevTodoList,
          { id: id, todo: todo, isChecked: false },
        ]);
        setId((prevId) => prevId + 1);
      }
    },
    [id]
  );

  const updateTodo = useCallback(
    (id, todo, isChecked) => {
      const index = todoList.findIndex((todoInfo) => todoInfo.id === id);
      const newTodoList = [...todoList];
      newTodoList.splice(index, 1, {
        id: id,
        todo: todo,
        isChecked: isChecked,
      });
      setTodoList(newTodoList);
    },
    [todoList]
  );

  const deleteTodo = useCallback(
    (id) => () => {
      const newTodoList = todoList.filter((todoInfo) => todoInfo.id !== id);
      setTodoList(newTodoList);
    },
    [todoList]
  );

  const toggleCheck = useCallback(
    (id) => () => {
      const index = todoList.findIndex((todoInfo) => todoInfo.id === id);
      const newTodoList = [...todoList];
      newTodoList[index].isChecked = newTodoList[index].isChecked
        ? false
        : true;
      setTodoList(newTodoList);
    },
    [todoList]
  );

  return (
    <div className="box">
      <div className="todolist-box">
        <h1>things to do</h1>
        <AddForm addTodo={addTodo} />
        <ul>
          {todoList.map((todoInfo) => {
            return (
              <Todo
                key={todoInfo.id}
                id={todoInfo.id}
                todo={todoInfo.todo}
                isChecked={todoInfo.isChecked}
                updateTodo={updateTodo}
                deleteTodo={deleteTodo}
                toggleCheck={toggleCheck}
              />
            );
          })}
        </ul>
      </div>
    </div>
  );
};

export default TodoList;

AddForm.js

import React, { useState, useRef, useEffect, memo } from 'react';
import './AddForm.css';

const AddForm = memo(({ addTodo }) => {
  const [value, setValue] = useState('');
  const input = useRef(null);

  useEffect(() => {
    input.current.focus();
    setValue('');
  }, [addTodo]);

  const onChangeInput = (e) => {
    setValue(e.target.value);
  };

  return (
    <form className="add-form">
      <input ref={input} value={value} onChange={onChangeInput} />
      <button type="submit" onClick={addTodo(value)}>
        add
      </button>
    </form>
  );
});

export default AddForm;

Todo.js

import React, { useState, useRef, useEffect, memo } from 'react';
import './Todo.css';

const Todo = memo(
  ({ id, todo, isChecked, deleteTodo, updateTodo, toggleCheck }) => {
    const [value, setValue] = useState(todo);
    const [isUpdate, setIsUpdate] = useState(false);
    const input = useRef(null);

    useEffect(() => {
      if (isUpdate) {
        input.current.focus();
      }
    }, [isUpdate]);

    useEffect(() => {
      setIsUpdate(false);
    }, [todo]);

    const onClickTodo = () => {
      setIsUpdate(true);
    };

    const onChangeInput = (e) => {
      setValue(e.target.value);
    };

    const onFormSubmit = (e) => {
      e.preventDefault();
      setIsUpdate(false);
      if (!value) {
        setValue(todo);
      } else {
        if (todo !== value) {
          updateTodo(id, value, isChecked);
        }
      }
    };

    const onBlurInput = () => {
      setIsUpdate(false);
    };

    const onKeyUpInput = (e) => {
      if (e.key === 'Escape') {
        setIsUpdate(false);
      }
    };

    return (
      <li className="list">
        <span className="check" onClick={toggleCheck(id)}>
          {isChecked ? '◼' : '◻'}
        </span>
        {isUpdate || (
          <span
            className={`todo ${isChecked ? 'checked' : ''}`}
            onClick={onClickTodo}
          >
            {todo}
          </span>
        )}
        {isUpdate && (
          <form className="update-form" onSubmit={onFormSubmit}>
            <input
              ref={input}
              value={value}
              onChange={onChangeInput}
              onBlur={onBlurInput}
              onKeyUp={onKeyUpInput}
            />
          </form>
        )}
        <button onClick={deleteTodo(id)}>X</button>
      </li>
    );
  }
);

export default Todo;

별 거 아닐 줄 알았는데 이제껏 공부한 걸 많이 활용할 수 있었고 배운 것도 많았던 예제! 아주 재밌었다 😊

0개의 댓글