프로젝트 구현에 앞서 구현에 필요한 준비 작업을 진행하자. 프로젝트 1을 준비할 때처럼 먼저 앱의 요구사항을 분석하고, 이를 토대로 필요한 기능을 하나씩 구현하겠다.
1-(1) 요구사항 분석하기
최종 구현 페이지에서 [할 일 관리] 앱에는 4가지의 기능이 있다.
요구사항 분석에 맞게 페이지의 각 UI 요소를 역할에 따라 구분할 수 있도록 컴포넌트 단위로 나누겠다.
위 사진은 [할 일 관리] 앱의 UI 요소를 컴포넌트로 구분한 사진이다.
1-(2) 리액트 앱 만들기
프로젝트 2를 위한 새 리액트 앱을 생성하고 불필요한 파일과 코드는 삭제한다. 이 과정은 [카운터] 앱을 만들 때와 동일하다.
프로젝트 준비를 모두 마쳤다면 UI를 구현하겠다. [할 일 관리] 앱의 UI 구현은 페이지의 전체 레이아웃 먼저 만들고, 세부 요소는 순서에 따라 차근차근 만들 예정이다.
2-(1) 페이지 레이아웃 만들기
위 그림은 [할 일 관리] 앱의 최종 형태를 UI 관점에서 보여준다. [할 일 관리] 앱의 UI 요소는 마치 핸드폰을 웹 브라우저 위에 올려놓은 것처럼 좌우 여백이 넓으며 페이지의 정중앙에 자리 잡고 있다.
먼저 App.js에 <h2> 태그를 추가한다.
import './App.css';
function App() {
return(
<div className="App">
<h2>헬로 리액트</h2>
</div>
)
}
export default App;
다음에는 index.css에 작성된 스타일 규칙은 모두 삭제하고 다음과 같이 작성한다.
body {
margin: 0px;
}
다음으로 App.css에 작성된 스타일 규칙은 모두 삭제하고 다음과 같이 작성한다.
.App {
max-width: 500px;
width: 100%;
margin: 0auto;
box-sizing: border-box;
padding: 20px;
border: 1px solid gray;
}
App에는 3개의 자식 컴포넌트 Header, TodoEditor, TodoList를 각각 세로로 배치할 예정이다. App에 배치할 자식 컴포넌트를 아직 구현하지 않았으므로, 임시 요소를 만들어 대신 배치하겠다.
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;
저장 후 렌더링 결과를 확인하면 페이지의 요소를 모두 세로로 배치했지만, 요소 사이에 간격이 없어 답답해 보인다. 이때 App 컴포넌트의 display 속성을 이용하면, 요소의 배치 간격을 좀 더 보기 좋게 만들 수 있다.
App.css를 수정한다.
.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;
}
수정 후 렌더링 결과를 확인하면 요소의 간격이 적절히 떨어져 있음을 알 수 있다. 이렇게 App 컴포넌트의 스타일링을 모두 마무리했다.
2-(2) Header 컴포넌트 만들기
이번에는 페이지 최상단에 위치할 Header 컴포넌트를 만들겠다.
src에 이 프로젝트의 컴포넌트 파일을 한곳에 모아 둘 component 폴더 생성 후 component 폴더에 Header.js 생성한다.
- App.js
Header 컴포넌트를 페이지에 렌더링하려면 App의 자식으로 배치해야 한다.
import './App.css';
import Header from "./component/Header";
function App() {
return(
<div className="App">
<Header />
<div>Header</div>
<div>Todo Editor</div>
<div>Todo List</div>
</div>
)
}
export default App;
- Header.js
Header 컴포넌트가 오늘의 날짜를 렌더링하도록 작성한다.
import "./Header.css" //css스타일 적용
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;
}
렌더링 결과를 확인하면 위 그림과 같다. 페이지에 렌더링 한 날짜는 당일 날짜가 표시된다.
2-(3) TodoEditor 컴포넌트 만들기
component 폴더에 컴포넌트와 스타일을 정의할 TodoEditor.js와 TodoEditor.css를 각각 생성한다.
- 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;
- 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;
}
저장 후 렌더링 결과를 확인하면 다음과 같다.
2-(4) TodoList, TodoItem 컴포넌트 만들기
[할 일 관리] 앱의 TodoList에는 TodoItem 컴포넌트가 여러 개 있다.
TodoList 컴포넌트 만들기
- TodoList.js
import "./TodoList.css";
const TodoList = () => {
return (
<div className="TodoLst">
<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 컴포넌트 만들기
- TodoItem.js
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에 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;
- 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 구현을 완료한 것을 알 수 있다. 이제 App 컴포넌트에서 경계를 확인할 필요가 없으니, App.css의 border 속성은 제거 또는 주석 처리한다.
최종적으로 구성된 UI는 위 사진과 같다. UI 구현을 흔히 '퍼블리싱' 또는 'UI 개발'이라고 한다.
컴포넌트별로 어떤 기능을 구현해야 하는지 다시 살펴보자.
구현할 컴포넌트의 기능을 잘 살펴보면 주로 데이터를 추가(생성)하고, 조회하고, 수정하고, 삭제하는 기능으로 이루어져 있음을 알 수 있다. 이렇듯 데이터를 다루는 4개의 기능, 즉 추가(Create), 조회(Read), 수정(Update), 삭제(Delete) 기능을 앞 글자만 따서 CRUD라고 한다. CRUD는 데이터 처리의 기본 기능으로, 웹 서비스라면 기본적으로 갖추고 있어야 한다.
3-(1) 기초 데이터 설정하기
- App.js
import { useState } from "react";
(..)
function App() {
const [todo, setTodo] = useState([]);
return (
(...)
);
}
export default App;
3-(2) 데이터 모델링하기
현실의 사물이나 개념을 프로그래밍 언어의 객체와 같은 자료구조로 표현하는 행위를 '데이터 모델링'이라고 한다. 데이터 모델링을 하는 이유는 [할 일 관리] 앱의 '할 일'처럼 현실 세계의 사물이나 개념을 프로그래밍 언어로 표현하고 다뤄야 하기 때문이다.
하나의 할 일 아이템에는 일의 완료 여부(isDone), 일의 종류(content), 생성 날짜(createdDate) 등 3가지 정보가 담겨 있다. 할 일 아이템에는 페이지에 렌더링 하지는 않지만, id라는 고유 식별자가 있다. 모든 아이템에는 해당 아잉템을 구별하기 위한 고유 식별자가 필요하기 때문이다. 고유 식별자가 없으면 특정 아이템을 삭제하거나 수정하는 등의 연산이 불가능하다.
{
id: 0,
isDone: false,
content: "React 공부하기",
createdDate: new Date().getTime(),
}
모델링한 정보를 토대로 할 일 아이템을 자바스크립트 객체로 만들면 위와 같다.
데이터를 모델링하는 이유는 데이터를 어떻게 관리할지 생각하기 위함이다.
3-(3) 목 데이터 설정하기
목(Mock) 데이터란 모조품 데이터라는 뜻이다. 기능을 완벽히 구현하지 않은 상태에서 테스트를 목적으로 사용하는 데이터다. 임시 데이터라 표현하기도 한다. 기능을 아직 개발하지 않아 데이터가 없는 상황일 때 목 데이터를 사용한다.
- App.js
(...)
const mockTodo = [ // 3개의 객체를 저장하는 배열 목 데이터 생성
{
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);
// todo의 기본값으로 목 데이터를 전달
return (
(...)
);
}
export default App;
비주얼 스튜디오 코드에 "(변수명) is assigned a value but never used"라는 경고 메시지가 나오지만 선언된 변수를 어디에도 사용하지 않을 때 발생하는 경고로 오류는 아니다. 따라서 무시해도 괜찮다.
아직 TodoList 컴포넌트에 목 데이터를 전달하지 않았기 때문에 데이터를 페이지에 렌더링하지는 않는다. 따라서 지금은 리액트 개발자 도구의 [Components] 탭울 통해 App 컴포넌트의 hooks 항목에서 State의 값을 확인해 데이터가 잘 설정되는지 확인해야 한다.
CRUD의 첫 번째 기능인 Create를 구현하겠다.
4-(1) 기능 흐름 살펴보기
위 사진은 [할 일 관리] 앱에서 할 일이 추가되는 과정을 도식화한 것이다.
4-(2) 아이템 추가 함수 만들기
TodoEditor 컴포넌트에서 <추가> 버튼을 클릭하면 App에 사용자가 입력한 할 일 데이터를 전달하고 추가 이벤트가 발생했음을 알려야 한다.
- App 컴포넌트
새 할 일 아이템을 추가하는 함수 onCreate 만들기.
(...)
function App() {
const idRef = useRef(3); // 앞에 작성한 목 데이터 id가 0, 1, 2이라 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에는 아이템을 새로 추가할 때마다 중복 id가 만들어지는 문제가 발생한다. 이를 Ref 객체를 사용하여 해결한다.
- App.js
(...)
function App() {
(...)
return (
<div className="App">
<Header />
<TodoEditor onCreate={onCreate} />
<TodoList />
</div>
);
}
export default App;
4-(3) 아이템 추가 함수 호출하기
- TodoEditor 컴포넌트 수정
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;
다음으로 <추가> 버튼을 클릭하면, 함수 onCreate를 호출하는 버튼 클릭 이벤트 핸들러를 만든다.
(...)
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;
아직 App 컴포넌트의 todo 값을 페이지에 렌더링하는 Read 기능은 개발하지 않았기 때문에 개발자 도구의 [Components] 탭에서 직접 확인해야 한다.
4-(4) Create 완성도 높이기
빈 입력 방지하기
빈 입력은 말뜻 그대로 아무것도 입력하지 않은 상태에서 <추가> 버튼을 누르는 행위다. 아무것도 입력하지 않은 상태로 아이템을 추가하는 것을 방지하기 위한 방법은 여럿 있으나, 이 프로젝트에서는 웹 서비스들이 일반적으로 채택하고 있는 빈 입력란에 포커스를 주는 기능을 구현한다.
- TodoEdiotr 컴포넌트 수정
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 수정
(...)
const TodoEditor = ({ onCreate }) => {
(...)
const onSubmit = () => {
if (!content) {
inputRef.current.focus();
return;
}
onCreate(content);
setContent("");
};
(...)
};
export default TodoEditor;
Ender 키를 눌러 아이템 추가하기
- TodoEditor 컴포넌트 수정
(...)
const TodoEditor = ({ onCreate }) => {
(...)
const onKeyDown = (e) => {
if (e.keyCode === 13) {
onSubmit();
} // 13은 <Enter> 키를 의미함
};
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;
출처 : 이정환, 『한 입 크기로 잘라먹는 리액트』, 프로그래밍인사이트(2023), p.290-326.
Corner React.js 1
Editor: Ninanyu
[React.js 1팀] Project 2 [할 일 관리] 앱 만들기 2 (Read: 할 일 리스트 렌더링하기 ~ Delete: 할 일 삭제하기) (0) | 2025.01.03 |
---|---|
[React.js 1팀] 8장. Hooks (1) | 2024.11.29 |
[React.js 1팀] project 1 [카운터] 앱 만들기 ~ 6장. 라이프 사이클과 리액트 개발자 도구 (0) | 2024.11.22 |
[React.js 1팀] 5장. 리액트의 기본 기능 다루기 (2) (0) | 2024.11.15 |
[React.js 1팀] 5장 리액트의 기본 기능 다루기 (1) (1) | 2024.11.08 |