[React] 간단한 투두리스트 만들기

soyeon·2022년 2월 10일
30

✨바로가기 https://soonmac.github.io/react-todolist/
✨깃허브 https://github.com/soonmac/react-todolist

🎈들어가며

<리액트를 다루는 기술> 10장 일정 관리 웹 애플리케이션 만들기를 보면서 만든 투두리스트 앱입니다.
책에서는 할 일 추가, 삭제, 체크 기능만 나와있는데 거기에 덧붙여 개린이르아나 님의 강의를 참고해 수정하기 기능도 구현해봤습니다.
투두리스트 앱을 제작하면서 배운 것들을 정리했습니다📚

🎈투두리스트 앱 구성

투두리스트 앱의 컴포넌트 구조를 그림으로 그려봤습니다.

ToDoTemplate 안에 입력창, 할 일 목록들을 넣었습니다.
JSX 형식으로 표현하면 이런 느낌입니다.

	<TodoTemplate> //앱을 이루는 컨테이너 박스
      <ToDoInsert /> //할 일 입력창
      <ToDoList> //할 일 목록(ul)
      	<ToDoListItem /> //할 일 (li)
      </ToDoList>
	  <ToDoEdit /> //수정하기 창(팝업창이라서 대충 빼놓음)
    </TodoTemplate>
	

🎈일정 항목 데이터 관리하기

일정 항목의 데이터는 어떤 모습일까


위의 설계도에서 일정 항목에 필요한 것은 텍스트(내용), 체크박스, 삭제버튼, 수정버튼입니다.
여기서 수정버튼은 텍스트의 내용을 바꾸는 것이고, 삭제버튼은 해당 항목 자체를 없애버리는 것이니 데이터로 만들 때 필요한 것은 텍스트, 체크유무, 그리고 각 항목을 구분해줄 고유한 id입니다.

    {
      id: 1,
      text: '리액트 기초 알아보기',
      checked: true,
    }

id, text, checked 세가지의 key를 가진 객체로 구성했습니다.

useState로 일정 항목들 관리하기

App.js

  const [todos, setTodos] = useState([
    {
      id: 1,
      text: '리액트 기초 알아보기',
      checked: true,
    },
    {
      id: 2,
      text: '컴포넌트 스타일링 하기',
      checked: true,
    },
    {
      id: 3,
      text: '투두리스트 만들기',
      checked: false,
    },
  ]);

useState를 사용하여 todos의 기본값을 정했습니다.
나중에 setTodos를 이용해 todos 배열에 항목을 추가, 수정, 삭제할거임

todos에 있는 일정 항목들을 ToDoList에 불러오기

TodoList 컴포넌트에 props로 todos를 전달합니다.

App.js

      <TodoList
        todos={todos}
      />

map() 메서드로 todos 배열의 각 항목들을 ToDoListItem 컴포넌트로 가공해줍니다.
ToDoListItem에 체크유무, 텍스트, id를 props로 전달해줍니다.
TodoList.js

function TodoList({ todos }) {
  return (
    <ul className="TodoList">
      {todos.map((todo) => (
        <ToDoListItem
          todo={todo}
          key={todo.id}
        />
      ))}
    </ul>
  );
}

ToDoListItem.js

function ToDoListItem({ todo }) {
  const { id, text, checked } = todo;
  return (
    <li className="TodoListItem">
      <div className={cn('checkbox', { checked })}>
        //checked=true일 때 checked라는 class를 추가 
        {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
        //checked=true면 체크된 박스 아이콘이 false면 빈 박스 아이콘이 뜸
        <div className="text">{text}</div>
      </div>
      <div className="edit">
        <MdModeEditOutline />
      </div>
      <div className="remove">
        <MdRemoveCircleOutline />
      </div>
    </li>
  );
}

🍧classnames(cn) 함수 : 인자에 들어간 값이 true면 class로 넣어주고 false면 그없
첫번째 인자 'checkbox'는 {checkbox:true}의 줄임말입니다.
체크유무에 상관없이 checkbox 클래스는 항상 필요하기 때문에 true 처리했습니다.

cn 예시)

classNames('foo', 'bar'); // => 'foo bar'
classNames('foo', { bar: true }); // => 'foo bar'
classNames('foo', { bar: false }); // => 'foo'

출처: https://www.npmjs.com/package/classnames

🎈일정 추가하기

입력창(Input)에 입력한 값을 useState로 관리하기

ToDoInsert.js

function ToDoInsert({onInsert}) {
    
    const [value, setValue] = useState('');
    const onChange = useCallback(e=>{
        setValue(e.target.value);
    },[])
    const onSubmit = useCallback(
        e => {
            setValue(''); //value 초기화
            //기본이벤트(새로고침) 방지
            e.preventDefault();
        }
    ,[value])
    return (
        <form className="TodoInsert" onSubmit={onSubmit}>
            <input 
            onChange={onChange}
            value={value} placeholder="할 일을 입력하세요" />
            <button type="submit">
                <MdAdd />
            </button>
        </form>
    )
}
  1. onChange 이벤트: 입력창에 친 값들 추적해서 setValue로 value에 저장합니다.
  2. onSubmit 이벤트: 나중에 todos 배열에 새 데이터(객체)를 추가하는 함수를 추가해줄겁니다1!
    기본적인 것들, 버튼(엔터키)을 누를 때 새로고침 방지, 입력창 리셋하기만 구현합니다.

useCallback이 뭔가요 :
컴포넌트는 자신의 state 혹은 부모에게서 받은 props가 변경될 때마다 리렌더링됩니다.
근데 굳이 렌더링 안해도 괜찮은 부분까지 리렌더링 되면 코스트 낭비로 이어질 수 있고,
거기에 리렌더링 될 때마다 함수도 다시 생성되는 불상사가 일어납니다.
이를 방지하기 위해 전에 생성된 함수를 다시 재활용할 수 있도록 해주는 기능입니다.

useCallback(생성하고 싶은 함수,[배열 안의 값이 바뀌었을 때 함수가 새로 생성됩니다.])

근데 솔직히 왜 쓰는지는 잘 모르겟음ㅎㅎ;

todos 배열에 새 객체 추가하기

App.js

const nextId = useRef(4);
  const onInsert = useCallback(
    (text) => {
      const todo = {
        id: nextId.current,
        text,
        checked: false,
      };
      setTodos(todos.concat(todo)); //concat(): 인자로 주어진 배열이나 값들을 기존 배열에 합쳐서 새 배열 반환
      nextId.current++; //nextId 1씩 더하기
    },
    [todos],
  );
  1. useRef로 id를 할당해줍니다. 이미 todos의 기본값으로 id가 3까지 있으니 초기값을 4로 했습니다.

🍧useRef: useRef(초기값) 여기서는 useRef를 로컬 변수로 활용했습니다.
로컬변수 : 렌더링과 상관없이 바뀔 수 있는 값
useRef의 current 속성은 인자로 넘어온 초기값을 current에 할당합니다.
useRef는 currnet 값이 바뀌어도 컴포넌트가 리렌더링 되지 않고, 컴포넌트가 리렌더링 되어도 current의 값을 잃지 않는다는 장점이 있습니다.

  1. onInsert 함수
  • 인자로 text(입력창의 텍스트)를 받아서 todo라는 객체를 생성합니다.
  • todo 객체는 todos의 객체와 같은 구조로 되어있습니다.
    {
      id: 1,
      text: '리액트 기초 알아보기',
      checked: true,
    }
  • todo 객체 역시 id, text, checked 유무를 넣어줍니다. 아직 체크를 안했으니까 checked: false로 하기!
  • setTodos으로 기존 todos에 todo 객체를 넣어줍니다.

    concat( )이 뭔데요:
    인자로 주어진 배열이나 값들을 기존 배열에 합쳐서 새 배열을 반환하는 메서드입니다.

예시)

const array1 = ['a', 'b', 'c'];
const array2 = ['d', 'e', 'f'];
const array3 = array1.concat(array2); //=> Array ["a", "b", "c", "d", "e", "f"]
  1. onInsert 함수를 ToDoInsert에 prop으로 넘겨주고, onSubmit 이벤트에 onInsert 함수를 전달합니다.

App.js

      <ToDoInsert onInsert={onInsert} />

ToDoInsert.js

    const onSubmit = useCallback(
        e => {
            onInsert(value);
            setValue(''); //value 초기화
            //기본이벤트(새로고침) 방지
            e.preventDefault();
        }
    ,[onInsert, value])

🎈일정 삭제하기

filter 메서드를 이용해 todos 배열에서 id로 항목 지우기

App.js

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

일정 항목을 클릭했을 때 onRemove 함수가 작동하게 하기

ToDoListItem.js

function ToDoListItem({ todo, onRemove }) {
  const { id, text, checked } = todo;
  return (
    <li className="TodoListItem">
      <div className={cn('checkbox', { checked:checked })}>
        {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
        <div className="text">{text}</div>
      </div>
      <div className="edit">
        <MdModeEditOutline />
      </div>
      <div className="remove" onClick={() => onRemove(id)}>
        <MdRemoveCircleOutline />
      </div>
    </li>
  );
}
  • ToDoListItem에 todo를 props로 전달했습니다. todo는 상위 컴포넌트인 ToDoList에서 받아온 것이고, todos 배열에 들어있는 각 원소(객체)입니다.
  • 구조 분해 할당으로 todo.id, todo.text, todo.checked를 id, text, checked에 할당했습니다.
    이제 id = todo.id , text = todo.text , checked = todo.checked인 거임
  • 삭제 아이콘에 onClick 이벤트로 onRemove(id)를 넘깁니다. 그러면 삭제 아이콘을 클릭했을 때 해당 일정 항목의 id가 onRemove의 인자로 넘어갑니다.

🎈일정 체크하기

map() 메서드로 Toggle 기능을 만들자...

ToDoList.js

function ToDoListItem({ todo, onRemove }) {
  const { id, text, checked } = todo;
  return (
    <li className="TodoListItem">
      <div className={cn('checkbox', { checked:checked })}>
        {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
        <div className="text">{text}</div>
      </div>
		.
        .
        .
    </li>
  );
}

삼항 연산자를 이용해 checked가 true 일 때 체크박스를 보여주고 checked가 false일 때 빈 박스를 보이는 형식인데요.
{checked ? ✅ : 🟩 }
checked가 true임 =? ✅
checked가 false임 => 🟩

문제는? 각 일정 항목을 클릭했을 때 해당 todo 객체 안에 있는 checked의 값을 바꿔야한다는 거죠...
아까 만든 삭제하기 기능과 거의 비슷합니다. id를 인자로 받아서 해당 id의 todo 객체의 checked의 값을 바꿔줍니다.
App.js

  const onToggle = useCallback(
    (id) => {
      setTodos(
        todos.map((todo) =>
          todo.id === id ? { ...todo, checked: !todo.checked } : todo,
        ),
      );
    },
    [todos],
  );
  • map() 메서드를 이용해 각 todo의 id가 인자로 받은 id와 해당할 경우 기존 todo 객체를 복사해와서 기존 id와 text의 정보는 유지하고 checked의 상태만 슬쩍 반대로 바꿔줍니다. id 일치하지 않은 경우는 냅두고요
  • onToggle 함수를 props로 ToDoListItem.js까지 전달하고 해당 div에 onClick 이벤트로 걸어줍니다. (코드는 생략)

🎈일정 수정하기

수정하기 팝업창 만들기(마크업, 스타일, 기본적인 form 이벤트들)


ToDoInsert.js와 거의 비슷합니다.

ToDoEdit.js

function ToDoEdit({ selectedTodo, onUpdate }) {
  const [value, setValue] = useState('');
  const onChange = useCallback((e) => {
    setValue(e.target.value);
  }, []);
  const onSubmit = useCallback(
    (e) => {
      setValue(''); //value 초기화
      //기본이벤트(새로고침) 방지
      e.preventDefault();
    },
    [value],
  );
  
  return (
    <div className="background">
      <form onSubmit={onSubmit} className="todoedit__insert">
        <h2>수정하기</h2>
        <input
          onChange={onChange}
          value={value}
          placeholder="할 일을 입력하세요"
        />
        <button type="submit">수정하기</button>
      </form>
    </div>
  );
}

ToDoEdit.scss

.TodoInsert {
  display: flex;
  background: #495057;
  input {
    width: 100%;
    background: none;
    outline: none;
    border: none;
    padding: 0.5rem;
    font-size: 1.125rem;
    line-height: 1.5;
    color: white;
    &::placeholder {
      color: #dee2e6;
    }
  }
  button {
    background: none;
    outline: none;
    border: none;
    background: #868e96;
    color: white;
    padding: 0 1rem;
    font-size: 1.5rem;
    display: flex;
    justify-content: center;
    align-items: center;
    transition: 0.1s background ease-in;
    &:hover {
      background: #adb5bd;
    }
  }
}

팝업창을 보여줄 toggle 기능 구현

먼저 insertToggle이라는 state를 만듭니다.
App.js

const [insertToggle, setInsertToggle] = useState(false); //플래그 역할을 해줄 state

.
.
  return (
    <TodoTemplate>
		.
     	.
      	.
      {insertToggle && (
        <ToDoEdit />
      )}
    </TodoTemplate>
  );

insertToggle의 값이 true면 todoEdit 팝업창을 부르고 false면 그없인데요.
이제 일정 항목을 클릭했을 때 insertToggle이 true로 바뀌는 함수를 작성합니다.

  const [selectedTodo, setSelectedTodo] = useState(null);
  const [insertToggle, setInsertToggle] = useState(false);

  const onInsertToggle = () => {
    if (selectedTodo) {
      setSelectedTodo(null);
    }
    setInsertToggle((prev) => !prev);
  };
  const onChangeSelectedTodo = (todo) => {
    setSelectedTodo(todo);
  };
  • selectedTodo라는 state를 만들었습니다. 이걸 이용해서 클릭한 일정 항목의 todo 객체를 가져올 것입니다.
  • onChangeSelectedTodo 함수 : toDoListItem의 수정 아이콘에 onClick 이벤트로 넣어줄 함수입니다.
    클릭했을 때 해당 항목의 todo 객체를 selectedTodo에 저장합니다.
  • onInsertToggle 함수 : toDoListItem의 '수정 아이콘'과 수정하기 팝업창의 '수정하기 버튼'에 onClick 이벤트로 넣어줄 함수입니다.
    만일 selectedTodo에 값이 있다면(=일정 항목을 클릭한 상태라면) selectedTodo를 null로 리셋하고,
    insertToggle의 값을 바꿔서 창을 껐다킬 수 있도록 합니다.

함수들을 ToDoListItem.js와 ToDoEdit.js에 props로 전달해서 써주세용
ToDoListItem

  return (
    <li className="TodoListItem">
		.
      	.
      <div className="edit" onClick={() =>
        {onChangeSelectedTodo(todo)
          onInsertToggle();
        }
         }>
        <MdModeEditOutline />
      </div>
		.
      	.
    </li>
  );
}

ToDoListItem

  useEffect(() => {
    if (selectedTodo) {
      setValue(selectedTodo.text);
    }
  }, [selectedTodo]);

  return (
    <div className="background">
      <form onSubmit={onSubmit} className="todoedit__insert">
        <h2>수정하기</h2>
        <input
          onChange={onChange}
          value={value}
          placeholder="할 일을 입력하세요"
        />
        <button type="submit">수정하기</button>
      </form>
    </div>
  );

❓아니? 뜬끔없이 왜 useEffect를?
todoListItem 컴포넌트를 클릭했을 때 해당 todo객체의 text 내용이 input에 뜨도록 useEffect를 썼습니다.

useEffect(2번째 인자에 해당하는 state가 변할 때만 실행될 코드, {해당 state})

텍스트를 변경해주는 onUpdate 함수

todoEdit.js 수정하기 폼의 onSubmit 이벤트로 들어갈 함수입니다.
App.js

  const onUpdate = (id, text) => {
    onInsertToggle();
    
    setTodos(todos.map((todo) => (todo.id === id ? { ...todo, text } : todo)));
  };
  • id와 text를 인자로 받아옵니다. id는 해당하는 todo 객체를 찾기 위해 쓰입니다.
  • onInsertToggle() : 여기서는 팝업창을 꺼주는 역할을 합니다.
  • map() 메서드를 이용해 각 todo의 id가 인자로 받은 id와 해당할 경우 기존 id와 checked의 정보는 유지하고 text만 업데이트 해줍니다. 위의 체크하기 기능과 비슷합니다.

ToDoEdit.js에 해당 함수를 props로 전달해서 써주세요^_^
ToDoEdit.js

  const onSubmit = useCallback(
    (e) => {
      onUpdate(selectedTodo.id, value);
      setValue(''); //value 초기화
      //기본이벤트(새로고침) 방지
      e.preventDefault();
    },
    [onUpdate, value],
    //이거 React Hook useEffect has a missing dependency 오류 생김; 돌아가긴하는데..
  );

🎈마치며

느낀점

  • state이랑 함수들 props 일일이 전달하는 거 너무 귀찮다.
  • React Hook useEffect has a missing dependency 어떻게 해야함?

만드는 것보다 블로그에 포스트 쓰는 시간이 훨씬 오래 걸리네요...😫
그래도 복습하면서 몰랐던 부분을 짚어볼 수 있어서 좋았습니다.😁
정보 공유해주시는 분들 항상 감사합니다~

📎참고

<리액트를 다루는 기술> - 김민준(벨로퍼트), 길벗
classnames - npm
18. useCallback 을 사용하여 함수 재사용하기 - 벨로퍼트
useCallback을 사용해보자 - kysung95
React Hooks: useRef 사용법 - DaleSeo
Array.prototype.concat()- MDN
[React로 Todo App 만들기] 4. Update와 Delete 기능 개발 - 개린이르아나 채널

profile
공부중

0개의 댓글