TodoDate 컴포넌트
오늘 날짜의 요일, 월, 일, 연도순으로 표현
TodoWrite 컴포넌트
할 일을 입력하는 input 칸, 추가 버튼을 누르면 Todo List에 추가
TodoList 컴포넌트
할 일 목록이 존재하고, 검색어를 입력하면 필터링되어 TodoList를 출력
TodoItem 컴포넌트
체크박스, 할 일, 등록 날짜, 제거버튼이 존재
UI를 바라볼 때 컴포넌트 단위로 어떻게 나눌 것인지 설계하는 것이 중요하다.
최상단에 위치할 컴포넌트이다. TodoDate 컴포넌트에는 문자열 오늘은
+ 이모티콘(window
+ .
)과 날짜로 구성된다.
const TodoDate = () => {
return(
<div>
<h3>오늘은 📅</h3>
<h1>{new Date().toDateString()}</h1>
</div>
)
}
export default TodoDate;
import TodoDate from './TodoDate'
import './App.css'
function App() {
return (
<div className="App">
<TodoDate />
</div>
)
}
export default App
.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;
}
max-width: 500px
-> 최대 너비 500px로 고정
width: 100%
-> 너비를 브라우저의 100%로 설정, 500px 이상 늘어나지 않음(줄어드는 건 가능)
margin: 0 auto
-> 여백을 위아래는 0
, 좌우는 auto
로 설정
box-sizing: border-box
-> 요소의 크기를 어떤 것을 기준으로 계산할 지 정하는 속성, border-box
로 설정하면 내부 여백이 요소의 크기에 영향을 미치지 않음
padding: 20px
-> 내부 여백을 20px
로 설정
border: 1px solid gray
-> 경계선을 1px 회색 실선으로 설정
display: flex
-> 페이지의 요소를 브라우저에서 어떻게 보여줄지 결정, 기본값은 수직이고 flex
로 설정하면 요소를 수평으로 배치, 주로 다른 요소들을 감싸는 컨테이너 용도로 사용됨
flex-direction: column
-> 컨테이너에 있는 요소들의 배치 방향을 조절하는 속성, 기본 값은 row
로 수평으로 배치하고 column
으로 설정하면 수직으로 배치
gap: 30px
-> 자식 컴포넌트 간의 여백을 조절
TodoWrtie 컴포넌트에는 문자열 새로운 Todo 작성하기
+ 이모티콘(window
+.
)과 할 일을 적을 input
과 추가
버튼이 존재한다.
const TodoWrite = () => {
return(
<div>
<h3>새로운 Todo 작성하기 🖋️</h3>
<input
placeholder="새로운 Todo..."/>
<button>추가</button>
</div>
)
}
export default TodoWrite;
import TodoDate from './TodoDate'
import TodoWrite from './TodoWrite'
import './App.css'
function App() {
return (
<div className='App'>
<TodoDate />
<TodoWrite />
</div>
)
}
export default App
.TodoWrite .WriteContent {
width: 100%;
display: flex;
gap: 10px;
}
.TodoWrite input {
flex: 1;
box-sizing: border-box;
border: 1px solid rgb(220, 220, 220);
border-radius: 5px;
padding: 15px;
}
.TodoWrite input:focus{
outline: none;
border: 1px solid #1f93ff;
}
.TodoWrite button{
cursor: pointer;
width: 80px;
border: none;
background-color: #1f93ff;
color: white;
border-radius: 5px;
}
.WriteContent
-> 입력 폼과 추가 버튼을 감싸는 요소의 className
flex: 1
-> 해당 요소의 너비가 브라우저의 크기에 따라 유연하게 늘어나고 줄어듬
border-radius: 5px
-> 모서리의 둥근 정도 조절
input:focus
-> 입력 폼을 클릭했을 때 반응을 설정
outline: none
-> 두꺼운 선 제거
cursor: pointer
-> 마우스를 버튼에 올렸을 때 마우스의 모양이 변경됨
TodoItem 컴포넌트는 체크박스
+ 할 일 내용
+ 작성 날짜
+ 제거 버튼
으로 구성된다.
import "./TodoItem.css"
const TodoItem = () => {
return(
<div className="TodoItem">
<input type="checkbox" />
<div className="Content">할 일</div>
<div className="WriteDate">{new Date().toLocaleDateString()}</div>
<button>제거</button>
</div>
)
}
export default TodoItem;
TodoList 컴포넌트에는 문자열 Todo List
+ 이모티콘(window
+.
) + Search Bar +TodoItem 컴포넌트
로 구성된다.
import "./TodoList.css"
import TodoItem from "./TodoItem";
const TodoList = () => {
return(
<div>
<h3>Todo List 🍀</h3>
<input
className="SearchBar"
placeholder="검색어를 입력하세요"/>
<div className="ItemList">
<TodoItem />
<TodoItem />
<TodoItem />
</div>
</div>
)
}
export default TodoList;
.TodoItem {
display: flex;
align-items: center;
gap: 20px;
padding-bottom: 20px;
border-bottom: 1px solid rgb(240, 240, 240);
margin-top: 20px;
}
.Content {
flex: 1;
}
.WriteDate{
color: gray;
}
Content에 flex: 1
을 주게 되면
flex-grow: 1;
flex-shrink: 1;
flex-basis: 0%
3가지 의미를 모두 갖는다.
-> 점유 크기를 0으로 지정한 후, 화면 비율에 따라 유연하게 늘어나거나 줄어든다.
.SearchBar{
width: 100%;
border: none;
box-sizing: border-box;
border-bottom: 1px solid rgb(240, 240, 240);
padding-bottom: 10px;
}
UI 구성은 끝났다. 이제 기능 구현으로 넘어가보겠다.
CRUD에 기초하여
Create
: TodoItem 생성하기
Read
: TodoItem을 TodoList에 보여주고, 필터링 기능 구현하기
Update
: 내용 수정은 포함하지 않으므로 checkBox
체크유무 구현하기
Delete
: TodoList에서 TodoItem 삭제하기
데이터의 구조는 체크박스
, 할 일 내용
, 작성 날짜
, 제거버튼
으로 구성되어있다. 제거버튼
을 제외한 내용은 작성자가 작성했을 때 받아와야 하는 정보이다. 초기 설정을 위해 mock data
를 작성할 것이다.
import './App.css'
const mockData = [
{
id:0,
isDone:false,
content: "React 공부하기",
writeDate: new Date().getTime()
},
{
id:1,
isDone:false,
content: "Spring 공부하기",
writeDate: new Date().getTime()
},
id:2,
isDone:false,
content: "Django 공부하기",
writeDate: new Date().getTime()
}
]
function App() { ... }
id
: 고유번호
isDone
: 체크유무
content
: 할 일 내용
writeDate
: 작성 날짜
할 일을 추가하는 기능이다.
function App() {
const [todo, setTodo] = useState(mockData);
const idRef = useRef(3);
const onCreate = (content) => {
const newData = {
id: idRef.current,
isDone: false,
content,
writeDate: new Date().getTime()
};
setTodo([newData, ...todo]);
idRef.current += 1;
};
return (
<div className='App'>
<TodoDate />
<TodoWrite onCreate={onCreate}/>
<TodoList />
</div>
)
}
export default App
추가 버튼
을 누르면 새로운 할 일을 생성하는 함수 onCreate
를 선언한다.newData
변수를 선언한다.setTodo
와 ...
연산자를 통해 기존 데이터에 추가한다.id
값의 변화는 렌더링이 필요하지 않으므로 useRef
를 사용하여 변경한다.onCreate
는 추가 버튼
이 클릭되어야지 호출되므로 TodoWrite
컴포넌트에 props로 전달한다.import { useRef, useState } from "react";
import "./TodoWrite.css"
const TodoWrite = ({ onCreate }) => {
const [content, setContent] = useState("");
const onChangeContent = (event) => {
setContent(event.target.value);
}
const onSubmit = () => {
onCreate(content);
}
return(
<div className="TodoWrite">
<h3>새로운 Todo 작성하기 🖋️</h3>
<div className="WriteContent">
<input
value={content}
onChange={onChangeContent}
placeholder="새로운 Todo..."/>
<button onClick={onSubmit}>추가</button>
</div>
</div>
)
}
export default TodoWrite;
onChangeContent
를 작성하고 input
태그의 value
를 content
로 선언한다.추가 버튼
이 클릭되었을 때 onCreate
함수를 호출하여 새로운 데이터를 저장한다.import { useRef, useState } from "react";
import "./TodoWrite.css"
const TodoWrite = ({ onCreate }) => {
( ... )
const inputRef = useRef();
const onSubmit = () => {
if (!content) {
inputRef.current.focus();
return;
}
onCreate(content);
}
return(
<div className="TodoWrite">
<h3>새로운 Todo 작성하기 🖋️</h3>
<div className="WriteContent">
<input
ref={inputRef}
value={content}
onChange={onChangeContent}
placeholder="새로운 Todo..."/>
<button onClick={onSubmit}>추가</button>
</div>
</div>
)
}
export default TodoWrite;
onSubmit
함수에서 content
가 비어있다면 inputRef의 현재값을 저장한 요소에 포커스한다.function App() {
const [todo, setTodo] = useState(mockData);
(...)
return (
<div className='App'>
<TodoDate />
<TodoWrite onCreate={onCreate}/>
<TodoList todo={todo}/>
</div>
)
}
export default App
TodoList 컴포넌트는 todo
를 리스트로 렌더링해야 한다. 리액트에서 배열 데이터를 렌더링할 때는 map
메서드를 주로 사용한다. map
메서드를 사용하면 컴포넌트를 순회하면서 매 요소를 반복하여 렌더링한다.
import "./TodoList.css"
import TodoItem from "./TodoItem";
const TodoList = ({ todo }) => {
return(
<div>
<h3>Todo List 🍀</h3>
<input
className="SearchBar"
placeholder="검색어를 입력하세요"/>
<div className="ItemList">
{todo.map((it) => (
<TodoItem key={it.id} {...it} />
))}
</div>
</div>
)
}
export default TodoList;
map
메서드를 사용한 부분을 보면, 매개변수로 it
을 받는데 이것은 하나의 요소를 의미한다.
it
내부에는 id, isDone, content, writeDate
가 존재한다. 이것을 전달하기 위해 ...
연산자를 통해 TodoItem 컴포넌트에 props로 전달한다.
key
는 어떤 컴포넌트를 업데이트할지 결정하는 요소다. 그러므로 고유의 값을 할당해야 한다. -> id
import "./TodoItem.css"
const TodoItem = ({id, isDone, content, writeDate}) => {
return(
<div className="TodoItem">
<input
checked={isDone}
type="checkbox" />
<div className="Content">{content}</div>
<div className="WriteDate">{new Date(writeDate).toLocaleDateString()}</div>
<button>제거</button>
</div>
)
}
export default TodoItem;
isDone
: 체크박스 상태를 변경하기 위한 props
content
: 내용을 페이지에 보여주기 위한 props
writeDate
: 저장된 writeDate를 Date 객체에 전달함으로써 해당 날짜를 Date 객체로 생성한다.
SearchBar에 텍스트를 입력하면 필터링되어 TodoList가 출력된다.
import "./TodoList.css"
import TodoItem from "./TodoItem";
import { useState } from "react";
const TodoList = ({ todo }) => {
const [search, setSearch] = useState("");
const onChangeSearch = (event) => {
setSearch(event.target.value);
}
const getSearchResult = () => {
return search === ""
? todo
: todo.filter((it) => it.content.toLowerCase().includes(search.toLowerCase())
)
}
return(
<div>
<h3>Todo List 🍀</h3>
<input
value={search}
onChange={onChangeSearch}
className="SearchBar"
placeholder="검색어를 입력하세요"/>
<div className="ItemList">
{getSearchResult().map((it) => (
<TodoItem {...it} />
))}
</div>
</div>
)
}
onChangeSearch
함수를 작성한다.getSearchResult
함수다.search
가 비어있으면 todo 전체를 출력하고, 비어있지 않다면 todo의 content에 search
가 들어있는 요소만 필터링하여 출력한다.체크 박스를 클릭하여 상태를 변경한다.
function App() {
const [todo, setTodo] = useState(mockData);
( ... )
const onUpdate = (targetId) => {
setTodo(
todo.map((it) =>
it.id === targetId ? {...it, isDone : !it.isDone} : it
)
)
}
return (
<div className='App'>
<TodoDate />
<TodoWrite onCreate={onCreate}/>
<TodoList todo={todo} onUpdate={onUpdate}/>
</div>
)
}
export default App
onUpdate
함수를 작성한다.todo
를 수정하기 위해 map
함수를 통해 targetId
와 같은 id
를 갖는 요소를 찾는다.id
값이 같다면 요소를 펼치고 isDone
의 현재값과 반대의 값으로 변경한다.id
값이 다르다면 변경하지 않는다.할 일 리스트에서 할 일을 삭제한다.
function App() {
const [todo, setTodo] = useState(mockData);
( ... )
const onDelete = (targetId) => {
setTodo(
todo.filter((it) => it.id !== targetId)
)
}
return (
<div className='App'>
<TodoDate />
<TodoWrite onCreate={onCreate}/>
<TodoList todo={todo} onUpdate={onUpdate} onDelete={onDelete}/>
</div>
)
}
export default App
onDelete
를 작성한다.onDelete
함수는 targetId
에 해당하는 id
를 제외한 요소들을 필터링하여 todo에 저장한다.const TodoList = ({ todo, onUpdate, onDelete}) => {
(...)
return(
<div>
<h3>Todo List 🍀</h3>
<input
value={search}
onChange={onChangeSearch}
className="SearchBar"
placeholder="검색어를 입력하세요"/>
<div className="ItemList">
{getSearchResult().map((it) => (
<TodoItem {...it} onUpdate={onUpdate} onDelete={onDelete}/>
))}
</div>
</div>
)
}
const TodoItem = ({id, isDone, content, writeDate, onUpdate, onDelete}) => {
(...)
const onDeleteBtn = () => {
onDelete(id);
}
return(
<div className="TodoItem">
<input
checked={isDone}
onChange={onUpdateCheck}
type="checkbox" />
<div className="Content">{content}</div>
<div className="WriteDate">{new Date(writeDate).toLocaleDateString()}</div>
<button onClick={onDeleteBtn}>제거</button>
</div>
)
}
export default TodoItem;