GIT : https://github.com/kangyongseok/portfolio/tree/master/src/pages/todo
DEMO : https://react-potfolio.firebaseapp.com/todo
ReactJS 로 Todo App 만들기
VELOPERT 님의 블로그에있는 TodoList 만들기를 참고하였습니다.
생초보자가 보기에는 여러가지 생략된 상태로 올라왔기때문에 여기서는 공부할겸 하나하나 풀어가면서 보려고 합니다.
# npm install -g create-react-app
# create-react-app todo_list
# cd todo_list
# npm start
컴포넌트들을 모아서 관리할 component 폴더를 만들고 그 안에 TodoList 와 관련된 새 파일들을 생성해서 구성해 나가도록 하겠습니다.
1. 화면구성
우선 기능을 넣기전에 화면구성에 필요한 작업을 먼저 하겠습니다.
src/component/TodoListTemplate.js
import React from 'react';
import './TodoListTemplate.css';
const TodoListTemplate = () => {
return (
<main className="todo-list-template">
<div className="title">
오늘할일
</div>
<section className="form-area">
할일입력
</section>
<section className="todo-list-area">
할일 목록
</section>
</main>
)
}
export default TodoListTemplate;
src/component/TodoListTemplate.css
body {
background: #eeeeee;
}
.todo-list-template {
background: white;
width:500px;
margin:0 auto;
margin-top:4rem;
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
}
.title {
background: #335d8d;
color:white;
text-align: center;
font-size:2rem;
padding:30px 0;
}
.form-area {
padding:1rem;
border-bottom:1px solid #cccccc;
}
.todo-list-area {
min-height:5rem;
}
src/App.js
import React, { Component } from 'react';
import TodoListTemplate from './component/TodoListTemplate';
class App extends Component {
render() {
return (
<div className="App">
<TodoListTemplate />
</div>
);
}
}
export default App;
결과
TodoListTemplate 는 화면에 정보를 뿌려주는 역할만 하기 때문에 component 가 아니라 함수로 리턴해주기만 합니다.
2. INPUT FORM 추가
할일입력 부분에 input form 을 추가하겠습니다.
src/component/Form.js
import React from 'react';
import './Form.css';
const Form = () => {
return (
<div className="form">
<input />
<div className="create-button">
추가
</div>
</div>
)
}
export default Form;
src/component/Form.css
.form {display: flex;}
.form input {
border:none;
border-bottom:1px solid #335d8d;
flex:1;
font-size: 1.25rem;
outline: none;
}
.create-button {
background: #335d8d;
color:white;
padding:0.5rem 1.5rem;
border-radius: 3px;
font-weight:600;
cursor: pointer;
}
.create-button:hover {
background: #20c8df;
}
src/component/TodoListTemplate.js
import React from 'react';
import './TodoListTemplate.css';
import Form from './Form';
const TodoListTemplate = () => {
return (
<main className="todo-list-template">
<div className="title">
오늘할일
</div>
<section className="form-area">
<Form />
</section>
<section className="todo-list-area">
할일 목록
</section>
</main>
)
}
export default TodoListTemplate;
VELOPERT 님의 블로그에서는 저 부분을 props로 넘겨받아 JSX 형태로 넘겨주는 방법을 취했었는데 이해하는데 조금 어려움이 있을것같아서 기존방식처럼 tag 형태로 넣도록 하겠습니다.
결과
원래방식대로 했을경우 동작 원리를 확인해보자면
src/component/TodoListTemplate.js
const TodoListTemplate = ({form}) => {
return (
<main className="todo-list-template">
<section className="form-area">
{console.log({form})}
...
</main>
)
}
이렇게 콘솔에 찍었을경우
undefined 가 나오게 됩니다.
form이 값을 가지려면 TodoListTemplate 를 출력해주고있는 App.js 에서 <TodoListTemplate /> 가 form={입력} 을 가져야 부모에게서 값을 받아와 적용하고 그걸 화면에 다시 보여주는 역할을 하게 됩니다.
App.js
class App extends Component {
render() {
return (
<div className="App">
<TodoListTemplate form={1} />
</div>
);
}
}
이렇게 1을 입력해 주었을경우 비로소 console 에 form 의 값이 할당되게 됩니다.
따라서 처음의 표현식과 동일한 표현은
App.js
import React, { Component } from 'react';
import TodoListTemplate from './component/TodoListTemplate';
import Form from './component/Form';
class App extends Component {
render() {
return (
<div className="App">
<TodoListTemplate form={<Form />} />
</div>
);
}
}
export default App;
src/component/TodoListTemplate.js
import React from 'react';
import './TodoListTemplate.css';
const TodoListTemplate = ({form}) => {
return (
<main className="todo-list-template">
<div className="title">
오늘할일
</div>
<section className="form-area">
{form}
</section>
<section className="todo-list-area">
할일 목록
</section>
</main>
)
}
export default TodoListTemplate;
사실 전자와 후자와의 어떤 차이가 있는지 정확히 인식은 사실 잘 모르겠습니다.
혹시 설명가능하신분들은 댓글로좀......
확인되는 부분은 App.js 만 보더라도 어떤 템플릿안에서 어떤 컴포넌트들이 사용되고있는지 바로 확인이 가능하다는점이네요.
기존처럼 했을경우 유지보수면에서 물타기식으로 계속 찾아들어가야 하니 좀더 효율적인 면은 있을수 있을것같습니다.
3. 할일목록 추가
src/component/TodoItemList.js
import React, { Component } from 'react';
class TodoItemList extends Component {
render() {
return (
<div>
<p>안녕</p>
<p>리액트</p>
<p>반가워</p>
</div>
)
}
}
export default TodoItemList;
동적인 기능이 들어갈 예정이기때문에 함수형이 아닌 class 로 component를 만들어 줍니다.
src/component/TodoListTemplate.js
import React from 'react';
import './TodoListTemplate.css';
const TodoListTemplate = ({form, children}) => {
return (
<main className="todo-list-template">
<div className="title">
오늘할일
</div>
<section className="form-area">
{form}
</section>
<section className="todo-list-area">
{children}
</section>
</main>
)
}
export default TodoListTemplate;
App.js
import React, { Component } from 'react';
import TodoListTemplate from './component/TodoListTemplate';
import Form from './component/Form';
import TodoItemList from './component/TodoItemList';
class App extends Component {
render() {
return (
<div className="App">
<TodoListTemplate form={<Form />}>
<TodoItemList />
</TodoListTemplate>
</div>
);
}
}
export default App;
children 에 다른 props 도 되나 넣어봤었는데 안되네요... children 만이 TodoListTemplate 의 자식요소인 TodoItemList 를 전달받아 자식컴포넌트에 전달해 줄수 있는것같습니다.
결과
전체적인 구색은 갖춰졌고 이제 기능과 관련된 내용들을 추가하도록 하겠습니다.
그전에 몇가지 사항들을 추가하자면
src/component/TodoItem.js
import React, { Component } from 'react';
import './TodoItem.css';
class TodoItem extends Component {
render() {
const { text } = this.props;
return (
<div className="todo-item">
<div className="remove">×</div>
<div className="todo-text">
<div>{text}</div>
</div>
<div>✓</div>
</div>
);
}
}
export default TodoItem;
여기서 'X 텍스트 v' 를 지정해주고
src/component/TodoItem.css
.todo-item {
padding:1rem;
display: flex;
align-items: center;
cursor: pointer;
transition: all 0.15s;
}
.todo-item:hover {
background: #e3fafc;
}
.todo-item:hover .remove {
opacity: 1;
}
.todo-item + .todo-item {
border-top: 1px solid #cccccc;
}
.remove {
margin-right:1rem;
color:red;
font-weight: 600;
opacity: 0;
}
.todo-text {
flex: 1;
word-break: break-all;
}
.checked {
text-decoration: line-through;
color: #adb5bd;
}
.check-mark {
font-size: 1.5rem;
line-height: 1rem;
margin-left: 1rem;
color: #3bc9db;
font-weight: 800;
}
src/component/TodoItemList.js
import React, { Component } from 'react';
import TodoItem from './TodoItem';
class TodoItemList extends Component {
render() {
return (
<div>
<TodoItem text="안녕" />
<TodoItem text="리액트" />
<TodoItem text="반가워" />
</div>
)
}
}
export default TodoItemList;
위와 같은 형식으로 수정해 준다.
결과
4. 함수만들기
이제 각 기능을 동작시킬 함수를 만들어준다.
최상위 부모컴포넌트인 App.js 에 class 내부에 초기 state 값을 만들어주고 change, create, keyPress 에 동작할 함수를 만들어준다.
초기 state 값
id = 3
state ={
input: '',
todos: [
{ id: 0, text: ' 리액트소개', checked: false },
{ id: 1, text: ' 리액트소개', checked: true },
{ id: 2, text: ' 리액트소개', checked: false }
]
}
이미 3개의 값이 있기때문에 id 는 3부터 시작한다. id는 각각의 할일이 가질 고유의 값이며 create 될때마다 id 는 1씩 증가한다.
handleChange 함수
handleChange = (e) => {
this.setState({
input: e.target.value
});
}
이 함수가 실행되면 이벤트가 발생하고 input에는 선택된 이벤트의 value 값이 들어간다.
handleCreate 함수
handleCreate = () => {
const { input, todos } = this.state;
this.setState({
input: '',
todos: todos.concat({
id: this.id++,
text: input,
checked: false
})
});
}
이 함수는 실행되면 state에 새로운 배열로 todos에 추가하게된다. concat은 새 배열을 생성해주는 함수다.
handleKeyPress 함수
handleKeyPress = (e) => {
if(e.key === 'Enter') {
this.handleCreate();
}
}
이 함수는 실행했을때 이벤트가 발생하고 'Enter' 키와 동일하면 handleCreate 함수를 실행시켜 동일한 동작을 하게 한다.
render
render() {
const { input } = this.state;
const {
handleChange,
handleCreate,
handleKeyPress
} = this;
return (
<div className="App">
<TodoListTemplate form={
<Form
value={input}
onKeyPress={handleKeyPress}
onChange={handleChange}
onCreate={handleCreate}
/>
}>
<TodoItemList />
</TodoListTemplate>
</div>
);
}
render 에서는 input 에 state 를 바인딩하고 위에 작성한 함수들 역시 바인딩해서 자식컴포넌트인 Form Component 에서 실행할수 있도록 전달해준다.
src/component/Form.js
import React from 'react';
import './Form.css';
const Form = ({value, onChange, onCreate, onKeyPress}) => {
return (
<div className="form">
<input value={value} onChange={onChange} onKeyPress={onKeyPress} />
<div className="create-button" onClick={onCreate}>
추가
</div>
</div>
)
}
export default Form;
부모컴포넌트에서 전달받은 값을 props 에 넣어주고 input 에는 입력시 들어갈 값을 value에 넣어주고 onChange 함수와 onKeyPress 함수가 작동하도록 넣어주고 버튼에는 onCreate 함수를 넣어준다.
여기까지하면 input에 값을 입력했을경우 input에서 값이 사라지는것을 볼 수 있다. 입력한 값이 아직 리스트에 출력이 되지않기때문에 출력에 필요한 코드를 작성해야한다.
list 를 보여주는 컴포넌트는 TodoItemList Component 이므로 거기서 코드를 수정한다.
src/component/TodoIteList.js
import React, { Component } from 'react';
import TodoItem from './TodoItem';
class TodoItemList extends Component {
render() {
const { todos } = this.props;
const todoList = todos.map(({id, text, checked}) => (
<TodoItem
id={id}
text={text}
checked={checked}
key={id}
/>
));
return (
<div>
{todoList}
</div>
)
}
}
export default TodoItemList;
props 로 todos 를 부모 컴포넌트에서 받아와 map 함수로 받아온 정보를 돌면서 TodoItem 을 생성한다.
여기까지하면 input에 입력했을경우 정상적으로 list에 추가되는것을 볼수 있지만 아직 삭제나 다른 선택기능들은 구현되어있지않다. 나머지도 추가해보자.
handleToggle 함수
handleToggle = (id) => {
const { todos } = this.state;
const index = todos.findIndex(todo => todo.id === id);
const selected = todos[index];
const nextTodes = [...todos];
nextTodes[index] = {
...selected,
checked: !selected.checked
};
this.setState({
todos: nextTodes
});
}
이 함수는 할일 목록을 클릭시 체크박스의 해제 여부를 실행할 함수이다.
id 를 받아와서 id에 해당하는 index를 찾고 거기에 해당되는 todos의 목록을 selected에 저장해준다.
nextTodes 에는 list를 복사한다.
기존의 값들을 복사하고 checked 값을 덮어쓴다고했는데 이게 어떻게 이렇게 동작되는건지는 지금으로서는 잘 모르겠다.
어쨋든 handleToggle 함수를 만들고 자식 컴포넌트로 prop 해준다.
우선 App.js 에 render 함수에 toggle 함수를 추가해주고
render() {
const { input, todos } = this.state;
const {
handleChange,
handleCreate,
handleKeyPress,
handleToggle
} = this;
return (
<div className="App">
<TodoListTemplate form={
<Form
value={input}
onKeyPress={handleKeyPress}
onChange={handleChange}
onCreate={handleCreate}
/>
}>
<TodoItemList todos={todos} onToggle={handleToggle} />
</TodoListTemplate>
</div>
);
}
src/component/TodoItemList.js
import React, { Component } from 'react';
import TodoItem from './TodoItem';
class TodoItemList extends Component {
render() {
const { todos, onToggle } = this.props;
const todoList = todos.map(({id, text, checked}) => (
<TodoItem
id={id}
text={text}
checked={checked}
key={id}
onToggle={onToggle}
/>
));
return (
<div>
{todoList}
</div>
)
}
}
export default TodoItemList;
src/component/TodoItem.js
import React, { Component } from 'react';
import './TodoItem.css';
class TodoItem extends Component {
render() {
const { text, onToggle, id, checked } = this.props;
return (
<div className="todo-item" onClick={() => onToggle(id)}>
<div className="remove"
onClick={(e) => {e.stopPropagation();}}>×</div>
<div className={`todo-text ${checked && 'checked'}`}>
<div>{text}</div>
</div>
{
checked && (<div className="check-mark">✓</div>)
}
</div>
);
}
}
export default TodoItem;
remove에는 상위요소의 이벤트가 적용되지않도록 e.stopPropagation 으로 해제해 준다.
그리고 text 는 checked 의 값을 받아오냐 아니냐에 따라 스타일이 변경된다.
마지막으로 remove 함수만 만들고 적용하면 된다.
App.js
handleRemove = (id) => {
const { todos } = this.state;
this.setState({
todos: todos.filter(todo => todo.id !== id)
});
}
내장함수 filter를 사용하여 id를 갖고있지않은 새 배열을 생성하고 todos에 적용하여 업데이트하면 선택된 리스트가 사라지게 된다.
문제점
- 아무것도 입력안하고 추가해도 빈리스트로 추가됨 (아무것도 입력이 안되었을경우 경고문구와 함께 추가X)
- 원하는 색상으로 선택해서 리스트를 입력할수 있도록 함수 추가 해야함
code-reading 블로그에 방문해 주셔서 환영합니다.
댓글은 모두 환영하니 많이 달아주세요.