본문 바로가기
프론트엔드 상식

React create 구현하기 ( + onSubmit, shouldComponentUpdate 함수 )

by Whiimsy 2021. 1. 13.

🧂 기본 개념 정리

지금까지 우리가 공부한 개념들을 정리해보자. Props는 컴포넌트 내 (우리의 경우 Content.js 내부)에서는 값을 변경할 수 없으며 읽기, 불러오기만 가능하지만 외부에선 변경할 수 있다. State는 setState 함수로 컴포넌트 내부에서도 동적으로 값을 변경할 수 있다. Props와 State 모두 render 함수를 호출하며 호출한 결과로 UI가 바뀌게 된다. 새로운 개념을 하나 추가하자면, 실제 브라우저 HTML을 의미하는 DOM이 있다.

 

 

상위 컴포넌트가 하위 컴포넌트에 명령할 때, 즉 데이터를 전달할 땐 Props를 사용한다. 여기서 위에 말한 것처럼 하위 컴포넌트는 Props를 읽기만 가능하다. 반대로, 하위 컴포넌트가 상위 컴포넌트에 데이터를 전달할 땐 Event를 이용한다.

 

오늘 배울 내용인 Create에 대해 잠깐 살펴보자. 정보 처리에는 CRUD 네 가지 속성이 존재한다. 각각 Create, Read, Update, Delete를 의미한다. 저번 시간까지 우린 Read에 대해 배웠고 오늘은 Create에 대해 배워보려 한다.

 

🧂 TOC 컴포넌트 영역 밑에 생성, 수정, 삭제 버튼을 만들어 contents의 state를 추가, 수정, 삭제하기

추가 버튼을 누르면 App의 mode가 create로 바뀌게 설정해 create mode에선 content 컴포넌트 영역이 TOC에 목록을 추가할 수 있는 폼으로 변경될 수 있도록 하자. 제출된 폼의 입력 값들은 App의 contents 배열에 추가되어 TOC에 목록을 추가할 것이다.

 

 

App.js > TOC와 Content 컴포넌트 사이에 다음과 같이 App 모드를 변경할 수 있는 목록을 만들어준다.

<ul>
          <li>
            <a href="/create">create</a>
          </li>
          <li>
            <a href="/update">update</a>
          </li>
          <li>
            <input type="button" value="delete" />
          </li>
        </ul>

 

이 코드를 다른 컴포넌트들처럼 Control 컴포넌트로 만들고 Control.js 파일에 코드를 옮기자. 

 

// App.js
import Control from "./components/Control";

<Control></Control>
// Control.js
import React, { Component } from "react";

class Control extends Component {
  render() {
    return (
      <ul>
        <li>
          <a href="/create">create</a>
        </li>
        <li>
          <a href="/update">update</a>
        </li>
        <li>
          <input type="button" value="delete" />
        </li>
      </ul>
    );
  }
}

export default Control;

 

각 버튼을 클릭했을 때 onChangeMode 함수가 실행되도록 코드를 수정한다.

// App.js
<Control onChangeMode={function () {}.bind(this)}></Control>
// Control.js
<ul>
        <li>
          <a
            href="/create"
            onClick={function (e) {
              e.preventDefault();
              this.props.onChangeMode("create");
            }.bind(this)}
          >
            create
          </a>
        </li>
        <li>
          <a
            href="/update"
            onClick={function (e) {
              e.preventDefault();
              this.props.onChangeMode("update");
            }.bind(this)}
          >
            update
          </a>
        </li>
        <li>
          <input
            type="button"
            value="delete"
            onClick={function (e) {
              e.preventDefault();
              this.props.onChangeMode("delete");
            }.bind(this)}
          />
        </li>
      </ul>

 

App.js > onChangeMode 함수의 파라미터로 _mode를 넣어주고, App의 모드를 _mode로 설정하는 코드를 작성한다.

<Control
          onChangeMode={function (_mode) {
            this.setState({ mode: _mode });
          }.bind(this)}
        ></Control>

 

🧂 모드가 전환됨에 따라 Content 컴포넌트 영역 바뀌게 하기

기존의 Content 컴포넌트를 읽기모드의 Content라는 뜻으로 ReadContent로 이름을 수정하고 지금부터 만들 create 모드의 Content를 의미하는 컴포넌트 이름을 CreateComponent로 설정한다.

// App.js
import ReadContent from "./components/ReadContent";
import CreateContent from "./components/CreateContent";

<ReadContent title={_title} desc={_desc}></ReadContent>
<CreateContent></CreateContent>
// Content.js >> ReadContent.js
import React, { Component } from "react";

class ReadContent extends Component {
  render() {
    return (
      <article>
        <h2>{this.props.title}</h2>
        {this.props.desc}
      </article>
    );
  }
}

export default ReadContent;
// CreateContent.js
import React, { Component } from "react";

class CreateContent extends Component {
  render() {
    return (
      <article>
        <form>
          <h2>Create</h2>
          <input type="text" value="title"></input>
          <input type="text" value="description"></input>
          <input type="button" value="submit"></input>
        </form>
      </article>
    );
  }
}

export default CreateContent;

 

App.js > render 함수에 _article 변수를 추가해 모드가 전환될 경우 각각 ReadContent, CreateContent로 동적으로 Content가 변하게 해 준다.

import React, { Component } from "react";
import TOC from "./components/TOC";
import Subject from "./components/Subject";
import ReadContent from "./components/ReadContent";
import CreateContent from "./components/CreateContent";
import Control from "./components/Control";
import "./App.css";

class App extends Component {


  constructor(props) {
    super(props);
    this.state = {
      mode: "welcome",
      selected_content_id: 2,
      welcome: { title: "Welcome", desc: "Hello React!!" },
      subject: { title: "MEOW", sub: "The cat rules the world!" },
      contents: [
        { id: 1, title: "HTML", desc: "HTML is for information" },
        { id: 2, title: "CSS", desc: "CSS is for design" },
        { id: 3, title: "JavaScript", desc: "JavaScript is for interactive" },
      ],
    };
  }


  render() {
    var _title,
      _desc,
      _article = null;
    if (this.state.mode == "welcome") {
      _title = this.state.welcome.title;
      _desc = this.state.welcome.desc;
      _article = <ReadContent title={_title} desc={_desc}></ReadContent>;
    } else if (this.state.mode == "read") {
      var i = 0;
      while (i < this.state.contents.length) {
        var data = this.state.contents[i];
        if (data.id == this.state.selected_content_id) {
          _title = data.title;
          _desc = data.desc;
          _article = <ReadContent title={_title} desc={_desc}></ReadContent>;
          break;
        }
        i = i + 1;
      }
    } else if (this.state.mode == "create") {
      _article = <CreateContent></CreateContent>;
    }


    return (
      <div className="App">

        <Subject
          title={this.state.subject.title}
          sub={this.state.subject.sub}
          onChangePage={function () {
            this.setState({ mode: "welcome" });
          }.bind(this)}
        ></Subject>

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

        <Control
          onChangeMode={function (_mode) {
            this.setState({ mode: _mode });
          }.bind(this)}
        ></Control>

        {_article}

      </div>
    );
  }
}

export default App;

 

🧂 CreateContent 폼 만들기

CreateContent 폼은 title 작성, description 작성, 제출 버튼으로 이루어진다. CreateContent.js의 코드를 다음과 같이 수정한다. 여기서 textarea는 입력해야 하는 줄이 여러 줄일 때 사용되고 placeholder는 사진과 같이 사용자가 입력하기 전 빈칸에 무엇을 입력해야 하는지 힌트를 주는 역할을 한다.

 

 

 

import React, { Component } from "react";

class CreateContent extends Component {
  render() {
    return (
      <article>
        <h2>Create</h2>
        <form
          action="/create_process"
          method="post"
        >
          <p>
            <input type="text" name="title" placeholder="title"></input>
          </p>
          <p>
            <textarea name="desc" placeholder="description"></textarea>
          </p>
          <p>
            <input type="submit"></input>
          </p>
        </form>
      </article>
    );
  }
}

export default CreateContent;

 

form 태그의 속성에 있는 action은 이 데이터를 어디에 전송할 것인지를 결정하는 역할을 하고 method는 post 방식으로 전달돼야 url이 노출되지 않고 안전하게 전달된다. 우리 코드에선 create_process라는 페이지에 입력 데이터를 전송할 것이므로 위와 같이 작성했다.

 

🧂 OnSubmit 함수 추가

submit 버튼을 포함하고 있는 form 태그에 onSubmit이라는 이벤트를 정의하면 submit 버튼을 클릭했을 때, 정의한 이벤트가 실행된다. onSubmit 함수를 이용해 submit 버튼을 눌렀을 때, 알림 창이 뜨도록 코드를 수정해보자.

<form
          action="/create_process"
          method="post"
          onSubmit={function (e) {
            e.preventDefault();
            alert("SUBMIT!!!!!!!");
          }.bind(this)}
        >

 

이번엔 제출 버튼을 클릭했을 때, 사용자가 입력한 정보가 App의 contents 배열에 추가되도록 해보자. App.js > CreateContent 컴포넌트에 제출 버튼이 클릭되었을 때를 뜻하는 onSubmit 함수를 추가한다.

else if (this.state.mode == "create") {
      _article = (
        <CreateContent
          onSubmit={function () {
            // add content to this.state.contents
          }.bind(this)}
        ></CreateContent>
      );
    }

 

이번엔 개발자 도구를 이용해 사용자가 입력한 정보가 어떤 경로에 저장되는지 확인한다. 저번 시간에 배웠던 target 개념을 활용하면 된다. 사용자가 입력한 title 값은 e.target.title.value 경로에 저장되고 desc 값은 e.target.desc.value에 저장되는 것을 알 수 있다.  CreateContent.js > form > onSubmit에서 App.js > render > CreateContent의 onSubmit 함수를 호출하기 위해 this.props.onSubmit 코드를 작성하고 파라미터로 앞서 찾은 경로를 넣어준다.

onSubmit={function (e) {
            e.preventDefault();
            this.props.onSubmit(e.target.title.value, e.target.desc.value);
            alert("SUBMIT!!!!!!!");
          }.bind(this)}

 

title과 desc 파라미터를 받기 위해 App.js > CreateContent > onSubmit 함수에 _title, _desc 파라미터를 설정하고 현재 contents state의 길이보다 1만큼 큰 id를 주고, 받은 _title, _desc를 setState로 기존 contents에 푸시해준다.

else if (this.state.mode == "create") {
      _article = (
        <CreateContent
          onSubmit={function (_title, _desc) {
            // add content to this.state.contents
            this.state.contents.push({
              id: this.state.contents.length + 1,
              title: _title,
              desc: _desc,
            });
            this.setState({
              contents: this.state.contents,
            });
          }.bind(this)}
        ></CreateContent>
      );
    }

 

이렇게 하면 다음과 같은 동작을 수행하는 페이지를 만들 수 있다!

 

하지만 이렇게 코드를 작성할 경우, 다음에 React를 이용해 코드를 수정하려 할 경우 매우 까다롭거나 코드를 수정할 수 없는 경우가 생길 수 있다. 그러므로 우린 원본을 바꾸는 push가 아닌 원본을 유지하되 원본을 변경한 새로운 배열이 만들어지는 concat을 사용할 것이다. 다음과 같이 코드를 수정하면 된다.

<CreateContent
          onSubmit={function (_title, _desc) {
            // add content to this.state.contents
            var _contents = this.state.contents.concat({
              id: this.state.contents.length + 1,
              title: _title,
              desc: _desc,
            });
            this.setState({
              contents: _contents,
            });
            this.setState({ mode: "welcome" });
          }.bind(this)}
        ></CreateContent>

 

그런데 우리 코드엔 문제점이 하나 있다. TOC 컴포넌트의 경우 TOC 목록이 추가되지 않는 경우, TOC와 전혀 관련이 없는 mode 가 변경되는 경우에도 TOC 컴포넌트가 다시 렌더된다. TOC 와 아무 연관 없는 동작을 수행할 때도 TOC의 render 함수가 실행된다는 것이다. 이걸 막기 위해 우리는 shouldComponentUpdate 함수를 활용할 수 있다. 이 함수는 render 함수 이전에 쓰이며, newProps.data로 새롭게 바뀐 값과 this.props.data로 기존 값에 각각 접근할 수 있고 두 값을 비교해 컴포넌트를 update 할지 (컴포넌트의 render 함수를 호출할지) 개발자가 직접 설정할 수 있게 해 준다. return false; 의 경우 render 함수를 호출하지 않고 return true; 일 때만 render 함수를 호출한다.

shouldComponentUpdate(newProps, newState) {
    if (newProps.data === this.props.data) {
      return false;
    }
    return true;
  }

 

여기서 push 가 아닌 concat을 이용하는 이유를 알 수 있다. push 의 경우 원본을 바꿔버리기 때문에 새로운 props 의 data 와 기존 props 의 data 가 같아지게 되는 것이다. 원본이 아닌 원본의 복제본을 활용하는 concat 을 이용하도록 하자! 작은 프로그램일 땐 차이가 많이 나지 않지만, 프로그램이 커질 경우 concat 가 큰 힘을 발휘할 것이다. 이렇게 원본을 바꾸지 않는 속성을 불변성(Immutable)이라고 부른다.