React 예제(2) - MoonGyeongHyeon/React_Study GitHub Wiki

React - 예제(2)

state 안의 array에 삽입/제거/수정

this.state 에 포함된 배열에 원소를 삽입/제거/수정할 때, 그 배열에 직접 접근해선 안된다. 예를 들어, 원소를 추가할 때 배열 객체의 push() 메소드를 사용하면 원하는대로 되지 않는다. this.state 가 변경된다고 해서 컴포넌트가 업데이트 되는건 아니기 때문이다. React Component API인 forceUpdate() 를 통하여 컴포넌트를 리렌더링 시키는 방법도 있지만 절대 권장하지 않는 방법이다.

React 매뉴얼에선 this.state 를 직접 수정하지 말고, this.setState() 메소드를 사용하여 수정할 것을 강조했다. (이 메소드가 실행되면 자동으로 리렌더링이 진행된다.)

Contact를 생성하는 컴포넌트

ContactCreator 컴포넌트를 만들어보자.

ContactCreator.js

import React from 'react';

class ContactCreator extends React.Component {
    constructor(props) {
        super(props);
        // Configure default state
        this.state = {
            name: "",
            phone: ""
        };
    }

    handleChange(e){
        var nextState = {};
        nextState[e.target.name] = e.target.value;
        this.setState(nextState);
    }

    handleClick(){
        this.props.onInsert(this.state.name, this.state.phone);
        this.setState({
            name: "",
            phone: ""
        });
    }

    render() {
        return (
            <div>
                <p>
                    <input type="text"
                           name="name"
                           placeholder="name"
                           value={this.state.name}
                           onChange={this.handleChange.bind(this)}/>

                    <input type="text"
                           name="phone"
                           placeholder="phone"
                           value={this.state.phone}
                           onChange={this.handleChange.bind(this)}/>

                    <button onClick={this.handleClick.bind(this)}>Insert</button>
                </p>
            </div>
        );
    }
}

export default ContactCreator;

App.js

import React from 'react';
import update from 'react-addons-update';
import ContactCreator from "./ContactCreator"

class App extends React.Component {
    render(){

        return (
            <Contacts/>
        );
    }
}

class Contacts extends React.Component {

    constructor(props) {
        super(props);
        this.state = {
            contactData: [
                {name: "Abet", phone: "010-0000-0001"},
                {name: "Betty", phone: "010-0000-0002"},
                {name: "Charlie", phone: "010-0000-0003"},
                {name: "David", phone: "010-0000-0004"}
            ]
        };
    }

    _insertContact(name, phone){
        let newState = update(this.state, {
            contactData: {
                $push: [{"name": name, "phone": phone}]
            }
        });
        this.setState(newState);
    }

    render(){
        return(
            <div>
                <h1>Contacts</h1>
                <ul>
                    {this.state.contactData.map((contact, i) => {
                        return (<ContactInfo name={contact.name}
                                             phone={contact.phone}
                                             key={i}/>);
                    })}
                </ul>

                <ContactCreator onInsert={this._insertContact.bind(this)}/>
            </div>
        );
    }
}

class ContactInfo extends React.Component {
    render() {
        return(
            <li>{this.props.name} {this.props.phone}</li>
        );
    }
}

export default App;
  • handleChange(e)

    인풋박스의 값을 변경할 때마다 실행되는 메소드이다. 파라미터 e는 Javascript의 Event 인터페이스로, e를 사용함으로써 한 메소드로 여러 인풋박스를 name에 따라 처리할 수 있다.(e.target.name은 인풋박스의 name 속성, e.target.value는 인풋박스의 value 속성값을 얻어온다.)

  • update(target, action) (react-addons-update)

    target(Array, Object 등)에 action을 취한 결과를 반환하는 메소드이다. 먼저 코드를 보자.

            let newState = update(this.state, {
                contactData: {
                    $push: [{"name": name, "phone": phone}]
                }
            });

    this.state는 Object이며 현재 name과 phone을 속성으로 갖는 객체의 배열 contactData를 저장하고 있다. 두 번째 인자를 보면,

    {
                contactData: {
                    $push: [{"name": name, "phone": phone}]
                }
    }

    contactData에 특별한 커맨드($)를 이용하고 있다. $push 는 Array를 값으로 갖는데, 배열의 값을 모두 target에 해당하는 부분에 추가하라는 의미이다. 즉, this.state가 target이 되며, this.state.contactData에 $push의 원소값을 추가하여 그 결과를 newState에 반환하게 된다.

Contact를 선택하는 컴포넌트

Contact를 마우스 클릭했을 때 Style 효과를 바꿔주도록 해보자.

ContactInfo

    handleClick(){
        this.props.onSelect(this.props.contactKey);
    }

    render() {
        return(
            <li
                onClick={this.handleClick.bind(this)}>
                {this.props.name} {this.props.phone}
            </li>
        );
    }

해당 컴포넌트가 클릭되면 handleClick() 메소드가 실행되며, 메소드 내부에선 parent 컴포넌트에서 prop로 전달받은 onSelect() 메소드를 실행한다. 여기서 인수는 현재 Contact의 Index이다.

Contacts

    constructor(props) {
        super(props);
        this.state = {
            contactData: [
                /* ... */
            ],
            selectedKey: -1
        };
    }
	/* ... */
	_onSelect(key){
        if(key==this.state.selectedKey){
            console.log("key select cancelled");
            this.setState({
                selectedKey: -1
            });
            return;
        }

        this.setState({
            selectedKey: key
        });
        console.log(key + " is selected");
    }

    _isSelected(key){
        if(this.state.selectedKey == key){
            return true;
        }else{
            return false;
        }
    }

    render() {
        /* ... */
                    {this.state.contactData.map((contact, i) => {
                        return (<ContactInfo name={contact.name}
                                            phone={contact.phone}
                                              key={i}
                                       contactKey={i}
                                       isSelected={this._isSelected.bind(this)(i)}
                                         onSelect={this._onSelect.bind(this)}/>);
                    })}

selectedKey 는 현재 선택된 컴포넌트의 고유 번호이다. 만약 선택된 Contact가 없을 경우 -1로 설정된다.

_onSelect() 메소드는 컴포넌트가 클릭될 때 실행할 메소드이다. 선택할 컴포넌트가 이미 선택되어있다면 선택을 해제한다.

_isSelect(key) 메소드는 child 컴포넌트에게 해당 컴포넌트가 선택된 상태인지 아닌지 알려준다.

ContactInfo

render() {

        let getStyle = isSelect => {
            if(!isSelect) return;

            let style = {
                fontWeight: 'bold',
                backgroundColor: '#4efcd8'
            };

            return style;
        };

        return(
            <li style={getStyle(this.props.isSelected)}
                onClick={this.handleClick.bind(this)}>
                {this.props.name} {this.props.phone}
            </li>
            );
    }

geyStyle() 함수는 arrow function 이 사용됐다. 매개변수가 오직 하나라면 괄호를 생략할 수 있다.

이 함수는 매개변수가 참이면 배경색을 아쿠아색으로 바꾸며, 거짓일 경우 색을 다시 없앤다.

Contact를 삭제하는 컴포넌트

ContactRemover

    handleClick() {
        this.props.onRemove();
    }

    render() {
        return (
            <button onClick={this.handleClick.bind(this)}>
                Remove selected contact
            </button>
        );
    }
}

버튼이 클릭되면 handleClick() 메소드가 실행되며, 해당 메소드에선 parent 컴포넌트에서 전달받은 onRemove() 메소드가 실행된다.

ContactInfo

    _removeContact(){
        if(this.state.selectedKey==-1){
            console.log("contact not selected");
            return;
        }

        this.setState({
            contactData: update(
                this.state.contactData,
                {
                    $splice: [[this.state.selectedKey, 1]]
                }
            ),
            selectedKey: -1
        });
    }

선택한 Contact를 삭제하는 메소드이다. 선택된 Contact가 없다면 작업을 취소한다.

this.setState(...) 가 실행되면 contactData에서 selectedKey 번째의 데이터를 제거하고 selectedKey의 값을 초기화한다.

Contacts

<ContactRemover onRemove={this._removeContact.bind(this)}/>

Contact를 수정하는 컴포넌트

구현하고자 하는 기능은 아래와 같다.

  1. ContactInfo를 선택하면 Contact의 name과 phone의 값이 input box에 복사됨.
  2. name과 phone을 임의의 값으로 수정.
  3. Edit 버튼을 누르면 값이 수정됨.

ContactEditor

class ContactEditor extends React.Component {
    constructor(props) {
        super(props);
        // Configure default state
        this.state = {
            name: "",
            phone: ""
        };
    }

    handleClick(){
        if(!this.props.isSelected){
            console.log("contact not selected");

            return;
        }

        this.props.onEdit(this.state.name, this.state.phone);
    }

    handleChange(e){
        var nextState = {};
        nextState[e.target.name] = e.target.value;
        this.setState(nextState);
    }
  
    componentWillReceiveProps(nextProps){
        this.setState({
            name: nextProps.contact.name,
            phone: nextProps.contact.phone
        });
    }

    render() {
        return (
            <div>
                <p>
                    <input type="text"
                        name="name"
                        placeholder="name"
                        value={this.state.name}
                        onChange={this.handleChange.bind(this)}/>

                    <input type="text"
                        name="phone"
                        placeholder="phone"
                        value={this.state.phone}
                        onChange={this.handleChange.bind(this)}/>
                    <button onClick={this.handleClick.bind(this)}>
                    Edit
                    </button>
                </p>
            </div>
        );
    }
}

Contacts

class Contacts extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            /* ... */
            selectedKey: -1,
            selected: {
                name: "",
                phone: ""
            }
        };
    }

    _onSelect(key){
        if(key==this.state.selectedKey){
            console.log("key select cancelled");
            this.setState({
                selectedKey: -1,
                selected: {
                    name: "",
                    phone: ""
                }
            });
            return;
        }

        this.setState({
            selectedKey: key,
            selected: this.state.contactData[key]
        });
        console.log(key + " is selected");
    }
    
    _editContact(name, phone){
        this.setState({
            contactData: update(
                this.state.contactData,
                {
                    [this.state.selectedKey]: {
                        name: { $set: name },
                        phone: { $set: phone }
                    }
                }
            ),
            selected: {
                name: name,
                phone: phone
            }
        });
    }
    
/* ... */
                <ContactEditor onEdit={this._editContact.bind(this)}
                           isSelected={(this.state.selectedKey !=-1)}
                              contact={this.state.selected}/>
/* ... */

CPU 낭비

위의 예제는 문제가 있다. 구현하고자 하는 기능은 모두 구현했지만 CPU를 낭비하는 부분이 존재한다.

  • 문제점

    데이터가 수정될 때마다 상태에 변동이 없는, 즉 리렌더링 될 필요가 없는 컴포넌트들이 리렌더링 되고 있다.

    ContactInfo의 render() 메소드에 로그를 남겨보자.

class ContactInfo extends React.Component {
/* ... */
    render() {
        console.log("rendered: " + this.props.name);
/* ... */

그 후, ContactInfo를 선택, 수정, 삭제하게 되면

![CPU낭비 로그](/Users/kakaogames/Documents/공부/React/정리/이미지/CPU낭비 로그.png)

위와 같은 로그가 반복적으로 출력되는 것을 알 수 있다.

  • 해결

    Component Lifecycle API 중 하나인 shouldComponentUpdate(nextProps, nextState) 메소드를 이용하면 간단하게 해결할 수 있다. 해당 생명주기 메소드는 현재 상태 ( this.props or this.state ) 와 다음 상태 ( nextProps or nextState ) 의 변화가 있나 없나를 React Component에게 알려주는 역할을 한다.(boolean 형태의 값을 반환한다.) 기본적으로, state의 변화가 있을 때마다 true를 반환하게 정의되어 있지만 필요에 따라 재정의하여 사용하면 된다.

ContactInfo

	shouldComponentUpdate(nextProps, nextState){
        return (JSON.stringify(nextProps) !== JSON.stringify(this.props));
    }
⚠️ **GitHub.com Fallback** ⚠️