[5주차 - 2] 투두리스트 기능 구현
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를 기록하기 위한 레퍼런스 객체가 필요하다
// 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));
}
- 필터링 기준 ->배열의 모든 아이템을 순회하면서 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);
};
투두리스트 삭제 기능
- List에서 button이 눌릴 때 호출되어야 한다