한 입 크기로 잘라 먹는 리액트StudyReact
[React 🔯] 08. 프로젝트 - TodoList
이규현2026-03-08
![[React 🔯] 08. 프로젝트 - TodoList](/images/useBlog/react.jpg)
TodoList 만들어보기
두번째 프로젝트로는 TodoList를 만들어보자.
UI 구현
먼저 기능 없이 화면 구조만 잡아보자.
components/Header.jsx/css
import './Header.css';
const Header = () => {
return (
<div className="Header">
<h3>오늘은 🗓️</h3>
<h1>{new Date().toDateString()}</h1>
</div>
);
};
export default Header;
.Header > h1 {
color: rgb(37, 137, 255);
}
현재 날짜를 나타내는 헤더 컴포넌트다. new Date().toDateString()으로 오늘 날짜를 렌더링한다.
components/Editor.jsx/css
import './Editor.css';
const Editor = () => {
return (
<div className="Editor">
<input placeholder="새로운 Todo..." />
<button>추가</button>
</div>
);
};
export default Editor;
.Editor {
display: flex;
gap: 10px;
}
.Editor input {
flex: 1;
padding: 15px;
border: 1px solid rgb(220, 220, 220);
border-radius: 5px;
}
.Editor button {
cursor: pointer;
width: 80px;
border: none;
background-color: rgb(37, 147, 255);
color: white;
border-radius: 5px;
}
새로운 Todo를 입력하고 추가하는 컴포넌트다. 입력창과 추가 버튼이 flex로 배치된다.
components/List.jsx/css
import './List.css';
import TodoItem from './TodoItem';
const List = () => {
return (
<div className="List">
<h4>Todo List 📋</h4>
<input placeholder="검색어를 입력하세요" />
<div className="todos_wrapper">
<TodoItem />
<TodoItem />
<TodoItem />
</div>
</div>
);
};
export default List;
.List {
display: flex;
flex-direction: column;
gap: 20px;
}
.List > input {
width: 100%;
border: none;
border-bottom: 1px solid rgb(220, 220, 220);
padding: 15px 0px;
}
.List > input:focus {
outline: none;
border-bottom: 1px solid rgb(37, 147, 255);
}
.List .todos_wrapper {
display: flex;
flex-direction: column;
gap: 20px;
}
Todo 목록을 나타내고 검색 기능을 제공하는 컴포넌트다. 여러 TodoItem을 렌더링한다.
components/TodoItem.jsx/css
import './TodoItem.css';
const TodoItem = () => {
return (
<div className="TodoItem">
<input type="checkbox" />
<div className="content">Todo...</div>
<div classame="date">Date</div>
<button>삭제</button>
</div>
);
};
export default TodoItem;
.TodoItem {
display: flex;
align-items: center;
gap: 20px;
padding-bottom: 20px;
border-bottom: 1px solid rgb(240, 240, 240);
}
.TodoItem input {
width: 20px;
}
.TodoItem .content {
flex: 1;
}
.TodoItem .date {
font-size: 14px;
color: gray;
}
.TodoItem button {
cursor: pointer;
color: white;
background-color: red;
font-size: 14px;
border: none;
border-radius: 5px;
padding: 5px;
}
각각의 Todo 항목을 나타낸다. 체크박스, 내용, 날짜, 삭제 버튼으로 구성돼 있다.
App.jsx/css
import './App.css';
import Header from './components/Header';
import Editor from './components/Editor';
import List from './components/List';
function App() {
return (
<div className="App">
<Header />
<Editor />
<List />
</div>
);
}
export default App;
body {
padding: 20px;
}
.App {
width: 500px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 10px;
margin: 0 auto;
width: 400px;
}
모든 컴포넌트를 조합하는 루트 컴포넌트다.
UI 화면

기능 구현하기
이제 기능을 추가해보자. 먼저 상태 관리를 위해 useState와 useRef를 사용한다.
create- Todo 추가하기
App.jsx
import './App.css';
import { useState, useRef } from 'react';
import Header from './components/Header';
import Editor from './components/Editor';
import List from './components/List';
const mockDate = [
{
id: 0,
isDone: false,
content: 'React 공부하기',
date: new Date().getTime(0),
},
{
id: 1,
isDone: false,
content: 'Node.js 공부하기',
date: new Date().getTime(0),
},
{
id: 2,
isDone: false,
content: '사이드 프로젝트 기획안 작성하기',
date: new Date().getTime(),
},
];
function App() {
const [todos, setTodos] = useState(mockDate);
const idRef = useRef(3);
const onCreate = (content) => {
const newTodo = {
id: idRef.current++,
isDone: false,
content: content,
date: new Date().getTime(),
};
setTodos([newTodo, ...todos]);
};
return (
<div className="App">
<Header />
<Editor onCreate={onCreate} />
<List />
</div>
);
}
export default App;
- todos: Todo 목록을 상태로 관리한다.
- idRef: 새 Todo의 id를 관리하기 위해
useRef를 사용한다. (렌더링을 유발하지 않음) - onCreate: 새 Todo를 생성해서 배열의 앞에 추가한다.
components/Editor.jsx
import './Editor.css';
import { useState, useRef } from 'react';
const Editor = ({ onCreate }) => {
const [content, setContent] = useState('');
const contentRef = useRef();
const onChangeContent = (e) => {
setContent(e.target.value);
};
const onKeydown = (e) => {
if (e.keyCode === 13) {
onSubmit();
}
};
const onSubmit = () => {
if (content === '') {
contentRef.current.focus();
return;
}
onCreate(content);
setContent('');
};
return (
<div className="Editor">
<input
ref={contentRef}
value={content}
onKeyDown={onKeydown}
onChange={onChangeContent}
placeholder="새로운 Todo..."
/>
<button onClick={onSubmit}>추가</button>
</div>
);
};
export default Editor;
- content: 입력값을 상태로 관리한다.
- contentRef: 입력창에 포커스를 주기 위해 사용한다.
- onKeydown: Enter 키(keyCode 13)를 감지해서 Todo를 추가한다.
- onSubmit: 내용이 비어있지 않으면
onCreate함수를 호출하고, 입력값을 초기화한다.
결과 화면

TodoList 렌더링하기 + 검색하기
App.jsx
function App() {
const [todos, setTodos] = useState(mockDate);
const idRef = useRef(3);
const onCreate = (content) => {
const newTodo = {
id: idRef.current++,
isDone: false,
content: content,
date: new Date().getTime(),
};
setTodos([newTodo, ...todos]);
};
return (
<div className="App">
<Header />
<Editor onCreate={onCreate} />
<List todos={todos} />
</div>
);
}
todos 상태를 List 컴포넌트에 props로 전달한다.
List.jsx
import './List.css';
import TodoItem from './TodoItem';
import { useState } from 'react';
const List = ({ todos }) => {
const [search, setSearch] = useState('');
const onChangeSearch = (e) => {
setSearch(e.target.value);
};
const getFilteredDate = () => {
if (search === '') {
return todos;
}
return todos.filter((todo) => todo.content.toLowerCase().includes(search.toLowerCase()));
};
const filteredTodos = getFilteredDate();
return (
<div className="List">
<h4>Todo List 📋</h4>
<input value={search} onChange={onChangeSearch} placeholder="검색어를 입력하세요" />
<div className="todos_wrapper">
{filteredTodos.map((todo) => {
return <TodoItem key={todo.id} {...todo} />;
})}
</div>
</div>
);
};
export default List;
- search: 검색 입력값을 상태로 관리한다.
- getFilteredDate(): 검색어에 맞는 Todo만 필터링한다. (대소문자 구분 없음)
- filteredTodos.map(): 필터링된 Todo 목록을 렌더링한다.
- {...todo}: Todo 객체의 모든 속성을 props로 전달한다.
TodoItem.jsx
import './TodoItem.css';
const TodoItem = ({ id, isDone, content, date }) => {
return (
<div className="TodoItem">
<input readOnly checked={isDone} type="checkbox" />
<div className="content">{content}</div>
<div classame="date">{new Date(date).toLocaleDateString()}</div>
<button>삭제</button>
</div>
);
};
export default TodoItem;
- destructuring: props에서 필요한 값들을 바로 추출한다.
- new Date(date).toLocaleDateString(): 타임스탬프를 로컬 날짜 형식으로 변환한다.
TodoList 수정하기
App.jsx
function App() {
const [todos, setTodos] = useState(mockDate);
const idRef = useRef(3);
const onCreate = (content) => {
const newTodo = {
id: idRef.current++,
isDone: false,
content: content,
date: new Date().getTime(),
};
setTodos([newTodo, ...todos]);
};
const onUpdate = (targetId) => {
setTodos(
todos.map((todo) => (todo.id === targetId ? { ...todo, isDone: !todo.isDone } : todo)),
);
};
return (
<div className="App">
<Header />
<Editor onCreate={onCreate} />
<List todos={todos} onUpdate={onUpdate} />
</div>
);
}
- onUpdate: 특정 Todo의 isDone 상태를 토글한다. .map(): 모든 Todo를 순회하면서 해당 id의 Todo만 isDone 값을 반전시킨다.
components/List.jsx
const List = ({ todos, onUpdate, onDelete }) => {
// ... 이전 코드 생략
return (
<div className="List">
<h4>Todo List 📋</h4>
<input value={search} onChange={onChangeSearch} placeholder="검색어를 입력하세요" />
<div className="todos_wrapper">
{filteredTodos.map((todo) => {
return <TodoItem key={todo.id} {...todo} onUpdate={onUpdate} onDelete={onDelete} />;
})}
</div>
</div>
);
};
onUpdate 함수를 props로 받아서 TodoItem에 전달한다.
components/TodoItem.jsx
import './TodoItem.css';
const TodoItem = ({ id, isDone, content, date, onUpdate }) => {
const onChangeCheckbox = () => {
onUpdate(id);
};
return (
<div className="TodoItem">
<input onChange={onChangeCheckbox} checked={isDone} type="checkbox" />
<div className="content">{content}</div>
<div className="date">{new Date(date).toLocaleDateString()}</div>
<button>삭제</button>
</div>
);
};
export default TodoItem;
- onChangeCheckbox: 체크박스 변경 시
onUpdate함수를 호출한다. - checked={isDone}: 체크박스 상태를
isDone값과 동기화한다.
결과 화면


TodoList 삭제하기
App.jsx
function App() {
const [todos, setTodos] = useState(mockDate);
const idRef = useRef(3);
const onCreate = (content) => {
const newTodo = {
id: idRef.current++,
isDone: false,
content: content,
date: new Date().getTime(),
};
setTodos([newTodo, ...todos]);
};
const onUpdate = (targetId) => {
setTodos(
todos.map((todo) => (todo.id === targetId ? { ...todo, isDone: !todo.isDone } : todo)),
);
};
const onDelete = (targetId) => {
setTodos(todos.filter((todo) => todo.id !== targetId));
};
return (
<div className="App">
<Header />
<Editor onCreate={onCreate} />
<List todos={todos} onUpdate={onUpdate} onDelete={onDelete} />
</div>
);
}
- onDelete:
filter를 사용해서 해당 id를 가진 Todo를 제외한 새로운 배열을 반환한다.
components/List.jsx
import './List.css';
import TodoItem from './TodoItem';
import { useState } from 'react';
const List = ({ todos, onUpdate, onDelete }) => {
const [search, setSearch] = useState('');
const onChangeSearch = (e) => {
setSearch(e.target.value);
};
const getFilteredDate = () => {
if (search === '') {
return todos;
}
return todos.filter((todo) => todo.content.toLowerCase().includes(search.toLowerCase()));
};
const filteredTodos = getFilteredDate();
return (
<div className="List">
<h4>Todo List 📋</h4>
<input value={search} onChange={onChangeSearch} placeholder="검색어를 입력하세요" />
<div className="todos_wrapper">
{filteredTodos.map((todo) => {
return <TodoItem key={todo.id} {...todo} onUpdate={onUpdate} onDelete={onDelete} />;
})}
</div>
</div>
);
};
export default List;
onDelete 함수를 props로 받아서 TodoItem에 전달한다.
components/TodoItem.jsx
import './TodoItem.css';
const TodoItem = ({ id, isDone, content, date, onUpdate, onDelete }) => {
const onChangeCheckbox = () => {
onUpdate(id);
};
const onClickDeleteButton = () => {
onDelete(id);
};
return (
<div className="TodoItem">
<input onChange={onChangeCheckbox} checked={isDone} type="checkbox" />
<div className="content">{content}</div>
<div className="date">{new Date(date).toLocaleDateString()}</div>
<button onClick={onClickDeleteButton}>삭제</button>
</div>
);
};
export default TodoItem;
- onClickDeleteButton: 삭제 버튼 클릭 시
onDelete함수를 호출한다.
결과화면
