
TodoList 컴포넌트의 기능이면서 CURD의 두 번째 요소인 Read 기능을 만들어보자.
Read 기능을 이용하면 배열에 저장한 여러 할 일 아이템을 반복해서 렌더링할 수 있다.
App 컴포넌트의 State 변수 todo에는 배열 형태로 여러 개의 할 일 아이템이 저장되어 있다. 따라서 배열 todo를 TodoList 컴포넌트에 Props로 전달한다.
(...)
function App() {
(...)
return (
<div className="App">
<Header />
<TodoEditor onCreate={onCreate} />
<TodoList todo={todo} />
</div>
);
}
export default App;
src/App.js
TodoList 컴포넌트에서는 App에서 Props로 전달된 todo를 리스트로 렌더링해야 한다. 이때 배열 메서드 map을 사용한다.
map을 이용하면 HTML 또는 컴포넌트를 순회하면서 매 요소를 반복하여 렌더링 할 수 있다.
TodoLisst 컴포넌트에서 배열 메서드 map을 이용할 수 있다.
import TodoItem from "./TodoItem";
import "./TodoList.css";
const TodoList = ({ todo }) => {
return (
<div className="TodoList">
<h4>Todo List 🌱</h4>
<input className="searchbar" placeholder="검색어를 입력하세요" />
<div className="list_wrapper">
{todo.map((it) => (
<div>{it.content}</div>
))}
</div>
</div>
);
};
export default TodoList;
src/component/TodoList.js
위 코드를 살펴보면,
Props를 구조 분해 할당하고, map 메서드를 이용하여 배열 todo의 모든 요소를 순차적으로 순회하며 HTML로 변환한다. 이 식의 결과값은 배열 todo에 저장된 모든 할 일을 <div> 태그로 감싼 것과 같다.
배열을 이용해 컴포넌트를 반복해 렌더링한다.
import TodoItem from "./TodoItem";
import "./TodoList.css";
const TodoList = ({ todo }) => {
return (
<div className="TodoList">
<h4>Todo List 🌱</h4>
<input className="searchbar" placeholder="검색어를 입력하세요" />
<div className="list_wrapper">
{todo.map((it) => (
<TodoItem {...it} />
))}
</div>
</div>
);
};
export default TodoList;
src/component/TodoList.js
TodoItem 컴포넌트에 전달된 Props를 이 컴포넌트에서 사용할 수 있도록 수정한다.
import "./TodoItem.css";
const TodoItem = ({ id, content, isDone, createdDate }) => {
return (
<div className="TodoItem">
<div className="checkbox_col">
<input checked={isDone} type="checkbox" />
</div>
<div className="title_col">{content}</div>
<div className="date_col">
{new Date(createdDate).toLocaleDateString()}
</div>
<div className="btn_col">
<button>삭제</button>
</div>
</div>
);
};
export default TodoItem;
src/component/TodoItem.js
저장한 다음, 할 일 입력 폼에 '독서하기'라는 새 아이템을 추가해 결과를 확인한다.

개발자 도구의 콘솔에서 앞서 확인한 것과 같은 여러 경고 메시지가 출력되는 걸 볼 수 있다.
Each child in a list should have a unique "key" prop.
경고 메시지를 직역하면 " 리스트의 모든 자식 요소는 key라는 고유한 prop을 반드시 가져야한다" 라고 해석할 수 있다.
다음으로,
You provided a 'checked' prop to a form without an 'onChange' handler …
이 메시지는 TodoItem 컴포넌트가 체크박스 입력 폼에 onChange 이벤트 핸들러를 설정하지 않아서 발생한 경고로 나중에 이 체크박스에 onChange 이벤트 핸들러를 설정할 예정이므로 지금은 무시해도 된다.
key는 리스트에서 각각의 컴포넌트를 구분하기 위해 사용하는 값이다.
리액트는 리스트에서 특정 컴포넌트를 수정, 추가, 삭제하는 경우 key로 어떤 컴포넌트를 업데이트할지 결정한다.
앞서 App 컴포넌트의 할 일 아이템 생성 과정에서 Ref 객체를 이용해 아이템마다 고유 id를 갖도록 만들었는데, id를 key로 전달하면 문제를 해결할 수 있다.
TodoList.js는 다음과 같이 수정한다.
import TodoItem from "./TodoItem";
import "./TodoList.css";
const TodoList = ({ todo }) => {
return (
<div className="TodoList">
(...)
<div className="list_wrapper">
{todo.map((it) => (
<TodoItem key={it.id} {...it} />
))}
</div>
</div>
);
};
export default TodoList;
이번에는 TodoList 컴포넌트에서 특정 할 일을 검색하는 기능을 만들자.
먼저 사용자가 입력하는 검색어를 처리할 State 변수를 만든 다음, 검색 폼에서 사용자가 입력한 내용을 처리하는 기능을 만든다.
TodoList.js는 다음과 같이 수정한다.
import { useState } from "react";
(...)
const TodoList = ({ todo }) => {
const [search, setSearch] = useState("");
const onChangeSearch = (e) => {
setSearch(e.target.value);
};
return (
<div className="TodoList">
<h4>Todo List 🌱</h4>
<input
value={search}
onChange={onChangeSearch}
className="searchbar"
placeholder="검색어를 입력하세요"
/>
<div className="list_wrapper">
{todo.map((it) => (
<TodoItem key={it.id} {...it} />
))}
</div>
</div>
);
};
export default TodoList;
사용자가 입력한 검색어에 따라 할 일 아이템을 필터링하는 기능을 만든다.
(...)
const TodoList = ({ todo }) => {
(...)
const getSearchResult = () => {
return search === ""
? todo
: todo.filter((it) => it.content.includes(search));
};
return (
<div className="TodoList">
(...)
<div className="list_wrapper">
{getSearchResult().map((it) => (
<TodoItem key={it.id} {...it} />
))}
</div>
</div>
);
};
export default TodoList;
이제 CURD의 세 번쨰 기능인 Update를 만들어보자

할 일 수정을 위해 함수 onUpdate를 만들어보자. 이 함수는 TodoItem 컴포넌트까지 전달되어야한다.
(...)
function App() {
(...)
const onUpdate = (targetId) => {
setTodo(
todo.map((it) => {
if (it.id === targetId) {
return {
...it,
isDone: !it.isDone,
};
} else {
return it;
}
})
);
};
return (
<div className="App">
<Header />
<TodoEditor onCreate={onCreate} />
<TodoList todo={todo} onUpdate={onUpdate} />
</div>
);
}
export default App;
src/App.js
위 코드를 보면 App에 할 일 수정 함수 onUpdate를 생성하고 TodoList 컴포넌트에 Props로 전달한다.
이제 TodoList에서 TodoItem 컴포넌트에 함수 onUpdate를 전달해야 한다. TodoList 컴포넌트를 다음과 같이 수정한다.
(...)
const TodoList = ({ todo, onUpdate }) => {
(...)
return (
<div className="TodoList">
(...)
<div className="list_wrapper">
{getSearchResult().map((it) => (
<TodoItem key={it.id} {...it} onUpdate={onUpdate} />
))}
</div>
</div>
);
};
export default TodoList;
리액트 컴포넌트는 바로 한 단계 아래의 자식 컴포넌트에만 데이터를 전달할 수 있다. 따라서 한 단계 이상 떨어져 있는 자식 컴포넌트에 데이터를 전달하려면, 현재로서는 전달에 전달을 반복해야한다.
따라서 TodoList 자신은 해당 함수를 사용하지 않지만, TodoItem 컴포넌트에 함수 onUpdate를 전달해야하므로 Props로 받아 다시 전달하는 일종의 매개 역할을 수행한다.
이는 리액트에서 State와 Props를 사용할 때 흔히 발생하는 일로, 이런 상황을 "Props가 마치 땅을 파고 내려가는 것 같다"고 하며 Props Drilling이라고 한다.
TodoItem을 다음과 같이 수정한다.
import "./TodoItem.css";
const TodoItem = ({ id, content, isDone, createdDate, onUpdate }) => {
const onChangeCheckbox = () => {
onUpdate(id);
};
return (
<div className="TodoItem">
<div className="checkbox_col">
<input onChange={onChangeCheckbox}
checked={isDone} type="checkbox" />
</div>
<div className="title_col">{content}</div>
<div className="date_col">
{new Date(createdDate).toLocaleDateString()}
</div>
<div className="btn_col">
<button>삭제</button>
</div>
</div>
);
};
export default TodoItem;
Reac 공부하기의 체크박스를 틱 했을 때 완료 여부를 표시하는 체크 표시가 나타나는지 확인하고,
[Components]탭을 열고 TodoItem 컴포넌트에서 이 아이템의 isDone 프로퍼티가 true로 변경되는지도 확인한다.

마지막으로 CURD의 Delete 기능을 구현해 할 일 아이템을 삭제하자.

App 컴포넌트에서 할 일을 삭제하는 함수 onDelete를 만든다.
(...)
function App() {
(...)
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;
src/App.js
TodoList는 Props로 받은 함수 onDelete를 다시 TodoItem 컴포넌트에 전달해야 한다.
(...)
const TodoList = ({ todo, onUpdate, onDelete }) => {
(...)
return (
<div className="TodoList">
(...)
<div className="list_wrapper">
{getSearchResult().map((it) => (
<TodoItem
key={it.id}
{...it}
onUpdate={onUpdate}
onDelete={onDelete}
/>
))}
</div>
</div>
);
};
export default TodoList;
src/Component/TodoList.js
TodoItem에서 <삭제> 버튼을 클릭하면 함수 onDelete를 호출하도록 구현하자.
import "./TodoItem.css";
const TodoItem = ({ id, content, isDone, createdDate, onUpdate, onDelete }) => {
const onChangeCheckbox = () => {
onUpdate(id);
};
const onClickDelete = () => {
onDelete(id);
};
return (
<div className="TodoItem">
<div className="checkbox_col">
<input onChange={onChangeCheckbox} checked={isDone} type="checkbox" />
</div>
<div className="title_col">{content}</div>
<div className="date_col">
{new Date(createdDate).toLocaleDateString()}
</div>
<div className="btn_col">
<button onClick={onClickDelete}>삭제</button>
</div>
</div>
);
};
export default TodoItem;
src/Component/TodoItem.js

여기까지 할 일 관리 앱을 모두 완성했다. 그러나 Props Drilling, 최적화 문제, 분리되지 않은 상태 관리 등 리액트서비스와 관련해 알아야 할 내용들이 더 있다. 이 개념들은 다음 과정에서 더 자세히 다룰 것이다.
출처 : 이정환, 『한 입 크기로 잘라먹는 리액트』, 프로그래밍인사이트(2023)
Corner React.js
Editor: jyeon
| [React.js] project 3 [감정 일기장] 만들기 (0) | 2026.01.09 |
|---|---|
| [React.js] 9장.컴포넌트 트리에 데이터 공급하기 (0) | 2026.01.02 |
| [React.js] project 2[할일 관리] 앱 만들기 (프로젝트 준비하기 ~ Create: 할 일 추가하기) (0) | 2025.12.19 |
| [React.js] UseReducer와 상태 관리 -최적화 (0) | 2025.11.28 |
| [React.js] 리액트를 다루는 기술-8장.hooks (0) | 2025.11.21 |