
- 요구사항 분석하기

[할 일 관리] 앱의 UI 요소를 컴포넌트 단위로 표현하면 아래와 같습니다.
Header: 오늘의 날짜를 형식에 맞게 보여줍니다.
TodoEditor: 새로운 할 일 아이템을 등록합니다.
TodoList: 필터링된 할 일 리스트를 렌더링합니다.(검색 폼이 공백일 경우, 필터링하지 않습니다.)
TodoItem: 기본 정보 외에도 체크박스와 <삭제>버튼이 있습니다. 체크박스를 클릭하면 할 일 완료 여부가 토글되고, 삭제 버튼을 누르면 해당 아이템이 삭제됩니다.
- 리액트 앱 만들기
1. 새로운 폴더 project2 생성.
2. project2를 불러온 후, 터미널을 열고 npx create-react-app . 명령을 입력해 리액트 앱 생성.
3. 아래 4개의 불필요한 파일은 삭제.
src/App.test.js
src/logo.svg
src/reportWebVitals.js
setupTest.js
4. App.js와 index.js에 작성된 불필요한 코드를 삭제
두 파일의 최종 코드는 다음과 같다.
import "./App.css";
function App() {
return <div className="App"></div>;
}
export default App;
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 />);
2. UI 구현하기
- 페이지 레이아웃 만들기

먼저 App.js에 <h2> 태그를 추가합니다.
import "./App.css";
function App() {
return (
<div className="App">
<h2>헬로 리액트</h2>
</div>
);
}
export default App;
다음에는 index.css에 작성된 스타일 규칙은 모두 삭제하고 다음과 같이 작성합니다.
body {
margin: 0px; ① // 여백을 0px로 설정하면 페이지의 외부 여백이 사라짐.
}
다음에는 App.css에 작성된 스타일 규칙은 모두 삭제하고 다음과 같이 작성합니다.
.App {
max-width: 500px; ① // 최대 너비를 500px로 고정.
width: 100%; ② // 너비를 브라우저의 100%로 설정.
margin: 0 auto; ③ // 여백 위 아래는 0, 좌우는 auto
box-sizing: border-box; ④ // 요소의 크기를 어떤 것을 기준으로 계산할 지 정하는 속성.
padding: 20px; ⑤ // 내부 여백을 20px로 설정.
border: 1px solid gray; ⑥
}

App.js에 3개의 자식 컴포넌트 Header, TodoEditor, TodoList를 각각 세로로 배치할 예정이므로, App.js를 다음과 같이 수정합니다.
import "./App.css";
function App() {
return (
<div className="App">
<div>Header</div>
<div>Todo Editor</div>
<div>Todo List</div>
</div>
);
}
export default App;

다만, 요소 사이 간격이 없어 답답해 보이기에 display 속성을 추가합니다.
.App {
(...)
display: flex; ① // 요소들을 수평으로 배치.(<=> 기본 속성 block은 세로로 배치)
flex-direction: column; ② // flex-direction은 플렉스 컨테이너 속 요소들의 배치 방향을 조절
하는 속성으로 기본값은 row지만,현재는 column으로 되어 있어 App 컴포넌트의 요소를 수직으로 배치.
gap: 30px;③ // 자식 요소 간의 여백 조절.
}

Header 컴포넌트 만들기
src/component/Header.js
-------------------------------------------------------
const Header = () => {
return <div className="Header">Header Component</div>;
};
export default Header;
import "./App.css";
import Header from "./component/Header";
-------------------------------------------------------
src/App.js
-------------------------------------------------------
import "./App.css";
import Header from ".component/Header"; // Header 컴포넌트를 App의 자식으로 배치.
function App() {
return (
<div className="App">
<Header />
<div>Todo Editor</div>
<div>Todo List</div>
</div>
);
}
export default App;

Header 컴포넌트가 오늘의 날짜를 렌더링 하도록 다음과 같이 수정합니다.
const Header = () => {
return (
<div className="Header">
<h3>오늘은 📅</h3> ①
<h1>{new Date().toDateString()}</h1> ②
</div>
);
};
export default Header;
---------------------------------------------
Header.css 생성
---------------------------------------------
.Header h1 { ①
margin-bottom: 0px;
color: #1f93ff;
}

- TodoEditor 컴포넌트 만들기

💕 src/component/TodoEditor.js
import "./TodoEditor.css";
const TodoEditor = () => {
return <div className="TodoEditor">TodoEditor Component</div>;
};
export default TodoEditor;
💕 src/App.js
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
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;
💕 src/component/TodoEditor.css
.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;
}

- TodoList, TodoItem 컴포넌트 만들기

💕TodoList.js
import "./TodoList.css";
const TodoList = () => {
return <div className="TodoList">TodoList Component</div>;
};
export default TodoList;
💕App.js
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;

TodoList 컴포넌트는 크게 할 일 아이템을 조회하는 검색 폼, 조회한 할일 아이템을 목록 형태로 보여주는 리스트 두 부분으로 구성되어 있습니다. 따라서 TodoList 컴포넌트 상단에 위치할 검색 폼부터 만들어야합니다.
💕TodoList.css
import "./TodoList.css";
const TodoList = () => {
return (
<div className="TodoList">
<h4>Todo List 🌱</h4>
<input className="searchbar" placeholder="검색어를 입력하세요" />
</div>
);
};
export default TodoList;
💕TodoList.css
/* 검색 폼에 스타일 적용 */
.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;
}

TodoItem 컴포넌트 만들기

💕 TodoItem.css
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;
💕TodoList.js
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;

리스트로 배치한 3개의 아이템이 간격이 없어 답답해보입니다. TodoList.css 파일에
TodoList .list_wrapper {
display: flex;
flex-direction: column;
gap: 20px;} 을 추가해 간격을 넓힙니다. 그 다음, 개별 요소의 스타일링을 위해 TodoItem.css를 작성합니다.
/* 할 일 아이템 박스 스타일 적용 */
.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;
}

프론트엔드 엔지니어의 기본 소양 중 하나는 UI구현!
UI 구현 = '퍼블리싱' 또는 'UI 개발'
컴포넌트의 기능은 주로 데이터를 추가(생성)하고, 조회하고, 수정하고, 삭제하는 기능으로 이루어져 있습니다.
이렇듯 데이터를 다루는 4개의 기능, 즉 추가(Create), 조회(Read), 수정(Update), 삭제(Delete) 기능을 앞글자만 따서 CRUD라고 합니다.
CRUD는 데이터 처리의 기본 기능으로, 웹 서비스라면 기본적으로 갖추고 있어야 합니다.
Create: 할 일 아이템 생성
Read: 할 일 아이템 렌더링
Update: 할 일 아이템 수정
Delete: 할 일 아이템 삭제
기초 데이터 설정하기
먼저 CRUD의 대상인 할 일 아이템부터 생성해야하므로, 다음과 같이 App.js를 수 정합니다.
src/App.js
import { useState } from "react";
(..)
function App() {
const [todo, setTodo] = useState([]); ①
return (
(...)
);
}
export default App;
데이터 모델링하기
자바스크립트에서는 보통 현실의 사물이나 개념을 표현할 때 객체를 사용합니다. 예를 들어 저자를 객체로 표현한다면 다음과 같습니다.
let author = { name : "이정환b", gender : "male"}
-> 자료구조로 표현하는 행위는 '데이터 모델링'
why? 현실 세계의 사물이나 개념을 프로그래밍 언어로 표현하고 다뤄야하기 때문입니다.

[할 일 관리] 앱의 할 일 아이템 데이터 모델링
위 그림에서는 [할 일 관리] 앱에서 만들 할 일 아이템의 요소를 세부적으로 나누어 표시하였습니다. 세 요소는 각각 isDone, content, createdDate 라는 별도의 이름으로 구분합니다.
또한 할 일 아이템에는 페이지에 렌더링하지는 않지만, id라는 고유 식별자가 있습니다.
모델링한 정보를 토대로 할 일 아이템을 자바스크립트 객체로 만들면 다음과 같습니다.
{
id: 0, ①
isDone: false, ②
content: "React 공부하기", ③
createdDate: new Date().getTime(), ④
}
데이터를 모델링하는 이유는 데이터를 어떻게 관리할지 생각하기 위함입니다.
모델링 과정이 잘못되면 모든 과정을 수정하게 되는데, 아예 프로젝트를 처음부터 다시 시작하는 상황도 발생합니다.
따라서 모델링은 반드시 데이터 관리 프로그램을 구현하기 전에 노트나 메모장 등에 적어 보면서 코드를 작성해야 합니다.
목 데이터 설정하기
목(Mock) 데이터(=임시 데이터)란 모조품 데이터라는 뜻입니다. 기능을 완벽히 구현하지 않은 상태에서 테스트를 목적으로 사용하
기능을 아직 개발하지 않아 데이터가 없는 상황일 때 목 데이터를 사용합니다. App.js에서 목 데이터를 다음과 같이 작성합니다.
src/App.js
(...)
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(),
},
];b
function App() {
const [todo, setTodo] = useState(mockTodo); ②
b
return (
(...)
);
}
export default App;
기능 흐름 살펴보기

[할 일 관리] 앱의 Create 기능 흐름
1. 사용자가 새로운 할 일을 입력합니다.
2. TodoEditor 컴포넌트에 있는 <추가> 버튼을 클릭합니다.
3. TodoEditor 컴포넌트는 부모인 App에게 아이템 추가 이벤트가 발생했음을 알리고 사용자가 추가한 할 일 데이터를 전달합니다.
4. App 컴포넌트는 TodoEditor 컴포넌트에서 받은 데이터를 이용해 새 아이템을 추가한 배열을 만들고 State 변수 todo 값을 업데이트합니다.
5. TodoEditor 컴포넌트는 할 일 입력 폼을 초기화합니다.
아이템 추가 함수 만들기
TodoEditor 컴포넌트에서 <추가> 버튼을 클릭하면 App에 사용자가 입력한 할 일 데이터를 전달하고 추가 이벤트가 발생했음을 알려야 합니다. 먼저 App 컴포넌트에서 새 할 일 아이템을 추가하는 함수 onCreate를 만듭니다.
src/App.js
(...)
function App() {
const [todo, setTodo] = useState(mockTodo);
const onCreate = (content) => { ①
const newItem = {
id: 0,
content,
isDone: false,
createdDate: new Date().getTime(),
};
setTodo([newItem, ...todo]); ②
};
return (
(...)
);
}
export default App;
다음과 같이 App.js에서 새로운 Ref 객체를 생성합니다.
src/App.js
import { useState, useRef } from "react";
(...)
function App() {
const idRef = useRef(3);
(...)
}
export default App;
초깃값이 3인 Ref 객체를 생성해 idRef에 저장합니다.
이제 다음과 같이 idRef를 이용해 아이템을 생성할 때마다 id가 1씩 늘어나도록 수정합니다.
src/App.js
(...)
function App() {
(...)
const idRef = useRef(3);
const onCreate = (content) => {
const newItem = {
id: idRef.current, ①
content,
isDone: false,
createdDate: new Date().getTime(),
};
setTodo([newItem, ...todo]);
idRef.current += 1; ②
};
return (
(...)
);
}
export default App;
함수 onCreate는 사용자가 TodoEditor 컴포넌트에서 <추가> 버튼을 클릭해야 호출 되기 때문에 이 컴포넌트에 Props로 전달해야 합니다.
src/App.js
(...)
function App() {
(...)
return (
<div className="App">
<Header />
<TodoEditor onCreate={onCreate} />
<TodoList />
</div>
);
}
export default App;
아이템 추가 함수 호출하기
사용자가 할 일 입력 폼에서 아이템을 입력하고 <추가> 버튼을 클릭합니다.
그러면 TodoEditor 컴포넌트는 새 할 일을 생성하기 위해 App에서 Props로 받은 함수 onCreate를 호출하고 현재 사용자가 작성한 할 일을 인수로 전달합니다.
함수 onCreate를 사용하기 위해 다음과 같이 TodoEditor 컴포넌트를 수정합니다.
src/component/TodoEditor.js
Copy
import "./TodoEditor.css";
const TodoEditor = ({ onCreate }) => { ①
return (
<div className="TodoEditor">
<h4>새로운 Todo 작성하기 🔏 </h4>
<div className="editor_wrapper">
<input placeholder="새로운 Todo..." />
<button>추가</button>
</div>
</div>
);
};
export default TodoEditor;
다음으로 TodoEditor 컴포넌트의 할 일 입력 폼에서 사용자가 입력하는 새 할 일 데이터를 저장할 State를 만듭니다.
src/component/TodoEditor.js
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;
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;
다음으로 <추가> 버튼을 클릭하면, 함수 onCreate를 호출하는 버튼 클릭 이벤트 핸들러를 만듭니다.
src/component/TodoEditor.js
(...)
const TodoEditor = ({ onCreate }) => {
(...)
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;
빈 입력 방지하기
빈 입력은 말뜻 그대로 아무것도 입력하지 않은 상태에서 <추가> 버튼을 누르는 행위를 방지해야합니다. 웹 서비스들이 일반적으로 채택하고 있는 빈 입력란에 포커스를 주는 기능을 구현합니다. 다음과 같이 TodoEditor 컴포넌트를 수정합니다.
src/component/TodoEditor.js
import { useState, useRef } from "react";
import "./TodoEditor.css";
const TodoEditor = ({ onCreate }) => {
const [content, setContent] = useState("");
const inputRef = useRef(); ①
(...)
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;
아이템 추가 후 입력 폼 초기화하기
아이템을 추가하면 자동으로 할 일 입력 폼을 초기화하는 기능을 구현합니다. 입력 폼을 초기화하는 기능이 없다면 의도치 않게 <추가> 버튼을 클릭해 중복 아이템을 생성할 수 있습니다. 또한 다른 할 일을 추가하려는 사용자에게는 기존에 작성했던 항목이 지워지지 않고 남아 있어 이를 지워야 하는 불편함이 생깁니다. TodoEditor 컴포넌트의 함수 onSubmit을 다음과 같이 수정합니다.
src/component/TodoEditor.js
(...)
const TodoEditor = ({ onCreate }) => {
(...)
const onSubmit = () => {
if (!content) {
inputRef.current.focus();
return;
}
onCreate(content);
setContent(""); ①
};
(...)
};
export default TodoEditor;
<Enter> 키를 눌러 아이템 추가하기
마지막으로 키보드의 <Enter> 키를 누르면, <추가> 버튼을 클릭한 것과 동일한 동작을 수행하는 키 입력 이벤트를 만들겠습니다.
TodoEditor 컴포넌트를 다음과 같이 수정합니다.
src/component/TodoEditor.js
(...)
const TodoEditor = ({ onCreate }) => {
(...)
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;
새로고침한 다음, 새로운 할 일 ‘에어컨 청소하기’를 입력하고 <Enter> 키를 눌러 아이템이 잘 추가되는지 확인합니다. 개발자 도구의 [Components] 탭에서 App 컴포넌트의 State 값을 확인하면 됩니다.
출처: 이정환, 『한 입 크기로 잘라먹는 리액트』, 프로그래밍인사이트(2023).
Corner React.js
Editor: 채옹
| [React.js] 9장.컴포넌트 트리에 데이터 공급하기 (0) | 2026.01.02 |
|---|---|
| [React.js] project 2[할일 관리] 앱 만들기 (Read: 할 일 리스트 렌더링하기 ~ Delete: 할 일 삭제하기) (0) | 2025.12.26 |
| [React.js] UseReducer와 상태 관리 -최적화 (0) | 2025.11.28 |
| [React.js] 리액트를 다루는 기술-8장.hooks (0) | 2025.11.21 |
| [React.js] project 1[카운터] 앱 만들기 ~ 6장. 라이프 사이클과 리액트 개발자 도구 (0) | 2025.11.14 |