Today I Learned ... react.js
🙋♂️ Reference Book
🙋 My Dev Blog
리액트를 다루는 기술 DAY 10
- 일정 관리 App
npm init
후에,
yarn create react-app [프로젝트명]
yarn add sass classname react-icons
로 설치.
Component |
---|
TodoTemplate |
TodoInsert |
TodoList |
TodoListItem |
.prettierrc
작성{
"singleQuote": true,
"semi": true,
"useTabs": false,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 80
}
body {
margin: 0;
padding: 0;
background-color: #e9ecef;
}
const App = () => {
return <div>Hello React!</div>
}
export default App;
/src/components
폴더에 생성.
import './TodoTemplate.scss';
const TodoTemplate = ({children}) => {
return (
<div className='TodoTemplate'>
<div className='app-title'>일정 관리</div>
<div className='content'>{children}</div>
</div>
);
};
export default TodoTemplate;
props.children
을 이용하여 하위 컴포넌트들을 렌더링함.
.TodoTemplate {
width: 512px;
margin: 6rem auto 0;
border-radius: 4px;
overflow: hidden;
.app-title {
background-color: #22b8cf;
color: #fff;
height: 4rem;
font-size: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
}
.content {
background: #fff;
}
}
.TodoTemplate
안에.app-title
과.content
를 넣음.
=.TodoTemplate .app-title
과 같음.
import TodoListItem from "./TodoListItem";
import './TodoList.scss';
const TodoList = () => {
return (
<div className="TodoList">
<TodoListItem />
<TodoListItem />
<TodoListItem />
</div>
);
};
export default TodoList;
import {
MdCheckBoxOutlineBlank,
MdCheckBox,
MdRemoveCircleOutline,
} from 'react-icons/md';
import './TodoListItem.scss';
const TodoListItem = () => {
return (
<div className='TodoListItem'>
<div className='checkbox'>
<MdCheckBoxOutlineBlank />
<div className='text'>할 일</div>
</div>
<div className='remove'>
<MdRemoveCircleOutline />
</div>
</div>
);
};
export default TodoListItem;
react-icons 의 md(=Material Design icons)을 임포트함.
- svg 파일을 컴포넌트처럼 사용 가능.
- 크기 조절시 font-size 조절 or props 이용.
.TodoList {
min-height: 320px;
max-height: 513px;
overflow-y: auto;
}
.TodoListItem {
padding: 1rem;
display: flex;
align-items: center;
&:nth-child(even) {
background: #f8f9fa;
}
.checkbox {
cursor: pointer;
flex: 1;
display: flex;
align-items: center;
svg {
font-size: 1.5rem;
}
.text {
margin-left: 0.5rem;
flex: 1;
}
&.checked {
svg {
color: #22b8cf;
}
.text {
color: #adb5bd;
text-decoration: line-through;
}
}
}
.remove {
display: flex;
align-items: center;
font-size: 1.5rem;
color: #ff6b6b;
cursor: pointer;
&:hover {
color: #ff8787;
}
}
& + & {
border-top: 1px solid #dee2e6;
}
}
- nth-child() : 부모요소의 n번째 자식요소.
-> even (짝수) / odd (홀수)- nth-of-type() : 부모요소의 n번째 타입이 일치하는 자식요소
flex : 1
은 차지할 수 있는 영역을 모두 찾하라는 의미.
-> flex-grow
참고 - 파비콘은 테스트겸 등록해봤는데, 아래 코드처럼 작성함.
<link rel="icon" href="./fav.ico" type="image/x-icon" sizes="16x16" />
state
를 갖게 함.
import { useState } from "react";
import TodoInsert from "./components/TodoInsert";
import TodoList from "./components/TodoList";
import TodoTemplate from "./components/TodoTemplate"
const App = () => {
const [todos, setTodos] = useState([
{
id: 1,
text: '리액트 기초 알아보기',
checked: true,
},
{
id: 2,
text: '컴포넌트 스타일링 해보기',
checked: true,
},
{
id: 3,
text: '일정 관리 앱 만들기',
checked: false,
}
]);
return (
<TodoTemplate>
<TodoInsert />
<TodoList todos={todos}/>
</TodoTemplate>
);
}
export default App;
todos
라는 state를 갖게 한 후, (useState)
TodoList 컴포넌트에 props
로 넘겨준다.
import TodoListItem from "./TodoListItem";
import './TodoList.scss';
const TodoList = ({todos}) => {
return (
<div className="TodoList">
{todos.map(todo => (
<TodoListItem todo={todo} key={todo.id} />
))}
</div>
);
};
export default TodoList;
props.todos
를 받아와서 각각의 todos 배열의 요소로 TodoListItem 컴포넌트를 생성함.todo
를 넘겨줌.import {
MdCheckBoxOutlineBlank,
MdCheckBox,
MdRemoveCircleOutline,
} from 'react-icons/md';
import cn from 'classnames';
import './TodoListItem.scss';
const TodoListItem = ({ todo }) => {
const { text, checked } = todo;
return (
<div className='TodoListItem'>
<div className={cn('checkbox', {checked})}>
{checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
<div className='text'>{text}</div>
</div>
<div className='remove'>
<MdRemoveCircleOutline />
</div>
</div>
);
};
export default TodoListItem;
props.todo
를 인자로 받고, <MdCheckBox />
를 렌더링하고, false면 <MdRemoveCircleOutline />
을 렌더링한다.
- classnames 임포트
import cn from 'classnames';
- cn함수 사용
<div className={cn('checkbox', {checked})}>
cn함수는 조건에 따라 클래스를 부여해줄 수 있다.
위 코드는 checkbox는 무조건 갖고, checked가 true면 checked라는 클래스도 갖게 한다.
- 주의 ❗️
반드시 cn함수 안에서 확정 클래스명은 ' '안에, 조건식이나 js코드는 { }안에 적어준다.
todos
배열에 새로운 객체를 추가하는 로직 구현.import { useState, useCallback } from 'react';
import { MdAdd } from 'react-icons/md';
import './TodoInsert.scss';
const TodoInsert = () => {
const [value, setValue] = useState('');
const onChangeInput = useCallback((e) => {
setValue(e.target.value);
}, []);
return (
<form className='TodoInsert'>
<input placeholder='할 일을 입력하세요' onChange={onChangeInput} value={value}/>
<button type="submit">
<MdAdd />
</button>
</form>
);
};
export default TodoInsert;
useRef
이용import { useState, useRef, useCallback } from "react";
import TodoInsert from "./components/TodoInsert";
import TodoList from "./components/TodoList";
import TodoTemplate from "./components/TodoTemplate"
const App = () => {
const [todos, setTodos] = useState([
{
id: 1,
text: '리액트 기초 알아보기',
checked: true,
},
{
id: 2,
text: '컴포넌트 스타일링 해보기',
checked: true,
},
{
id: 3,
text: '일정 관리 앱 만들기',
checked: false,
}
]);
const nextId = useRef(4);
const onInsert = useCallback((text) => {
const todo = {
id: nextId.current,
text,
checked: false,
}
setTodos(todos.concat(todo));
nextId.current += 1;
}, [todos]);
return (
<TodoTemplate>
<TodoInsert onInsert={onInsert}/>
<TodoList todos={todos}/>
</TodoTemplate>
);
}
export default App;
props
로 넘겨줌.todos
라는 state에 의존하는 함수이므로 두번째 인자로 [todos]를 적어줌.import { useState, useCallback } from 'react';
import { MdAdd } from 'react-icons/md';
import './TodoInsert.scss';
const TodoInsert = ({onInsert}) => {
const [value, setValue] = useState('');
const onChangeInput = useCallback((e) => {
setValue(e.target.value);
}, []);
const onSubmitForm = useCallback((e) => {
onInsert(value);
setValue('');
e.preventDefault();
}, [onInsert, value])
return (
<form className='TodoInsert' onSubmit={onSubmitForm}>
<input placeholder='할 일을 입력하세요' onChange={onChangeInput} value={value}/>
<button type="submit">
<MdAdd />
</button>
</form>
);
};
export default TodoInsert;
Form 제출시 발생하는 onSubmitForm 함수를 정의하고, useCallback으로 감싸줌.
이 함수는 onInsert(props
)와 value(state
)에 의존하므로, 두번째 인자로 [onInsert, value]을 작성해줌.
form 제출시 발생하는 기본동작(=새로고침)을 막기 위해 e.preventDefault
를 해줌.
filter
을 이용.Array.prototype.filter()
- 조건을 만족하는 (=true인) 모든 요소들을 모아 새 배열로 반환함.
- 원본 불변성을 지킴.
import { useState, useRef, useCallback } from "react";
import TodoInsert from "./components/TodoInsert";
import TodoList from "./components/TodoList";
import TodoTemplate from "./components/TodoTemplate"
const App = () => {
const [todos, setTodos] = useState([
{
id: 1,
text: '리액트 기초 알아보기',
checked: true,
},
{
id: 2,
text: '컴포넌트 스타일링 해보기',
checked: true,
},
{
id: 3,
text: '일정 관리 앱 만들기',
checked: false,
}
]);
const nextId = useRef(4);
const onInsert = useCallback((text) => {
const todo = {
id: nextId.current,
text,
checked: false,
}
setTodos(todos.concat(todo));
nextId.current += 1;
}, [todos]);
const onRemove = useCallback((id) => {
setTodos(todos.filter(todo => todo.id !== id));
}, [todos])
return (
<TodoTemplate>
<TodoInsert onInsert={onInsert}/>
<TodoList todos={todos} onRemove={onRemove}/>
</TodoTemplate>
);
}
export default App;
state
)에 의존하므로 두번째 인자로 [todos]를 전달해줌.todo.id
와 인자로 받은 id
가 일치하면 - 필터링되게.!==
를 적어줘야 나머지가 false가 되어 배열의 요소로 들어감)<TodoListItem todo={todo} key={todo.id} onRemove={onRemove} />
import {
MdCheckBoxOutlineBlank,
MdCheckBox,
MdRemoveCircleOutline,
} from 'react-icons/md';
import cn from 'classnames';
import './TodoListItem.scss';
const TodoListItem = ({ todo, onRemove }) => {
const { id, text, checked } = todo;
return (
<div className='TodoListItem'>
<div className={cn('checkbox', {checked})}>
{checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
<div className='text'>{text}</div>
</div>
<div className='remove' onClick={() => onRemove(id)}>
<MdRemoveCircleOutline />
</div>
</div>
);
};
export default TodoListItem;
onClick={onRemove(id)}
로 작성하면 에러가 발생한다.<div className='remove' onClick={() => onRemove(id)}>
import { useState, useRef, useCallback } from "react";
import TodoInsert from "./components/TodoInsert";
import TodoList from "./components/TodoList";
import TodoTemplate from "./components/TodoTemplate"
const App = () => {
const [todos, setTodos] = useState([
{
id: 1,
text: '리액트 기초 알아보기',
checked: true,
},
{
id: 2,
text: '컴포넌트 스타일링 해보기',
checked: true,
},
{
id: 3,
text: '일정 관리 앱 만들기',
checked: false,
}
]);
const nextId = useRef(4);
const onInsert = useCallback((text) => {
const todo = {
id: nextId.current,
text,
checked: false,
}
setTodos(todos.concat(todo));
nextId.current += 1;
}, [todos]);
const onRemove = useCallback((id) => {
setTodos(todos.filter(todo => todo.id !== id));
}, [todos])
const onToggle = useCallback((id) => {
setTodos(todos.map(todo => todo.id === id? {...todo, checked: !todo.checked} : todo))
}, [todos])
return (
<TodoTemplate>
<TodoInsert onInsert={onInsert}/>
<TodoList todos={todos} onRemove={onRemove} onToggle={onToggle}/>
</TodoTemplate>
);
}
export default App;
onToggle 로직
- id가 일치하는 todo를 골라서 todo.checked를 토글함.
const onToggle = useCallback((id) => { setTodos(todos.map(todo => todo.id === id? {...todo, checked: !todo.checked} : todo)) }, [todos])
props
로 넘겨준다.import {
MdCheckBoxOutlineBlank,
MdCheckBox,
MdRemoveCircleOutline,
} from 'react-icons/md';
import cn from 'classnames';
import './TodoListItem.scss';
const TodoListItem = ({ todo, onRemove, onToggle }) => {
const { id, text, checked } = todo;
return (
<div className='TodoListItem'>
<div className={cn('checkbox', {checked})} onClick={() => onToggle(id)} >
{checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
<div className='text'>{text}</div>
</div>
<div className='remove' onClick={() => onRemove(id)}>
<MdRemoveCircleOutline />
</div>
</div>
);
};
export default TodoListItem;
onClick={() => onToggle(id)}
와 같이 적어줘야한다.1. insert
2. delete
3. toggle
todoList
에서 todos.map( todo => ... )에서 클릭한 것이 어떤 컴포넌트인지, id가 무엇인지 알 수 있다. (중간다리 역할)
todoListItem
에서는 직접 click이 발생한다. (onClick을 실제로 달아주고, onDelete과 onToggle을 인자와 함께 넣어줌)
KEY POINT
- todos (
state
)는 App.js에서 관리 - useState(초기값)- onInsert, onRemove, onToggle함수 정의도 App.js에서.
-> 전부 App의 state인 todos를 참조하기 때문.- TodoInsert에는 onInsert를,
TodoList에는 (->TodoListItem) onRemove, onToggle을props
로 넘김- TodoInsert는 onChangeInput과 onSubmitForm이 존재.
- TodoInsert는
value
라는 state가 존재. = Input의 value
onSubmitForm은 onInsert함수와 value에 의존함.- TodoList는 todos.map(todo => ...)하여 각 요소들을 TodoListItem으로 렌더링함.
-> todo, onToggle을props
로 넘김. (todo는 자기 자신, 즉 todos의 각 요소. = 객체 자체)- TodoListItem 에서는 classname를 사용하여 todo.checked 여부에 따라 클래스명 부여
- todo.id, todo.text, todo.checked를 이용하여 렌더링.
div.checkbox
클릭시 - onToggle(id)div.remove
클릭시 - onRemove(id)