"리액트를 다루는 기술" 책을 읽고 정리 ( 저자 : 김민준 )
저자 블로그 : https://velopert.com/reactjs-tutorials
저자 블로그 : https://velog.io/@velopert/tags/react
[10장] 일정관리 웹 어플리케이션
-프로젝트 셋팅-
1. 프로젝트 생성
$ create-react-app todo-list
생성 후, index.js 와 serviceWorker.js 남기고 나머지 파일 삭제
2. css Module 및 sass 적용
$ yarn eject
$ yarn add sass-loader node-sass classnames
3. webpack 설정 파일 수정
- webpack.config.dev.js 내 수정
- 바뀌기 전 설정 내용
{
test: sassRegex,
exclude: sassModuleRegex,
use: getStyleLoaders({ importLoaders: 2 }, 'sass-loader'),
}
- 바뀐 후 설정 내용 (USE 부분이 바뀜)
{
test: sassRegex,
exclude: sassModuleRegex,
use: getStyleLoaders({ importLoaders: 2 }).concat({
loader: require.resolve('sass-loader'),
options: {
includePaths: [paths.appSrc + '/styles']
}
})
}
4. open-color 적용
$ yarn add open-color
5. 폴더 생성
src/styles 디렉토리에 utils.scss 생성 후 import
@import '~open-color/open-color';
6. 메인 스타일 지정 ( 위치 : src/styles/main.scss)
@import 'utils';
body {
background: $oc-gray-1;
margin:0px;
}
7. index.js 수정
css 부분과 App.js 폴더 위치 변경
import './styles/main.scss';
import App from './component/App';
8. App 생성
src/component 에 App.js 생성
import React, { Component } from 'react';
class App extends Component {
render() {
return (
<div>
일정관리
</div>
);
}
}
export default App;
### yarn start로 로컬 서버를 띄우고, 화면이 잘 뜨는지 확인!!
* 필요한 컴포넌트
- PageTemplate(메인화면), TodoInput(할일 받기), TodoItem(할일), TodoList(할일을 리스트로 뿌림)
-UI 셋팅-
9. PageTemplate 컴포넌트 만들기 ( App.js 에 추가 )
- git : https://github.com/velopert/learning-react/tree/master/10/todo-list/src/components/PageTemplate
src/component/PageTemplate 폴더 내 index.js, PageTemplate.js, PageTemplate.scss 생성
import React from 'react';
import styles from './PageTemplate.scss';
import classNames from 'classnames/bind';
const cx = classNames.bind(styles);
const PageTemplate = ({children}) => {
return (
<div className={cx('page-template')}>
<h1>일정 관리</h1>
<div className={cx('content')}>
{children}
</div>
</div>
);
};
export default PageTemplate;
.page-template {
margin-top: 5rem;
margin-left: auto;
margin-right: auto;
width: 500px;
background: white;
// 그림자를 띄워줍니다.
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
padding-top: 2rem;
// 브라우저의 크기가 768px 미만으로 됐을 땐,
@media(max-width: 768px) {
margin-top: 1rem;
width: calc(100% - 2rem); // 양 옆에 1rem의 여백을 남기고 꽉 채워줍니다.
}
h1 {
text-align: center;
font-size: 4rem;
font-weight: 300;
margin: 0;
}
.content {
margin-top: 2rem;
}
}
export { default } from './PageTemplate';
### 화면 확인 ( 일정 관리 <br> 안녕하세요 떴는지..)
10. todoInput 컴포넌트 생성 ( App.js 에 추가 )
- git : https://github.com/velopert/learning-react/blob/master/10/todo-list/src/components/TodoInput
- 위치 : src/component/TodoInput
import React from 'react';
import styles from './TodoInput.scss';
import classNames from 'classnames/bind';
const cx = classNames.bind(styles);
// 인풋과 버튼이 함께 있는 컴포넌트입니다.
/*
value: 인풋 값
onChange: 인풋 변경 이벤트
onInsert: 추가버튼 클릭 이벤트
*/
const TodoInput = ({value, onChange, onInsert}) => {
// 엔터키가 눌리면 onInsert 를 실행합니다.
const handleKeyPress = (e) => {
if(e.key === 'Enter') {
onInsert();
}
}
return (
<div className={cx('todo-input')}>
<input onChange={onChange} value={value} onKeyPress={handleKeyPress}/>
<div className={cx('add-button')} onClick={onInsert}>추가</div>
</div>
);
};
export default TodoInput;
@import 'utils'; // open-color 를 사용해야 하므로 utils를 불러왔습니다.
.todo-input {
border-top: 1px solid $oc-gray-2;
border-bottom: 1px solid $oc-gray-2;
// 손쉬운 레이아웃 설정을 위하여 flex를 사용합니다.
display: flex;
padding: 1rem;
input {
// 인풋의 기본 스타일을 지우고 새 스타일을 정의합니다.
flex: 1; // 부모 엘리먼트에서 add-button을 제외한 나머지 공간을 차지합니다.
font-size: 1.1rem;
outline: none;
border: none;
background: transparent;
border-bottom: 1px solid $oc-gray-4;
&:focus {
border-bottom: 1px solid $oc-cyan-6;
}
}
.add-button {
width: 5rem;
height: 2rem;
margin-left: 1rem;
border: 1px solid $oc-green-7;
color: $oc-green-7;
font-weight: 500;
font-size: 1.1rem;
display: flex;
// 내용을 가운데에 정렬시킵니다.
align-items: center;
justify-content: center;
cursor: pointer;
&:hover {
background: $oc-green-7;
color: white;
}
&:active {
background: $oc-green-8;
}
}
}
export { default } from './TodoInput';
11. TodoItem 컴포넌트 생성 ( App.js 에 추가 안함 TodoList에 추가)
- git: https://github.com/velopert/learning-react/tree/master/10/todo-list/src/components/TodoItem
- 위치 : src/component/TodoItem
import React, { Component } from 'react';
import styles from './TodoItem.scss';
import classNames from 'classnames/bind';
const cx = classNames.bind(styles);
class TodoItem extends Component {
render() {
const {done, children, onToggle, onRemove} = this.props;
/* 위 코드에선 비구조화 할당을 통하여 this.props 안에 있는
done, children, onToggle, onRemove 에 대한 레퍼런스를 만들어주었습니다. */
return (
<div className={cx('todo-item')} onClick={onToggle}>
<input className={cx('tick')} type="checkbox" checked={done} readOnly/>
<div className={cx('text', { done })}>{children}</div>
<div className={cx('delete')} onClick={(e) => {
onRemove();
e.stopPropagation();
}
}>[지우기]</div>
</div>
);
}
}
export default TodoItem;
@import 'utils';
.todo-item {
padding: 1rem;
display: flex;
align-items: center;
cursor: pointer;
.tick {
margin-right: 1rem;
}
.text {
flex: 1;
word-break: break-all;
&.done {
text-decoration: line-through;
}
}
.delete {
margin-left: 1rem;
color: $oc-red-7;
font-size: 0.8rem;
&:hover {
color: $oc-red-5;
text-decoration: underline;
}
}
&:nth-child(odd) {
// 홀수 번째 엘리먼트에는 회색 배경
background: $oc-gray-0;
}
&:hover {
background: $oc-gray-1;
}
}
.todo-item + .todo-item {
// 컴포넌트 사이 사이에 위쪽 테두리를 설정합니다.
border-top: 1px solid $oc-gray-1;
}
export { default } from './TodoItem';
12. TodoList
- git : https://github.com/velopert/learning-react/tree/master/10/todo-list/src/components/TodoList
- 위치 : src/components/TodoList
import React, { Component } from 'react';
import TodoItem from '../TodoItem';
class TodoList extends Component {
render() {
return (
<div>
<TodoItem done>리액트 공부하기 </TodoItem>
<TodoItem>소스 수정하기 </TodoItem>
</div>
);
}
}
export default TodoList;
export { default } from './TodoList';
-status 셋팅-
13. todoInput 값 변경 적용 작업
import React, { Component } from 'react';
import PageTemplate from './PageTemplate';
import TodoInput from './TodoInput';
import TodoList from './TodoList';
class App extends Component {
state = {
input : ''
}
handleChange = (e) => {
const { value } = e.target;
this.setState({
input:value
});
}
render() {
const { input } = this.state;
const { handleChange } = this;
return (
<PageTemplate>
<TodoInput onChange={handleChange} value={input}/>
<TodoList />
</PageTemplate>
);
}
}
export default App;
14. 초기 일정 데이터 정의 및 랜더링
- 리스트 노출을 위한 App.js 수정
import React, { Component } from 'react';
import PageTemplate from './PageTemplate';
import TodoInput from './TodoInput';
import TodoList from './TodoList';
class App extends Component {
state = {
input : '',
todos :
[
{id:0, text:'리액트공부하기', done:true},
{id:1, text:'소스 수정하기', done:false}
]
}
handleChange = (e) => {
const { value } = e.target;
this.setState({
input:value
});
}
render() {
const { input, todos } = this.state;
const { handleChange } = this;
return (
<PageTemplate>
<TodoInput onChange={handleChange} value={input}/>
<TodoList todos={todos}/>
</PageTemplate>
);
}
}
export default App;
- todoList.js 수정
import React, { Component } from 'react';
import TodoItem from '../TodoItem';
class TodoList extends Component {
render() {
const {todos} = this.props;
const todoList = todos.map(
todo=>(
<TodoItem key={todo.id} done={todo.done}>{todo.text}</TodoItem>
)
);
return (
<div>
{todoList}
</div>
);
}
}
export default TodoList;
15. 데이터 추가
import React, { Component } from 'react';
import PageTemplate from './PageTemplate';
import TodoInput from './TodoInput';
import TodoList from './TodoList';
class App extends Component {
state = {
input : '',
todos :
[
{id:0, text:'리액트공부하기', done:true},
{id:1, text:'소스 수정하기', done:false}
]
}
id = 1
getId = () => {
return ++this.id;
}
handleInsert = () => {
const {todos, input } = this.state;
const newTodo = {
text:input,
done:false,
id:this.getId()
};
this.setState({
todos : [...todos, newTodo],
input :''
});
}
handleChange = (e) => {
const { value } = e.target;
this.setState({
input:value
});
}
render() {
const { input, todos } = this.state;
const { handleChange, handleInsert } = this;
return (
<PageTemplate>
<TodoInput onChange={handleChange} onInsert={handleInsert} value={input} />
<TodoList todos={todos}/>
</PageTemplate>
);
}
}
export default App;
16. 체크박스 활성화/비활성화
import React, { Component } from 'react';
import PageTemplate from './PageTemplate';
import TodoInput from './TodoInput';
import TodoList from './TodoList';
class App extends Component {
state = {
input : '',
todos :
[
{id:0, text:'리액트공부하기', done:true},
{id:1, text:'소스 수정하기', done:false}
]
}
id = 1
getId = () => {
return ++this.id;
}
handleInsert = () => {
const {todos, input } = this.state;
const newTodo = {
text:input,
done:false,
id:this.getId()
};
this.setState({
todos : [...todos, newTodo],
input :''
});
}
handleChange = (e) => {
const { value } = e.target;
this.setState({
input:value
});
}
handleToggle = (id) => {
const { todos } = this.state;
const index = todos.findIndex(todo => todo.id === id);
const toggled = {
...todos[index],
done:!todos[index].done
};
this.setState({
todos : [
...todos.slice(0, index),
toggled,
...todos.slice(index+1, todos.length)
]
});
}
render() {
const { input, todos } = this.state;
const { handleChange, handleInsert, handleToggle } = this;
return (
<PageTemplate>
<TodoInput onChange={handleChange} onInsert={handleInsert} value={input} />
<TodoList todos={todos} onToggle={handleToggle}/>
</PageTemplate>
);
}
}
export default App;
import React, { Component } from 'react';
import TodoItem from '../TodoItem';
class TodoList extends Component {
render() {
const {todos, onToggle} = this.props;
const todoList = todos.map(
todo=>(
<TodoItem key={todo.id} done={todo.done} onToggle={() => onToggle(todo.id)}>
{todo.text}
</TodoItem>
)
);
return (
<div>
{todoList}
</div>
);
}
}
export default TodoList;
17. 데이터 제거
- App.js 에 Remove 함수 넣어주면 됨
import React, { Component } from 'react';
import PageTemplate from './PageTemplate';
import TodoInput from './TodoInput';
import TodoList from './TodoList';
class App extends Component {
state = {
input : '',
todos :
[
{id:0, text:'리액트공부하기', done:true},
{id:1, text:'소스 수정하기', done:false}
]
}
id = 1
getId = () => {
return ++this.id;
}
handleInsert = () => {
const {todos, input } = this.state;
const newTodo = {
text:input,
done:false,
id:this.getId()
};
this.setState({
todos : [...todos, newTodo],
input :''
});
}
handleChange = (e) => {
const { value } = e.target;
this.setState({
input:value
});
}
handleToggle = (id) => {
const { todos } = this.state;
const index = todos.findIndex(todo => todo.id === id);
const toggled = {
...todos[index],
done:!todos[index].done
};
this.setState({
todos : [
...todos.slice(0, index),
toggled,
...todos.slice(index+1, todos.length)
]
});
}
handleRemove = (id) => {
const { todos } = this.state;
const index = todos.findIndex(todo => todo.id === id);
this.setState({
todos : [
...todos.slice(0, index),
...todos.slice(index+1, todos.length)
]
});
}
render() {
const { input, todos } = this.state;
const { handleChange, handleInsert, handleToggle, handleRemove } = this;
return (
<PageTemplate>
<TodoInput onChange={handleChange} onInsert={handleInsert} value={input} />
<TodoList todos={todos} onToggle={handleToggle} onRemove={handleRemove}/>
</PageTemplate>
);
}
}
export default App;
import React, { Component } from 'react';
import TodoItem from '../TodoItem';
class TodoList extends Component {
render() {
const {todos, onToggle, onRemove} = this.props;
const todoList = todos.map(
todo=>(
<TodoItem key={todo.id} done={todo.done} onToggle={() => onToggle(todo.id)} onRemove={() => onRemove(todo.id)}>
{todo.text}
</TodoItem>
)
);
return (
<div>
{todoList}
</div>
);
}
}
export default TodoList;
- 이때 그냥 지우기를 하면 안 지워짐.
import React, { Component } from 'react';
import styles from './TodoItem.scss';
import classNames from 'classnames/bind';
const cx = classNames.bind(styles);
class TodoItem extends Component {
render() {
const {done, children, onToggle, onRemove} = this.props;
/* 위 코드에선 비구조화 할당을 통하여 this.props 안에 있는
done, children, onToggle, onRemove 에 대한 레퍼런스를 만들어주었습니다. */
return (
<div className={cx('todo-item')} onClick={onToggle}>
<input className={cx('tick')} type="checkbox" checked={done} readOnly/>
<div className={cx('text', { done })}>{children}</div>
<div className={cx('delete')} onClick={(e) => {
onRemove();
e.stopPropagation();
}
}>[지우기]</div>
</div>
);
}
}
export default TodoItem;