React Todo 앱 - 추가 기능 구현 (콜백 패턴, 불변성)
Todo 앱의 첫 번째 기능인 "추가"를 구현한다. 자식 컴포넌트에서 부모에게 데이터를 요청하는 콜백 패턴, 고유 id 생성, 그리고 React에서 배열 state를 다룰 때 반드시 지켜야 할 불변성 개념까지 다룬다.
TodoForm.jsx 만들기 - 입력값을 state로 관리
// src/TodoForm.jsx
import { useState } from "react";
function TodoForm() {
const [text, setText] = useState("");
return (
<form>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="할 일을 입력하세요"
/>
<button type="submit">추가</button>
</form>
);
}
export default TodoForm;
부모(App)에게 추가 요청하기 - 콜백 함수 전달
text state는 TodoForm 자신이 가지고 있지만, 진짜 할 일 목록(todos)은 부모인 App이 가지고 있다. 그래서 TodoForm은 새 할 일을 직접 추가할 수 없고, App으로부터 "추가해줘"라고 요청할 함수를 props로 받아서 호출해야 한다.
// src/TodoForm.jsx
import { useState } from "react";
function TodoForm({ onAdd }) {
const [text, setText] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
onAdd(text);
setText("");
};
return (
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="할 일을 입력하세요"
/>
<button type="submit">추가</button>
</form>
);
}
export default TodoForm;
제출 후에는 setText("")로 입력창을 다시 비워준다.
새로운 todo 객체 만들기 - 고유 id 부여
const newTodo = { id: Date.now(), text: text, completed: false };
왜 id를 1, 2, 3 ... 처럼 직접 정하지 않고 Date.now()를 쓸까? 할 일을 추가했다가 삭제하는 상황이 반복되면, 단순히 순서로 번호를 매기는 방식은 항목끼리 번호가 겹치기 쉽다. Date.now()는 "지금 이 순간의 시각"을 숫자로 돌려주는데, 같은 밀리초에 두 개를 동시에 만들지 않는 한 항상 서로 다른 값이 나오기 때문에 간단하면서도 안전하게 고유한 id를 만들 수 있다.
배열 state에 불변성을 지키며 항목 추가하기
배열에 값을 추가할 때 자바스크립트의 push()를 바로 떠올리기 쉽지만, React의 state를 다룰 때는 push()를 사용하면 안 된다.
// ❌ 이렇게 하면 안 됩니다
todos.push(newTodo);
setTodos(todos);
// ✅ 스프레드 연산자로 새 배열을 만들어야 합니다
setTodos([...todos, newTodo]);
push()는 기존 배열 자체를 그 자리에서 바꿔버린다. React는 state가 바뀌었는지 확인할 때 "이전 배열과 새 배열이 완전히 다른 객체인지"를 비교하는데, push()를 쓰면 배열의 내용은 바뀌어도 배열 자체(메모리 주소)는 그대로이기 때문에 React가 변화를 감지하지 못해 화면이 갱신되지 않을 수 있다.
[...todos, newTodo]처럼 스프레드 연산자(...)를 사용하면, 기존 todos의 내용을 그대로 복사한 완전히 새로운 배열을 만들어서 setTodos에 전달하기 때문에 React가 변화를 확실하게 감지한다.
⚠️ 이 원칙(불변성)은 배열뿐 아니라 객체 state를 다룰 때도 똑같이 적용된다. React state는 직접 수정하지 말고 항상 "새로운 값을 만들어서" 교체해야 한다.
App.jsx에서 전체 연결하기
// src/App.jsx
import { useState } from "react";
import TodoForm from "./TodoForm";
import TodoList from "./TodoList";
function App() {
const [todos, setTodos] = useState([
{ id: 1, text: "React 공부하기", completed: false },
{ id: 2, text: "장보기", completed: true },
{ id: 3, text: "운동하기", completed: false },
]);
const handleAdd = (text) => {
const newTodo = { id: Date.now(), text: text, completed: false };
setTodos([...todos, newTodo]);
};
return (
<div style={{ maxWidth: "400px", margin: "0 auto", padding: "24px" }}>
<h1>Todo 리스트</h1>
<TodoForm onAdd={handleAdd} />
<TodoList todos={todos} />
</div>
);
}
export default App;
실습 정리
실습 1) TodoForm.jsx 만들기 — text state와 input을 연결한 Controlled Component로 작성한다.
실습 2) onAdd 콜백 연결하기 — TodoForm이 onAdd props를 받도록 수정하고, 제출 시 onAdd(text)를 호출한 뒤 입력창을 비운다.
실습 3) 고유 id 부여하고 todos 배열에 추가하기 — App.jsx에 handleAdd 함수를 작성해서 새 할 일 객체를 만들고 스프레드 연산자로 todos에 추가한다.
실습 4) [선택] 빈 문자열 추가 방지하기
const handleSubmit = (e) => {
e.preventDefault();
if (text.trim() === "") {
return;
}
onAdd(text);
setText("");
};
마무리
- 자식이 부모의 데이터를 바꿔야 할 때는, 부모가 함수를 props로 내려주고 자식은 그 함수를 호출(
onAdd(text))하는 콜백 패턴을 사용한다. Date.now()는 간단하면서도 겹치지 않는 고유 id를 만드는 실용적인 방법이다.- React state(특히 배열/객체)는 절대 직접 수정하지 말고, 스프레드 연산자(
[...todos, newTodo])로 항상 새로운 값을 만들어서 교체해야 한다. 이게 바로 "불변성을 지킨다"는 의미다.
'FullStack FrontEnd' 카테고리의 다른 글
| React Todo 앱 - 완료 토글 기능 구현 (map, 객체 불변성) (0) | 2026.09.18 |
|---|---|
| React Todo 앱 - 삭제 기능 구현 (props drilling, filter) (0) | 2026.09.18 |
| React Todo 앱 - 컴포넌트 분리하기 (0) | 2026.09.17 |
| React Todo 앱 설계하기 - 컴포넌트 트리와 상태 위치 결정 (0) | 2026.09.17 |
| React 폼 다루기 - Controlled Component 정리 (0) | 2026.09.17 |