React로 todoList 만들어보며 사용한 주요 개념들을 정리해보았습니다.
import "./App.css";
import Header from "./component/Header";
import TodoEditor from "./component/TodoEditor";
import TodoList from "./component/TodoList";
import { useState, useRef } from "react";
function App() {
const idRef = useRef(3);
const mockTodo = [
{
id: 0,
isDone: false,
content: "React 공부하기",
createDate: new Date().getTime(),
},
{
id: 1,
isDone: false,
content: "빨래 널기",
createDate: new Date().getTime(),
},
{
id: 2,
isDone: false,
content: "노래 연습",
createDate: new Date().getTime(),
},
];
const [todo, setTodo] = useState(mockTodo);
const onCreate = (content) => {
const newItem = {
id: idRef.current,
content,
isDone: false,
createdDate: new Date().getTime(),
};
setTodo([newItem, ...todo]);
idRef.current += 1;
};
const onUpdate = (targetId) => {
setTodo(
todo.map((todo) =>
todo.id === targetId ? { ...todo, isDone: !todo.isDone } : todo
)
);
};
const onDelete = (targetId) => {
setTodo(todo.filter((it) => it.id !== targetId));
};
return (
<div className="App">
<Header />
<TodoEditor onCreate={onCreate} />
<TodoList todo={todo} onUpdate={onUpdate} onDelete={onDelete} />
</div>
);
}
export default App;
Create(todoList 추가)
const onCreate = (content) => {
const newItem = {
id: idRef.current,
content,
isDone: false,
createdDate: new Date().getTime(),
};
setTodo([newItem, ...todo]);
idRef.current += 1;
};
Read(TodoList 조회, 렌더링)
prop하여 자식 컴포넌트로 todo(할일 리스트를 담은 객체 배열)데이터를 내려 전달하고, onCreate, onUpdate, onDelete 함수를 전달해 자식 컴포넌트의 이벤트에 따라 해당 함수들이 실행되도록 했습니다.
// App.jsx
return (
<div className="App">
<Header />
<TodoEditor onCreate={onCreate} />
<TodoList todo={todo} onUpdate={onUpdate} onDelete={onDelete} />
</div>
);
// TodoList.jsx
import "./TodoList.css";
import TodoItem from "./TodoItem";
import { useState } from "react";
const TodoList = ({ todo, onUpdate, onDelete }) => {
const [search, setSearch] = useState("");
const onChangeSearch = (e) => {
setSearch(e.target.value);
};
const getSearchResult = () => {
return search === ""
? todo
: todo.filter((it) =>
it.content.toLowerCase().includes(search.toLowerCase())
);
};
return (
<div className="TodoList">
<h3>Todo List🍄</h3>
<input
value={search}
className="searchbar"
onChange={onChangeSearch}
type="text"
placeholder="검색어를 입력하세요"
/>
<div className="list__wrap">
{getSearchResult().map((it) => (
<TodoItem
onDelete={onDelete}
key={it.id}
{...it}
onUpdate={onUpdate}
/>
))}
</div>
</div>
);
};
export default TodoList;
// TodoItem.jsx
import "./TodoItem.css";
import { useEffect } from "react";
const TodoItem = ({ id, content, isDone, createdDate, onUpdate, onDelete }) => {
useEffect(() => {
console.log("content", content);
}, [content]);
const onChangeCheckbox = () => {
onUpdate(id);
};
const onClickDeleteButton = () => {
onDelete(id);
};
return (
<div className="TodoItem">
<div>
<input onChange={onChangeCheckbox} checked={isDone} type="checkbox" />
<div className={`list__title ${isDone ? "done" : ""}`}>{content}</div>
</div>
<div>
<span>{new Date(createdDate).toLocaleDateString()}</span>
<div className="btn__col">
<button onClick={onClickDeleteButton}>X</button>
</div>
</div>
</div>
);
};
export default TodoItem;
Update(업데이트 = 완료/미완료 체크)
map을 이용해 onUpdate 실행 시, 해당 targetId 에 해당되는 todo 요소의 isDone(checkbox의 Value)이 토글되도록 하여, TodoItem 별로 완료/미완료 상태가 업데이트 되도록 했습니다..
const onUpdate = (targetId) => {
setTodo(
todo.map((todo) =>
todo.id === targetId ? { ...todo, isDone: !todo.isDone } : todo
)
);
};
Delete(삭제)
filter를 사용하여 해당 id를 제외한 나머지 todo 내부 요소들을 반환하도록 했습니다.
const onDelete = (targetId) => {
setTodo(todo.filter((it) => it.id !== targetId));
};
부가기능 (검색)
search라는 useState 변수를 설정하고, search 값의 변경에 따라 getSearchResult가 실행되도록 하여, search 값이 유효할 경우 content에 search가 포함된 todo 요소들을 필터링 하도록 하여 구현하였습니다.
검색 시, 대소문자 구분을 없애기 위해 todo 요소들의 내용(content)과 검색어(search)를 toLowerCase()로 변환하여 비교하도록 하였습니다.
import "./TodoList.css";
import TodoItem from "./TodoItem";
import { useState } from "react";
const TodoList = ({ todo, onUpdate, onDelete }) => {
const [search, setSearch] = useState("");
const onChangeSearch = (e) => {
setSearch(e.target.value);
};
const getSearchResult = () => {
return search === ""
? todo
: todo.filter((it) =>
it.content.toLowerCase().includes(search.toLowerCase())
);
};
return (
<div className="TodoList">
<h3>Todo List🍄</h3>
<input
value={search}
className="searchbar"
onChange={onChangeSearch}
type="text"
placeholder="검색어를 입력하세요"
/>
<div className="list__wrap">
{getSearchResult().map((it) => (
<TodoItem
onDelete={onDelete}
key={it.id}
{...it}
onUpdate={onUpdate}
/>
))}
</div>
</div>
);
};
export default TodoList;
TodoList 코드 가독성 향상 시키기 : useState를 대체할 수 있는 useReducer
useReducer란 컴포넌트에 새로운 State를 생성하는 React Hook 으로 모든 useState는 useReducer로 대체가 가능하며, useReducer를 이용하면 컴포넌트 내부에서만 관리하던 상태 관리 코드를 컴포넌트 외부로 분리하여 보다 깔끔한 코드 작성이 가능합니다.
※ 현재 사진 상으로는 코드가 그렇게 깔끔해보이나? 큰 차이가 없어 보일 수 있지만, 만약 더 큰 규모의 프로젝트에서 각 함수의 내부 실행 코드가 복잡해질 경우에, 내부 실행 코드를 컴포넌트 외부로 아예 정리할 수 있다는 점에서 reducer를 활용하는 것이 훨씬 가독성에 도움이 됩니다.
React 컴포넌트의 주된 역할은 UI를 렌더링하는 것인데, useState를 사용하여 상태관리를 하게되면, State 변수는 컴포넌트 내부에서만 접근이 가능하므로 컴포넌트 내부의 코드가 길고 복잡해질 수 밖에 없습니다. 따라서, 컴포넌트에서 렌더링 하는 UI가 무엇인지 한눈에 파악하기 어렵게 되고, 코드 가독성이 저하됩니다.
따라서 컴포넌트 외부에 상태 관리 코드를 정리할 필요를 느끼게 되는데, 이를 가능하게 하는 것이 useReducer라는 훅 입니다.
useReducer 사용법
const [state, dispatch] = useReducer(reducer, 0);
//const [state 변수, 상태 변화 촉발 함수] = 생성자(상태변화 함수, 초깃값)
Counter에 적용한 useReducer 적용 예시
import { useReducer } from "react";
// reducer : 변환기
// => 상태를 실제로 변환시키는 변환기 역할할
function reducer(state, action) {
console.log(state, action);
switch (action.type) {
case "INCREASE":
return state + action.data;
case "DECREASE":
return state - action.data;
default:
return state;
}
}
const Exam = () => {
// dispatch : 발송하다. 급송하다.
// => 상태 변화가 있어야한다는 사실을 알리는, 발송하는 함수
const [state, dispatch] = useReducer(reducer, 0);
const onClickPlus = () => {
// 인수 : 상태가 어떻게 변화되길 원하는지
// => 액션 객체
dispatch({
type: "INCREASE",
data: 1,
});
};
const onClickMinus = () => {
// 인수 : 상태가 어떻게 변화되길 원하는지
// => 액션 객체
dispatch({
type: "DECREASE",
data: 1,
});
};
return (
<div>
<h1>{state}</h1>
<button onClick={onClickPlus}>+</button>
<button onClick={onClickMinus}>-</button>
</div>
);
};
export default Exam;
useReducer를 적용한 TodoList의 App.jsx
import "./App.css";
import Header from "./component/Header";
import TodoEditor from "./component/TodoEditor";
import TodoList from "./component/TodoList";
import Exam from "./component/Exam";
import { useState, useRef, useReducer } from "react";
const mockTodo = [
{
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 reducer(state, action) {
switch (action.type) {
case "CREATE":
return [action.data, ...state];
case "UPDATE":
return state.map((item) =>
item.id === action.targetId ? { ...item, isDone: !item.isDone } : item
);
case "DELETE":
return state.filter((item) => item.id !== action.targetId);
default:
return state;
}
}
function App() {
const [todo, dispatch] = useReducer(reducer, mockTodo);
const idRef = useRef(3);
// const [todo, setTodo] = useState(mockTodo);
const onCreate = (content) => {
dispatch({
type: "CREATE",
data: {
id: idRef.current++,
isDone: false,
content: content,
date: new Date().getTime(),
},
});
};
const onUpdate = (targetId) => {
dispatch({
type: "UPDATE",
targetId: targetId,
});
};
const onDelete = (targetId) => {
dispatch({
type: "DELETE",
targetId: targetId,
});
};
return (
<div className="App">
<Header />
<TodoEditor onCreate={onCreate} />
<TodoList todo={todo} onUpdate={onUpdate} onDelete={onDelete} />
</div>
);
}
export default App;
🤔그럼, useReducer는 어떨 때 사용하는 것이 좋을까?
counter 앱처럼 굉장히 간단한 상태변화만 있다면 useState를 사용하면 되지만, todo처럼 배열 안에 복잡한 객체를 관리하는 경우에는 useReducer가 일반적으로 사용된다.
Q&A
1) 아래 코드에서 onDelete를 onClick에 직접 연결하지 않고, 중간 매개 함수를 만드는 이유?
import "./TodoItem.css";
import { useEffect } from "react";
const TodoItem = ({ id, content, isDone, createdDate, onUpdate, onDelete }) => {
useEffect(() => {
console.log("content", content);
}, [content]);
const onChangeCheckbox = () => {
onUpdate(id);
};
const onClickDeleteButton = () => {
onDelete(id);
};
return (
<div className="TodoItem">
<div>
<input onChange={onChangeCheckbox} checked={isDone} type="checkbox" />
<div className="listtitle">{content}</div>
</div>
<span>{new Date(createdDate).toLocaleDateString()}</span>
<div className="btncol">
<button onClick={onClickDeleteButton}>X</button>
</div>
</div>
);
};
export default TodoItem;
코드가 간단한 경우에는 아래 코드와 같이 직접 연결해도 좋지만, 나중에 로직이 복잡해질 것을 대비해서 분리하는 것이 좋습니다. 특히 컴포넌트가 커지거나 여러 곳에서 같은 삭제 로직을 재사용해야 할 때 유용합니다.
<button onClick={() => onDelete(id)}>X</button>
이유 1 ) 디버깅의 용이성
const onClickDeleteButton = () => {
// 여기에 디버깅용 console.log를 추가하기 쉽습니다
console.log(`Deleting item with id: ${id}`);
onDelete(id);
}
이유 2) 기능 확장성
const onClickDeleteButton = () => {
// 삭제 전 추가 로직을 넣기 쉽습니다
if(window.confirm("정말 삭제하시겠습니까?")) {
onDelete(id);
}
}
이유 3) 코드 가독성
// 이것보다는:
<button onClick={() => onDelete(id)}>X</button>
// 이게 더 명확합니다:
<button onClick={onClickDeleteButton}>X</button>
※ 단, 이렇게 사용하는 것엔 유의하자 !
onClick={onDelete(id)}이런 코드는 렌더링 시점에 즉시 함수를 실행해버리기 때문에 주의해야된다.
// 이렇게 작성하면:
<button onClick={onDelete(id)}>X</button>
// 컴포넌트가 렌더링될 때마다 onDelete(id)가 실행됩니다
// 버튼을 클릭하지 않았는데도 삭제가 발생합니다!
올바른 방법은
// 방법 1: 화살표 함수로 감싸기
<button onClick={() => onDelete(id)}>X</button>
// 방법 2: 별도의 핸들러 함수 만들기
const onClickDeleteButton = () => onDelete(id);
<button onClick={onClickDeleteButton}>X</button>
실제 테스트를 통해 확인해보기
// 이렇게 테스트해보세요:
const TestComponent = () => {
const handleDelete = (id) => {
console.log(`Deleting ${id}`);
};
console.log("컴포넌트 렌더링");
return (
<div>
{/* 잘못된 방법 - 렌더링할 때마다 즉시 실행됨 */}
<button onClick={handleDelete(1)}>잘못된 방법</button>
{/* 올바른 방법 - 클릭할 때만 실행됨 */}
<button onClick={() => handleDelete(1)}>올바른 방법</button>
</div>
);
};
2) map으로 요소 return 시 유의점
(1) => 뒤에 소괄호를 사용하는 경우
{getSearchResult().map((it) => (
<TodoItem key={it.id} {...it} />
))}
- 화살표 함수의 암시적 반환(implicit return) 사용
- 소괄호 () 안의 표현식이 자동으로 반환됨
- 간단한 반환값에 적합
- 더 간결한 문법
(2) => 뒤에 중괄호와 return 사용하는 경우
{getSearchResult().map((it) => {
return <TodoItem key={it.id} {...it} />
})}
- 명시적 return문 사용
- 중괄호 {} 사용으로 함수 본문 블록을 생성
- 반환 전에 추가 로직을 넣을 수 있음
- 여러 줄의 로직이 필요할 때 적합
(3) 정리
// 1. 단순 반환시 첫 번째 방식이 깔끔
{items.map(it => (
<TodoItem key={it.id} {...it} />
))}
// 2. 추가 로직이 필요할 때는 두 번째 방식
{items.map(it => {
const updatedProps = processProps(it);
console.log(it);
return <TodoItem key={it.id} {...updatedProps} />;
})}