[React] Todo 추가, 삭제, 수정

merci·2023년 5월 25일
0

Todo 컴포넌트 생성

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

리액트에서 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

이제 여기서 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>;
  }

아래 처럼 화면에 렌더링 된다.


Todo 추가하기

먼저 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를 추가하기 위한 컴포넌트인 AddTodoApp에 추가한다.

render() {
	// ...
    return (
      <div className="App">
        <Container maxWidth='md'>
          <AddTodo />
          <div className='TodoList'>{todoItems}</div>
        </Container>
      </div>
    );
  }

이제 아래 화면이 렌더링 된다.

Add 핸들러 추가

항목을 추가 후 버튼이나 엔터를 누르면 서버로 데이터를 전송하고 서버에서 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함수를 이용해서 입력칸에 항목을 입력하면 추가된 리스트가 렌더링 된다.


Todo 삭제

위 과정과 다를게 없다. 삭제해서 리스트를 다시 갱신하면 된다.

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 표현식을 호출해서 리스트를 갱신한다.


Todo 수정

이제 할 일은 체크박스 변경과 타이틀 내용 변경이다.
텍스트의 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 }
    />

체크박스 기능과 수정기능이 적용된 화면

profile
작은것부터

0개의 댓글