실습 목표 : Todo List에서 Todo 추가, 삭제 기능 만들기
우선 추가 기능부터 만들어보자.
Step 1. 두 종류의 State가 필요하다
const [input, setInput] = useState(""); // 현재 input에 적혀 있는 글
const [todos, setTodos] = useState([]); // Todo 목록으로 처음에는 Todo가 없으므로 빈 배열로 초기화
Step 2. input 연결하기
import { useState } from "react";
function App() {
const [input, setInput] = useState("");
const [todos, setTodos] = useState([]);
function handleChange(event) {
setInput(event.target.value);
}
return (
<div>
<h1>Todo List</h1>
<input
value={input}
onChange={handleChange}
placeholder="할 일을 입력하세요"
/>
</div>
);
}
export default App;
사용자가 <input> 태그에 입력한 내용은 input State가 된다.
Step 3. Todo 데이터의 구조
Todo 데이터는 문자열이 아닌 객체로 다룰 것이다. 왜냐하면 Todo 데이터를 고유 id나 완료 여부를 작성 내용과 함께 관리하기 좋기 때문이다.
따라서 Todo 데이터는 앞으로 다음과 같은 구조가 된다.
[
{
id: 1,
text: "React 공부하기"
},
{
id: 2,
text: "운동하기"
}
]
Step 4. 추가 버튼 만들기
먼저 버튼을 추가한다.
<button onClick={handleAdd}>
추가
</button>
그리고 handleAdd 함수를 만든다.
function handleAdd() {
const newTodo = {
id: Date.now(),
text: input,
};
}
Date.now()는 특정 시점부터 현재까지 경과한 시간을 숫자로 반환하는 정적 메서드로 주로 고유한 값을 얻기 위해 사용한다.
여기서는 고유한 id를 만들기 위해 사용한다.
Step 5. 배열에 Todo 추가하기
React가 컴포넌트를 리렌더링하기 위해선 State의 메모리 주소가 변경되어야 한다. 따라서 배열에 추가한 Todo를 화면에 표시하기 위해 스프레드 문법을 이용해 Todo를 추가한 새로운 배열을 생성한다.
function handleAdd() {
const newTodo = {
id: Date.now(),
text: input,
};
setTodos([...todos, newTodo]);
setInput("");
}
Step 6. Todo 목록 출력하기
map()을 사용하여 배열의 각 객체를 JSX 코드로 변환한다.
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.text}
</li>
))}
</ul>
현재까지의 전체 코드는 다음과 같다.
import { useState } from "react";
function App() {
const [input, setInput] = useState("");
const [todos, setTodos] = useState([]);
function handleChange(event) {
setInput(event.target.value);
}
function handleAdd() {
const newTodo = {
id: Date.now(),
text: input,
};
setTodos([...todos, newTodo]);
setInput("");
}
return (
<div>
<h1>Todo List</h1>
<input
value={input}
onChange={handleChange}
placeholder="할 일을 입력하세요"
/>
<button onClick={handleAdd}>추가</button>
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}
export default App;
브라우저를 통해 확인해보면

정상적으로 Todo 목록이 출력되고 Todo 요소가 추가되는 것을 볼 수 있다.
Step 7. form 태그를 사용하여 Enter로 Todo를 추가해 보자.
input 태그와 button 태그를 form 태그로 감싼다.
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleChange}
placeholder="할 일을 입력하세요"
/>
<button type="submit">추가</button>
</form>
그리고 form을 사용하므로 기존의 handleAdd 함수를 다음과 같이 변경한다.
function handleSubmit(event) {
// React가 화면을 관리하고 있으므로 브라우저의 기본 동작(페이지 새로고침)을 막는다.
// 현재 학습 단계에선 React에서 form submit 처리하기 위해 preventDefault()를 사용한다고 이해한다.
event.preventDefault();
const newTodo = {
id: Date.now(),
text: input,
};
setTodos([...todos, newTodo]);
setInput("");
}
이제 입력한 뒤 Enter를 눌러도 Todo가 추가된다.
그런데 현재 빈 Todo를 추가하거나 공백만을 입력한 Todo가 추가되는 문제가 있다.
이를 해결하기 위해
handleSubmit 함수를 다음과 같이 수정한다.
function handleSubmit(event) {
event.preventDefault();
if (input.trim() === "") {
return;
}
const newTodo = {
id: Date.now(),
text: input.trim(),
};
setTodos([...todos, newTodo]);
setInput("");
}
여기서 trim()은 문자열의 양 사이드에 있는 모든 공백을 제거한 새 문자열을 반환하는 메서드이다.
예를 들어, " A B " 문자열에 trim()을 적용하면 "A B"가 반환된다.
전체 코드:
import { useState } from "react";
function App() {
const [input, setInput] = useState("");
const [todos, setTodos] = useState([]);
function handleChange(event) {
setInput(event.target.value);
}
function handleSubmit(event) {
event.preventDefault();
if (input.trim() === "") {
return;
}
const newTodo = {
id: Date.now(),
text: input.trim(),
};
setTodos([...todos, newTodo]);
setInput("");
}
return (
<div>
<h1>Todo List</h1>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleChange}
placeholder="할 일을 입력하세요"
/>
<button type="submit">추가</button>
</form>
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}
export default App;
이제 삭제 기능을 만들어보자.
Step 8. filter()
삭제 기능을 구현하기위해 JS의 filter()를 사용한다.
filter()는 조건이 true인 것만 남긴다.
const numbers = [1, 2, 3, 4];
const result = numbers.filter((number) => number !== 2);
// 결과 : [1, 3, 4]
"id가 2가 아닌 Todo만 남겨라"라는 뜻으로 결과적으로 id 2에 해당하는 요소가 삭제된다.
Step 9. 삭제 버튼 만들기
각 Todo에 버튼을 추가한다.
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.text}
<button onClick={() => handleDelete(todo.id)}>삭제</button>
</li>
))}
</ul>
어떤 Todo를 삭제할지 알아야 하므로 Todo의 id를 삭제 함수에 전달한다.
삭제 함수:
function handleDelete(id) {
setTodos(todos.filter((todo) => todo.id !== id));
}
지금까지의 전체 코드:
import { useState } from "react";
function App() {
const [input, setInput] = useState("");
const [todos, setTodos] = useState([]);
function handleChange(event) {
setInput(event.target.value);
}
function handleSubmit(event) {
event.preventDefault();
if (input.trim() === "") {
return;
}
const newTodo = {
id: Date.now(),
text: input.trim(),
};
setTodos([...todos, newTodo]);
setInput("");
}
function handleDelete(id) {
setTodos(todos.filter((todo) => todo.id !== id));
}
return (
<div>
<h1>Todo List</h1>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleChange}
placeholder="할 일을 입력하세요"
/>
<button type="submit">추가</button>
</form>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.text}
<button onClick={() => handleDelete(todo.id)}>삭제</button>
</li>
))}
</ul>
</div>
);
}
export default App;
Step 10. 더 React답게 State 변경하기
지금까지 다음과 같이 작성했다.
setTodos([...todos, newTodo]);
그러나 State의 이전 값을 기반으로 새로운 State를 만드는 경우에 다음 형태를 익혀두는 것이 좋다.
setTodos((prevTodos) => [...prevTodos, newTodo]);
여기서 prevTodos는 업데이트되기 직전의 최신 todos 상태 배열을 자동으로 받아온다.
현재 학습 단계에선
setState((prevState) => newState);
와 같은 형태로 작성하는 경우, PrevState는 업데이트되기 직전의 상태 배열을 자동으로 받아온다고 이해하면 된다.
다듬은 전체 코드:
import { useState } from "react";
function App() {
const [input, setInput] = useState("");
const [todos, setTodos] = useState([]);
function handleChange(event) {
setInput(event.target.value);
}
function handleSubmit(event) {
event.preventDefault();
if (input.trim() === "") {
return;
}
const newTodo = {
id: Date.now(),
text: input.trim(),
};
setTodos((prevTodos) => [...prevTodos, newTodo]);
setInput("");
}
function handleDelete(id) {
setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
}
return (
<div>
<h1>Todo List</h1>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleChange}
placeholder="할 일을 입력하세요"
/>
<button type="submit">추가</button>
</form>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.text}
<button onClick={() => handleDelete(todo.id)}>삭제</button>
</li>
))}
</ul>
</div>
);
}
export default App;
중요한 건 데이터 흐름을 이해하는 것이다.
[추가]
input
↓
handleSubmit
↓
newTodo
↓
setTodos
↓
todos 배열 변경
↓
map
↓
화면에 새 Todo 표시
[삭제]
삭제 클릭
↓
todo.id 전달
↓
handleDelete
↓
filter
↓
해당 Todo가 빠진 새 배열
↓
setTodos
↓
화면에서 사라짐
이번 학습 단계에서 가장 중요한 세 가지 코드
1. State 배열에 무언가를 추가한다.
setTodos((prevTodos) => [...prevTodos, newTodo]);
2. State 배열에서 무언가를 삭제한다.
setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
3. State 배열을 화면에 표시한다.
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.text}
</li>
))}
</ul>
작은 실습
현재 Todo에 다음 기능을 하나 추가해보자.
Todo 개수: 3개
즉 화면에
Todo List
Todo 개수: 3개
라고 표시하는 것이다.
답안
<h1>Todo List</h1>
<p>Todo 개수: {todos.length}개</p>
여기서 중요한 점은 Todo 개수를 별도의 State로 만들지 않았다는 것이다. 다음과 같은 State는 필요 없다.
const [todoCount, setTodoCount] = useState(0);
왜냐하면 개수는 이미 다음과 같이 구할 수 있기 때문이다.
todos.length
즉, 이미 가지고 있는 State로 구할 수 있는 값이라면 별도의 State를 만들지 않는다.
'개발 > React' 카테고리의 다른 글
| [React] 컴포넌트 분리와 State 끌어올리기 (0) | 2026.09.22 |
|---|---|
| [React] Todo List 만들기 실습 (2) [배열 항목 수정] (0) | 2026.09.20 |
| [React] 조건부 렌더링과 리스트 렌더링 (0) | 2026.09.15 |
| [React] Input과 State (0) | 2026.09.15 |
| [React] Event와 State (0) | 2026.09.14 |