[React] 22. TODO LIST 앱 / Create - 투두 추가하기

서회정's avatar
Mar 05, 2026
[React] 22. TODO LIST 앱 / Create - 투두 추가하기

 
저번 단계에서 목업데이터 모델링까지 해보았다.
이 번에는 사용자가 투두를 추가할 수 있는 기능을 만들어보자.
 
import "./App.css"; import { useState } from "react"; import Header from "./components/Header"; import Editor from "./components/Editor"; import List from "./components/List"; const mockData = [ { id: 0, isComplete: false, content: "React 공부하기", date: new Date().getTime(), }, { id: 1, isComplete: false, content: "빨래하기기", date: new Date().getTime(), }, { id: 2, isComplete: false, content: "노래 연습하기", date: new Date().getTime(), }, ]; function App() { const [todos, setTodos] = useState(mockData); return ( <div className="App"> <Header /> <Editor onCreate={onCreate} /> <List /> </div> ); } export default App;
 

1. App에서 Todo 추가 함수 만들기

 
먼저 App 컴포넌트에 Todo를 추가하는 함수를 만들어보자.
이 함수는 content라는 매개변수를 받아
setTodos를 호출하여 todos 상태를 변경하는 역할을 한다.
 
함수 내부에서 새로운 Todo 객체(newTodo)를 만들어준다.
 
  • id → 임시로 0
  • isComplete → 초기값 false
  • content → 사용자가 입력한 값
  • date → 작성 시간 (new Date().getTime())
 
const onCreate = (content) => { const newTodo = { id: 0, isComplete: false, content: content, date: new Date().getTime(), }; };
 
기존 todosnewTodo만 넣으면 되기 때문에
todos.push(newTodo)를 사용할 수 있다고 착각할 수 있다.
 
하지만 React에서는 상태를 직접 수정하면 안 되고 반드시 상태 변경 함수(setState)를 사용해야 한다.
 
// 잘못된 예시 todos.push(newTodo);
 
따라서 setTodos를 사용해 새로운 배열을 만들어 상태를 변경한다.
 
배열에 기존에 있던 값을 유지한 채 가장 앞단에 추가된 정보를 넣는
형태이기 때문에 전개연산자로 이미 존재하고있는 값인 todos를
깔아주고, newTodo를 앞쪽에 넣어주자.
 
const onCreate = (content) => { const newTodo = { id: 0, isComplete: false, content: content, date: new Date().getTime(), }; setTodos([newTodo, ...todos]); };
 

2. Editor 컴포넌트와 추가 기능 연결하기

 
이제 Todo를 추가하는 함수 onCreate
Editor 컴포넌트에 props로 전달 한다.
 
return ( <div className="App"> <Header /> <Editor onCreate={onCreate} /> <List /> </div> );
 
Editor 컴포넌트에서는 props로 전달받은 onCreate
구조분해 할당으로 받아 사용한다.
 
그리고 Todo를 추가하는 동작을 수행할 onSubmit 함수를 만든다.
 
버튼이 클릭될 때 onSubmit이 실행되고
그 안에서 content를 매개변수로 받는 onCreate가 호출된다.
 
import "./Editor.css"; const Editor = ({ onCreate }) => { const onSubmit = () => { onCreate(content); }; return ( <div className="Editor"> <input type="text" placeholder="새로운 TODO..." /> <button onClick={onSubmit}>추가</button> </div> ); }; export default Editor;
 

3. 입력값을 state로 관리하고 전달하기

 
이제 input에 입력된 값을 상태로 관리해야 한다.
먼저 useState를 사용해 content 상태를 만든다.
 
const [content, setContent] = useState();
 
사용자의 입력이 바뀔 때마다 상태를 업데이트하기 위해
onChangeContent이벤트 핸들러를 만든다.
 
const onChangeContent = (e) => { setContent(e.target.value); };
 
그리고 input 태그에 다음 속성을 추가한다.
  • value={content}
  • onChange={onChangeContent}
 
<input value={content} onChange={onChangeContent} type="text" placeholder="새로운 TODO..." />
 
이렇게 하면 사용자의 입력값이 content state로 관리된다.
 
마지막으로 버튼을 클릭하면 onSubmit이 실행되고
onCreate(content)가 호출되어 App 컴포넌트의 todos 상태에
새로운 Todo가 추가된다.
 
import "./Editor.css"; import { useState } from "react"; const Editor = ({ onCreate }) => { const [content, setContent] = useState(); const onChangeContent = (e) => { setContent(e.target.value); }; const onSubmit = () => { onCreate(content); }; return ( <div className="Editor"> <input value={content} onChange={onChangeContent} type="text" placeholder="새로운 TODO..." /> <button onClick={onSubmit}>추가</button> </div> ); }; export default Editor;
 
여기까지 따라왔으면 리액트 개발자 도구를 통해
입력한 값이 App컴포넌트의 state에 문제없이 저장된다.
 
notion image
 
하지만 몇 가지 완성도가 떨어지는 부분이 있다.
이 부분도 보완하고 넘어가보도록 하자.
 

4. 완성도 높이기

 

1. id 고유값으로 관리

 
현재 추가된 값을 보면 id가 모두 동일하게 0으로 저장된다.
이는 우리가 Todo를 추가하는 함수를 만들 때 id를 상수로
넣었기 때문이다.
 
notion image
 
const newTodo = { // 문제 원인! id: 0, isComplete: false, content: content, date: new Date().getTime(), };
 
id는 Todo 각각이 가지는 고유값이 되어야 하기 때문에
이를 개선하기 위해서는 값을 저장하고 추가 로직이 수행될 때마다
1씩 더하는 로직을 만들면 된다.
 
먼저 값을 변수에 저장하고 관리하기위해 useRef훅을 사용하여
새로운 ref를 만들어주자.
 
목업 데이터에 사용된 id가 2까지 있기 때문에
우리는 isRef의 초기값을 3으로 저장하자.
 
const isRef = useRef(3);
 
그리고 newTodo에서 상수로 입력된 값을 지우고
isRef에 저장된 current값을 1씩 더해주는 증감식으로 변경해주면 해결된다.
 
const newTodo = { id: isRef.current++, isComplete: false, content: content, date: new Date().getTime(), };
 
notion image
 

2. 투두 추가 후 input 비우기

 
투두 추가 후에 input창이 비워지지 않는 현재 상황은
사용자가 느끼기에 동작이 잘 수행되었는지 헷갈리게 만들 수 있다.
 
따라서 추가 버튼을 클릭한 뒤에 input안에 있는 내용이
비워질 수 있도록 수정해보도록 하자.
 
 
notion image
 
input이 비워지는 시점은 onCreate함수가 끝나는 시점이다.
 
따라서 inSubmit함수 내부에서 가장 마지막 순서에
setContent라는 상태 변경 함수를 호출하여 빈 문자열을
인수로 넣으면 Todo가 추가된 후에 content의 상태가 “”
초기화되며 인풋창을 비울 수 있다.
 
const onSubmit = () => { onCreate(content); // setContent(""); };
 

3. 빈 문자열 제출 막기

 
사용자가 아무것도 입력하지 않고 제출을 눌렀을 때도
상태가 저장된다면 데이터 관리가 복잡해질 수 있다.
 
따라서 content의 상태가 빈 문자열일 때 제출하는 로직을 막고
제출되지 않았다고 다시 input창을 포커스해주어 사용자에게
알려주는 로직까지 추가해보자.
 
notion image
 
추가 버튼을 누를 때 빈 문자열을 감지하고 동작을
막아야하기 때문에 추가 버튼 로직이 실행되는 onSubmit함수 내부에서
조건문을 사용하여 content가 빈 문자열일 때 onCreate가 실행되지 않고
onSubmit을 강제로 종료하는 return문을 작성하자.
 
const onSubmit = () => { if (content === "") { return; } onCreate(content); setContent(""); };
 
다음으로 빈 문자열을 제출했을 때 input에 focus되는
로직도 추가해보자.
 
DOM에 접근하고 조작해야하기 때문에 ref를 사용해야한다.
 
const contentRef = useRef();
 
input의 ref 속성에 연결해주자.
 
<input ref={contentRef} value={content} onChange={onChangeContent} type="text" placeholder="새로운 TODO..." />
 
아까 작성해둔 조건문 내에서 focus메서드를 호출하자.
 
const onSubmit = () => { if (content === "") { contentRef.current.focus(); return; } onCreate(content); setContent(""); };
 
notion image
 

4. 엔터 키로 입력 추가하기

 
엔터 키로 input에 입력된 값을 제출하기 위해서는
input태그의 onKeyDown속성을 사용할 수 있다.
 
이 속성은 사용자가 키보드를 눌렀을 때 발생하는 이벤트를
넣어줄 수 있다.
 
inputonKeyDown속성으로 onKeyDown이벤트 핸들러를
넣어주자.
 
<input ref={contentRef} value={content} onKeyDown={onKeyDown} onChange={onChangeContent} type="text" placeholder="새로운 TODO..." />
 
그리고 onKeyDown 이벤트 핸들러를 만들면 되는데,
매개변수로 e를 받고 keyCode가 13일때 onSubmit 함수가
호출되도록 로직을 완성할 수 있다.
 
13은 Enter 키의 번호이다!
 
const onKeyDown = (e) => { if (e.keyCode === 13) { onSubmit(); } };
 
Share article

clubnerdy