React 예제(2) - MoonGyeongHyeon/React_Study GitHub Wiki
this.state 에 포함된 배열에 원소를 삽입/제거/수정할 때, 그 배열에 직접 접근해선 안된다. 예를 들어, 원소를 추가할 때 배열 객체의 push() 메소드를 사용하면 원하는대로 되지 않는다. this.state 가 변경된다고 해서 컴포넌트가 업데이트 되는건 아니기 때문이다. React Component API인 forceUpdate() 를 통하여 컴포넌트를 리렌더링 시키는 방법도 있지만 절대 권장하지 않는 방법이다.
React 매뉴얼에선 this.state 를 직접 수정하지 말고, this.setState() 메소드를 사용하여 수정할 것을 강조했다. (이 메소드가 실행되면 자동으로 리렌더링이 진행된다.)
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를 마우스 클릭했을 때 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 이 사용됐다. 매개변수가 오직 하나라면 괄호를 생략할 수 있다.
이 함수는 매개변수가 참이면 배경색을 아쿠아색으로 바꾸며, 거짓일 경우 색을 다시 없앤다.
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)}/>구현하고자 하는 기능은 아래와 같다.
- ContactInfo를 선택하면 Contact의 name과 phone의 값이 input box에 복사됨.
- name과 phone을 임의의 값으로 수정.
- 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를 낭비하는 부분이 존재한다.
- 
문제점 데이터가 수정될 때마다 상태에 변동이 없는, 즉 리렌더링 될 필요가 없는 컴포넌트들이 리렌더링 되고 있다. ContactInfo의 render() 메소드에 로그를 남겨보자. 
class ContactInfo extends React.Component {
/* ... */
    render() {
        console.log("rendered: " + this.props.name);
/* ... */그 후, ContactInfo를 선택, 수정, 삭제하게 되면

위와 같은 로그가 반복적으로 출력되는 것을 알 수 있다.
- 
해결 Component Lifecycle API 중 하나인 shouldComponentUpdate(nextProps, nextState) 메소드를 이용하면 간단하게 해결할 수 있다. 해당 생명주기 메소드는 현재 상태 ( this.propsorthis.state) 와 다음 상태 (nextPropsornextState) 의 변화가 있나 없나를 React Component에게 알려주는 역할을 한다.(boolean 형태의 값을 반환한다.) 기본적으로, state의 변화가 있을 때마다 true를 반환하게 정의되어 있지만 필요에 따라 재정의하여 사용하면 된다.
ContactInfo
	shouldComponentUpdate(nextProps, nextState){
        return (JSON.stringify(nextProps) !== JSON.stringify(this.props));
    }