리액트로 ToDoList 만들기 (useState , useReducer)

ChoiYongHyeun·2024년 2월 24일
0

망가뜨린 장난감들

목록 보기
15/19

리액트 공식문서를 50% 정도 읽고 만들어본 To Do List!


2024-02-25 업데이트


useContext 를 이용하여 리팩토링 한 게시글이 있습니다.
좀 더 공부한 후의 프로젝트를 보고 싶다면 해당 게시글 을 통해 확인해보세요 ><


useState 이용해서 만들어보기

HTML

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="../src/index.css" />
    <link rel="stylesheet" href="../src/App.css" />
    <title>useState TodoList</title>
    <style>
      @import url('https://fonts.googleapis.com/css2?family=Bagel+Fat+One&display=swap');
    </style>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

깔쌈한 폰트 하나 다운로드 해주고 body 태그에는 div 태그 하나만 생성해주고

컴포넌트들을 생성하러 가보자

각 컴포넌트들을 만들 때 컴포넌트의 역할을 명시하면서 해보도록 하겠다.

App.js

state 정의

export default function App() {
  const [tasks, setTasks] = useState([]);
  const [text, setText] = useState('');
  ...
}

최상위 컴포넌트에서 사용할 state 는 두가지이다.

tasks 상태는 현재 투 두 리스트들의 내용을 담은 객체를 배열로 담은 상태이다.

tasks 는 다음과 같이 구성된다.

[
...
{
	id : todolist 의 id,
    content : 적힌 내용,
    isEdit : 해당 todolist가 수정 중인지, 아닌지를 담은 boolean
}
... ]

textinput 값에서 적힌 값들을 저장하는 state 이다.

Input

function Input({ onChange }) {
  return (
    <input type='text' placeholder='할 일을 입력해주세요' onChange={onChange} />
  );
}

인풋 컴포넌트는 입력값을 받아 props 로 받은 onChange 콜백함수를 통해 현재 입력된 값들을 담고 있는 state 값을 변경하는 컴포넌트이다.

Button


function Button({ onClick, children }) {
  return <button onClick={onClick}>{children}</button>;
}

버튼 컴포넌트는 state 를 변경시키는 onClick 콜백함수를 props 로 받는다.

기본적인 기능은 일반적인 <button ..> 과 다를 것 없지만 컴포넌트를 구성 할 때 전부 이쁘게 대문자로 통일 하고 싶어서 만들어주었다.

ToDoText

function TodoText({ task, onEdit }) {
  return (
    <>
      <p>{task.content}</p>
      <Button
        onClick={() => {
          onEdit(task.id);
        }}
      >
        Edit
      </Button>
    </>
  );
}

ToDoText 컴포넌트는 props 로 받은 taskcontent 를 게시판에 띄우는 역할을 하는 p 태그와

수정 기능이 있는 Button 컴포넌트를 가지고 있다.

task 는 위에서 Input 컴포넌트에 의해 입력된 값을 content 라는 프로퍼티에 담고 있다.

ToDoInput

function TodoInput({ task, onSave }) {
  const [localText, setLocalText] = useState(task.content);
  return (
    <>
      <input
        type='text'
        onChange={(e) => setLocalText(e.target.value)}
        value={localText}
      />
      <Button
        onClick={() => {
          onSave(task.id, localText);
        }}
      >
        Save
      </Button>
    </>
  );
}

ToDoInput 컴포넌트는 ToDoText 에서 edit 버튼이 눌렸을 때 나타나는 컴포넌트이다.

수정 버튼을 눌렀을 때에는 이전에 입력된 값들을 가지고 있어야 하기 때문에 task 값을 props 로 받는다.

지역 statelocalText 를 가지고 있어 input 값에서 입력된 값들로 지역 상태를 변경하고

Save 버튼이 눌리면 local state 를 상위 컴포넌트의 상태를 onSave 콜백함수를 이용해 변경시키도록 한다.

ToDoList

function TodoList({ tasks, onSave, onEdit, onRemove }) {
  if (!tasks) return;

  return (
    <>
      {tasks.map((task) => {
        return (
          <div key={task.id} className='container'>
            {task.isEdit ? (
              <TodoInput key={task.id} task={task} onSave={onSave} />
            ) : (
              <TodoText key={task.id} task={task} onEdit={onEdit} />
            )}
            <Button onClick={() => onRemove(task.id)}>Remove</Button>
          </div>
        );
      })}
    </>
  );
}

ToDoList 컴포넌트는 입력값에 따른 task 들을 컴포넌트에 넣어 렌더링 하는 컴포넌트이다.

렌더링 할 때 포인튼느 isEdit 값에 따라 ToDoInput , ToDoText 컴포넌트를 결정하고 렌더링 한다는 것이다.

이를 통해 위에서 버튼들이 눌려 taskstask 객체의 isEdit 이 변경되면

렌더링 되는 컴포넌트가 p 태그가 되기도, input 태그가 되기도 하게 하였다.

App

let nextId = 0;

export default function App() {
  const [tasks, setTasks] = useState([]);
  const [text, setText] = useState('');

  function handleType(e) {
    setText(e.target.value);
  }

  function handleAdd() {
    setTasks([...tasks, { id: nextId++, content: text, isEdit: false }]);
  }

  function handleSave(targetId, newContent) {
    setTasks(
      tasks.map((task) => {
        if (task.id === targetId)
          return { id: targetId, content: newContent, isEdit: false };
        return task;
      }),
    );
  }

  function handleEdit(targetId) {
    setTasks(
      tasks.map((task) => {
        if (task.id === targetId) return { ...task, isEdit: true };
        return task;
      }),
    );
  }

  function handleRemove(targetId) {
    setTasks(tasks.filter((task) => task.id !== targetId));
  }

  return (
    <>
      <h1>To Do List</h1>
      <div className='header'>
        <Input onChange={handleType} />
        <Button onClick={handleAdd}>Add</Button>
      </div>
      <TodoList
        tasks={tasks}
        onSave={handleSave}
        onEdit={handleEdit}
        onRemove={handleRemove}
      />
    </>
  );
}

최상위 컴포넌트인 App 컴포넌트에서 각 이벤트 핸들러들을 정의해주고 컴포넌트를 구성하였다.

이벤트 핸들러들은 type , add , save , edit , remove 등의 인터렉션을 담당하고 있다.

결과물

끝 ~!~!


useReducer 를 이용해 만들어보기

React Learn - 이벤트 핸들링과 인터렉션 분해의 필요성 , useReducer

이전에 useReducer 의 개념과 필요성에 대해 공부해보았으니 useReducer 를 이용해서 App 컴포넌트를 수정해보자

let nextId = 0;

export default function App() {
  const [tasks, dispatch] = useReducer(taskReducer, []);
  const [text, setText] = useState('');

  function handleType(e) {
    setText(e.target.value);
  }

  function dispatchAdd() {
    dispatch({
      type: 'add',
      text,
    });
  }

  function dispatchSave(targetId, newContent) {
    dispatch({
      type: 'save',
      targetId,
      newContent,
    });
  }

  function dispatchEdit(targetId) {
    dispatch({
      type: 'edit',
      targetId,
    });
  }

  function dispatchRemove(targetId) {
    dispatch({
      type: 'remove',
      targetId,
    });
  }

  return (
    <>
      <h1>To Do List</h1>
      <div className='header'>
        <Input onChange={handleType} />
        <Button onClick={dispatchAdd}>Add</Button>
      </div>
      <TodoList
        tasks={tasks}
        onSave={dispatchSave}
        onEdit={dispatchEdit}
        onRemove={dispatchRemove}
      />
    </>
  );
}

/**
 * @param {Array} tasks 컴포넌트의 State
 * @param {Object} action 이벤트 핸들러에서 디스패치한 이벤트 객체
 * 액션 타입과 이벤트 핸들시 필요한 파라미터를 프로퍼티로 가지고 있음
 * @returns {Array} 새로 갱신 할 State
 */
function taskReducer(tasks, action) {
  // state 변경이 일어날 떄 발생한 action 을 debuging 하기 좋음
  console.log(action);

  switch (action.type) {
    // add 일 때 필요한 action 프로퍼티는 text
    case 'add': {
      return [...tasks, { id: nextId++, content: action.text, isEdit: false }];
    }

    case 'save': {
      // save 일 때 필요한 action 프로퍼티는 targetId , newContent
      return tasks.map((task) => {
        if (task.id === action.targetId)
          return {
            id: action.targetId,
            content: action.newContent,
            isEdit: false,
          };
        return task;
      });
    }

    case 'edit': {
      // edit 일 떄 필요한 action 프로퍼티는 targetId
      return tasks.map((task) => {
        if (task.id === action.targetId) return { ...task, isEdit: true };
        return task;
      });
    }

    case 'remove': {
      // remove 일 떄 필요한 action 프로퍼티는 targetId
      return tasks.filter((task) => task.id !== action.targetId);
    }

    default: {
      throw new Error('처음 보는 Type 인디요');
    }
  }
}

결과물은 동일하다.


CSS

두 파일 모두 CSS 는 같으니 마지막에 명시해두었다.

* {
  margin: 0px;
  padding: 0px;
  color: white;
  font-family: 'Bagel Fat One', system-ui;
}

body {
  background: url('https://images.pexels.com/photos/807598/pexels-photo-807598.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2');
  background-size: cover;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
}

#root {
  backdrop-filter: blur(30px);
  padding: 50px;
  width: 300px;
  /* border: 5px solid white; */
  border-radius: 20%;
}

.header {
  display: flex;
  gap: 10px;
  padding: 10px 0px;
}

button {
  width: 50x;
  font-size: 10px;
  background-color: green;
  padding: 10px;
  border-radius: 20px;
  border: none;
}

input {
  border-radius: 20px;
  border: none;
  padding: 10px;
  color: black;
}

.container {
  display: flex;
  gap: 10px;
  padding: 10px;
}

.container p {
  width: 200px;
}

li {
  display: flex;
  gap: 5px;
}

회고

한 달전 자바스크립트 공부를 끝내고 투 두 리스트를 만들어봤던 적이 있다.

이 때는 완전 러프하게 이벤트 별 인터렉션을 모두 정의해두었기 때문에

같이 공부하는 스터디원이 수정 기능이 있으면 좋겠다는 이야기를 들었을 때

기능을 추가할 엄두가 나지 않았다.

그 때는 상태 관리의 개념에 대해 모르고 있었기 때문에 기능을 추가하려면 새로 다시 만드는게 나은 수준이였다.

하지만 이번에 상태 관리의 개념을 알고 나서 해보니

이전에 이벤트 별 인터렉션을 모두 정의해두는 것이 얼마나 확장성이 낮은 것인지 더 체감 할 수 있었다.

물론 지금의 투 두 리스트도 완벽한 것은 아니다.

입력값이 들어올 때 마다 상위 컴포넌트의 text 가 변할 때 마다 모든 하위 컴포넌트가 재렌더링 되고 있기 때문이다.
(상위 컴포넌트의 state 가 변하고 있기 때문이다.)

이를 방지하기 위한 다양한 Hook 이 있던데 열심히 공부해봐야겠다 :)

profile
빨리 가는 유일한 방법은 제대로 가는 것이다

0개의 댓글