React + vite로 TodoList 만들기(초기셋팅부터 시작)

const job = '프론트엔드';·2023년 10월 1일
0
post-thumbnail

TodoList 만들기

todolist를 여러번 만들었지만, 이번 연휴가 길다보니까 뭔가 복잡한 건 하고 싶지 않고, next js 강의 듣다가 리액트 복습할 겸 다시 만들어 봄

Settings

npm create vite@latest
  • React
  • Javascript
npm install

Stack

react + vite

local start

npm run dev
  • port: 5173

  • 초기 파일 형태는 create-react-app으로 생성하는 것과 같음
  • assets와 public에 있는 svg파일을 삭제함
  • 그리고 필요없는 다른 파일도 지웠음(이거는 필수가 아님)

  • 포트번호는 5173
  • 그리고 화면이 잘 뜨는지 봣더니 잘 됨

화면구성(UI구현)

  • Header
  • Todo Editor
  • Todo List - TodoItem

컴포넌트를 만들기 전에 위치 잡아보기

  • 최상위 App컴포넌트에 전체적으로 적용할 CSS를 적용함

flex-direction ?

  • display는 객체의 오른쪽(옆)에 위치하게 함
  • 그런데 추가로 flex-direction: column을 주면 객체의 아래에 위치함

  • {new Date().toDateString()}를 통해 오늘(현재)날짜를 받아옴

TodoEditor

  • input창과 button으로 구성

TodoList

  • h4태그로 '오늘의 할 일' 제목 만들고
  • 이미 입력한 할 일을 검색하는 검색 input창 만들기
  • TodoEditor를 통해서 할 일을 추가하면 List형식 출력
  • 추가될 List는 추가할 때마다 똑같은 형태의 구성이 반복되기 때문에 컴포넌트로 별도 관리(TodoItem 컴포넌트 생성)
  • TodoItem컴포넌트 안에는 체크박스, 내용, 버튼 한 세트로 구성

기능구현

  • TodoEditor컴포넌트에서 create해서 추가하면
  • TodoItem에서 read, update, delete를 함
  • 즉, 두 컴포넌트는 동일한 상태를 공유
  • 따라서 두 컴포넌트를 감싸고 있는 공통 부모 App컴포넌트에 상태관리를 해야 함

데이터 넣어보기


  • Item안에 들어갈 프로퍼티를 만들어서 mock데이터 꾸러미를 만들었음

추가 버튼을 누르면 데이터 추가하기

import { useRef, useState } from "react";
import "./App.css";
import Header from "./components/Header";
import TodoEditor from "./components/TodoEditor";
import TodoList from "./components/TodoList";

const mockDate = [
  {
    id: 0,
    isDone: true,
    content: "공부하기",
    createdDate: new Date().getTime(),
  },
  {
    id: 1,
    isDone: true,
    content: "다영이 괴롭히기",
    createdDate: new Date().getTime(),
  },
  {
    id: 2,
    isDone: true,
    content: "다영이 놀리기",
    createdDate: new Date().getTime(),
  },
];

function App() {
  // 1. todolist를 위한 상태관리 useState hook
  const [todos, setTodos] = useState(mockDate);
  // 3. id 값을 고유값으로 증가하게 하기 위함
  const idRef = useRef(3);
  // 2. 생성되는 데이터 값, mock데이터의 프로퍼티와 동일
  const onCreate = (content) => {
    const newTodo = {
      id: idRef.current++,
      isDone: false,
      content,
      createdDate: new Date().getTime(),
    };

    // 2-1. setTodos로 todolist의 현재 상태로 만들기
    // newTodo(새로추가) + 기존에 있던 todos로 배열로 보관
    setTodos([newTodo, ...todos]);
  };
  return (
    <div className="App">
      <Header />
      {/* 4. props로 넘겨주기 */}
      <TodoEditor onCreate={onCreate} />
      <TodoList />
    </div>
  );
}

export default App;
import { useState } from "react";
import "./TodoEditor.css";

// 5. props로 받아오기
const TodoEditor = ({ onCreate }) => {
  // 6. 입력하는 content의 상태관리를 위한 useState
  const [content, setContent] = useState("");

  const onChangeContent = (e) => {
    setContent(e.target.value);
  };
  const onClick = () => {
    onCreate(content);
  };
  return (
    <div className="TodoEditor">
      {/* 7. value값 : input창에 입력하는 값 */}
      {/* 8. onChange를 이용해서 입력 value값을 새로운 setContent 값으로 전달 */}
      <input
        value={content}
        onChange={onChangeContent}
        placeholder="추가할 할 일"
      />
      {/* 9. 추가 버튼을 누르면 새로 입력받아서 onChange로 넘긴 value값을 onCreate로 넘김 */}
      <button onClick={onClick}>추가</button>
    </div>
  );
};

export default TodoEditor;

기능문제점

  1. 아무것도 입력하지 않고, 추가 버튼을 눌러도 추가됨
  2. 입력하고 추가 버튼을 누르면, 추가되고 해당 입력창은 비워졌으면 좋겠음
  3. 추가 버튼을 직접 누르지 않고, 엔터를 눌러도 추가됐으면 좋겠음

개선하기

  1. useRef로 content가 빈 값이면 추가되지 않고, 해당 input창에 포커싱하기
const TodoEditor = () => {

  const inputRef = useRef();
  
    const onClick = () => {
    //value(content)에 아무것도 입력하지 않으면, 현재 포커스를 current창에 둠
    if (content === "") {
      inputRef.current.focus();
      return;
    }
    onCreate(content);
  };
  
  return (
    <div className="TodoEditor">
      <input
        ref={inputRef}
        value={content}
        onChange={onChangeContent}
        placeholder="추가할 할 일"
      />
      <button onClick={onClick}>추가</button>
    </div>
  );


}
  • useRef로 inputRef를 선언해줌
  • input창 ref에 inputRef를 적용해줌
  • onClick()이벤트시 if문을 사용해서 content가 빈 값이라면 inputRef의 현재위치에 포커싱하게 함

useRef가 뭐지?
1. useState 같은 저장공간
useState와 차이점? useState는 state가 변화하면 렌더링이 발생하고, 컴포넌트 내부 변수들이 초기화 됨. 하지만 useRef는 Ref가 변화하더라도 렌더링이 되지 않고 변수들의 값이 그대로 유지됨. 따라서 state의 변화로 렌더되더라도 Ref의 값은 유지
2. Document.querySelector같은 역할
input창에 focus()를 하듯이 DOM 요소에 접근이 가능

  1. 추가버튼을 누르면, input창의 값을 비우기
    const onClick = () => {
    if (content === "") {
      inputRef.current.focus();
      return;
    }
    onCreate(content);
    setContent("")
  };
  • setContent로 현재 값을 비워둠
  1. 엔터를 눌러도 추가되도록 하기
  • onKeyDown이벤트를 사용
  • 엔터키의 번호는 13번임
  • 따라서 조건문으로 keyCode의 이벤트가(눌리는 키보드가) 13번일 경우 onClick이벤트를 실행시키도록 함
  const onKeyDown = (e) => {
    if (e.keyCode === 13) {
      onClick();
    }
 	 <input
        ref={inputRef}
        value={content}
        onChange={onChangeContent}
        onKeyDown={onKeyDown}
        placeholder="추가할 할 일"
      />

추가버튼을 누르면 리스트로 랜더하기

todos 배열 형태를 App컴포넌트에서 TodoList컴포넌트로 props로 넘겨주기

  • 확인해보니, TodoList에 App컴포넌트의 todos배열이 잘 들어가 있는 것을 확인할 수 있음

map사용해서 배열에 있는 프로퍼티 요소를 보여주기

{todos.map((todo) => (
          <div>{todo.content}</div>
        ))}

  • map메소드를 이용해서 배열에 있는 모든 형태가 한번씩 훑어서 돌아가도록 만들면
  • content가 잘 보여지는걸 확인 할 수 있음

TodoItem에 props로 넘겨주기

  1. TodoList
      <div className="todos_wrapper">
        {todos.map((todo) => (
          <TodoItem {...todo} />
        ))}
      </div>
  • 하나 하나 프로퍼티를 입력할 필요 없이 ...todo로 넘겨줌
  1. TodoItem
const TodoItem = ({ id, isDone, createdDate, content }) => {
  return (
    <div className="TodoItem">
      <input className="checkbox_col" type="checkbox" checked={isDone} />
      <div className="content">{content}</div>
      <div className="date">{createdDate}</div>
      <button>삭제</button>
    </div>
  );
};
  • 받고자 하는 속성 값을 구조분해 할당 방식으로 받아서 각각에 뿌려줌

검색창에 입력하면, 해당 검색 키워드가 포함된 '오늘의 할 일'을 찾아주기

 const [search, setSearch] = useState();
  • 검색창의 상태를 관리한 useState를 만들어주고
  const onChangeSearch = (e) => {
    setSearch(e.target.value);
  };
  • onChange이벤트를 통해 해당 입력값을 받아오고
  const filterTodos = () => {
    if (search === "") {
      return todos;
    }
    return todos.filter((todo) =>
      todo.content.toLowerCase().includes(search.toLowerCase())
    );
  };
  • 조건문을 사용해서 search 입력창에 빈 값이라면 아무것도 필터없이 그냥 todos를 반환하고
  • 그러니깐 인수로 전달한 search 값을 todo.content가 포함하고 있는지(만약에 있으면 True: 해당 값 반환 / 없으면 False: 아무것도 반환하지 않음)
  • 입력값이 있다면 입력된 해당 키워드가 포함된 결과를 반환

Todo update하기

checkbox 누르면 상태바꾸기

  1. App컴포넌트
  const onUpdate = (targetId) => {
    setTodos(
      todos.map((todo) => {
        if (todo.id === targetId) {
          return {
            ...todo,
            isDone: !todo.isDone,
          };
        } else {
          return todo;
        }
      })
    );
  };
  • onUpdate 함수를 만들어서 todo.id의 값이 targetId와 일치하면 idDone(checkbox의 상태) 값을 반대로 바꿔줌
  • 일치하지 않을 경우 그냥 todo상태를 반환하게 함
  1. TodoList 컴포넌트를 거쳐 TodoItem으로 prop전달
  • TodoItem 컴포넌트에 onChange함수를 만들어서 해당 onUpdate를 작동시키기
  const onChangeCheckbox = () => {
    onUpdate(id);
  };
 <input
        onChange={onChangeCheckbox}
        className="checkbox_col"
        type="checkbox"
        checked={isDone}
      />

delete기능 구현하기

  1. App컴포넌트
  const onDelete = (targetId) => {
    setTodos(todos.filter((todo) => todo.id !== targetId));
  };
 <TodoList todos={todos} onUpdate={onUpdate} onDelete={onDelete} />
  • onDelete 함수를 만들어서 todo.id 값이 targetId와 일치하면 삭제
  • todos.filter((todo) => todo.id !== targetId)는 todos 배열에서 id가 targetId와 일치하지 않는 모든 항목을 유지하고 해당 항목을 제외. 즉, targetId와 일치하는 항목은 삭제
  1. TodoList 컴포넌트
 <div className="todos_wrapper">
        {filterTodos().map((todo) => (
          <TodoItem
            key={todo.id}
            {...todo}
            onUpdate={onUpdate}
            onDelete={onDelete}
          />
        ))}
      </div>
  • prop으로 onDelete 함수를 넘겨받음
  1. TodoItem 컴포넌트
  const onClickDeleteButton = () => {
    onDelete(id);
  };
 <button onClick={onClickDeleteButton}>삭제</button>
  • onClick 이벤트로 onDelete함수를 실행

아무튼 구체적으로 설정부터 CRUD까지 모두 해보았음. 이제 다른 훅을 사용해서 상태관리까지 해보겠음 ! 끄읕 -

profile
`나는 ${job} 개발자`

0개의 댓글