컴포넌트 분리한 todo 앱 만들기
결과물
파일 구성
- components 폴더 생성
- components/Controller.js, viewers.js 파일 생성
코드 작성
App.js
- App.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import "./App.css";
// (1) useReducer과 useRef를 가져오도록한다
import { useReducer, useRef } from "react";
import Header from "./components/Header";
import TodoEditor from "./components/TodoEditor";
import TodoList from "./components/TodoList";
// (2) TodoList 컴포넌트에서 사용할 배열 데이터로 useReducer로 관리 예정이다
const mockTodo = [
{
id: 0,
isDone: false,
content: "React 공부하기",
createdDate: new Date().getTime(),
},
{
id: 1,
isDone: false,
content: "노래 연습하기",
createdDate: new Date().getTime(),
},
];
// (3) useReducer를 사용하여 자식 컴포넌트에서 데이터의 CRUD 요청이왔을때 처리하기 위한 함수
function reducer(state, action) {
// 상태 변화 코드
switch (action.type) {
// (4) CREATE 요청이 오면 action 객체에 들어있는 newItem속성의 값을 복사하고
// 기존의 todo 데이터(useRef로 등록한 mockTodo데이터)와 합쳐서 todo 데이터로 리턴한다
case "CREATE": {
return [action.newItem, ...state];
}
// (5) 이 앱에서의 UPDATE 요청은 할일완료/미완료 체크박스 선택으로, 이벤트 발생시 해당 아이템을 찾기위해
// state인 todo 데이터를 map을 돌려서 id값이 업데이트 요청온 action.targetId와 비교후 맞다면
// 스프레드 연산자를 통한 복사를 통해 isDone의 값만 반대로 변경해주고 아니라면 기존 데이터 그대로를 리턴한다
case "UPDATE": {
return state.map((it) =>
it.id === action.targetId ? { ...it, isDone: !it.isDone } : it
);
}
// (6) Delete 요청이 오면 filter키워드를 사용해서 해당 id가 아닌 데이터들만 따로 필터링하여 리턴해준다
case "DELETE": {
return state.filter((it) => it.id !== action.targetId);
}
default:
return state;
}
return state;
}
function App() {
// 위에서 생성한 mockTodo 배열 데이터를 초기값으로 갖는 todo라는 데이터 변수를 만들고 useReducer로 관리하도록 선언한다
// dispatch를 호출하면 위에서 미리 생성해둔 reducer함수를 호출하며 인자값을 전달하고, reducer함수에서 리턴한 값은 todo 변수에 반영하고 컴포넌트 재렌더링을 진행한다.
const [todo, dispatch] = useReducer(reducer, mockTodo);
// 삭제, 업데이트 등의 데이터 변경을 해주기 위해서는 해당 데이터들마다 구분할 수 있는 중복되지 않는 특별한 id값을 가지고 있어야하는데
// 이 앱에서는 간단하게 변수의 데이터가 변하더라도 재렌더링이 되지 않는 useRef를 사용하여 idRef변수를 생성후 id값을 관리하도록 한다
// 초기값은 기존의 데이터가 2개가 이미 들어가있기 때문에 3부터 시작하도록 주었다
const idRef = useRef(3);
// 데이터 생성을 하는 컴포넌트는 TodoEditor 컴포넌트로 해당 자식컴포넌트가 데이터를 생성할 때 사용하기 위한 함수를 만들어서 제공해주려한다
// onCreate함수로 선언하고 dispatch는 위에서 만들어둔 reducer를 통한 데이터 처리를 하기위해 호출하며
// reducer함수에서 CREATE처리에 필요한 인자값인 type과 newItem 객체데이터를 생성하여 전달한다
// id값의 idRef.current는 현재 idRef의 값을 불러올때 사용된다 content는 자식 컴포넌트에서 데이터를 생성하기위한 데이터를 전달하면 해당 값을 넣어준다
// isDone은 완료/비완료 항목으로 기본은 false로 주도록한다
// 생성날짜인 createdDate속성은 현재 시간을 넣어준다
const onCreate = (content) => {
dispatch({
type: "CREATE",
newItem: {
id: idRef.current,
content,
isDone: false,
createdDate: new Date().getTime(),
},
});
idRef.current += 1;
};
// 데이터를 보여주는 역할을 담당하는 TodoList컴포넌트에서 사용할 onUpdate함수는 자식 컴포넌트인 TodoList로부터 업데이트할 데이터의 id값(targetId)를 전달받아서
// reducer함수로 전달한다
const onUpdate = (targetId) => {
dispatch({ type: "UPDATE", targetId });
};
// 데이터를 보여주는 역할을 담당하는 TodoList컴포넌트에서 사용할 onDelete함수는 자식 컴포넌트인 TodoList로부터 삭제할 데이터의 id값(targetId)를 전달받아서
// reducer함수로 전달한다
const onDelete = (targetId) => {
dispatch({ type: "DELETE", targetId });
};
return (
<div className="App">
<Header />
<TodoEditor onCreate={onCreate} />
<TodoList todo={todo} onUpdate={onUpdate} onDelete={onDelete} />
</div>
);
}
export default App;
TodoEditor.js
- /components/TodoEditor.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import "./TodoEditor.css";
import { useState, useRef } from "react";
const TodoEditor = ({ onCreate }) => {
const [content, setContent] = useState("");
const inputRef = useRef();
const onChangeContent = (e) => {
setContent(e.target.value);
};
// 추가 버튼을 클릭시 동작하는 함수로 위에서 생성한 content변수가 비어있다면 useRef를 통해 텍스트 입력창으로 포커스를 넘긴다
// alert 메세지 창 호출 함수를 사용해서 메세지를 띄워주도록한다
// content가 비어있지 않다면 부모 컴포넌트한테 받은 onCreate함수로 content데이터를 전달한다, 이후 setContent 함수를 호출하여 content값을 빈값으로 변경해주도록했다
const onSubmit = () => {
if (!content) {
inputRef.current.focus();
alert("No Value");
return;
}
onCreate(content);
setContent("");
};
// 편의성 기능
// 엔터 키코드는 13번 코드로 onKeyDown 이벤트가 발생하면 이벤트 키코드를 비교하여 13일 경우 onSubmit함수를 호출하도록하였다
const onKeyDown = (e) => {
if (e.keyCode === 13) {
onSubmit();
}
};
return (
<div className="TodoEditor">
<h4>새로운 Todo 작성하기 ✏️</h4>
<div className="editor_wrapper">
<input
ref={inputRef}
value={content}
onChange={onChangeContent}
onKeyDown={onKeyDown}
placeholder="새로운 Todo ..."
/>
<button onClick={onSubmit}>추가</button>
</div>
</div>
);
};
export default TodoEditor;
TodoList.js
- /components/TodoList.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import "./TodoList.css";
import TodoItem from "./TodoItem";
import { useState, useMemo } from "react";
const TodoList = ({ todo, onUpdate, onDelete }) => {
const [search, setSearch] = useState("");
const memoTodo = useMemo(() => {
const totalCount = todo.length;
const doneCount = todo.filter((it) => it.isDone).length;
const notDoneCount = totalCount - doneCount;
return { totalCount, doneCount, notDoneCount };
}, [todo]);
const onChangeSearch = (e) => {
setSearch(e.target.value);
};
const getSearchResult = () => {
return search === ""
? todo
: todo.filter((it) => it.content.toLowerCase().includes(search));
};
const { totalCount, doneCount, notDoneCount } = memoTodo;
return (
<div className="TodoList">
<h4>Todo List💫</h4>
<div>
<div>
<div>총개수: {totalCount}</div>
<div>완료된 할일: {doneCount}</div>
<div>아직 완료하지 못한 할 일: {notDoneCount}</div>
</div>
</div>
<input
onChange={onChangeSearch}
className="searchbar"
placeholder="검색어를 입력하세요"
/>
<div className="list_wrapper">
{getSearchResult().map((p) => (
<TodoItem key={p.id} {...p} onUpdate={onUpdate} onDelete={onDelete} />
))}
</div>
</div>
);
};
export default TodoList;
TodoList.js
- /components/TodoList.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import "./TodoItem.css";
const TodoItem = ({ id, content, isDone, createDate, onUpdate, onDelete }) => {
console.log(`${id} TodoItem 업데이트`);
const onChangeCheckbox = () => {
onUpdate(id);
};
const onClickDelete = () => {
onDelete(id);
};
return (
<div className="TodoItem">
<div className="checkbox_col">
<input checked={isDone} type="checkbox" onChange={onChangeCheckbox} />
</div>
<div className="title_col">{content}</div>
<div className="date_col">
{new Date(createDate).toLocaleDateString()}
</div>
<div onClick={onClickDelete} className="btn_col">
<button>삭제</button>
</div>
</div>
);
};
export default TodoItem;
This post is licensed under
CC BY 4.0
by the author.