src 패키지에 Todo.js
파일을 생성하고 다음 코드 입력
import React from 'react';
class Todo extends React.Component {
render() {
return (
<div className="Todo">
<input type="checkbox" id="todo0" name="todo0" value="todo0"/>
<label for="todo0"> Todo 컴포넌트 만들기 </label>
</div>
);
}
}
export default Todo;
리액트가 렌더링 하는 App컴포넌트에 Todo컴포넌트를 추가한다.
import logo from './logo.svg';
import Todo from './Todo'; // 컴포넌트 import
import './App.css';
function App() {
return (
<div className="App">
// 주석처리함
<Todo />
</div>
);
}
export default App; // 리액트가 랜더링 하는 App
저장을 하면 브라우저의 화면이 변경된다.
여기서 컴포넌트로 만든 Todo를 쉽게 추가할 수 있다.
<Todo />
<Todo />
<Todo />
이제 Todo의 타이틀을 변경해보자
리액트에서 props는 properties의 약자로 컴포넌트간의 데이터 전달 방법을 의미한다.
리액트의 중요 원칙중 하나가 단방향 데이터 흐름인데 이 원칙은 데이터가 항상 부모에서부터 자식으로만 흐르는것을 의미한다.
단방향으로 앱의 동작을 예측하고 디버깅하기 좋아진다.
다음과 같이 사용한다.
function Greeting(props) {
return <h1>Hello, {props.name}</h1>;
}
// 사용 예시
<Greeting name="Alice" />
컴포넌트를 생성자로 생성한다면 아래 코드를 이용한다.
constructor(props) {
super(props);
this.state = { item: props.item };
}
state는 리액트가 관리하는 오브젝트다.
state가 변경된다면 setState
를 이용해서 변경하고 다시 렌더링 한다.
자바스크립트로 된 변수를 jSX에서 사용하려면 기존의 따옴표를 중괄호로 변경해서 사용한다.
<input
type="checkbox"
id={this.state.item.id}
name={this.state.item.id}
checked={this.state.item.done}
/>
props
상태로 만들어지는 Todo 컴포넌트는 아래와 같이 만든다.
import React from 'react';
class Todo extends React.Component {
constructor(props) {
super(props);
this.state = { item: props.item };
}
render() {
return (
<div className="Todo">
<input
type="checkbox"
id={this.state.item.id}
name={this.state.item.id}
checked={this.state.item.done}
/>
<label id={this.state.item.id}>{this.state.item.title}</label>
</div>
);
}
}
export default Todo;
리액트가 렌더링하는 컴포넌트는 App
컴포넌트 이므로 App클래스에서 Todo
컴포넌트를 사용한다.
props
에 item을 넘겨주려면 this.state
를 아래처럼 작성한다.
import React from 'react';
import Todo from './Todo';
import './App.css';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
item: { id: 0, title: "아침에 일어나서 운동하기", done: true }
};
}
render() {
return (
<div className="App">
<Todo item={this.state.item} />
</div>
);
}
}
export default App;
App 컴포넌트는 item 프로퍼티를 초기화했고 Todo는 item 프로퍼티를 받아서 읽는다.
App 에서 배열로된 리스트를 초기화해서 Todo에 넘긴다면
map함수를 이용해서 배열을 반복한다.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
items: [
{ id: 0, title: "아침에 일어나서 운동하기", done: true },
{ id: 1, title: "점심먹고 운동하기", done: false }
]
};
}
render() {
let todoItems = this.state.items.map((item, idx) => (
<Todo item={item} key={item.id} />
));
return (
<div className="App">
{todoItems}
</div>
);
}
}
이제 여기서 Material UI를 이용해서 이쁘게 바꾸기 위해 코드를 수정한다.
Todo 컴포넌트
import { ListItem, ListItemText, InputBase, Checkbox } from '@material-ui/core';
// ....
render() {
const item = this.state.item;
return (
<ListItem>
<Checkbox checked={item.done} />
<ListItemText>
<InputBase
inputProps={{ "aria-label": "naked" }}
type="text"
id={item.id}
name={item.id}
value={item.title}
multiline={true}
fullWidth={true}
/>
</ListItemText>
</ListItem>
);
}
App 컴포넌트
import { Paper, List } from '@material-ui/core';
// .....
render() {
let todoItems = this.state.items.length > 0 && (
<Paper style={{ margin: 16 }}>
<List>
{this.state.items.map((item, idx) => (
<Todo item={item} key={item.id} />
))}
</List>
</Paper>
);
return <div className="App"> {todoItems} </div>;
}
아래 처럼 화면에 렌더링 된다.
먼저 UI를 그린다.
import React from 'react';
import { TextField, Paper, Button, Grid } from '@material-ui/core';
class AddTodo extends React.Component {
constructor(props) {
super(props);
this.state = { item: { title: "" } };
}
render() {
return (
<Paper style={{ margin: 16, padding: 16 }} elevation={ 2 }>
<Grid container>
<Grid xs={11} md={11} item style={{ paddingRight: 16 }}>
<TextField placeholder='Add Todo here' fullWidth />
</Grid>
<Grid xs={1} md={1} item>
<Button fullWidth color='secondary' variant='outlined'>
+
</Button>
</Grid>
</Grid>
</Paper>
);
}
}
export default AddTodo;
Todo를 추가하기 위한 컴포넌트인 AddTodo
를 App
에 추가한다.
render() {
// ...
return (
<div className="App">
<Container maxWidth='md'>
<AddTodo />
<div className='TodoList'>{todoItems}</div>
</Container>
</div>
);
}
이제 아래 화면이 렌더링 된다.
항목을 추가 후 버튼이나 엔터를 누르면 서버로 데이터를 전송하고 서버에서 200응답을 받으면 응답받은 내용을 추가한다.
지금은 Mock함수를 만들어 응답받았다고 가정한다.
추가할 기능은 +
버튼을 눌렀거나 Enter
를 눌렀을때 입력한 Todo를 리스트에 추가한다.
먼저 AddTodo.js에 추가한다.
// 입력칸에 입력하면 상태를 스택에 임시 저장
onInputChange = (e) => {
const thisItem = this.state.item;
thisItem.title = e.target.value;
this.setState({ item: thisItem });
}
// 상태를 리스트에 추가
onButtonClick = () => {
this.add(this.state.item);
this.setState({ item: { title: "" } })
}
enterKeyEventHandler = (e) => {
if ( e.key === 'Enter') {
this.onButtonClick();
}
}
입력한 내용을 상태에 저장후 setState
를 호출했으므로 리액트는 추가된 내용만 다시 렌더링해서 리스트에 추가가 된다.
하지만 AddTodo
컴포넌트는 상위 컴포넌트의 items
리스트에 접근할 수 없다. 따라서 추가하는 표현식은 Add
컴포넌트에 추가한다.
add = (item) => {
const thisItems = this.state.items;
item.id = "ID-" + thisItems.length;
item.done = false;
thisItems.push(item);
this.setState({ items: thisItems });
}
그리고 AddTodo
컴포넌트에 표현식을 생성자로 넣는다.
<AddTodo add={this.add}/>
AddTodo
는 생성자를 수정하고
constructor(props) {
super(props);
this.state = { item: { title: "" } };
this.add = props.add; // 부모에게서 받은 프로퍼티
}
각 하위 컴포넌트에 함수를 추가한다.
<TextField
placeholder='Add Todo here'
fullWidth
onChange={this.onInputChange}
value={this.state.item.title}
onKeyDown={this.enterKeyEventHandler}
/>
<Button
fullWidth
color='secondary'
variant='outlined'
onClick={this.onButtonClick}
>
이제 Mock함수를 이용해서 입력칸에 항목을 입력하면 추가된 리스트가 렌더링 된다.
위 과정과 다를게 없다. 삭제해서 리스트를 다시 갱신하면 된다.
App
컴포넌트에서 리스트에 접근해 삭제하는 표현식을 만들고 Todo
컴포넌트에 생성자로 넣는다.
delete = (item) => {
const thisItems = this.state.items;
const newItems = thisItems.filter(e => e.id !== item.id);
this.setState({ items: newItems })
}
// ...
<Todo item={item} key={item.id} delete={this.delete} />
Todo
컴포넌트에서 삭제에 사용할 컴포넌트를 추가로 import 하고 생성자에 프로퍼티를 추가한다.
import {
ListItem,
ListItemText,
InputBase,
Checkbox,
ListItemSecondaryAction,
IconButton
} from '@material-ui/core';
import DeleteOutlined from '@material-ui/icons/DeleteOutlined';
// ...
constructor(props) {
super(props);
this.state = { item: props.item };
this.delete = props.delete; // 추가
}
이제 삭제할 함수를 만들고 버튼에 함수를 추가한다.
deleteEventHandler = () => {
this.delete(this.state.item)
}
//...
<ListItemSecondaryAction>
<IconButton aria-label="Delete Todo"
onClick={this.deleteEventHandler}>
<DeleteOutlined />
</IconButton>
</ListItemSecondaryAction>
생성된 삭제버튼을 클릭하면 상위 컴포넌트 App
의 delete 표현식을 호출해서 리스트를 갱신한다.
이제 할 일은 체크박스 변경과 타이틀 내용 변경이다.
텍스트의 InputBase
컴포넌트의 readOnly의 기본상태를 true
로 설정하고 텍스트를 클릭된 상태일때만 false
설정을 줘서 수정할 수 있게 한다.
체크박스 설정도 추가해서 체크가 됐을때 줄을 긋도록 textDecoration
도 설정한다.
<InputBase
inputProps={{
style: { textDecoration: item.isChecked ? 'line-through' : 'none' },
readOnly: this.state.readOnly,
}}
//...
/>
생성자에도 추가된 상태를 넣는다.
this.state = { item: props.item, readOnly: true, isChecked: true };
이제 함수를 이용해서 수정된 내용을 적용시키도록 한다.
offReadOnlyMode = () => {
this.setState({ readOnly: false });
}
enterKeyHandler = (e) => {
if (e.key ==='Enter'){
this.setState({ readOnly: true });
}
}
editEventHandler = (e) => {
const thisItem = this.state.item;
thisItem.title = e.target.value;
this.setState({ item: thisItem });
}
checkboxEventHandler = (e) => {
const thisItem = this.state.item;
thisItem.done = !thisItem.done;
thisItem.isChecked = !thisItem.isChecked;
this.setState({ item: thisItem }, () => {
// 디버깅용
console.log("isChecked? ", this.state.item.isChecked)
console.log("done? ", this.state.item.done)
});
}
텍스트필드에 함수를 적용시킨다.
<Checkbox checked={item.done}
disableRipple
onChange={ this.checkboxEventHandler }
/>
// ...
<InputBase
// ...
onClick={ this.offReadOnlyMode }
onChange={ this.editEventHandler }
onKeyDown={ this.enterKeyHandler }
/>
체크박스 기능과 수정기능이 적용된 화면