1. 프로젝트 준비하기
>$ yarn create react-app todo-app
>$ cd todo-app
>$ yarn add node-sass classnames react-icons
1-1) prettier 설정
.prettierrc
{
"singleQuote": true,
"semi": true,
"useTabs": false,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 80
}
1-2) index.css 수정
font 삭제, background 설정
body {
margin: 0;
padding: 0;
background: #e9ecef;
}
1-3) App.js 초기화
import React from 'react';
const App = () => {
return (
<div>
Todo App을 만들자!
</div>
);
};
export default App;
2. UI 구성하기
앞으로 생성할 컴포넌트 정리
- TodoTemplate : 화면을 가운데에 정렬시켜주며, 일정관리를 보여주는 컴포넌트
- TodoInsert : 새로운 항목을 입력하고 추가할 수 있는 컴포넌트
- TodoListItem : 각 할 일 항목에 대한 정보를 보여주는 컴포넌트
- TodoList : todos 배열을 props로 받아 온 후, 이를 배열 내장 함수 map을 사용해서 여러 개의 TodoListItem로 변환해주는 컴포넌트
위 4개의 컴포넌트를 구현할 componenets 디렉토리를 생성한다.
2-1) TodoTemplate 생성
TodoTemplate.js
import React from 'react';
import './TodoTemplate.scss';
const TodoTemplate = ({ children }) => {
return (
<div className="TodoTemplate">
<div className="app-title">일정 관리</div>
<div className="content">{children}</div>
</div>
);
};
export default TodoTemplate;
App.js 에 해당 컴포넌트 렌더링
App.js
import React from 'react';
import TodoTemplate from './components/TodoTemplate';
const App = () => {
return (
<TodoTemplate>
Todo App을 만들자!
</TodoTemplate>
);
};
export default App;
TodoTemplate.scss
.TodoTemplate {
width: 512px;
// width가 주어진 상태에서 좌우 중앙 정렬
margin-left: auto;
margin-right: auto;
margin-top: 6rem;
border-radius: 4px;
overflow: hidden;
.app-title {
background: #22b8cf;
color: white;
height: 4rem;
font-size: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
}
.content {
background: white;
}
}
화면 레이아웃시 많이 사용되는 flex에 대해 익히면 좋음으로 관련 링크는 아래와 같다.
https://d2.naver.com/helloworld/8540176
2-2) TodoInsert 생성
TodoInsert.js
import React from 'react';
import { MdAdd } from 'react-icons/md';
import './TodoInsert.scss';
const TodoInsert = () => {
return (
<form className="TodoInsert">
<input placeholder="할 일을 입력하세요"/>
<button type="submit">
<MdAdd />
</button>
</form>
);
};
export default TodoInsert;
App.js
import React from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/TodoInsert';
const App = () => {
return (
<TodoTemplate>
<TodoInsert></TodoInsert>
</TodoTemplate>
);
};
export default App;
TodoInsert.scss
.TodoInsert {
display: flex;
background: #495057;
input {
// 기본 스타일 초기화
background: none;
outline: none;
border: none;
padding: 0.5rem;
font-size: 1.125rem;
line-height: 1.5;
color: white;
&::placeholder {
color: #dee2e6;
}
// 버튼을 제외한 영역을 모두 차지하기
flex: 1;
}
button {
// 기본 스타일 초기화
background: none;
outline: none;
border: none;
background: #868e96;
color: white;
padding-left: 1rem;
padding-right: 1rem;
font-size: 1.5rem;
display: flex;
align-items: center;
cursor: pointer;
transition: 0.1s background ease-in;
&:hover {
background: #adb5bd;
}
}
}
2-3) TodoListItem과 TodoList 만들기
TodoListItem.js
import React from 'react';
import {
MdCheckBoxOutlineBlank,
MdCheckBox,
MdRemoveCircleOutline,
} from 'react-icons/md';
import cn from 'classnames';
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;
TodoList.js
import React from 'react';
import TodoListItem from './TodoListItem';
import './TodoList.scss';
const TodoList = () => {
return (
<div className="TodoList">
<TodoListItem/>
<TodoListItem/>
<TodoListItem/>
</div>
);
};
export default TodoList;
App.js
import React from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
const App = () => {
return (
<TodoTemplate>
<TodoInsert/>
<TodoList/>
</TodoTemplate>
);
};
export default App;
TodoList.scss
.TodoList {
min-height: 320px;
max-height: 513px;
overflow-y: auto;
}
TodoListItem.scss
.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;
}
}
3. 기능 구현하기
3-1) App에서 todos 상태 사용하기
App.js
import React, { useState } from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
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 배열 안에 객체의 id, 내용, 완료여부 항목이 포함되어 있다.
TodoList에서 props 로 전달받은 값을 TodoItem으로 변환할 수 있도록 렌더링 설정을 해줘야 한다.
TodoList.js
import React from 'react';
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 배열을 map을 통해 TodoListItem으로 이뤄진 배열로 변환 후 렌더링한다.
map을 통한 컴포넌트 변환시 반드시 key props를 전달해야 한다. 그래서 key에 todo.id값을 매핑한다.
또한, 데이터를 각각 전달하는 것보다 todo로 전부 전달하는 것이 성능적 측면에서 좋기에 todo props에 todo 전체를 매핑한다.
TodoListItem.js
import React from 'react';
import {
MdCheckBoxOutlineBlank,
MdCheckBox,
MdRemoveCircleOutline,
} from 'react-icons/md';
import cn from 'classnames';
import './TodoListItem.scss';
const TodoListItem = ({ todo }) => {
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">
<MdRemoveCircleOutline />
</div>
</div>
);
};
export default TodoListItem;
조건부 스타일링을 위해 classNames을 사용한다.
3-2) 항목 추가 기능 구현하기
TodoInsert 컴포넌트에서 인풋에 입력하는 값을 관리할 수 있도록 useState를 사용하여 value라는 상태를 정의한다.
이때 onChange 함수를 작성하는데 컴포넌트 리렌더링될 때마다 함수를 재사용할 수 있도록 useCallback Hook을 사용한다.
TodoInsert.js
import React {useState, useCallback} from 'react';
import { MdAdd } from 'react-icons/md';
import './TodoInsert.scss';
const TodoInsert = () => {
const [value, serValue] = useState('');
const onChange = userCallback(e => {sertValue(e.target.value);}, []);
return (
<form className="TodoInsert">
<input placeholder="할 일을 입력하세요" value={value} onChange={onChange}/>
<button type="submit">
<MdAdd />
</button>
</form>
);
};
export default TodoInsert;
onChange 함수 안에 console.log를 찍어 보면 input 텍스트가 잘 들어가는걸 확인할 수 있다.
3-3) todos 배열에 새 객체 추가하기 (onInsert 함수 구현)
onInsert 함수를 통해 새로운 객체를 생성할 것이며, id 값은 useRef 를 사용하여 1씩 증가시킬 것이다.
컴포넌트 성능을 위해 onInsert함수를 useCallback 함수로 감싸 주는 것이 좋다.
App.js
import React, { useState } from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
const App = () => {
const [todos, setTodos] = useState([
{
id: 1,
text: '리액트의 기초 알아보기',
checked: true,
},
{
id: 2,
text: '컴포넌트 스타일링해 보기',
checked: true,
},
{
id: 3,
text: '일정 관리 앱 만들어 보기',
checked: false,
},
]);
// 고유 값으로 사용 될 id
// ref 를 사용하여 변수 담기
const nextId = useRef(4);
const onInsert = useCallback(
text => {
const todo = {
id: nextId.current,
text,
checked: false,
};
setTodos(todos.concat(todo));
nextId.current += 1; // nextId 1 씩 더하기
},
[todos],
);
return (
<TodoTemplate>
<TodoInsert onInsert={onInsert} />
<TodoList todos={todos}/>
</TodoTemplate>
);
};
export default App;
3-4) TodoInsert에서 onSubmit 이벤트 설정하기
onInsert 함수에 현재 useState를 통해 관리하고 있는 value 값을 파라미터로 넣어준다.
TodoInsert.js
import React {useState, useCallback} from 'react';
import { MdAdd } from 'react-icons/md';
import './TodoInsert.scss';
const TodoInsert = ({onInsert}) => {
const [value, serValue] = useState('');
const onChange = userCallback(e => {sertValue(e.target.value);}, []);
const onSubmit = useCallback(e => {onInsert(value); setValue(''); e.preventDefault();
},
[onInsert, value],
);
return (
<form className="TodoInsert" onSubmit={onSubmit}>
<input placeholder="할 일을 입력하세요" value={value} onChange={onChange}/>
<button type="submit">
<MdAdd />
</button>
</form>
);
};
export default TodoInsert;
submit 이벤트는 브라우저에서 새로고침을 발생시킵니다. 이를 방지하기 위하여 e.preventDefault() 함수를 호출합니다.
onSubmit함수가 호출되면 props로 받아 온 onInsert 함수에 현재 value 값을 파라미터로 넣어서 호출하고, 현재 value 값을 초기화한다.
또한, onSubmit 대신에 버튼 클릭 이벤트로도 구현 가능하다. onSubmit으로 구현하면 input 태그에서 엔터만으로도 클릭이벤트를 발생시킴으로 권장하는 방법이다.
참고서적 : 리엑트를 다루는 기술(김민준)
'IT > React' 카테고리의 다른 글
[Leaning-React] 09. 컴포넌트 스타일링 (0) | 2020.01.29 |
---|