React 실습- basic 3: TODO 앱의 CRUD 기능 구현해보기

재로미·2021년 9월 13일
1

React

목록 보기
4/4
post-custom-banner

지지난 실습 포스트에 이어서 코드 설명이 진행됩니다. 클래스형 컴포넌트로 진행하는 실습을 보시려면 여기를 먼저 참고해주세요.

지난 포스트에서 react의 state와 props 에 대해 알아보았다. state와 props만 잘 이해해도 기본적인 CRUD(Create, Read, Update, Delete) 기능은 구현 가능하다.

최종적으로 만들어지는 간단한 TODO 앱의 형태는 아래 GIF 파일과 같다.

CRUD 중에서도 읽는 것이 제일 쉽다. 삭제 기능 역시 크게 어렵지 않으며, 생성도 state만 잘 알면 잘 할 수 있다. 그러나 수정 기능은 읽고 쓰기, 저장 등을 고려해야 해서 조금 난해하다고 느낄 수 있는데, Read -> Create -> Delete -> Update 순서대로 차근 차근 따라가보자.

0. 데이터 준비

이전 포스트에서 HTML,CSS, Javascript 등의 TOC(Table Of Contents) 값을 입력해주거나 props로 관리하며 컴포넌트를 생성하는 것까지 하였다.

그렇게 해서 나눈 컴포넌트가 Title, TOC, Content 컴포넌트 였고 이것들이 App이라는 상위 컴포넌트의 하위로 들어가는 식이었다.

HTML 같은 과목을 subject로, 그에 관한 설명을 desc로 하고, 초기 화면과 과목을 클릭했을 때 바뀐 화면을 나타나게 할 mode라는 변수를 두고 이들의 값이나 길이가 변할 수 있기 때문에 state로 지정하면 다음과 같이 구상할 수 있다.

import React, { Component } from 'react';
...

class App extends Component {
  // App 컴포넌트 초기화시 다음 정보들을 state로 지정하여 관리한다.
  // contents라는 키를 가진 배열이 Todo 리스트가 되고,
  // max_content_id는 이 배열의 길이
  // selected_id는 클릭된 content의 id, 
  // mode는 welcome, read 등의 상태를 담당하게 된다.
  constructor(props){
    super(props);
    this.max_content_id = 2;
    this.state = {
      mode: 'welcome',
      selected_id: 0,
      header: { title: "Review react basic", sub: "written by Jerome" },
      welcome: { subject: "Basic React", desc: "welcome to this page programmed using React" },
      contents: [
        { id: 0, subject:"HTML", desc:"HTML is a basic markup language to learn about WEB" },
        { id: 1, subject:"CSS", desc:"CSS is Cascading Style Sheet language." },
        { id: 2, subject:"Javascript", desc:"Javascript is the basic language to deal with DOM in browser." }
      ]
    }
  }
  
  render(){
    return (
      ...
    );
  }

1. Read

mode는 처음 로딩했을 때의 'welcome'에서 어떤 todo 항목을 클릭했을 때 'read'로 바뀌면서 해당 과목명과 과목 설명을 화면에 랜더링해줘야 한다.

먼저 Content라고 명명했던 컴포넌트 파일을 ReadContent라고 rename한다. Content 파일과 형태는 같으나 뒤이어 생성할 Create, Update 컴포넌트와 차별점을 두기 위해 명칭을 좀 더 직관적으로 하는 것이다.

import React, { Component } from 'react';

class ReadContent extends Component {
  render() {
    return (
      <article>
        <h1>{this.props.subject}</h1>
        <p>{this.props.desc}</p>
      </article>
    );
  }
}

export default ReadContent;

그 다음, 이 ReadContent를 사용할 상위 컴포넌트인 App()을 다음과 같이 변경한다.


import React, { Component } from 'react';
import Title from './components/Title';
import TOC from './components/TOC';
import ReadContent from './components/ReadContent';
import './App.css';

class App extends Component {
  constructor(props){
    super(props);
    this.max_content_id = 2;
    this.state = {
      mode: 'welcome',
      selected_id: 0,
      header: { title: "Review react basic", sub: "written by Jerome" },
      welcome: { subject: "Basic React", desc: "welcome to this page programmed using React" },
      contents: [
        { id: 0, subject:"HTML", desc:"HTML is a basic markup language to learn about WEB" },
        { id: 1, subject:"CSS", desc:"CSS is Cascading Style Sheet language." },
        { id: 2, subject:"Javascript", desc:"Javascript is the basic language to deal with DOM in browser." }
      ]
    }
  }
  getReadContent() {
    for (let content of this.state.contents) {
      if (content.id === this.state.selected_id) {
        return content;
      }
    }
  }
  printContent() {
    let _subject, _desc, _article = null;
    if (this.state.mode === 'welcome') {
      // welcome 일 때 동작 수행하게
      _subject = this.state.welcome.subject;
      _desc = this.state.welcome.desc;
      _article = <ReadContent subject={_subject} desc={_desc}></ReadContent>;
    }
    else if (this.state.mode === 'read') {
      let _content = this.getReadContent();
      _subject = _content.subject;
      _desc = _content.desc;
      _article = <ReadContent subject={_subject} desc={_desc}></ReadContent>;
      
    }
    
    return _article;
  }
  
  render() {
    return (
      <div className="App">
        <Title
          title={this.state.header.title}
          sub={this.state.header.sub}
          onChangePage={function () {
            this.setState({ mode: 'welcome' });
          }.bind(this)}
        ></Title>

        <TOC
          data={this.state.contents}
          onChangePage={function (id) {
            this.setState({
              selected_id: Number(id),
              mode: 'read'
            });
          }.bind(this)}
        ></TOC>
       
        {this.printContent()}
      </div>
    );
  }
}

export default App;

클릭된 리스트의 id대로 컨텐츠를 불러오기 위해 getReadContent()함수를 생성하고, mode 변경에 따른 랜더링 컨텐츠를 담을 printContent() 함수를 위와 같이 정의한다. 우선은 mode가 'welcome'일 때, 'read'일 때가 필요하며, 각각의 mode일 때 state로 정의 한 TODO의 과목명(subject)과 설명(desc)을 할당하여 ReadContent 컴포넌트의 props로 주입한다. 그렇게 만든 컴포넌트가 printContent의 반환값이 되는 것이다.

이렇게 하면 이렇게 항목을 클릭했을 때 해당 과목과 과목설명이 아래 랜더링이 되는 것을 확인할 수 있다.
basic_page
처음 로드하거나 Review~ title을 클릭했을 때 화면

when click js
아무 TODO list 아이템 클릭했을 때 아래 부분이 렌더링 됨

2. Create

이제 Create 기능을 익혀볼 차례이다. Create와 Delete, Update를 수행하기 위해 먼저 동작 버튼을 셋업할 필요가 있다. 이 동작버튼을 생성되는 TODO와 랜더링 되는 맨아래 부분 사이 가운데에 위치 하는 것으로 하고, 이를 수행할 Control.js 라는 파일을 생성하여 컴포넌트를 생성해준다.

  • 1. Controller 컴포넌트 생성
import React, { Component } from 'react';

class Control extends Component {
  render() {
    const opStyles = {
      display: "flex",
      justifyContent: "space-around",
      flexDirection: "row"
    }
    return (
      <div style={opStyles}>
        <a href={"/create"}
          onClick={function (e) {
            e.preventDefault();
            this.props.onChangeMode('create');
          }.bind(this)}
        >과목생성</a>
        <a href={"/update"}
          onClick={function (e) {
            e.preventDefault();
            this.props.onChangeMode('update');
          }.bind(this)}
        >과목수정</a>
        <input type="button" value="과목삭제"
          onClick={function (e) {
              e.preventDefault();
              this.props.onChangeMode('delete');
            }.bind(this)}
        ></input>
      </div>
    );
  }
}

export default Control;

생성할 버튼들을 a태그로 해서 클릭시 동작만 정의를 해준다. 각 operation의 style은 우선 저렇게 변수로 저장을 해서 입혀주고, click했을 때는 props로 onChangeMode라는 함수를 받았을 때 인자로 'create', 'update', 'delete'같은 문자열을 넣어준다.

  • 2. CreateComponent라는 파일 생성, 정의하기
import React, { Component } from 'react';

class CreateContent extends Component {
  render() {
    return (
      <article>
        <h1>과목 생성하기</h1>
        <form action={"create_process"} onSubmit={function (e) {
          e.preventDefault();
          this.props.onSubmit(
            e.target.subject.value,
            e.target.description.value
          )
        }.bind(this)}>
          <p><input type="text" name="subject" placeholder="과목을 입력하세요"></input></p>
          <p><textarea name="description" placeholder="내용을 입력하세요"></textarea></p>
          <p><input type="submit" value="생성하기"></input></p>
        </form>
      </article>
    );
  }
}

export default CreateContent;

생성 버튼을 누르면 title과 desc를 받을 input field가 있으면 좋겠다. 그렇게 해서 입력한 값들을 저장하기 위해서는 form 태그를 사용하여 submit을 해야하며, 형태는 다음과 같다.

생성 화면

  • 3. 상위컴포넌트(App.js)에서 mode가 create일 때 정의해주기
import React, { Component } from 'react';
...
// 여기에 CreateComponent를 추가해준다
import CreateContent from './components/CreateContent';

...

printContent(){
  ...
  // read 조건문 뒤에 다음을 추가한다.
  else if (this.state.mode === 'create') {
      // create 동작일 때 최대 id 값을 올려주고 _article은 createContent의 결과여야 
      // 그리고 mode와 content, selected_id를 업그레이드 해줘야
      this.max_content_id++;
      _article = <CreateContent
        onSubmit={function (_subject, _desc) {
          const newContent = this.state.contents.concat({ id: this.max_content_id, subject: _subject, desc: _desc });
          this.setState({
            mode: 'read',
            contents: newContent,
            selected_id: this.max_content_id
          });
        }.bind(this)}
      ></CreateContent>
    }
  return _article;
}


render(){
  return (
  	... 
    // TOC 컴포넌트 다음에 아래를 추가한다. 
    <Control onChangeMode={function (_mode) {
        this.setState({
          mode: _mode
        });
      }.bind(this)}></Control>
    {this.printContent()}
  );
}

submit으로 전달해준 과목명과 desc 값들을 가져와 이를 저장한다. 여기서 주의해야 할 것은, state로 정의한 배열은 가급적이면 직접 수정하지 않는 것이 좋다는 것이다.

이를 가능케 하는 것이 Array.from(배열) 을 사용하거나 배열.concat()을 활용하는 것이다.
concat을 써서 state에 담긴 contents에 새로운 content를 넣은 컨텐츠를 newContent라는 변수에 할당해주고 setState() 메서드로 값을 다음과 같이 변경해준다.

그리고 Control에서 받아온 mode로 이것들이 처리됨에 따라 printContent()가 결정되는 것이며 완성했을 때는 다음과 같이 된다.
과목생성전
과목 생성하기 전에 input field

과목생성후
과목 생성 후 'React'가 만들어지면서 read 모드로 바뀌고 새로 렌더링 됨

3. Delete

이제 또 하나의 쉬운 기능인 delete도 해보자. delete는 위 코드에서 printContent()의 조건으로 따로 두지 않고, Control 컴포넌트에서 받아오는 mode 값을 확인하고 바로 처리하는 식으로 접근해 볼 수 있다.

// App.js
	// 생략
    ...
    
    printContent(){
        ...
    }
    // 아래 delete할 때 로직 함수를 추가해준다.
    deleteContent() {
      // 해당하는 content를 불러서 pop을 한뒤 max id 바꿔.
      let _contents = Array.from(this.state.contents);
      if (window.confirm("정말로 삭제하시겠습니까?")) {
        for (let i = 0; i < _contents.length; i++) {
          if (_contents[i].id === this.state.selected_id) {
            _contents.splice(i, 1);
            break;
          }
        }
        this.setState({
          mode: 'welcome',
          contents: _contents
        });
        alert("삭제되었습니다.");
      }
    }
    render(){
      return (
        ...
        // Control 컴포넌트 부분을 delete를 바로 처리하게끔 다음과 같이 보완한다.
        <Control onChangeMode={function (_mode) {
            // delete일 때는 바로 실행하게 하자
            if (_mode === 'delete') {
              this.deleteContent();
            }
            else { // delete가 아닌 모든 오퍼레이션들 
              this.setState({
                mode: _mode
              });
            }
          }.bind(this)}></Control>
      );
    }
	
        

이번에는 Array.from()을 써서 배열을 안전하게 복사하고 read로 설정되어 selected_id가 가리키는 TODO 과목에 해당하는 것을 찾아 splice라는 메서드로 지우는 것을 deleteContent()에 구현하였다. 또 삭제의 경우, 사용자의 실수로 잘못 삭제할 수 있기 때문에 confirm과 alert 를 이용해서 간단한 앱에도 세심한 테크닉을 곁들였다.

그리고 Control에서 onChangeMode로 mode를 받아올 때 delete면 바로 함수가 실행되게 함으로써 단조로운 if - else if 반복을 피했다.

삭제 전
삭제 버튼 클릭시 confirm이 동작하게 함.

삭제 후
삭제 이후에는 welcome모드로 돌아가며 과목이 정상 삭제되고 랜더링이 된다.

4. Update

이제 대망의 Update만 남았다. Update는 Create랑 비슷한데, 이미 저장된 값을 읽어서 다시 띄우고, input 값 변화에 맞게 handling을 해줘야 하며, 그런 다음 state를 바꾼다는 것에서 차이가 있다.

  • 1. CreateComponent를 복사하여 UpdateComponent 만들고 수정하기

먼저 CreateComponent 파일을 복사하여 UpdateComponent 파일로 바꾼다. 그러면 생성화면과 비슷한 화면이 그려지는데, 여기서 해야 할 것은 저장된 값을 가져오는 것이다. 그런데 CreateComponent처럼 하면 값이 바뀌지 않는다는 것을 알 수 있다.

이는 React props가 READ-ONLY 속성을 갖기 때문이며, 값의 변화를 담기 위해서는 state로 만들어줘야 한다. 따라서 이를 반영한 UpdateComponent 파일은 아래와 같다.

import React, { Component } from 'react';

class UpdateContent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      id: this.props.data.id,
      subject: this.props.data.subject,
      desc: this.props.data.desc
    }
  }

  render() {
    
    return (
      <article>
        <h1>과목 수정하기</h1>
        <form action={"update_process"} method={'post'}
          onSubmit={function (e) {
            e.preventDefault();
            this.props.onSubmit(
              this.state.id,
              this.state.subject,
              this.state.desc
            )
        }.bind(this)}>
          <p><input type="text" name="subject" placeholder="과목을 입력하세요"
            value={this.state.subject}
          ></input></p>
          <p><textarea name="desc" placeholder="내용을 입력하세요"
            value={this.state.desc}
          ></textarea></p>
          <p><input type="submit" value="생성하기"></input></p>
        </form>
      </article>
    );
  }
}

export default UpdateContent;

에러 출력. 업데이트 보완 필요

이렇게 하면 수정하기 위한 값은 잘 받아오는데 수정하려고 하면 다음과 같은 에러가 생긴다.
뭐가 문제일까?

  • 2. inputFormHandler로 input 값의 변화를 처리하기

위 캡처화면에서 에러메시지를 잘 읽어보면 'onChange' 정의가 있어야 value를 바꿀 수 있다고 한다. 그렇기 때문에 return한 코드에서 onChange가 input 태그, textarea 태그 각각에 있어야 하고 변화를 처리할 Handler 함수가 따로 필요하게 된다. 이를 보완한 코드가 아래와 같다.

import React, { Component } from 'react';

class UpdateContent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      id: this.props.data.id,
      subject: this.props.data.subject,
      desc: this.props.data.desc
    }
    // 여기가 추가되었다. 정의한 함수를 state에도 바인딩을 시켜주는 것이 필요하다 
    this.inputFormHandler = this.inputFormHandler.bind(this);
  }
	
  // key 값에 [] 괄호를 씌우면 해당하는 key들 모두에 대응한다는 뜻으로, 특정 key를 따로 지정하지 않아도 된다. 
  // 이렇게 변화된 값을 setState() 메서드로 상태값을 변경해주면 된다.
  inputFormHandler(e) {
    this.setState({
      [e.target.name]: e.target.value
    });
  }
  render() {
    
    return (
      <article>
        <h1>과목 수정하기</h1>
        <form action={"update_process"} method={'post'}
          onSubmit={function (e) {
            e.preventDefault();
            this.props.onSubmit(
              this.state.id,
              this.state.subject,
              this.state.desc
            )
        }.bind(this)}>
          <p><input type="text" name="subject" placeholder="과목을 입력하세요"
            value={this.state.subject} onChange={this.inputFormHandler}
               // onChange에 다음 handler들을 추가해준다. 
          ></input></p>
          <p><textarea name="desc" placeholder="내용을 입력하세요"
            value={this.state.desc} onChange={this.inputFormHandler}
          ></textarea></p>
          <p><input type="submit" value="생성하기"></input></p>
        </form>
      </article>
    );
  }
}

export default UpdateContent;

이제 에러는 나타나지 않을 것이다.

  • 3. 상위 컴포넌트(App.js)에 Update로직 정의

마지막으로, App.js 파일의 printContent() 함수에 mode가 update일 때를 넣어주면 된다!

... 중략

printContent(){
	...
    // update 되는 것에 대한 아래 코드를 더해준다.
    else if (this.state.mode === 'update') {
      // update는 좀 복잡해. read 된 것의 내용을 create 양식에 그대로 가져오고
      // 이걸 수정한 다음 submit 하면 setState로 갱신이 되어야 해 
      _article = <UpdateContent 
        onSubmit={function (_id, _subject, _desc) {
          const renewContent = Array.from(this.state.contents);
          for (let i = 0; i < renewContent.length; i++) {
            if (_id === renewContent[i].id) { // 해당하는 id값을 찾으면 그 내용을 변경하는거야
              renewContent[i] = { id: _id, subject: _subject, desc: _desc };
              break;
            }
          }
          this.setState({
            mode: 'read',
            contents: renewContent
          });
        }.bind(this)}
        data={this.getReadContent()}
      ></UpdateContent>
    }
    return _article;
  
 ... 중략

수정 전
TODO 과목 하나 생성 후 read 모드일 때

수정 후
과목 수정시 바뀐 TODO 내용, 그리고 re-rendering

마치며...

이렇게 React 기초 실습으로 다지는 CRUD 까지 알아보았다.
18년도 생활코딩 유튜브를 바탕으로 실습하고, 이 후 영상 없이 스스로 review 하는 차원에서 이렇게 다시 만들어 보았다. 그러고 난 다음에는 클래스형식으로 작성한 컴포넌트를 함수 컴포넌트로 바꾸는 작업까지 하였는데, 이를 위해서는 React Hooks 등 개념을 알아야 하므로 다음 포스트에서는 Hooks에 대해 살펴보는 걸로 하자.

참고

  • React 기본 학습 - 유튜브 생활코딩 강의
profile
정확하고 체계적인 지식을 가진 개발자 뿐만 아니라, 가진 지식을 사람들과 함께 나눌 수 있는 계발자가 되고 싶습니다
post-custom-banner

0개의 댓글