
1. 프로젝트 준비하기
1. 요구사항 분석하기
1. 오늘의 날짜를 요일, 월, 일, 연도순으로 표시합니다.
2. 할 일을 작성하는 입력 폼이 있고, <추가> 버튼을 클릭하면 할 일 아이템을 생성합니다.
3. [할 일 관리] 앱은 생성한 아이템을 페이지 하단에 리스트로 표시하는데, 키워드 검색으로 원하는 할 일만 추출할 수 있습니다.
4. 리스트로 표시하는 낱낱의 할 일 아이템은 일을 마쳤는지 여부를 표시하는 체크박스, 아이템 이름. 등록 날짜 그리고 <삭제>버튼으로 이루어져 있습니다.

2. 리액트 앱 만들기
1. npx create-react-app.
2. src/App.test.js, src/logo.svg, src/reportWebVitals.js, setupTest.js 삭제
3. 코드입략
// src/App.js
// 1. 리액트 앱만들기
import "./App.css";
function App() {
return <div className="App"></div>;
}
export default App;
// src/index.js
// 1. 리액트 앱만들기
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
4. npm run start (빈 페이지 출력)
2. UI 구현하기
1. 페이지 레이아웃 만들기
// src/App.js
//2.1.1 페이지 레이아웃 만들기
import "./App.css";
function App() {
return (
<div className="App">
<h2>헬로 리액트</h2>
</div>
);
}
export default App;
// src/index.js
// 2. UI구현하기
//2.1.1 페이지 레이아웃 만들기
import "./App.css";
function App() {
return (
<div className="App">
<h2>헬로 리액트</h2>
</div>
);
}
export default App;
/*
index.css
2. UI구현하기
2.1.1 페이지 레이아웃 만들기
margin은 여밷으로 외부 여백이 전부 사라진 상태
*/
body {
margin: 0px;
}
/*
App.css
2. UI구현하기
2.1.1 페이지 레이아웃 만들기
틀잡기
*/
.App {
max-width: 500px;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
padding: 20px;
border: 1px solid gray;
}

자식 컨포넌트 배치
// App.js
//2.1.2 자식 컨포넌트 배치하기
import "./App.css";
function App() {
return (
<div className="App">
<div>Header</div>
<div>Todo Editor</div>
<div>Todo List</div>
</div>
);
}
export default App;
/*
App.css
//2.1.2 자식 컨포넌트 배치하기
*/
.App {
max-width: 500px;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
padding: 20px;
border: 1px solid gray;
display: flex;
flex-direction: column;
gap: 30px;
}

2. Header 컴포넌트 만들기
// App.js
//2.2.1 Header 컴포넌트 만들기
import "./App.css";
import Header from "./component/Header";
function App() {
return (
<div className="App">
<Header />
<div>Todo Editor</div>
<div>Todo List</div>
</div>
);
}
export default App;//
// src/component/Header.js
//2. UI구현하기
//2.2.1 Header 컴포넌트 만들기
const Header = () => {
return <div className="Header">Header Component</div>;
};
export default Header;

Header 컴포넌트가 오늘의 날짜를 렌더링
// src/component/Header.js
//2.2.2 오늘의 날짜 렌더링
import "./Header.css";
const Header = () => {
return (
<div className="Header">
<h3>오늘은 📅</h3>
<h1>{new Date().toDateString()}</h1>
</div>
);
};
export default Header;
/*
Header.css
2.2.2 오늘의 날짜 렌더링
*/
.Header h1 {
margin-bottom: 0px;
color: #1f93ff;
}

3. TodoEditor 컨포넌트 만들기
// App.js
// 2.3 TodoEditor 컨포넌트 만들기
// 2.3.1 TodoList컴포넌트 만들기
import "./App.css";
import Header from "./component/Header";
import TodoEditor from "./component/TodoEditor";
function App() {
return (
<div className="App">
<Header />
<TodoEditor />
<div>Todo List</div>
</div>
);
}
export default App;
// src/component/TodoEditor.js
// 2.3 TodoEditor 컨포넌트 만들기
// 2.3.1 TodoList컴포넌트 만들기
import "./TodoEditor.css";
const TodoEditor = () => {
return (
<div className="TodoEditor">
<h4>새로운 Todo 작성하기 ✏️ </h4>
<div className="editor_wrapper">
<input placeholder="새로운 Todo..." />
<button>추가</button>
</div>
</div>
);
};
export default TodoEditor;
/*
// 2.3 TodoEditor 컨포넌트 만들기
// 2.3.1 TodoList컴포넌트 만들기
*/
.TodoEditor .editor_wrapper {
width: 100%;
display: flex;
gap: 10px;
}
.TodoEditor input {
flex: 1;
box-sizing: border-box;
border: 1px solid rgb(220, 220, 220);
border-radius: 5px;
padding: 15px;
}
.TodoEditor input:focus {
outline: none;
border: 1px solid #1f93ff;
}
.TodoEditor button {
cursor: pointer;
width: 80px;
border: none;
background-color: #1f93ff;
color: white;
border-radius: 5px;
}

4. TodoList, TodoItem 컨포넌트 만들기
(1) TodoList컴포넌트 만들기
// App.js
// 2.4 TodoList, TodoItem 컨포넌트 만들기
// 2.4.1 TodoList컴포넌트 만들기
import "./App.css";
import Header from "./component/Header";
import TodoEditor from "./component/TodoEditor";
import TodoList from "./component/TodoList";
function App() {
return (
<div className="App">
<Header />
<TodoEditor />
<TodoList />
</div>
);
}
export default App;
// src/component/TodoList.js
// 2.4 TodoList, TodoItem 컨포넌트 만들기
// 2.4.1 TodoList컴포넌트 만들기
import "./TodoList.css";
const TodoList = () => {
return (
<div className="TodoList">
<h4>Todo List 🌱</h4>
<input className="searchbar" placeholder="검색어를 입력하세요" />
</div>
);
};
export default TodoList;
/*
// src/component/TodoList.css
// 2.4 TodoList, TodoItem 컨포넌트 만들기
// 2.4.1 TodoList컴포넌트 만들기
*/
/* 검색 폼에 스타일 적용 */
.TodoList .searchbar {
margin-bottom: 20px;
width: 100%;
border: none;
border-bottom: 1px solid rgb(220, 220, 220);
box-sizing: border-box;
padding-top: 15px;
padding-bottom: 15px;
}
/* 검색 폼을 클릭했을 때의 스타일 적용 */
.TodoList .searchbar:focus {
outline: none;
border-bottom: 1px solid #1f93ff;
}

(2) TodoItem 컨포넌트 만들기
// src/component/TodoItem.js
// 2.4 TodoList, TodoItem 컨포넌트 만들기
// 2.4.2TodoItem 컨포넌트 만들기
import "./TodoItem.css";
const TodoItem = () => {
return (
<div className="TodoItem">
<div className="checkbox_col">
<input type="checkbox" />
</div>
<div className="title_col">할 일</div>
<div className="date_col">{new Date().toLocaleDateString()}</div>
<div className="btn_col">
<button>삭제</button>
</div>
</div>
);
};
export default TodoItem;
(3) TodoList에 Todoitm 컴포넌트 배치하기
// src/component/TodoList.js
// 2.4.3 TodoList에 Todoitm 컴포넌트 배치하기
import TodoItem from "./TodoItem";
import "./TodoList.css";
const TodoList = () => {
return (
<div className="TodoList">
<h4>Todo List 🌱</h4>
<input className="searchbar" placeholder="검색어를 입력하세요" />
<div className="list_wrapper">
<TodoItem />
<TodoItem />
<TodoItem />
</div>
</div>
);
};
export default TodoList;
/*
// src/component/TodoList.css
// 2.4 TodoList, TodoItem 컨포넌트 만들기
// 2.4.3 TodoList에 Todoitm 컴포넌트 배치하기
*/
/* 검색 폼에 스타일 적용 */
.TodoList .searchbar {
margin-bottom: 20px;
width: 100%;
border: none;
border-bottom: 1px solid rgb(220, 220, 220);
box-sizing: border-box;
padding-top: 15px;
padding-bottom: 15px;
}
/* 검색 폼을 클릭했을 때의 스타일 적용 */
.TodoList .searchbar:focus {
outline: none;
border-bottom: 1px solid #1f93ff;
}
.TodoList .list_wrapper {
display: flex;
flex-direction: column;
gap: 20px;
}
/*
// src/component/TodoItem.css
// 2.4 TodoList, TodoItem 컨포넌트 만들기
// 2.4.3 TodoList에 Todoitm 컴포넌트 배치하기
*/
/* 할 일 아이템 박스 스타일 적용 */
.TodoItem {
display: flex;
align-items: center;
gap: 20px;
padding-bottom: 20px;
border-bottom: 1px solid rgb(240, 240, 240);
}
/* 체크박스를 감싼 박스에 스타일 적용 */
.TodoItem .checkbox_col {
width: 20px;
}
/* 할 일 텍스트를 감싼 박스에 스타일 적용 */
.TodoItem .title_col {
flex: 1;
}
/* 할 일 아이템 등록 시간을 감싼 박스에 스타일 적용 */
.TodoItem .date_col {
font-size: 14px;
color: gray;
}
/* 삭제 버튼에 스타일 적용 */
.TodoItem .btn_col button {
cursor: pointer;
color: gray;
font-size: 14px;
border: none;
border-radius: 5px;
padding: 5px;
}
/*
//src/App.css
// 2.4 TodoList, TodoItem 컨포넌트 만들기
// 2.4.3 TodoList에 Todoitm 컴포넌트 배치하기
*/
.App {
max-width: 500px;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
padding: 20px;
/* border: 1px solid gray; <- 삭제하거나 주석 처리 하세요 */
display: flex;
flex-direction: column;
gap: 30px;
}

3. 기능 구현 준비하기
- App 컴포넌트: 할 일 데이터 관리하기
- Header 컴포넌트: 오늘의 날짜 표시
- TodoEditor 컴포넌트: 새로운 할 일 아이템 생성
- TodoList 컴포넌트: 검색에 따라 필터링된 할 일 아이템 렌더링
- TodoItem 컴포넌트: 할 일 아이템의 수정 및 삭제
CRUD는 데이터 처리의 기본 기능으로, 웹 서비스라면 기본적으로 갖추고 있어야 합니다.
- Create: 할 일 아이템 생성
- Read: 할 일 아이템 렌더링
- Update: 할 일 아이템 수정
- Delete: 할 일 아이템 삭제
1. 기초 데이터 설정하기
// src/App.js
// 3.1 기능 구현 준비하기
// 3.1.1 기초 데이터 설정하기
import "./App.css";
import Header from "./component/Header";
import TodoEditor from "./component/TodoEditor";
import TodoList from "./component/TodoList";
function App() {
const [todo, setTodo] = useState([]);
return (
<div className="App">
<Header />
<TodoEditor />
<TodoList />
</div>
);
}
export default App;
State 변수 todo는 [할 일 관리] 앱에서 데이터를 저장하는 배열이면서 동시에 일종의 데이터베이스 역할을 수행합니다. 예를 들어 사용자가 새 할 일 아이템을 만들면, 빈 배열이었던 todo 값은 아이템이 추가된 배열로 업데이트됩니다. 이는 삭제, 수정 모두 동일합니다.
2. 데이터 모델링하기
데이터 모델링: 현실의 사물이나 개념을 프로그래망 언어의 객체와 같은 자료구조로 표현하는 행위

일의 완료 여부, 일의 종류, 생성날짜 등의 정보가 남겨 있습니다. 또한 렌더링 되진 않지만, id라는 고유의 식별자가 있습니다.
{
id: 0, // 고유값
isDone: false, // 불리언 자료형, 일의 완료 여부
content: "React 공부하기", // 문자열, 일이 무엇인지
createdDate: new Date().getTime(), // 할일 생성 시간
}
데이터를 모델링하는 이유는 데이터를 어떻게 관리할지 생각하기 위함입니다.
3. 목 데이터 설정하기
목데이터: 모조품 데이터, 기능을 완벽하게 구현하지 않은 상태에서 테스트를 목적으로 사용하는 데이터입니다.
// App.js
// 3.3 목데이터 설정하기
import "./App.css";
import Header from "./component/Header";
import TodoEditor from "./component/TodoEditor";
import TodoList from "./component/TodoList";
import { useState } from "react";
const mockTodo = [
{
id: 0,
isDone: false,
content: "React 공부하기",
createdDate: new Date().getTime(),
},
{
id: 1,
isDone: false,
content: "빨래 널기",
createdDate: new Date().getTime(),
},
{
id: 2,
isDone: false,
content: "노래 연습하기",
createdDate: new Date().getTime(),
},
];
function App() {
const [todo, setTodo] = useState([mockTodo]);
return (
<div className="App">
<Header />
<TodoEditor />
<TodoList />
</div>
);
}
export default App;

“(변수명) is assigned a value but never used”: 선언된 변수를 어디에서도 사용하지 않을때 나오는 경고메세지
4. Create: 할일 준비하기
CRUD의 첫 번째 기능인 Create를 구현
1. 기능 흐름 살펴보기

사용자 입력 -> [추가]버튼 -> App컨포넌트 아이쳄 추가 이벤트 발생 -> 할 일 데이터 전달 -> 추가 배열 만들고 State변수 todo값 업데이트 -> 입력 폼 초기화
2. 아이템 추가 함수 만들기
[추가]버튼 -> App에 데이터 전달
onCreate
id 생성: 고유한 id값을 가져야 하므로, Ref 객체 사용 (Ref는 리액트에서 주로 돔을 조직할 때 사용하지만, 컨포넌트의 변수로도 자주 활용됩니다.
// App.js
// 4.2 아이템 추가 함수 만들기
import "./App.css";
import Header from "./component/Header";
import TodoEditor from "./component/TodoEditor";
import TodoList from "./component/TodoList";
import { useState, useRef } from "react";
const mockTodo = [
{
id: 0,
isDone: false,
content: "React 공부하기",
createdDate: new Date().getTime(),
},
{
id: 1,
isDone: false,
content: "빨래 널기",
createdDate: new Date().getTime(),
},
{
id: 2,
isDone: false,
content: "노래 연습하기",
createdDate: new Date().getTime(),
},
];
function App() {
const idRef = useRef(3);
const [todo, setTodo] = useState(mockTodo);
const onCreate = (content) => {
const newItem = {
id: idRef.current,
content,
isDone: false,
createdDate: new Date().getTime(),
};
setTodo([newItem, ...todo]);
idRef.current += 1;
};
return (
<div className="App">
<Header />
<TodoEditor onCreate={onCreate} />
<TodoList />
</div>
);
}
export default App;
3. 아이템 추가 함수 호출하기
컴포넌트 트리구조에서 데이터를 전달한다는 의미는 여러 컴포넌트가 동시에 동일한 데이터를 이용한다는 뜻입니다.
변하는 값인 State는 부모에서 자식으로 Props를 이용해서만 전달할 수 있습니다.
따라서, 버튼을 클릭하는 이벤트는 컨포넌트 트리 구조상의 데이터 전달이 아닌, 일종의 '이벤트'가 전달됩니다.
다음은 아이템 추가 후 함수를 호출하는 코드 입니다.
// src/component/TodoEditor.js
// 4.3.1 아이템 추가 함수 호출하기
import { useState } from "react";
import "./TodoEditor.css";
const TodoEditor = ({ onCreate }) => {
const [content, setContent] = useState("");
const onChangeContent = (e) => {
setContent(e.target.value);
};
return (
<div className="TodoEditor">
<h4>새로운 Todo 작성하기 ✏ </h4>
<div className="editor_wrapper">
<input
value={content}
onChange={onChangeContent}
placeholder="새로운 Todo..."
/>
<button>추가</button>
</div>
</div>
);
};
export default TodoEditor;

사용자가 입력한 값이 실시간으로 content에 반영되어 있습니다.
다음은 <추가> 버튼을 누르면, 함수 onCreate를 호출하는 버튼 클릭 이벤트 핸들러를 만들겠습니다.
새롭게 추가한 아이템은 App 컴포넌트의 todo 배열 맨 앞에 추가됩니다. 그 이유는 앞서 함수 onCreate를 만들 때 새로 추가할 아이템은 배열의 0번 인덱스에 위치하도록 만들었기 때문입니다.
// src/component/TodoEditor.js
// 4.3.2 <추가>버튼 클릭시
import { useState } from "react";
import "./TodoEditor.css";
const TodoEditor = ({ onCreate }) => {
const [content, setContent] = useState("");
const onChangeContent = (e) => {
setContent(e.target.value);
};
const onSubmit = () => {
onCreate(content);
};
return (
<div className="TodoEditor">
<h4>새로운 Todo 작성하기 ✏ </h4>
<div className="editor_wrapper">
<input
value={content}
onChange={onChangeContent}
placeholder="새로운 Todo..."
/>
<button onClick={onSubmit}>추가</button>
</div>
</div>
);
};
export default TodoEditor;

4. Create 완성도 높이기
(1) 빈 입력 방지하기
웹 서비스들이 일반적으로 채택하고 있는 빈 입력란에 포커스를 주는 기능을 구현합니다.
할 일 입력 폼을 관리할 Ref 객체를 하나 만들고, 함수 onSubmit에서 content 값이 비어 있으면 입력 폼에 포커스를 구현하는 방식입니다.
// src/component/TodoEditor.js
// 4.4.1 빈 입력 방지하기
import { useState, useRef } from "react";
import "./TodoEditor.css";
const TodoEditor = ({ onCreate }) => {
const [content, setContent] = useState("");
const inputRef = useRef();
const onChangeContent = (e) => {
setContent(e.target.value);
};
const onSubmit = () => {
if (!content) {
inputRef.current.focus();
return;
}
onCreate(content);
};
return (
<div className="TodoEditor">
<h4>새로운 Todo 작성하기 ✏ </h4>
<div className="editor_wrapper">
<input
ref={inputRef}
value={content}
onChange={onChangeContent}
placeholder="새로운 Todo..."
/>
<button onClick={onSubmit}>추가</button>
</div>
</div>
);
};
export default TodoEditor;
(2) 아이템 추가 후 입력 폼 초기화 하기
아이템을 추가하면 자동으로 할 일 입력 폼을 초기화하는 기능을 구현합니다.
입력 폼을 초기화하는 기능이 없다면 의도치 않게 <추가> 버튼을 클릭해 중복 아이템을 생성할 수 있습니다.
// src/component/TodoEditor.js
// 4.4.2 아이템 추가 후 입력 폼 초기화하기
import { useState, useRef } from "react";
import "./TodoEditor.css";
const TodoEditor = ({ onCreate }) => {
const [content, setContent] = useState("");
const inputRef = useRef();
const onChangeContent = (e) => {
setContent(e.target.value);
};
const onSubmit = () => {
if (!content) {
inputRef.current.focus();
return;
}
onCreate(content);
setContent(""); // 빈문자열 전달하면 새 아이템을 추가하고, content값은 빈 문자열이 되고 입력폼은 초기화 됩니다.
};
return (
<div className="TodoEditor">
<h4>새로운 Todo 작성하기 ✏ </h4>
<div className="editor_wrapper">
<input
ref={inputRef}
value={content}
onChange={onChangeContent}
placeholder="새로운 Todo..."
/>
<button onClick={onSubmit}>추가</button>
</div>
</div>
);
};
export default TodoEditor;

(3) <Enter> 키를 눌러 아이템 추가하기
// src/component/TodoEditor.js
// 4.4.3 <Enter> 키를 눌러 아이템 추가하기
import { useState, useRef } from "react";
import "./TodoEditor.css";
const TodoEditor = ({ onCreate }) => {
const [content, setContent] = useState("");
const inputRef = useRef();
const onChangeContent = (e) => {
setContent(e.target.value);
};
const onSubmit = () => {
if (!content) {
inputRef.current.focus();
return;
}
onCreate(content);
setContent("");
};
const onKeyDown = (e) => {
if (e.keyCode === 13) {
onSubmit();
}
};
return(
<div className="TodoEditor">
<h4>새로운 Todo 작성하기 ✏ </h4>
<div className="editor_wrapper">
<input
ref={inputRef}
value={content}
onChange={onChangeContent}
onKeyDown={onKeyDown}
placeholder="새로운 Todo..."
/>
<button onClick={onSubmit}>추가</button>
</div>
</div>
)
};
export default TodoEditor;
5. Read: 할일 리스트 렌더링 하기
1. 배열을 리스트로 렌더링하기
// src/App.js
// 5.1.1 배열을 리스트로 렌더링하기
import "./App.css";
import Header from "./component/Header";
import TodoEditor from "./component/TodoEditor";
import TodoList from "./component/TodoList";
import { useState, useRef } from "react";
const mockTodo = [
{
id: 0,
isDone: false,
content: "React 공부하기",
createdDate: new Date().getTime(),
},
{
id: 1,
isDone: false,
content: "빨래 널기",
createdDate: new Date().getTime(),
},
{
id: 2,
isDone: false,
content: "노래 연습하기",
createdDate: new Date().getTime(),
},
];
function App() {
const idRef = useRef(3);
const [todo, setTodo] = useState(mockTodo);
const onCreate = (content) => {
const newItem = {
id: idRef.current,
content,
isDone: false,
createdDate: new Date().getTime(),
};
setTodo([newItem, ...todo]);
idRef.current += 1;
};
return (
<div className="App">
<Header />
<TodoEditor onCreate={onCreate} />
<TodoList todo={todo} />
</div>
);
}
export default App;
TodoList 컴포넌트에서는 App에서 Props로 전달된 todo를 리스트로 렌더링해야 합니다. 리액트에서 배열 데이터를 렌더링할 때는 배열 메서드 map을 주로 이용합니다. map을 이용하면 HTML 또는 컴포넌트를 순회하면서 매 요소를 반복하여 렌더링합니다.
(1) map을 이용한 HTML 반복하기
TodoList 컴포넌트에서 배열 메서드 map을 이용해 HTML 요소를 반복해 렌더링합니다.
// src/component/TodoList.js
// 5.1.2 map을 이용한 HTML 반복하기
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) => ( //모든 요소를 순차적으로 HTML로 변환합니다.
<div>{it.content}</div>
))}
</div>
</div>
);
};
export default TodoList;

(2) map을 이용해 컴포넌트 반복하기
배열을 이용해 콜백함수가 컨포넌트를 반환하도록 한다.
// src/component/TodoList.js
// 5.1.3 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) => (
<TodoItem {...it} />
))}
</div>
</div>
);
};
export default TodoList;
// src/component/TodoItem.js
// 5.1.3 map을 이용해 컴포넌트 반복하기
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;

경고
- 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 이벤트 핸들러를 설정할 예정이므로 지금은 무시해도 됩니다.
(3) key 설정하기
key는 리스트에서 각각의 컴포넌트를 구분하기 위해 사용하는 값입니다.
리액트는 리스트에서 특정 컴포넌트를 수정, 추가, 삭제하는 경우, 이 key로 어떤 컴포넌트를 업데이트할지 결정합니다.
그렇다면 무엇을 key로 사용하는 게 좋을까요? 우리는 이미 아이템마다 고유한 id를 갖도록 데이터를 모델링했습니다. 그리고 App 컴포넌트의 할 일 아이템 생성 과정에서 Ref 객체를 이용해 아이템마다 고유 id를 갖도록 만들었습니다. 따라서 id를 key로 전달하면 문제를 간단히 해결할 수 있습니다.
// src/component/TodoList.js
// 5.1.4 key 설정하기
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 key={it.id} {...it} />
))}
</div>
</div>
);
};
export default TodoList;
2. 검색어에 따라 필터링하기
(1) 검색 기능 만들기
검색 폼에서 검색어를 입력해서 문자열 필터링 기능
검색어를 처리할 State 변수를 만든 다음, 검색폼에서 처리하는 기능
// src/component/TodoList.js
// 5.2.1 검색 기능 만들기
import { useState } from "react";
import TodoItem from "./TodoItem";
import "./TodoList.css";
const TodoList = ({ todo }) => {
const [search, setSearch] = useState("");
const onChangeSearch = (e) => {
setSearch(e.target.value);
};
const getSearchResult = () => {
return search === ""
? todo
: todo.filter((it) => it.content.includes(search));
};
return (
<div className="TodoList">
<h4>Todo List 🌱</h4>
<input
value={search}
onChange={onChangeSearch}
className="searchbar"
placeholder="검색어를 입력하세요"
/>
<div className="list_wrapper">
{getSearchResult().map((it) => (
<TodoItem key={it.id} {...it} />
))}
</div>
</div>
);
};
export default TodoList;

(2) 대소 문자를 구별하지 않게 하기
// src/component/TodoList.js
// 5.2.2 대소 문자를 구별하지 않게 하기
import { useState } from "react";
import TodoItem from "./TodoItem";
import "./TodoList.css";
const TodoList = ({ todo }) => {
const [search, setSearch] = useState("");
const onChangeSearch = (e) => {
setSearch(e.target.value);
};
const getSearchResult = () => {
return search === ""
? todo
: todo.filter((it) =>
it.content.toLowerCase().includes(search.toLowerCase())
);
};
return (
<div className="TodoList">
<h4>Todo List 🌱</h4>
<input
value={search}
onChange={onChangeSearch}
className="searchbar"
placeholder="검색어를 입력하세요"
/>
<div className="list_wrapper">
{getSearchResult().map((it) => (
<TodoItem key={it.id} {...it} />
))}
</div>
</div>
);
};
export default TodoList;

6. Update: 할일 수정하기
1. 기능 흐름 살펴보기

- 사용자가 TodoItem의 체크박스에 틱(체크 표시)합니다.
- TodoItem 컴포넌트는 함수 onUpdate를 호출하고 어떤 체크박스에 틱이 발생했는지 해당 아이템의 id를 인수로 전달합니다. 물론 그 전에 함수 onUpdate를 App 컴포넌트에서 Props로 TodoItem에 전달해야 합니다.
- App 컴포넌트의 함수 onUpdate는 틱이 발생한 아이템의 상태(완료 또는 미완료)를 토글하기 위해 State 값을 업데이트합니다.
- App 컴포넌트의 State 값이 변경되면 TodoList에 전달하는 Props의 값 또한 변경됩니다.
- TodoList는 변경된 State 값을 다시 리스트로 렌더링합니다. 결과적으로 수정 사항이 반영됩니다.
2. 아이템 수정 함수 만들기
수정을 위한 함수 onUpdate
// src/App.js
// 6.2.1 아이템 수정 함수 만들기
import "./App.css";
import Header from "./component/Header";
import TodoEditor from "./component/TodoEditor";
import TodoList from "./component/TodoList";
import { useState, useRef } from "react";
const mockTodo = [
{
id: 0,
isDone: false,
content: "React 공부하기",
createdDate: new Date().getTime(),
},
{
id: 1,
isDone: false,
content: "빨래 널기",
createdDate: new Date().getTime(),
},
{
id: 2,
isDone: false,
content: "노래 연습하기",
createdDate: new Date().getTime(),
},
];
function App() {
const idRef = useRef(3);
const [todo, setTodo] = useState(mockTodo);
const onCreate = (content) => {
const newItem = {
id: idRef.current,
content,
isDone: false,
createdDate: new Date().getTime(),
};
setTodo([newItem, ...todo]);
idRef.current += 1;
};
const onUpdate = (targetId) => {
setTodo(
todo.map((it) =>
it.id === targetId ? { ...it, isDone: !it.isDone } : it
)
);
};
return (
<div className="App">
<Header />
<TodoEditor onCreate={onCreate} />
<TodoList todo={todo} onUpdate={onUpdate} />
</div>
);
}
export default App;
// src/component/TodoList.js
// 6.1.2 아이템 수정 함수 만들기
import { useState } from "react";
import TodoItem from "./TodoItem";
import "./TodoList.css";
const TodoList = ({ todo, onUpdate }) => {
const [search, setSearch] = useState("");
const onChangeSearch = (e) => {
setSearch(e.target.value);
};
const getSearchResult = () => {
return search === ""
? todo
: todo.filter((it) =>
it.content.toLowerCase().includes(search.toLowerCase())
);
};
return (
<div className="TodoList">
<h4>Todo List 🌱</h4>
<input
value={search}
onChange={onChangeSearch}
className="searchbar"
placeholder="검색어를 입력하세요"
/>
<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’ 이라고 합니다.
3. TodoItem 컴포넌트에서 아이템 수정 함수 호출하기
TodoItem 컴포넌트에서 틱 이벤트가 발생하면 함수 onUpdate를 호출합니다.
// src/component/TodoItem.js
// 6.3.1 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;

7. Delete: 할 일 삭제하기
1. 기능 흐름 살펴보기

- 삭제하려는 할 일 아이템에서 <삭제> 버튼을 클릭합니다.
- 할 일을 삭제하는 함수 onDelete를 호출합니다. 이 함수는 App의 State 값을 업데이트하므로 미리 App 컴포넌트에서 Props로 전달해야 합니다.
- <삭제> 버튼을 클릭하면 삭제할 할 일 아이템만 빼고, 새 배열을 만들어 State 값을 업데이트합니다.
- State 변수 todo가 업데이트되면, App가 TodoList 컴포넌트에 전달한 Props 의 값도 변경됩니다.
- TodoList 컴포넌트는 Props의 값이 변경되면 리렌더됩니다. 이때 새로운 배 열 todo로 할 일 리스트 다시 렌더링합니다.
2. 아이템 삭제 함수 만들기
// src/App.js
// 7.2.1 아이템 삭제 함수 만들기
import "./App.css";
import Header from "./component/Header";
import TodoEditor from "./component/TodoEditor";
import TodoList from "./component/TodoList";
import { useState, useRef } from "react";
const mockTodo = [
{
id: 0,
isDone: false,
content: "React 공부하기",
createdDate: new Date().getTime(),
},
{
id: 1,
isDone: false,
content: "빨래 널기",
createdDate: new Date().getTime(),
},
{
id: 2,
isDone: false,
content: "노래 연습하기",
createdDate: new Date().getTime(),
},
];
function App() {
const idRef = useRef(3);
const [todo, setTodo] = useState(mockTodo);
const onCreate = (content) => {
const newItem = {
id: idRef.current,
content,
isDone: false,
createdDate: new Date().getTime(),
};
setTodo([newItem, ...todo]);
idRef.current += 1;
};
const onUpdate = (targetId) => {
setTodo(
todo.map((it) =>
it.id === targetId ? { ...it, isDone: !it.isDone } : it
)
);
};
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/component/TodoList.js
// 7.2.2 아이템 삭제 함수 만들기
import { useState } from "react";
import TodoItem from "./TodoItem";
import "./TodoList.css";
const TodoList = ({ todo, onUpdate, onDelete }) => {
const [search, setSearch] = useState("");
const onChangeSearch = (e) => {
setSearch(e.target.value);
};
const getSearchResult = () => {
return search === ""
? todo
: todo.filter((it) =>
it.content.toLowerCase().includes(search.toLowerCase())
);
};
return (
<div className="TodoList">
<h4>Todo List 🌱</h4>
<input
value={search}
onChange={onChangeSearch}
className="searchbar"
placeholder="검색어를 입력하세요"
/>
<div className="list_wrapper">
{getSearchResult().map((it) => (
<TodoItem
key={it.id}
{...it}
onUpdate={onUpdate}
onDelete={onDelete}
/>
))}
</div>
</div>
);
};
export default TodoList;
3. TodoItem 컨포넌트에서 삭제 함수 호출하기
TodoItem에서 <삭제> 버튼을 클릭하면 함수 onDelete를 호출하도록 구현합니다.
// src/component/TodoItem.js
// 7.3.1 TodoItem 컴포넌트에서 삭제 함수 호출하기
import "./TodoItem.css";
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;

출처 : 이정환, 『한 입 크기로 잘라먹는 리액트』, 프로그래밍인사이트(2023).
'DEVELOPMENT > react' 카테고리의 다른 글
| 12. 최적화 (0) | 2025.01.06 |
|---|---|
| 11장 useReducer와 상태 관리 (0) | 2025.01.06 |
| 9. 리액트를 다루는 기술 - hooks (0) | 2024.12.23 |
| 8. 라이프 사이클과 리액트 개발자 도구 (0) | 2024.12.23 |
| 7. 카운터 앱만들기 (0) | 2024.11.11 |