React(생활코딩)_7~8일차_CRUD_Create 구현

Lina Hongbi Ko·2023년 9월 1일
0

React_생활코딩

목록 보기
9/23

CRUD란?

: 'C-Create, R-Read, U-Update, D-Delete'를 뜻한다.
생코님은 '모든 정보 기술은 CRUD 안에 갇혀 있다.'라고 말했다.

맞는말이다. Create, Read, Update, Delete 이렇게 4가지는 어떤 정보 기술에서든 핵심적인 것이다. 위의 4가지가 구현되는 것이 리액트 프로그래밍에서도 중요할 것이다.

그 중, C R (Create, Read)은 핵심적인 오퍼레이션이다. 읽어야 만들어지고, 만들어야 읽기도 가능하니까.

그래서 이 책에서는 먼저 리액트의 Create기능을 설명하고 있다.

앞 전까지의 실습들은 기본적으로 코드를 모두 입력해둔 상태에서 시작했지만 지금 배우는 내용은 그렇지 않다.

생성, 수정, 삭제 버튼을 만들고 그 버튼들을 누르면 버튼에 따라 mode가 바뀌고, 그에 따라 기존의 컴포넌트가 새로운 컴포넌트로 바뀌는 실습을 따라서 해보려고 한다. 그리고 새로운 컴포넌트 안에는 폼이 있고, 정보를 입력해서 저장하면 App 컴포넌트의 콘텐츠 목록에 새로운 항목의 id값, 제목과 본문이 콘텐츠에 추가되도록 구현하려고 한다. 말 그대로 생성, Create 구현을 실습해보자.
(user가 그때그때 입력하는 데이터를 가지고 생성)

  1. mode에 'create', 'update', 'delete'라는 세 개의 mode를 추가할 것이다. 'create' 버튼을 누르면 App 컴포넌트의 mode가 'create'되고, 그에 따라서 기존 콘텐츠를 표시하는 부분이 글을 수정하기 위한 인터페이스를 제공하는 내용으로 바뀔 것이다. ('create'링크를 클릭하면 App 컴포넌트 mode가 'create'으로 바뀌고, ReadContent 컴포넌트가 CreateContent라는 컴포넌트로 바뀐다)

이것이 구현해야할 첫번째 기능.

  1. CreateContent 컴포넌트에 글을 작성하고 submit버튼을 누르면 App 컴포넌트의 state인 contents라는 배열의 끝에 입력한 내용이 추가된다. 추가되는 데이터는 자신의 고유한 id값을 가지고 추가된다. 그리고 추가된 목록을 클릭하면 mode가 'read'로 바뀌고 selected_content_id가 해당 인덱스로 바뀌면서 ReadContent 컴포넌트로 바뀌고 해당 정보가 화면에 표시된다.

그리고 다음으로 구현해야 할 두번째 기능.

요렇게 두 파트로 나누어 책은 Create 구현을 설명하므로 나도 그것을 따라하려고 한다:)

1. Create 구현 _ 모드 변경 기능

일단 TOC 컴포넌트와 Content 컴포넌트 사이에 'create', 'update', 'delete'버튼을 만들어보자.

// App.js 파일

... 생략 ...
class App extends Component {
...	생략 ...
	return(
    	<div className="App">
        	... 생략 ...
            <TOC onChangePage={function(id){
            	this.setState({
                	mode:'read',
                    selected_content_id: Number(id)
                })
            }.bind(this)} data={this.state.contents}></TOC>
            <ul>
            	<li><a href="/create">create</a></li>
                <li><a href="/update">update</a></li>
                <li><input type="button" value="delete"></input></li>
            </ul>
            <Content title={_title} desc={_desc}></Content>
        </div>
        ... 생략 ...

ul 태그목록을 만들고, create과 update버튼은 Create, Update 페이지로 가도록 a링크를 사용했다. 하지만 delete버튼의 경우에는 a링크를 사용하지 않았다. 그 이유는 의도치않게 원하는 기능을 구현하지 못할 수 있기 때문이다. 책에서는 예를 들어 사용자들이 페이지에 방문할때 좀 더 빨리 방문할 수 있게 미리 방문해 두는 소프트웨어가 컴퓨터에 설치돼 있다면 그러한 프로그램에 의해 삭제가 의도치 않게 진행될 수도 있다고 했다. 그래서 delete는 페이지 개념이 아닌 오퍼레이션 개념의 기능을 쓰는게 맞다고 한다.

그리고 3개의 버튼을 만든 항목을 다른 컴포넌트로 분리한다.
(새로운 컴포넌트 파일을 만들어 새로운 컴포넌트 생성)

// 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"></input></li>
            </ul>
        );
    }
}

export default Control;

그리고 App.js 파일에 연결해주도록 한다.

// App.js 파일

import React, { Component } from 'react';
import TOC from './components/TOC';
import Content from './components/Content';
import Subject from './components/Subject';
import Control from './components/Control';
import './App.css';

... 생략 ...

그리고 TOC 컴포넌트와 Content 컴포넌트 사이에 입력했던 내용을 Control 컴포넌트로 변경한다.

// App.js 파일

... 생략 ...

class App extends Component {
	... 생략 ...
    return(
    	... 생략 ...
        <TOC onChangePage = {function(id){
        	this.setState({
            mode:'read',
            selected_content_id: Number(id),
            });
        }.bind(this) data={this.state.contents}}></TOC>
        <Control></Control>
        <Content title={_title} desc={_desc}></Content>
    ... 생략 ...

다음으로, 이벤트 핸들러 함수를 작성한다. Control 컴포넌트의 onChangeMode props를 아래처럼 작성해준다.

// App.js 파일

... 생략 ...

class App extends Component {
	... 생략 ...
    return(
    	... 생략 ...
        <Control onChangeMode = {function(_mode){
        	this.setState({
            	mode: _mode;
            })
        }.bind(this)}></Control>
        <Content title={_title} desc={_desc}></Content>
    ... 생략 ...

Control 컴포넌트에서 onChangeMode라는 props에 인자가 _mode인 함수를 지정했다. 그리고 이 함수는 인자로 받은 _mode의 값을 state의 mode값으로 지정한다. 이 함수는 Control 컴포넌트에 있는 3개 중 하나를 클릭했을 때, 실행해야 하기 때문에 이벤트 핸들러를 이렇게 설치했다고 한다. (이벤트가 발생했을때 실행되는 함수 -> 핸들러(handler)라고 한다.)

그리고나서 Control 컴포넌트에서 각 링크와 버튼(create, update, delete)을 클릭했을 때 실행될 함수를 작성한다.

// Control.js 파일

import React, { Component } from 'react';
class Control extends Component {
	render(){
    	return(
        	<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)}></input></li>
            </ul>
        );
    }
}

export default Control;

각 이벤트 함수는 링크 및 버튼이 클릭됐을 때의 이벤트를 받아서 페이지 변경을 막는 preventDefault함수를 호출하고나서 this.props.onChangeMode를 호출한다. 이때 인자값으로 현재 모드를 알려준다. (각 버튼을 누르면 onChangeMode함수에 해당 문자열을 넣어서 호출한다) 해당 문자열 값으로 App 컴포넌트의 state.mode의 값을 변경시키는 것이다.

그리고나서 개발자도구의 Component탭을 열어서 각 버튼을 클릭하면 mode가 바뀌는 것을 확인할 수 있다.

2. Create 구현 _ 모드 전환 기능

모드를 변경했으면 그에 맞게 이제 모드를 전환시켜야한다. 그렇게 하기 위해서는 'create'버튼을 클릭했을때 쓰기에 사용될 컴포넌트를 만들어야한다. 현재 Content 컴포넌트는 읽기에 사용되는 컴포넌트이다. 하지만 버튼을 누르면 쓰기에 사용될 컴포넌트로 교체해야한다.

먼저, 기존의 Content 컴포넌트를 읽기 컴포넌트로 바꾼다. (Content -> ReadContent) 그리고 CreateContent 컴포넌트를 만들어서 App 컴포넌트의 mode값이 "create"이면 ReadContent 컴포넌트에서 CreateContent 컴포넌트로 바뀌게 해야한다.

차례대로 실행해보자. Content 컴포넌트를 ReadContent 컴포넌트로 변경하기 위해서는 파일명과 이름을 변경해준다.

// 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;

그리고 App.js파일 또한 변경해준다.

// App.js 파일

import React, { Component } from 'react';
import TOC from './components/TOC';
import ReadContent from './components/ReadContent';
import Subject from './components/Subject';
... 생략 ...

class App extends Component {
	... 생략 ...
   	return (
    	... 생략 ...
        <Contorl onChangeMode = {function(_mode){
        	this.setState({
            	mode:_mode;
            });
        }bind(this)}></Control>
        <ReadContent title={_title} desc={_desc}></ReadContent>
       </div>
       ... 생략 ...

파일이름과 변경된 컴포넌트의 이름을 잘 써줬으면 CreateContent 컴포넌트도 만들어보자.

// CreateContent.js 파일

class CreateContent extends Component {
	render(){
    	return(
        	<article>
            	<h2>Create</h2>
                <form>
                
                </form>
            </article>
        );
    }
}

export default CreateContent

form 태그는 나중에 작성하기로 하고,
현재 Control에서 'create'을 클릭하면 App 컴포넌트의 mode값이 create로 바뀌는 것까지 구현됐으므로 mode의 값이 create일때 ReadContent 컴포넌트가 CreateContent 컴포넌트로 변경되는 것을 만들어보자.

// App.js 파일

import React, { Component } from 'react';
import TOC from './components/TOC';
import ReadContent from './components/ReadContent';
import CreateContent from './components/CreateContent';
... 생략 ...

class App extends Component {
	... 생략 ...
    render() {
    	let _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') {
        	for(let i = 0; i < this.state.contents.length; i++) {
            	const data = this.state.contents[i];
                _title = data.title;
                _desc = data.desc;
                _article = <ReadContent title={_title} desc={_desc}></ReadContent>
            } else if (this.state.mode === 'create') {
            	_article = <CreateContent></CreateContent>
            }
        }
        return(
        	<div className="App">
    		... 생략 ...
              <Control onChangeMode={function(_mode){
                 this.setState({mode:_mode});
              }.bind(this)}></Control>
              {_ariticle}
            </div>
            ... 생략 ...

App 컴포넌트 내부의 ReadContent 영역이 가변적으로 바뀔수 있도록 이 부분을 _article 이라는 변수로 작성했다.
_article이라는 변수를 선언한다음, mode가 'welcome'일때나 'read'일때도 ReadContent 컴포넌트가 출력되어야 하므로 두 경우 모두 _article 변수에 ReadContent 컴포넌트를 넣었다. 그리고 mode가 'create'일때는 CreateContent 컴포넌트가 나오게 작성했다.

위처럼 작성하고 'create'버튼을 클릭하면, CreateContent 컴포넌트가 출력되는 것을 확인할 수 있다.

3. Create 구현 _ form

create 버튼을 누르면 CreateContent 컴포넌트가 나오는 것까지 만들었으면 이제 CreateContent 컴포넌트 안에 글을 추가하는 기능, 즉 form을 완성해보자.

// CreateContent.js 파일

import React, { Component } from 'react';

class CreateContent extends Component {
	render(){
    	return(
        	<article>
            	<h2>Create</h2>
                <form action="/create_process" method="post" onSubmit={function(e){
                	e.prevenDefault();
                    alert("Submit!!!");
                }.bind(this)}>
                	<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 안에 input과 textarea를 넣었다. form태그의 속성값인 action은 리액트가 아니라 순수한 웹상에서 돌아갈때는 데이터를 어디에 전송할 것인지를 나타내는 것이다. 위의 action 속성에 '/create_process'라고 지정하면 지정된 그 페이지로 사용자가 입력한 정보를 전송할 것이라는 뜻이다. 그리고 method라는 속성을 'post'로 지정했는데 이는 url에 데이터가 노출되지 않도록 데이터를 추가/수정/삭제한다. (post와 반대되는 방식은 get)

그리고 onSubmit 이라는 이벤트를 호출한다. submit 버튼을 클릭했을때 이 버튼을 포함하고 있는 form태그의 onsubmit 이벤트를 설치해두면 해당 이벤트가 실행된다. (html form이 고유하게 가지고 있는 기능)

이렇게 입력하고 form 양식에 값을 넣은후 submit 버튼을 누르면 경고창이 나타나지만 페이지는 전환되지 않는 것을 확인 할 수 있다.

4. Create 구현 _ onSubmit 이벤트

이제 App 컴포넌트의 콘텐츠 끝에 사용자가 입력한 정보를 추가해보자. 그러면 그에 따라 글 목록 또한 자동으로 변경될 것이다. (onSubmit 이벤트가 실행됐을때 CreateContent 컴포넌트를 가져다쓰는 App 컴포넌트의 state인 contents에 데이터(리스트) 추가하기)

submit 버튼을 클릭했을때 CreateContent 컴포넌트의 이벤트로 설치된 함수를 실행시켜야 한다. App 컴포넌트 내 CreateContent 컴포넌트에 onSubmit이라는 props를 지정한다.

// App.js 파일

... 생략 ...

class App extends Component {
	... 생략 ...
    render() {
    	... 생략 ...
    	} else if(this.state.mode === 'create') {
        	_article = <CreateContent onSubmit = {function(_title, _desc) {
            	console.log(_title, _desc);
            }.bind(this)}></CreateContent>
    	}
        ... 생략 ...

그리고 CreateContent 컴포넌트를 아래처럼 작성해준다.

// CreateContent.js 파일
... 생략 ...
class CreateContent extends Component {
	render() {
    	... 생략 ...
        <form action="/create_process" method="post" onSubmit={function(e){
        	e.preventDefault();
            debugger;
            alert("Submit!!!");
        }.bind(this)}>
        ... 생략 ...

submit버튼을 누르고 제출했을때 입력한 값을 가져오기 위해서 onSubmit 이벤트 함수에 debugger;을 추가해서 잠깐 멈춘 다음 개발자도구를 열고 폼입력양식에 값을 입력한 후 버튼을 누르면 debugger;에서 멈춘다.
그리고 esc버튼을 누르고 console탭으로 이동한 다음, e객체를 보면 target이라는 프로퍼티를 확인할 수 있다.

요로코럼 제출버튼을 누른후의 e.target은 데이터를 입력한 input과 textarea를 포함한 form 자체를 가리킨다. 그리고 우리는 e.target을 통해 form에 접근할 수 있다는 것을 알게 되었다. 그리고 form객체에 title이라는 프로퍼티가 있는데, 이것은 form안에 있는 input을 가르키므로 title 프로퍼티를 통해 입력값을 가져올 수 있다. 그리고 textarea 또한 desc라는 이름의 프로퍼티로 접근할 수 있는 것을 알 수 있다.

이제 이 값을 가지고 props로 넘어온 함수를 호출한다.
CreateContent 컴포넌트를 아래처럼 수정해준다.

// CreateContent.js 파일
class CreateContent extends Component {
	render(){
    	... 생략 ...
        <form action="/create_process" method="post" onSubmit={function(e){
        	e.preventDefault();
            this.props.onSubmit(
            	e.target.title.value,
                e.target.desc.value
            );
            alert("submit!!!");
        }.bind(this)}
        >
        ... 생략 ...

그리고 debugger을 삭제하고 this.props.onSubmit함수의 인자로 두 값을 전달하면 앞에서 App 컴포넌트에서 CreateContent 컴포넌트의 onSubmit props의 인자값에 전달된다.
나는 title에 title이라는 값을, description에는 description이라는 값을 넣었다.

이렇게 해서 title과 description값을 얻었으면 이 값을 state.contents의 데이터에 추가하면 된다.

5. Create 구현 _ 콘텐츠 변경

CreateContent 컴포넌트의 onSubmit 이벤트가 실행되면 입력받은 onSubmit props가 실행되게 만들었다. 이제 state.contents에 입력한 데이터가 추가되게 구현해야한다.

그렇게 하기 위해서 일단, 기존에 추가했던 데이터의 id값을 읽어서 1만큼 더 큰 id값을 만들어야하므로 id값을 App 컴포넌트 클래스에 max_content_id라는 프로퍼티를 추가한다.

// App.js 파일
... 생략 ...
class App extends Component {
	constructor(props) {
		super(props);
        this.max_content_id = 3;
        this.state = {
        ... 생략 ...

id값을 3으로 지정해준다. 이 값은 현재 state.contents 배열에 있는 id값중 가장 큰 값이어야 한다. id값을 저장하는 프로퍼티 max_content_id를 state안에 넣지 않은 이유는 이 값은 우리가 어떤 데이터를 추가해서(push를 수행할때) id값을 지정할때 참고하는 정보일뿐, UI에 영향을 주지 않는 값이기 때문에 넣지 않는다고 했다. 요런 값들이 state에 들어가면 불필요한 렌더링이 발생할 수 있다고 한다. 헷갈리면 state안에 다 넣어서 작성해도 된다고 하지만 성능을 위해서 알아두어야겠다:)

그리고 App 컴포넌트 내의 CreateContent에 대한 onSubmit props를 아래처럼 변경한다.

// App.js 파일

... 생략 ...
class App extends Component {
	... 생략 ...
	render() {
    	... 생략 ...
        } else if (this.state.mode === "create") {
        	_article = <CreateContent onSubmit={function(_title, _desc){
            	this.max_content_id = this.max_content_id + 1;
                const _contents = this.state.contents.push(
                	{id:this_max_content_id, title:_title, desc:_desc}
                );
                this.setState({
                	contents:_contents
                });
                console.log(_title, _desc);
            }.bind(this)}></CreateContent>
        }
        ... 생략 ...

onSubmit이 실행될때마다 1씩 증가시켜서 새로 추가될 항목의 id로 지정한다. 그리고 title과 description에 입력받은 값(인자)을 넣어주고 this.state.contents의 배열에 추가한다. 그리고 반드시 setState함수를 호출해서 달라진 배열을 알려준다.

이렇게 작성하고 폼에 정보들을 입력하고 제출버튼을 누르면 항목이 추가되고 id값은 기존항목의 마지막값보다 1 커져 있는 것을 확인 할 수 있다.

BUT!!

리액트의 성능을 개선시키기 위해서는 'concat'이라는 함수를 쓰는게 좋다.

그렇다면 concat함수는 무엇일까??

💡 concat 함수

: 배열에 데이터를 추가하는 방법은 다양하다. 그 중 우리가 현재 살펴볼 내용은 push함수와 concat함수를 이용하는 것이다. 그 둘의 차이점은 push함수는 배열에 값을 추가하면 원본이 변경되지만, concat함수는 원본을 변경하지 않고 새로운 배열을 반환한다는 것이다.

아래의 실습을 통해 확인해보자.

arr배열에 push를 이용해 3을 추가했다. 그리고 arr은 3을 추가한 [1, 2, 3]이라는 배열이 되었다. 하지만 concat을 이용해 3을 추가했을때는 result라는 새로운 배열이 만들어진 것을 확인할 수 있다.

기존의 파일에 concat을 사용해보자.

// App.js 파일

class App extends Component {
	... 생략 ...
    render() {
    	... 생략 ...
        } else if (this.state.mode === "create") {
        	_article = <CreateContent onSumbit={function(_title, _desc){
            	this.max_conetent_id = this.max_content_id + 1;
                const _contents = this.state.contents.concat({id:this.max_content_id, title:_title, desc:_desc});
                this.setState({contents:_contents});
                console.log(_title, _desc);
            }.bind(this)}></CreateContent>
        }
        ... 생략 ...

기존의 this.state.push는 원본 데이터인 this.state.contents를 변경하지만, concat을 사용해 데이터를 추가하면 concat의 반환값(새로운 값, 즉 새로운 배열)을 setState함수의 contents 값으로 지정해서this.state.contents의 가지고 있던 값이 새롭게 만들어진 데이터로 교체된다.

6. Create 구현_shouldComponentUpdate

글 목록에 해당하는 컴포넌트는 TOC 컴포넌트이다. 그리고 TOC를 화면에 표시하는데 필요한 데이터가 있는 곳은 App 컴포넌트의 state.contents이다. 그렇다면, 이 배열의 내용이 바뀌면 당연히 TOC 컴포넌트의 render 메서드가 호출되어 TOC가 다시 그려져야 한다. (props와 state가 바뀌면 render함수가 호출된다고 앞전에 말했다)
그런데 만약, state.contents가 바뀌지 않는데도(contents의 배열이 추가되지 않을때도) TOC 컴포넌트의 render 메서드가 호출된다면?

❗️"성능면에서 좋지 않다." ❗️

다음 코드를 입력해서 알아보자.

// TOC.js 파일
import React, {Component} from 'react';

class TOC extends Component {
	shouldComponentUpdate(){
    	console.log("==> TOC render shouldComponentUpdate");
        return true;
    }
	render(){
    	console.log("==> TOC render");
        let lists = [];
		... 생략 ...

요로코럼 저장하고 새로고침을 하면 먼저 TOC 컴포넌트가 호출되는 것을 확인할 수 있다. 처음에 한번은 그려져야 하니까 자연스러운 일이겠지?! 그럼 글목록에서 JavaScript를 클릭하면 어떻게 될까?
이때도 TOC의 render가 호출되는 것을 볼 수 있다. ( shouldComponentUpdate도 호출되는 것을 볼 수 있지? ) 우리는 클릭만 하고, TOC의 내용인 state.contents가 바뀐 것이 없는데 render함수가 호출된 것을 확인 할 수 있다.

심지어 목록에 해당되지 않는 'delete'버튼을 누르고, 아무런 관계가 없는 state.mode가 바뀌어도 TOC 컴포넌트의 render 메서드가 실행되는 것을 볼 수 있다.

state의 값이 바뀔 때마다 모든 컴포넌트의 render 함수가 실행되는데, 전혀 실행될 필요가 없는 state 값이 바뀔때도 render 함수가 실행된다.

이것은 아주 불합리한 상황이라고 책에서는 말하고 있다. 불필요한 함수 호출은 규모가 큰 프로그램에서 중요한 이슈로 작용한다고 한다.

그래서 리액트에서 성능을 향상시키고 싶을때 사용할 수 있는 함수를 제공한다. 즉, 어떠한 컴포넌트의 render 함수가 실행돼야 하는지, 실행되지 않아야하는지를 리액트 개발자가 결정할 수 있게 특수한 함수를 제공한다. 그것이 바로 shouldComponentUpdate 함수이다.

위의 코드를 보면, 이 함수가 true를 반환하는 것을 볼 수 있다. 그리고 shouldComponentUpdate 함수가 호출된다음에 render함수가 호출된다는 것을 알 수 있다.

그럼 return값을 false로 바꿔보자.

// TOC.js 파일
import React, { Component } from 'react';
class TOC extends Component {
	shouldComponentUpdate(){
    	console.log("==> TOC render shouldComponentUpdate");
        return false;
    }
... 생략 ...

그리고나서 'create'링크를 클릭한 다음 title, description 이라는 값들을 쓰고 제출버튼을 눌러보자.

render 함수가 호출되지 않고, shouldComponentUpdate함수만 호출된것을 확인할 수 있다. 그래서 화면상에서 새로운 항목이 생긴것을 확인할 수 없다. state.contents에는 글 내용이 추가됐지만 (contents의 배열에 아이템 하나가 더 생겼다) 화면에서는 글목록이 반영되지 않는다.

"그것은 render 함수가 호출되지 않았기 때문!!"


💡💡 shouldComponentUpdate의 사용설명서를 보면 두 개의 매개변수를 갖도록 약속돼있다. 첫번째는 newProps이고 두번째는 newState이다.

shouldComponentUpdate(newProps, newState) {return false;}

newProps는 TOC 컴포넌트의 props가 바뀌었을때 바뀐값, newState는 state가 바뀌었을때 바뀐 state 값을 나타낸다.

그렇담, newProps를 확인해보자.

// TOC.js 파일

import React, { Component } from 'react';

class TOC extends Component {
	shouldComponentUpdate(newProps, newState){
    		console.log("==>TOC render shouldComponentUpdate",
            newProps.data,
            this.props.data);
     return false;
    }
    ... 생략 ...

shouldComponentUpdate 함수를 위처럼 수정해주고, 폼에 입력값(title-hello, description-react)을 넣은뒤 콘솔창을 확인하면,

이렇게 확인할 수 있다. newProps.data, 즉 새로운 props는 원소가 4개인 배열이 되었다. 하지만 this.props.data는 원소가 3개인 배열 데이터인것을 확인할 수 있다.
이를 통해서 shouldComponentUpdate의 newProps를 통해 바뀐 값을 알 수 있고, this.props.data를 통해 현재값을 알 수 있다.

💡 종합해보면,

  • render 이전에 shouldComponentUpdate가 실행된다.
  • shouldComponentUpdate의 반환값이 true이면 render가 호출되고, flase이면 render가 호출되지 않도록 약속되어있다.
  • shouldComponentUpdate를 통해 새롭게 바뀐 값과 이전에 바뀐 값에 접근할 수 있다.

이 세 가지의 특징을 가지고,
TOC로 들어오는 props의 값이 바뀌었을때만 render가 호출되도록 해보자.

// TOC.js 파일

import React, { Component } from 'react';

class TOC extends Component {
	shouldComponentUpdate(newProps, newState){
    	console.log("==>TOC render shouldComponentUpdate", newProps.data, this.props.data);
        if(this.props.data === newProps.data) {
        	return false;
        }
        return true;
    }
	... 생략 ...

this.props.data와 newProps.data의 값이 일치한다면 바뀐값이 없다는 의미로 false를 반환해 render함수를 호출하지 않고, 값이 다르다면 바뀐값이 있어서 render 함수를 반환한다.

수정하고 폼을 입력해 콘솔창을 확인해보자.

로그를 보면 처음에 컴포넌트가 렌더링 되었을때 TOC의 render가 호출되는 것을 확인 할 수 있다. 그리고 'create'버튼을 눌렀을때 바뀐 값이 없으므로 render를 호출하지 않고 shouldComponentUpdate만 호출된 것을 확인할 수 있다. 마지막으로, 폼에 값을 입력하고 submit 버튼을 눌렀을때는 contents에 새로운 값이 추가되었으므로 render함수도 호출하고 shouldComponentUpdate 함수도 같이 호출되는 것을 볼 수 있다.

✏️ 그렇다면 this.state.contents에 항목을 추가할 때, 왜 concat를 사용해야 하는지 대충 감이 올 것이다.

만약, concat이 아니라 push를 사용했다면 어떻게 됐을까?

// App.js 파일
... 생략 ...
class App extends Component {
	... 생략 ...
    render() {
		... 생략 ...
        } else if (this.state.mode === "create") {
        	_article = <CreateContent onSumbit={function(_title, _desc){
            	this.max_content_id = this.max_content_id + 1;
                this.state.contents.push({id:this.max_content_id, title:_title, desc:_desc});
                // 주석
                // const _contents = this.sate.contents.concat(
                // {id:this.max_content_id, title:_title, desc:_desc});
                //
                this.setState({
                	conents: this.state.contents
                });
                console.log(_title, _desc);
            }.bind(this)}></CreateContent>
        }
        ... 생략 ...
// TOC.js 파일
... 생략...
shouldComponentUpdate(newProps, newState) {
    console.log("==> TOC render shouldComponentUpdate");
    console.log(newProps.data, this.props.data);
    if (this.props.data === newProps.data) {
      return false;
    }
    return true;
  }

이렇게 바꾸고 폼에 입력값(title-hello, descripiton-react)을 넣은 후, 콘솔창을 확인해보자.

push는 this.state.contents의 원본을 변경하기때문에 this.props.data와 newProps.data를 비교해도 차이가 없다. 그래서 push를 이용하면 render 함수가 호출되지 않는다.

바로 이 같은 이유로 state에 있는 값을 바꿀때는 원본을 수정하지 말고, 복제본을 수정해야 한다. 성능이슈가 없는 작은 프로그램일 경우에는 상관없지만 규모가 커지면 언젠가 최적화가 필요하기 때문에 잘 선택해야 하는 부분이다.

이렇게 왜 state의 원본을 변경하지 않는가를 살펴보았다.

7. Create 구현_immutable

'원본을 바꾸지 않는다' = '불변성'
이라고 책에서는 말하고 있다. 영어로는 'immutable'.

배열의 불변성을 유지하는 방법에는 concat도 있지만, Array.from()을 사용하는 방법도 있다. 로그를 보면 a와 b는 내용이 같을 뿐, 서로 다르다는 것을 확인할 수 있다. b는 a를 복사해서 새로운 배열을 만들어낸다. 그래서 Array.from()을 사용한 다음, 복사한 배열에 push를 사용해도 된다. 위의 실습 예제에 적용해보자.

// App.js 파일

... 생략 ...

class App extends Component {
	... 생략 ...
    render() {
    	... 생략 ...
        } else if (this.state.mode === "create") {
        	_article = <CreateContent onSubmit = {function(_title, _desc){
            	this.max_content_id = this.max_content_id + 1;
                // this.state.contents.push({
                //	{id:this.max_content_id, title:_title, desc:_desc}
                // });
                // const _contents = this.state.concat({
                // id:this.max_content_id, title:_title, desc:_desc});
                const newContents = Array.from(this.state.contents);
                newContents.push({id:this.max_content_id, title:_title, desc:_desc});
                this.setState({
                	contents: neContents
                });
                console.log(_title, _desc);
            }.bind(this)}></CreateContent>
        }
        ... 생략 ...

newContents 변수에 Array.from을 이용해 this.state.contents를 복사한 다음, push함수를 이용해 새 항목을 추가했다. 그리고 setState로 contents를 적용시켰다.

그렇다면 배열이 아니라 객체의 경우는 어떻게 할까?

객체의 내용을 바꾸지 않고 복제된 새로운 객체를 만들고 싶을 때는 Object.assgin()을 사용한다.
assgin의 첫번째 인자에는 빈객체를 지정해도 되고 다른 객체를 지정해도 된다. 두번째 인자에는 복사할 객체를 지정한다. 로그를 통해 예를 보자.

먼저, assign()의 첫번째 인자에 빈 객체를 넣었다. (새로운 객체가 될 객체) 그리고 두번째 인자에 a를 넣었다. (첫번째 인자에 넣을 객체) 그리고나서 로그를 봤을 때 a === b는 false를 반환했다. 그리고 b.name을 통해 b객체의 name값을 변경해줬지만 a는 변경되지 않았다.

그리고 Object.assgin의 첫번째 인자에 빈 객체가 아닌 객체를 넣으면 입력한 해당 객체에 두번째 인자의 객체를 추가하게 된다. 예제에서는 {left:1, righ:2} 객체에 name 프로퍼티가 추가되고 name 값이 변경된 것을 확인할 수 있다.

객체의 불변성을 유지하는 방법은 이것말고도 여러가지가 있다.

this.setState({state:"welcome"});

이 코드는 mode의 값을 'welcome'으로 바꾼 것인데 원본을 교체한 것이지 변경한 것은 아니다.

하지만,

this.state.contents.push({id:this.max_content_id, title:_title, desc:_desc});

이코드는 push를 사용해 this.state.contents 원본을 변경한 것이다.

const newContents = Array.from(this.state.contents);
newContents.push({id:this.max_content_id, title:_title, desc:_desc});

이 코드도 array.from을 사용해 복사한 다음, 복사본에 push를 하고 setState를 호출하기 때문에 원본을 변경하지 않고 교체한 것이다.

this.state.contents.push({}) // 원본변경 O
this.steate.contents.concat({}) // 원본변경 X

push는 원본을 변경하고 concat은 원본을 복사해 사본을 만든다.

책에서는 함수가 원본객체의 값을 변경하는지 변경하지 않는지를 사용자가 기억해서 알아야 한다고 한다. 그래서 immutable.js 라이브러리를 알려주었다.
이것을 사용하면 객체의 불변성을 유지할 수 있게 도움을 주는 유사배열객체를 만들 수 있다고 하는데 나중에 한 번 써 봐야 겠다. (이 라이브러리에서 객체는 Map, 배열은 List 라고 한다)


출처: 생활코딩! 리액트 프로그래밍 책

profile
프론트엔드개발자가 되고 싶어서 열심히 땅굴 파는 자

0개의 댓글