• [React] 리액트 실습 Todo List 만들기 #1

    2023. 6. 26.

    by. Soozinyy

    이 프로젝트는 항해99의 리액트 입문 강의 개인 과제로 제작한 실습 프로젝트입니다.

     

     

     

    🔗github 바로가기

    🔗vercel로 배포한 프로젝트 바로가기

     

     

     

    ✔️ 새롭게 알게 된 점

     

    1. useState()의 수정 가능한 변수인 'set···'도 props로 전달 가능하다.

    컴포넌트를 여러개로 분리하는 과정에서 '어떤 컴포넌트를 분리하고, 어떤 데이터를 보낼 것인가'에 대한 고민을 하게되었다. 컴포넌트 분리에 대해서는 리서칭을 통해 대략적으로 Head / Creat / List / LisItem 으로 분리될 수 있다는 걸 알게 되었는데, 데이터를 props로 태워 보내는 부분은 선사례와 코드도 다르고 문법을 잘 모르니 분석적으로 접근하기 어려웠다.

     

    처음 시도한 방법은 컴포넌트별로 나누지 않고 App.jsx에 전부 적는 것이다.

     

    [ App.jsx ]

    const App = () => {
      const [todos, setTodos] = useState([
        { id: 1, title: "투두리스트 만들기", contents: "구조 짜기", isDone: false },
        { id: 2, title: "여행 가기", contents: "장소 정하기", isDone: true },
      ]);
    
      const addTodo = (newTodo) => {
        setTodos([...todos, newTodo]);
      };
    
      const removeTodo = (id) => {
        const updatedTodos = todos.filter((todo) => todo.id !== id);
        setTodos(updatedTodos);
      };
    
      const doneTodo = (id) => {
        const updatedTodos = todos.map((todo) =>
          todo.id === id ? { ...todo, isDone: true } : todo
        );
        setTodos(updatedTodos);
      };
    
      const cancleTodo = (id) => {
        const updatedTodos = todos.map((todo) =>
          todo.id === id ? { ...todo, isDone: false } : todo
        );
        setTodos(updatedTodos);
      };
    
      return (
        <div className="layout">
          <TodoHead />
          <TodoCreate addTodo={addTodo} />
          <TodoList
            todos={todos}
            removeTodo={removeTodo}
            doneTodo={doneTodo}
            cancleTodo={cancleTodo}
          />
        </div>
      );
    };

    주요 컴포넌트가 메인에 모두 적혀있으니 페이지를 나눈 의미가 100% 충족되지 않는 것 같아서 각 컴포넌트를 해당하는 페이지에 분류해 보았다.

     

    todos만 전달하면 setTodos는 알아서 포함되는 줄 알고 전달해주지 않았는데,

    setTodos가 정의되지 않았다는 에러가 떴다.

    const App = () => {
      const [todos, setTodos] = useState([
        { id: 1, title: "투두리스트 만들기", contents: "구조 짜기", isDone: false },
        { id: 2, title: "여행 가기", contents: "장소 정하기", isDone: true },
      ]);
    
      return (
        <div className="layout">
          <TodoHead />
          <TodoCreate todos={todos} />
          <TodoList todos={todos} />
        </div>
      );
    };

     

    이번에는 반신반의한 상태로 setTodos를 props로 태워 보냈다.

    예상 외로 잘 작동하였다.

    const App = () => {
      const [todos, setTodos] = useState([
        { id: 1, title: "투두리스트 만들기", contents: "구조 짜기", isDone: false },
        { id: 2, title: "여행 가기", contents: "장소 정하기", isDone: true },
      ]);
    
      return (
        <div className="layout">
          <TodoHead />
          <TodoCreate todos={todos} setTodos={setTodos} />
          <TodoList todos={todos} setTodos={setTodos} />
        </div>
      );
    };

     

    그렇다면 useState()의 수정 가능한 set··· 변수를 props로 태워 보내는 방법이 클린하다고 할 수 있을까?

    추후 질문을 통해 기술 매니저님의 답변을 받을 수 있었다.

    답은 Yes였다.

     

    현 상황에서는 props로 태워 보내는 것이 해당 변수를 수정할 수 있는 유일한 방법이므로 맞다고 볼 수 있지만

    나중에는 전역 컴포넌트로 관리하는 방법을 배우게 된다. 그때는 그것이 더 좋은 방법이라고 할 수 있다.

     

     

     

    2. 버튼 클릭시 페이지가 refresh되는 현상

    리액트 입문 강의에서와 같은 방법으로 button에 onClick 속성을 부여했는데, 강의 예제와는 다르게 페이지가 refresh되어 리스트가 추가되지 않는 현상이 발생했다. 예제 코드와의 차이점을 찾지 못했고 다른 분의 코드를 빌려 event.preventdefault()로 해결을 하였는데. 이 방법은 refresh현상을 강제로 stop해주는 것이라 근본적인 원인을 해결한 것이 아니었다.

     

    컴포넌트 자체에 문제가 있는 줄 알고 한참을 부여잡고 있었는데  이 문서를 통해 원인이 마크업에 있음을 알게되었다.

     

    이 현상의 원인은 다음과 같다.

     

    ❗️ button이 form 태그 안에 위치하고, button의 타입이 sumit일 경우

     

    참고로 button type 속성의 기본값은 submit이므로 따로 지정하지 않는 경우 submit으로 동작한다.

     

     

    해결 방법은 다음과 같다.

     

    ✔️ 버튼의 type을 button으로 설정한다.

    <form method="POST">
        <button name="data" type="button" onclick="getData()">Click</button>
    </form>

     

    ✔️ form 태그를 div로 바꿔준다.

    <div method="POST">
        <button name="data" onclick="getData()">Click</button>
    </div>

     

    ✔️ onClick 함수에 event.preventdefault()를 추가한다.

    function getData(e) {
        e.preventDefault();
    }

     

    내 코드의 경우 세번째 제시한 방법인 event.preventdefault()로 돌려막기를 한 상태였고

      const handleAddTodo = (event) => {
        event.preventDefault();
        const newTodo = {
          id: Date.now(),
          title,
          contents,
          isDone: false,
        };
        setTodos([...todos, newTodo]);
    
        // input값 초기화
        setTitle("");
        setContents("");
      };
    
      return (
        <form className="text-form">
          <div className="input-area">
            <div className="con-1">
              <input
                value={title}
                onChange={titleChangeHandler}
                placeholder="제목을 입력하세요"
              />
            </div>
            <div className="con-2">
              <input
                value={contents}
                onChange={contentsChangeHandler}
                placeholder="내용을 입력하세요"
              />
            </div>
          </div>
    
          <button className="add-button" onClick={handleAddTodo}>
            추가하기
          </button>
        </form>

     

    이후에 button의 type을 button으로 설정하는 방법으로 수정하였다.

    const handleAddTodo = () => {
        const newTodo = {
          id: Date.now(),
          title,
          contents,
          isDone: false,
        };
        setTodos([...todos, newTodo]);
        
        // input값 초기화
        setTitle("");
        setContents("");
      };
    
      return (
        <form className="text-form">
          <div className="input-area">
            <div className="con-1">
              {/*<span className="input-title">제목</span>*/}
              <input
                type="text"
                value={title}
                onChange={titleChangeHandler}
                placeholder="제목을 입력하세요"
              />
            </div>
            <div className="con-2">
              {/*<span className="input-title">내용</span>*/}
              <input
                type="text"
                value={contents}
                onChange={contentsChangeHandler}
                placeholder="내용을 입력하세요"
              />
            </div>
          </div>
    
          <button className="add-button" type="button" onClick={handleAddTodo}>
            추가하기
          </button>
        </form>
      );
    };

     

     

    📣 추가사항 (23.10.25)

    submit 이벤트는 인풋에서 Enter를 눌렀을 때도 발생하기 때문에 button의 타입을 submit으로 하고 리프레시 방지는 event.preventdefault()를 활용하는 것이 좋다.

     

     

     

    3. 중복되지 않는 id값 부여하기

    강의 예제에서는 배열.length + 1로 id 값을 부여했는데, 이렇게 되면 중간에 값을 삭제했을 때 중복 id 값이 부여되는 오류가 있다.

     

    배열[3]과 [4]의 id가 5로 겹친다.

    해결방법 다음과 같다.

     

    ✔️ Date.now()로 현재까지 경과된 밀리초를 반환해준다.

     

    배열[2]부터 id가 밀리초로 부여되었다.

     

    이 방법은 모두 다른 숫자를 반환한다는 점에서 장점이지만 다소 길고 숫자 간의 차이가 제각각인 부분은 단점이라고 할 수 있다. 만약 1, 2, 3, 4, 5 ··· 와 같은 순차적인 숫자를 부여하고 싶다면 이렇게 해결할 수 있다.

     

     

    ✔️ 마지막 객체의 id값 + 1을 한다. todos[todos.length - 1].id + 1

     

    배열[3]를 삭제 했지만 이후에도 값이 순차적으로 부여됨

    id가 3인 객체를 삭제한 이후에 5와6, 7을 추가했는데, 마지막 객체의 id값 + 1로 부여되어 순차적인 id를 완성했다.

     

     

     

     

    📍 개선할 점

    • 아무것도 입력하지 않았을 경우, 리스트에 추가되지 않도록 한다.
    • 일정 글자수 이상 입력했을 경우, 레이아웃에 영향을 주지 않도록 제한한다.

     

     

     

     

     

    🔗 Reference

    벨로퍼트와 함께하는 모던리액트 - 3장 멋진 투두리스트 만들기

    stackoverflow.com | 버튼 클릭시 페이지가 reload되는 현상 해결법

    댓글