*토글: 디지털 신호가 1 또는 0을 되풀이하는 상태 (이 일을 마쳤는지 아닌지)
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 />
);
4. 실행 npm run start
import './App.css';
function App() {
return (
<div className="App">
<h2>헬로 리액트</h2>
</div>
);
}
export default App;
body {
margin: 0px; // 페이지의 외부 여백 전부 사라짐
}
.App {
max-width: 500px; // 최대 너비
width: 100%; // 브라우저의 100%
margin: 0 auto; // 여백 위아래 0, 좌우 자동 --> 가운데 배치
box-sizing: border-box; // 요소 크기 계산의 기준
padding: 20px; // 내부 여백
border: 1px solid gray; // 경계선
}
2. App에 배치할 자식 컴포넌트를 아직 구현하지 않았으므로, 임시 요소를 만들어 대신 배치한다.
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; // 플렉스 컨테이너에 이는 요소들의 배치 방향 조절, 수직 (수평: row)
gap: 30px; // 자식 간 여백 조절
...
}
✔️src 폴더 밑에 컴포넌트 파일들을 모아 둘 componentes 폴더를 만들어 관리한다.
기본 구성하기
const Header = () => {
return (
<div className="Header">
Header Component
</div>
)
}
export default Header;
Header 컴포넌트를 페이지에 렌더링 하기 위해 App의 자식으로 배치한다.
import './App.css';
import Header from './components/Header';
function App() {
return (
<div className="App">
<Header />
<div>Todo Editor</div>
<div>Todo List</div>
</div>
);
}
export default App;
오늘 날짜 출력하고 Header.css 적용하기
Header 컴포넌트에 오늘 날짜를 출력하는 코드를 작성한다. 계속해서 components 폴더에 Header 컴포넌트를 스타일링하기 위한 Header.css를 생성하고 다음과 같이 작성한다.
현재의 날짜와 시간을 저장하는 Date 객체를 만들고 toDateString 메서드를 이용해 날짜를 문자열로 표시한다.
작성할 스타일 규칙을 컴포넌트에 적용하기 위해 Header.css를 불러오는 import문을 작성한다.
윈도우키 + .키를 눌러 적절한 이모지를 고른다. 이모지는 문자열로 취급하므로 일반 문자열을 입력해도 상관이 없다.
import "./Header.css";
const Header = () => {
return (
<div className="Header">
<h3>오늘은 📆</h3>
<h1>{new Date().toDateString()}</h1>
</div>
)
}
export default Header;
<h1> 태그 요소의 여백을 0으로 하고, 글꼴 색을 지정한다.
.Header h1 {
margin-bottom: 0px;
color: #1f93ff;
}
componets 폴더에 컴포넌트와 스타일을 정의할 파일 생성하기
TodoEditor.js와 TodoEditor.css 파일을 생성하고 TodoEditor.js를 기본적으로 작성한다. 해당 컴포넌트를 페이지에 렌더링 하기 위해 App.js에 자식으로 배치한다.
import "./TodoEditor.css";
const TodoEditor = () => {
return (
<div className="TodoEditor">
TodoEditor Compoent
</div>
)
}
export default TodoEditor;
import './App.css';
import Header from './components/Header';
import TodoEditor from './components/TodoEditor';
function App() {
return (
<div className="App">
<Header />
<TodoEditor />
<div>Todo List</div>
</div>
);
}
export default App;
TodoEditor UI 만들기
컴포넌트는 요소의 제목, 할 일 아이템을 생성하는 입력 폼, 클릭하면 실제 할 일 아이템을 생성하는 버튼으로 구성되어 있다. 그 후 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 컴포넌트 만들기
동일하게 컴포넌트 파일과 css 파일을 생성하고 App의 자식으로 배치한다.
import "./TodoList.css";
const TodoList = () => { return ( <div className="TodoList"> TodoList Component </div> ) };
export default TodoList;
import './App.css';
import Header from './components/Header';
import TodoEditor from './components/TodoEditor';
import TodoList from './components/TodoList';
function App() {
return (
<div className="App">
<Header />
<TodoEditor />
<TodoList />
</div>
);
}
export default App;
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 컴포넌트를 만들지 않았으므로 할 일 아이템을 출력하지는 못한다.
이제 TodoList에서 낱낱의 할 일 아이템을 표현하는 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;
TodoItem에 스타일을 적용하기 전에 TodoList에 이 컴포넌트를 배치해야 한다.
import "./TodoList.css";
import TodoItem from "./TodoItem";
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에서 여러 개의 TodoItem을 감싸고 있는 ‘list_wrapper’ 요소에 스타일링을 적용해야 한다.
(...)
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 컴포넌트에서 경계를 확인할 필요가 없으니 App.css의 border 속성은 제거 또는 주석 처리한다.
.App {
display: flex;
flex-direction: column;
gap: 30px;
max-width: 500px;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
padding: 20px;
/* border: 1px solid gray; */
}
UI를 완료했으니 이제 컴포넌트 별로 어떤 기능을 구현해야 하는지 살펴보자.
데이터를 다루는 4개의 기능, 즉 추가(Create), 조회(Read), 수정(Update), 삭제(Delete) 기능을 앞글자만 따서 CRUD라고 한다. CRUD는 데이터 처리의 기본 기능으로, 웹 서비스라면 기본적으로 갖추고 있어야 한다.
따라서 이번 프로젝트의 기능 구현은 CRUD 순서에 따라 하나씩 진행한다.
기능 구현에 앞서 CRUD의 대상인 할 일 아이템부터 생성해야 한다.
import { useState } from "react";
(..)
function App() {
const [todo, setTodo] = useState([]);
return (
(...)
);
}
export default App;
함수 useState는 리액트 훅으로 react 라이브러리에서 불러온다. 리액트에서는 보통 리스트 형태의 데이터를 보관할 때 배열을 이용한다. State 변수 todo는 [할 일 관리] 앱에서 데이터를 저장하는 배열이면서 동시에 일종의 데이터베이스 역할을 수행한다.
예를 들어 사용자가 새 할 일 아이템을 만들면, 빈 배열이었던 todo 값은 아이템이 추가된 배열로 업데이트된다. 이는 삭제, 수정도 동일하다.
현실의 사물은 일반적으로 여러 속성을 동시에 가지고 있어 자바스크립트에서는 보통 현실의 사물이나 개념을 표현할 때 객체를 사용한다. 여기서 ‘데이터 모델링’이란 현실의 사물이나 개념을 프로그래밍 언어의 객체와 같은 자료구조로 표현하는 행위이다.
하나의 할 일 아이템에는 일의 완료 여부, 일의 종류, 생성 날짜 등 3가지 정보가 담겨 있다. 세 요소는 각각 isDone, content, createdDate라는 별도의 이름으로 구분한다.
또한, 할 일 아이템에는 페이지에 렌더링 하진 않지만, 모든 아이템에는 해당 아이템을 구별하기 위한 id라는 고유 식별자가 있다. 고유 식별자가 없으면 특정 아이템을 삭제하거나 수정하는 등의 연산이 불가능하다.
따라서, 데이터 모델링을 하는 이유는 데이터를 어떻게 관리할지 생각하기 위함이다.
목(Mock) 데이터란 모조품 데이터라는 뜻이다. 기능을 완벽히 구현하지 않은 상태에서 테스트를 목적으로 사용하는 데이터로 임시 데이터라 표현하기도 한다. 기능을 아직 개발하지 않아 데이터가 없는 상황일 때 목 데이터를 사용한다.
App.js에서 목 데이터를 사용해 보자. 3개의 객체를 저장하는 배열 목 데이터를 만들고 State 변수 todo의 기본값으로 목데이터를 전달한다.
(...)
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 (
(...)
);
}
export default App;
아직 TodoList 컴포넌트에 목 데이터를 전달하지 않았기 때문에 데이터를 페이지에 렌더링 하지는 않는다. 따라서 지금은 리액트 개발자 도구를 이용해 데이터가 잘 설정되는지 확인해야 한다.
개발자 도구에서 [Components] 탭을 열어 App 컴포넌트의 hooks 항목에서 State의 값을 확인한다.
CRUD의 첫 번째 기능인 Create를 구현해 보자
TodoEditor 컴포넌트에서 <추가> 버튼을 클릭하면 App에 사용자가 입력한 할 일 데이터를 전달하고 추가 이벤트가 발생했음을 알려야 한다.
먼저 App 컴포넌트에서 새 할 일 아이템을 추가하는 함수 onCreate 만든다.
TodoEditor 컴포넌트에서 추가 버튼을 클릭하면 onCreate 함수를 호출한다. 이 함수는 사용자가 작성한 할 일 데이터를 받아 매개변수 content에 저장한다. 이 데이터를 토대로 새 할 일 아이템 객체를 만들어 newItem에 저장한다.
배열의 스프레드 연산자를 활용해 newItem을 포함한 새 배열을 만들어 State 변수 todo를 업데이트한다. 새롭게 추가된 아이템은 항상 배열의 0번 요소가 된다.
(...)
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;
그런데 지금의 함수 onCreate는 모든 아이템은 고유한 id를 가져야 하는데, 새롭게 추가할 아이템의 id가 모두 0으로 고정되기 때문에 아이템을 추가할 때마다 중복 id가 만들어져 문제가 발생한다.
Ref 객체를 사용하면 이 문제를 간단히 해결할 수 있다. Ref 객체는 전에 살펴본 적이 있는데, 리액트 훅인 함수 useRef로 생성한다.
📌 Ref 객체는 리액트에서 주로 돔을 조작할 때 사용하지만, 컴포넌트의 변수로도 자주 활용한다.
초깃값이 3인 Ref 객체를 생성해 idRef에 저장한다.
import { useState, useRef } from "react";
(...)
function App() {
const idRef = useRef(3);
(...)
}
export default App;
다음으로 idRef를 이용해 아이템을 생성할 때마다 id가 1씩 늘어나도록 수정한다. 앞서 작성한 목 데이터의 id가 0, 1, 2이므로 아이템을 처음으로 추가하면 해당 아이템의 id는 3이 된다.
(...)
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로 전달해야 한다.
(...)
function App() {
(...)
return (
<div className="App">
<Header />
<TodoEditor onCreate={onCreate} />
<TodoList />
</div>
);
}
export default App;
사용자가 할 일 입력 폼에서 아이템을 입력하고 <추가> 버튼을 클릭한다. 그러면 TodoEditor 컴포넌트는 새 할 일을 생성하기 위해 App에서 Props로 받은 함수 onCreate를 호출하고 현재 사용자가 작성한 할 일을 인수로 전달한다.
import "./TodoEditor.css";
const TodoEditor = ({ onCreate }) => { // Props 객체를 구조 분해 할당
return (
<div className="TodoEditor">
<h4>새로운 Todo 작성하기 ✏️ </h4>
<div className="editor_wrapper">
<input placeholder="새로운 Todo..." />
<button>추가</button>
</div>
</div>
);
};
export default TodoEditor;
다음으로 TodoEditor 컴포넌트의 할 일 입력 폼에서 사용자가 입력하는 새 할 일 데이터를 저장할 State를 만든다.
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;
[Components] 탭에서 App의 TodoEditor를 클릭해 State(content)에 사용자가 지금 입력한 내용이 제대로 반영되는지 확인한다.
다음으로 추가 버튼을 클릭하면, 함수 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] 탭에서 직접 확인해야 한다.
새롭게 추가한 아이템은 App 컴포넌트의 todo 배열 맨 앞에 추가된다.
아무것도 입력하지 않은 상태에서 추가 버튼을 눌러 아이템 추가하는 것을 방지하기 위한 방법은 일반적으로 빈 입력란에 포커스를 주는 기능을 구현한다. 할 일 입력 폼을 관리할 Ref 객체를 하나 만들고, 함수 onSubmit에서 content 값이 비어 있으면 입력 폼에 포커스를 구현하는 방식이다.
onSubmit은 현재 content 값이 빈 문자열이면, inputRef가 현재값으로 저장한 요소에 포커스 하고 종료한다.
아무것도 입력하지 않고 추가 버튼을 눌러도 입력 폼은 아이템을 추가하지 않고 포커스 상태로 멈춰 있게 된다. 개발자 도구의 [Components] 탭에서 확인하면 Ref 값도 변화하지 않는다.
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;
2. 아이템 추가 후 입력 폼 초기화하기
(...)
const TodoEditor = ({ onCreate }) => {
(...)
const onSubmit = () => {
if (!content) {
inputRef.current.focus();
return;
}
onCreate(content);
setContent("");
};
(...)
};
export default TodoEditor;
3. Enter 키를 눌러 아이템 추가하기
(...)
const TodoEditor = ({ onCreate }) => {
(...)
const onKeyDown = (e) => {
if (e.keyCode === 13) { // 13: enter 키
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;
1. css에서 display 속성을 flex로 적용한 요소는 보통 다른 요소들을 감싸는 컨테이너 용도로 사용하는데, 이를 ( 플렉스 컨테이너 )라고 한다.
2. 데이터를 다루는 4개의 기능은 ( Create, Read, Update, Delete )이다.
3. ( 데이터 모델링 )은 현실의 사물이나 개념을 프로그래밍 언어의 객체와 같은 자료구조로 표현하는 행위이다.
4. ( 목 데이터 )는 모조품 데이터라는 뜻으로 기능을 완벽히 구현하지 않은 상태에서 테스트를 목적으로 사용한다.
5. 새롭게 추가할 아이템의 id가 모두 하나로 고정되어 중복 id가 만들어질 경우 ( Ref 객체 )를 이용해 해결할 수 있다.
6. 현재의 날짜와 시간을 저장하는 Date 객체를 만들고 날짜를 문자열로 표시하기 위해 ( toDateString ) 메서드를 활용한다.
7. Create를 구현할 때 빈 입력을 방지할 때 빈 입력란에 ( 포커스 )를 주는 기능을 사용한다.
1. 다음 코드에서 새롭게 추가할 아이템의 id가 모두 0으로 고정되기 때문에 아이템을 추가할 때마다 중복 id가 만들어져 문제가 발생한다. 초기값이 3인 Ref 객체를 생성하여 id 중복 문제를 해결하는 코드를 작성한다.
(...)
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;
2. 다음 코드에서 enter 키를 눌렀을 때 아이템이 추가되도록 코드를 작성한다. enter 키의 keycode는 13이다.
(...)
const TodoEditor = ({ onCreate }) => {
(...)
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;
정답
1.
(...)
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;
2.
(...)
const TodoEditor = ({ onCreate }) => {
(...)
const onKeyDown = (e) => {
if (e.keyCode === 13) { // 13: enter 키
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;
출처 : 이정환, 『한 입 크기로 잘라 먹는 리액트』, 인사이트, p290-326.
Corner React.js 3
Editor: so2
[리액터 스타터3] 7장 useReducer와 상태 관리 / 8장 최적화 (0) | 2023.12.29 |
---|---|
[리액터 스타터3] project 2. [할 일 관리] 앱 만들기 2 (1) | 2023.12.22 |
[리액트 스타터3] 8장. hooks (0) | 2023.11.24 |
[리액트 스타터3] project 1 [카운터] 앱 만들기 6장. 라이프 사이클과 리액트 개발자 도구 (0) | 2023.11.17 |
[리액트 스타터3] 5장. 리액트의 기본 기능 다루기 2 (0) | 2023.11.10 |