코딩 공부/React.js 리액트

[5주차 - 2] 투두리스트 기능 구현

recordmastd 2024. 11. 13. 21:58

Editor 컴포넌트에 새로운 투두를 입력하고 추가버튼을 클릭하면 List컴포넌트에 새로운 TodoItem이 추가되어야 함

투두리스트 아이템

추가된 투두 아이템은 List컴포넌트 내부에서 수정, 삭제, 검색이 가능해야 함
>> TodoItem을 state로 만들어서 보관해야 됨    (화면에 변화를 바로바로 렌더링하기 위해)

→ State는 모든 컴포넌트의 조상이 되는 App 컴포넌트에 배치

 

import './App.css'
import { useState } from 'react';
import Header from './components/Header';
import Editor from './components/Editor';
import List from './components/List';

 // 투두아이템의 보관형태를 정하기 위한 임시데이터
 const mockData = [
  {
    id: 0,
    isDone: false,
    content: "React 공부하기",
    date: new Date().getTime(),
  },
  {
    id: 1,
    isDone: false,
    content: "빨래하기",
    date: new Date().getTime(),
  },
  {
    id: 2,
    isDone: false,
    content: "노래하기",
    date: new Date().getTime(),
  },
]

function App() {
  // 여러 개의 투두테이터를 보관하기 위해 초기값 배열로 설정 
  const [todos, setTodos] = useState(mockData);
  return (
    <div className="App">
      <Header />
      <Editor />
      <List />
    </div>
  );
}

export default App

- 임시데이터는 리렌더링마다 다시 생설될 필요도 없고, 상수이기 때문에 컴포넌트 외부에 선언해도 문제없다

개발자 모드에서 잘 저장된 것을 확인 가능

 

생성된 투두아이템 리스트에 추가

// 여러 개의 투두테이터를 보관하기 위해 초기값 배열로 설정 
  const [todos, setTodos] = useState(mockData);

  // 이전에 만들었던 객체 형태로 새로운 투두 생성
  const OnCreate = (content) => {
    const newTodo = {
      id: 0,
      isDone: false,
      content: content,
      date: new Date().getTime(),
    };

     // 생성한 투두 리스트에 추가
     setTodos([newTodo, ...todos]);
  };

- 상태값은 상태변화함수를 호출해서 수정해야한다
그래야 state값을 리액트가 감지하고 컴포넌트를 정상적으로 리렌더링할 수 있다

- [newTodo, ...todos]는 새로운 할 일을 기존 할 일 배열의 맨 앞에 추가하는 방식


추가 버튼 클릭 > onCreate 함수 호출 > 에디터 컴포넌트의 입력값 전달 

import "./Editor.css";
import {useState} from "react";

const Editor = ({onCreate}) => {

    const Editor = ({onCreate}) => {
        const [content, setContent] = useState("");     // 초기값 설정

        const onChangeContent = (e) => {    // 변경값 적용
            setContent(e.target.value);
        };

        const onSubmit = () => {
            onCreate(content);      // onCreate 함수 호출하면서 content 전달
        };

    return  (
    <div className="Editor">
        <input
        value={content}
        onChange={onChangeContent}
        placeholder="새로운 Todo..."/>
        <button onClick={onSubmit}>추가</button>
    </div>
    );
    };
};

export default Editor;

- 여기서 만든 투두아이템을 setTodos함수를 통해 Todosstate 추가

- 추가버튼을 누르면 onSubmit 함수가 호출되고, onSubmit 함수에서 onCreate함수가 호출된다

정상적으로 추가된 모습, 하지만 id가 동일

- id를 기록하기 위한 레퍼런스 객체가 필요하다

// App.jsx 일부 변경
import { useState, useRef } from 'react';

const idRef = useRef(3);

  // 이전에 만들었던 객체 형태로 새로운 투두 생성
  const onCreate = (content) => {
    const newTodo = {
      id: idRef.currunt++,
      isDone: false,
      content: content,
      date: new Date().getTime(),
    };

 

여기까지의 문제점

- 아무것도 입력하지 않은 상태에서도 투두리스트가 추가됨

해결방식 >> 빈칸입력시 onSubmit함수 끝낸 후, 인풋창 포커싱

    const onSubmit = () => {
        // 빈 입력창 추가 예외 처리
        if (content === "") {
            // 입력창 포커싱
            contentRef.current.focus();
            return;
        }
           onCreate(content);      // onCreate 함수 호출하면서 content 전달
    };

    return  (
        <div className="Editor">
            <input
            ref={contentRef}
            value={content}
            onChange={onChangeContent}
            placeholder="새로운 Todo..."
          />
        <button onClick={onSubmit}>추가</button>
    </div>
    );

- 입력 후 추가버튼을 눌러도 입력폼이 비워지지 않아 추가 여부 헷갈릴 가능성 있음

해결방식 >> 인풋창 초기화

    const onSubmit = () => {
        // 빈 입력창 추가 예외 처리
        if (content === "") {
            // 입력창 포커싱
            contentRef.current.focus();
            return;
        }
           onCreate(content);      // onCreate 함수 호출하면서 content 전달
           setContent(""); // ""으로 초기화 설정
    };

- 추가버튼을 마우스로 눌러야만 

해결방식 >> 엔터키를 눌러도 투두리스트가 추가되도록 하기

    // 키보드를 눌렀을 때의 반응 처리
    const onKeyDown = (e) => {
        if (e.onKeyDown === 13) {
            onSubmit();
        }
    }
    
    
    return  (
        <div className="Editor">
            <input
            ref={contentRef}
            value={content}
            onKeyDown={onKeyDown}
            onChange={onChangeContent}
            placeholder="새로운 Todo..."
          />
        <button onClick={onSubmit}>추가</button>
    </div>

App 컴포넌트에 있는 todos를 List에서 map메서드를 활용해 리스트 형태로 렌더링하기

 

  return (
    <div className="App">
      <Header />
      <Editor onCreate={onCreate}/>
      <List todos={todos}/>
    </div>
  );

- todos를 props로 List 에 넘겨준다

컴포넌트에 스프레드 연산자를 사용하여 작성해도 됨(좌우 다 가능한 코드)

- todods의 형태를 리스트로 렌더링

- 리스트에서 반복적인 데이터를 렌더링하려면 map메서드를 사용한다
- map함수는 배열의 모든 요소에 대해 콜백함수를 실행, 새로운 배열로 만들어 반환

데이터 정상적으로 전달됨을 확인

 

props기반으로 보내진 각각의 투두아이템의 데이터가 서로 다른 UI를 렌더링하도록 하기

const TodoItem = ({ id, isDone, content, date }) => {
    return (
        <div className="TodoItem">
            <input checked={isDone} type="checkbox" />
            <div className="content">{content}</div>
            <div className="date">{new Date(date).toLocalDateString()}</div>
            <button>삭제</button>
        </div>
    );
};

- 새로운 Date 객체 생성하고, 인수로 toLocalDateString메서드에 보내주면 가독성이 오른다

 

지금까지 필연적으로 발생하는 오류

 

체크박스 오류

- 체크박스가 존재하기만 하고, 체크박스의 상태를 변환할 수 없어서 뜨는 에러

<input readOnly checked={isDone} type="checkbox" />

- 임시방편으로 readOnly를 사용하면 에러 문구가 뜨지 않는다

키 오류

- 리스트로 어떠한 컴포넌트를 사용하고 있을 때, 모든 아이템값에 고유한 key가 필수이다

{todos.map((todo)=>{
                    return <TodoItem key={todo.id} {...todo}/>;
                })}

- 아이템을 구별하기 위해 만들었던 id값을 주며 고유 key값을 설정해주면 된다

 

검색기능

import { useState } from "react";

const List = ({todos}) => {
    const [search, setSearch] = useState("");

    const onChangeSearch = (e) => {
        setSearch(e.target.value);
    };

    const getFilteredData = () => {
        if (search === "") {
            return todos;
        }
        return todos.filter((todo)=>
            todo.content.includes(search));

    }

filter와 include 설명

 

- 필터링 기준 ->배열의 모든 아이템을 순회하면서 todo.content.includes(search)가 참이 되는 것을 리턴

 // 리렌더링마다 호출하여 변수에 저장
    const filteredTodos = getFilteredData();

            <div className="todos_wrapper">
                {filteredTodos.map((todo)=>{
                    return <TodoItem key={todo.id} {...todo}/>;
                })}
            </div>

- todos에서 필터링된 filteredTodos를 사용한다

 

검색시대소문자 구별 없애기

return todos.filter((todo)=>
            todo.content.toLowerCase().includes(search.toLowerCase())
        );

- 다 소문자로 변환하여 대소문자 구분없이 검색이 가능하도록 한다


Update 체크박스 수정 가능하게 하기

- todo State에 있는 하나의 값을 바꿔야 한다

  const onUpdate = (targetId) => {
    // todos State의 값들중에
    //targetId와 일치하는 id를 갖는 투두 아이템의 isDone변경

    // 인수: todos 배열에서 targetID와 일치하는 id를 갖는 요소의 데이터만 바꾼 새로운 배열
    setTodos(
      todos,map((todo)=>
          todo.id === targetId
          ? {...todo, isDone: !todo.isDone}
          : todo
    )
  );
}


- 체크박스를 누르면 isDone을 t/f로 변환할 수 있어야 한다
- todo의 모든 값을 가져온 후 isDone프로퍼티만 바꿔준다

 

- onUpdate는 체크박스의 표시가 바뀔 때 호출되어야 한다
App > List > TodoItem 의 checkbox부분에 클릭이 발생하면 메서드가 호출되어야 함

  return (
    <div className="App">
      <Header />
      <Editor onCreate={onCreate}/>
      <List todos={todos} onUpdate={onUpdate}/>
    </div>
  );

- 클릭 동작이 아닌 인풋에 값을 넣어주는 것이므로 onChange를 사용한다

// App
<List todos={todos} onUpdate={onUpdate}/>

// List
const List = ({todos, onUpdate}) =>
const TodoItem = ({ id, isDone, content, date, onUpdate }) => {

    const onChange = () => {
        onUpdate(id);
    };

 

투두리스트 삭제 기능

app

- List에서 button이 눌릴 때 호출되어야 한다