[할 일 관리] 앱의 기능들
[할 일 관리] 앱의 UI 요소를 컴포넌트 단위로 나누기
+ 토글 : 디지털 신호가 1 또는 0을 되풀이하는 상태라는 뜻 → 이 앱에서는 체크 표시 여부에 따라 할 일을 마쳤는지 아닌지 확인할 때 사용합니다.
프로젝트1의 준비 과정과 동일합니다.
// App.js
import "./App.css";
function App() {
return <div className="App"></div>;
}
export default App;
// index.js
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 />);
→ [할 일 관리] 앱의 UI 요소는 좌우 여백이 넓으며 페이지의 정중앙에 자리 잡고 있습니다.
body {
margin: 0px;
}
<body> 태그의 margin을 0px로 설정 → 페이지의 외부 여백 전부 사라지게 합니다.
.App {
max-width: 500px; // 앱 페이지의 최대 너비를 500px로 고정
width: 100%; // 페이지 너비를 브라우저의 100%로 설정
margin: 0 auto; // 여백을 위아래는 0, 좌우는 자동으로 설정
box-sizing: border-box; // 내부 여백이 요소의 크기에 영향을 미치지 않도록 설정
padding: 20px; // 내부 여백을 20px로 설정
border: 1px solid gray; // 경계선을 1px 두께의 회색 실선으로 표시, 경계선 표시를 위한 것으로 이후 삭제 예정
}
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: flex;
flex-direction: column; // App 컴포넌트의 요소를 수직으로 배치
gap: 30px; // 자식 요소 간의 여백을 조절하는 속성
}
display : 페이지의 요소를 브라우저에서 어떻게 보여줄지 결정하는 속성
src에 이 프로젝트의 컴포넌트 파일을 한곳에 모아 둘 component 폴더 생성 후 component 폴더에 Header.js 생성합니다.
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;
import "./Header.css"; // Header.css 파일을 불러와 작성한 스타일 규칙을 적용
const Header = () => {
return (
<div className="Header">
<h3>오늘은 📅</h3>
<h1>{new Date().toDateString()}</h1>
</div>
);
};
export default Header;
.Header h1 {
margin-bottom: 0px;
color: #1f93ff;
} // <h1> 태그 요소의 여백을 0으로 하고 글꼴 색을 지정
component 폴더에 컴포넌트와 스타일을 정의할 TodoEditor.js와 TodoEditor.css 를 각각 생성합니다.
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 .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 컴포넌트 만들기
import "./TodoList.css";
const TodoList = () => {
return (
<div className="TodoList">
<h4>Todo List 🌱</h4>
<input className="searchbar" placeholder="검색어를 입력하세요" />
</div>
);
};
export default 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;
}
TodoItem 컴포넌트 만들기
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 컴포넌트 배치하기
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 .list_wrapper {
display: flex;
flex-direction: column;
gap: 20px;
}
/* 할 일 아이템 박스 스타일 적용 */
.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.css의 border 속성 제거
+ UI 구현을 흔히 ‘퍼블리싱’ 또는 ‘UI 개발’이라고 합니다. UI 개발은 데이터를 가공하고 상태를 관리하는 구현 기능과 더불어 프론트엔드 엔지니어의 기본 소양 중 하나이므로 꾸준히 학습하고 경험을 쌓는 게 꼭 필요합니다.
각 컴포넌트별 기능
컴포넌트는 주로 데이터를 다루는 기능으로 이루어져 있습니다.
→ 데이터를 다루는 4개의 기능, 즉 추가(Create), 조회(Read), 수정(Update), 삭제(Delete) 기능을 앞글자만 따서 CRUD라고 합니다.
import { useState } from "react";
(..)
function App() {
const [todo, setTodo] = useState([]);
return (
(...)
);
}
export default App;
데이터 모델링 : 현실의 사물이나 개념을 프로그래밍 언어의 객체와 같은 자료구조로 표현 하는 행위
하나의 할 일 아이템에 담겨있는 정보
{
id: 0,
isDone: false,
content: "React 공부하기",
createdDate: new Date().getTime(),
}
데이터를 모델링 하는 이유 : 데이터를 어떻게 관리할지 생각하기 위함입니다.
목(Mock) 데이터란 모조품 데이터라는 뜻으로 기능을 완벽히 구현하지 않은 상태에서 테스트를 목적으로 사용하는 데이터입니다.
→ 기능이 개발되지 않아 데이터가 없는 상황일 때 임시 데이터 역할을 하는 목 데이터를 사용하면 데이터 관리 기능 개발이 한결 수월해집니다.
(...)
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의 값을 확인해야 합니다.
할 일이 추가되는 과정을 도식화
TodoEditor 컴포넌트에서 <추가> 버튼 클릭시 App에 사용자가 입력한 할 일 데이터를 전달하고 추가 이벤트가 발생했음을 알려야 합니다.
(...)
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;
(...)
function App() {
(...)
return (
<div className="App">
<Header />
<TodoEditor onCreate={onCreate} />
<TodoList />
</div>
);
}
export default App;
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;
+ 버튼을 클릭하는 이벤트가 발생했을 때 인수로 전달하는 데이터는 컴포넌트 트리 구조상의 데이터 전달이 아니라 일종의 ‘이벤트’가 전달된다고 생각하면 됩니다.
→ 컴포넌트 트리 구조에서 데이터를 전달한다는 의미는 여러 컴포넌트가 동시에 동일한 데이터를 이용한다는 뜻이라 변하는 값인 State는 부모에서 자식으로 Props를 이용해서만 전달할 수 있습니다.
빈 입력 방지하기
빈 입력란에 포커스를 주는 기능 구현
→ 입력 폼을 관리할 Ref 객체 만들어 함수 onSubmit에서 content 값이 비어 있으면 입력 폼에 포커스를 구현하는 방식
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;
아이템 추가 후 입력 폼 초기화하기
아이템 추가하면 자동으로 할 일 입력 폼 초기화하는 기능 구현
(...)
const TodoEditor = ({ onCreate }) => {
(...)
const onSubmit = () => {
if (!content) {
inputRef.current.focus();
return;
}
onCreate(content);
setContent("");
};
(...)
};
export default TodoEditor;
함수 setContent를 호출해 인수로 빈 문자열을 전달 → 새 아이템 추가 후 content 값은 빈 문자열이 되며 입력 폼 초기화
<Enter>키를 눌러 아이템 추가하기
키보드의 <Enter> 키를 누르면, <추가> 버튼을 클릭한 것과 동일한 동작을 수행하는 키 입력 이벤트 구현
(...)
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;
1. 새 할 일 아이템을 추가하는 함수 onCreate를 생성하기 → Ref 객체를 사용해 아이템마다 고유한 id를 가지게 하며 목 데이터를 사용하지 않았음을 주의하기
import { useState } from "react";
(..)
function App() {
const [todo, setTodo] = useState([]);
return (
(...)
);
}
export default App;
정답 :
import { useState, useRef } from "react";
(...)
function App() {
const [todo, setTodo] = useState([]);
const idRef = useRef(0);
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;
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;
정답 :
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);
setContent("");
};
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;
출처 : 이정환, 『한 입 크기로 잘라먹는 리액트』, 프로그래밍인사이트(2023)
Corner React.js 1
Editor: ssxbin
[React.js 1] 7-8장. useReducer와 상태관리 & 최적화 (0) | 2023.12.29 |
---|---|
[React.js 1] p2. 할 일 관리 앱 만들기(2) (1) | 2023.12.22 |
[React.js 1] 8장. Hooks (0) | 2023.11.24 |
[React.js 1] p1. 카운터 앱 만들기, 6장. 라이프 사이클과 리액트 개발자 도구 (0) | 2023.11.17 |
[React.js 1] 5장. 리액트의 기본 기능 다루기(2) (0) | 2023.11.10 |