React.js Immutability Helper

React.js Immutability Helper – State 내부 Array 에 원소 삽입/제거/수정

컴포넌트에서 사용할 데이터 배열을 매핑하여 효율적으로 렌더링하는 방법에 이어 이번엔 데이터 배열에 변화를 주는 방법을 알아보도록 하겠다. 이 과정은 생각보다 쉽지만은 않다.

1. state 안의 array 에 원소 삽입/제거/수정

this.state 에 포함된 배열에 원소를 삽입/제거/수정 을 할 때 그 배열에 직접 접근하면 안된다. 예를들어, 원소를 추가 할 때 배열객체의 push() 메소드를 사용하면 원하는대로 되지 않는다.
this.state가 변경된다고해서 컴포넌트가 업데이트되지 않기 때문이다. 물론 변경 후 React 컴포넌트 API 인 forceUpdate()를 통하여 컴포넌트가 render()를 다시 실행 하게 끔 하는 방법이 있긴하지만 이건 절대 권장되지 않는 방법이다.
React 메뉴얼 에서도 this.state를 직접 수정하지 말고 this.setState()를 사용하여 수정 할 것을 강조하고 있다. (이 메소드가 실행되면 자동으로 re-rendering 이 진행된다.)


1.1 원소 삽입하기

state 내부의 배열에 원소를 추가하는 방법은 다음과 같다.

1
2
3
this.setState({
list: this.state.list.concat(newObj)
});

concat 을 사용함으로서 현재의 list 배열에 newObj 원소를 추가한 새로운 배열을 생성 한 후, 그 값을 현재의 list 로 설정한다.

배열을 수정 할 땐 원시적인 방법으론 위와 같이 배열 전체를 복사하고 처리 후 기존 값에 덮어씌우는 과정을 거쳐야 한다.
허나, 만약에 배열의 크기가 클 땐 성능이 좀 저하될 것이다.

다른 방법으로는 Immutability Helpers 를 사용하는 방법이 있다.
이는 배열을 더 효율적으로 수정 할 수 있게 해주는 페이스북의 Immutable-js 를 사용한다.

이를 사용하려면 라이브러리를 사전 설치해주어야 한다.

React 구버전에서는 해당 라이브러리가 내장되어 import React from 'react/addons'; 으로 React를 import 하여 React.addons.update() 를 사용 할 수 있었으나, 이제 이 방법은 deprecated 되었다.
아직도 이렇게 사용은 가능 하나, 브라우저 상에서 ‘react-addon-update’ 를 import 하라고 권장하는 오류 메시지가 발생한다.

라이브러리 설치 방법

$ npm install --save react-addons-update 를 통하여 라이브러리를 저장 후, js 파일 상단에 import update from 'react-addons-update' 를 삽입해 준다.

1
2
3
this.setState({
list: update(this.state.list, {$push: [newObj, newObj2]})
});

update() 메소드의 첫 파라미터는 처리 할 배열이며 두번째는 처리 할 명령들을 지니고 있는 객체 타입 이다. $push: [newObj, newObj2]는 list 배열에 newObj 와 newObj2 를 추가해 준다.
한 객체를 추가 할 때도 [] 안에 배열형태로 감싸 줘야 한다.
Immutable-js 의 syntax 는 MongoDB 쿼리 언어에서 영감을 받았다고 한다.
브라우저상에서 react-with-addons를 불러와서 사용하는 경우에는 update 가 아닌 React.addons.update 를 사용 해야 한다.
(jsfiddle이 이에 해당한다.)

1.2 원소 제거하기

원소를 제거 할 때 역시, state 의 배열에 직접 접근하면 안 되고, 배열을 복사한 후 원소 제거 후 기존 값에 덮어씌워져야 한다.
JavaScript Array의 내장 함수인 splice()를 사용하면 되지만, 이는 생략하고 더 효율적인 Immutability Helper를 사용하는 예제를 알아 보겠디.

1
2
3
this.setState({
list: update(this.state.list, {$splice: [[index, 1]]})
});

위 코드는 list 배열의 index번째 아이템부터 시작해서 1개의 만큼의 데이터를 제거 한다.
$splice 에 전달 되는 데이터는 배열로 이루어진 배열이다.

1.3 원소 수정하기

Immutability Helper를 사용하여 특정 원소를 수정하는 예제를 살펴보자.

1
2
3
4
5
6
7
8
this.setState({
list: update(this.state.list, {
[index]: {
field: {$set: 'value'},
field2: {$set: 'value2'}
}
}
});

위 코드는 list 배열의 index 번째 아이템의 field 와 field2 의 값을 변경 한다.

2. 적용하기

그럼, 위에서 배운것들을 적용해보도록 하겠다.
앞으로 만들 클래스는 다음과 같다.

  • ContactCreator: Contact 를 생성하는 컴포넌트
  • ContactRemover: Contact 를 제거하는 컴포넌트
  • ContactEditor: Contact 를 수정하는 컴포넌트

앞으로 구현하고자 하는 기능은, 인풋박스에 입력하여 원하는 Contact를 추가 하고, 리스트에 있는 Contact를 선택하여 수정 및 제거를 하는 것 이다.

2.1 ContactCreator 컴포넌트 만들기

위에서 배운것을 적용을 해보도록 하기위해, ContactInfo 를 생성하기 위해 필요한 텍스트박스 두개와 버튼 하나를 지니고있는 컴포넌트를 만들어 보자.

클래스는 강의 편의상 같은 파일에 작성하도록 하겠다.

a. ContactCreator: 클래스 생성 및 렌더링

1
2
3
4
5
6
7
8
9
10
11
12
13
class ContactCreator extends React.Component {
render() {
return (
<div>
<p>
<input type="text" name="name" placeholder="name" />
<input type="text" name="phone" placeholder="phone" />
<button>Insert</button>
</p>
</div>
);
}
}

b. Contact: 렌더링 할 컴포넌트에 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Contacts extends React.Component {
/* ... */

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/>
</div>
);
}

/* ... */

c. ContactCreator: Input 의 값을 컴포넌트의 state 로 사용하기

1
2
3
4
5
6
7
8
9
10
11
class ContactCreator extends React.Component {
constructor(props) {
super(props);
// Configure default state
this.state = {
name: '',
phone: ''
};
}

/* ... */
1
2
<input type="text" name="name" placeholder="name" value={this.state.name} />
<input type="text" phone="phone" placeholder="phone" value={this.state.phone} />

초기 state 값을 지정하고, 렌더링 부분 코드에서 input 의 value를 state를 사용하도록 수정한 후, 인풋박스에 텍스트를 적으려고 시도해보면 값이 고정되서 변경되지 않는다.
이 부분을 해결하기 위하여, onChange 이벤트를 통하여 인풋박스에 텍스트를 입력 시 status 를 업데이트하도록 설정해야 한다.

d. ContactCreator: onChange 이벤트 사용하기

1
2
3
4
5
6
7
8
9
10
class ContactCreator extends React.Component {
/* ... */

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

/* ... */
1
2
3
4
5
6
7
8
9
10
11
<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)} />

인풋박스의 값을 변경 할 때 실행 될 handleChange(e) 메소드를 만들었다. 여기서 파라미터 e 는 JavaScript 의 Event 인터페이스이다.
e 를 사용함으로서 한 메소드로 여러 인풋박스를 인풋박스의 name 에 따라 처리 할 수 있게된다.
렌더링 부분의 코드를 보기 좋게 하기위해 줄바꿈을 하였으며 onChange={this.handleChange.bind(this)}를 넣어주었다.
인풋박스가 변경 될 때 해당 메소드를 실행한다는 의미 이다. bind 를 통하여 컴포넌트의 this 에 접근 할 수 있게 된다.

e. ContactCreator: Insert 버튼 기능 구현하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ContactCreator extends React.Component {
/* ... */

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

/* ... */

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

/* ... */

버튼을 클릭 했을 때 실행 될 메소드를 만들었다. handleClick() 에서는 parent 컴포넌트인 Contacts 에서 props 로 받아온 메소드를 실행합니다. 그 후, 인풋 박스 값을 비운다.

f. Contacts: _insertContact 메소드 만들기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Contacts extends React.Component {
/* ... */

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

this.setState(newState);
}

/* ... */

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

/* ... */

Immutability Helpers 를 사용하여 배열에 원소를 추가하였으며, _insertContact(name, phone) 메소드를 ContactCreator 의 prop 으로 전달 해 주었다.
참고: jsfiddle 에선 React.addons.update 를 사용해아한다.

2.2 선택 기능 구현하기

배열에서 데이터를 수정 하거나 제거 할 때 필요할 마우스로 선택하는 기능을 구현해보자.

a. ContactInfo: handleClick() 메소드 및 onClick prop 추가

1
2
3
4
5
6
7
8
9
10
11
12
class ContactInfo extends React.Component {
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() 메소드를 실행한다.
여기서 인수 contactKey 는 해당 컴포넌트의 고유 번호이다.
컴포넌트를 매핑할 때 key 를 사용하긴 하였으나, 이는 prop으로 간주되지 않으며 React 내부에서 사용하는 용도이기에 직접 접근이 불가하다.

b. Contacts: _onSelect(), _isSelected() 메소드 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class Contacts extends React.Component {
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
key={i}
name={contact.name}
phone={contact.phone}
contactKey={i}
isSelected={this._isSelected.bind(this)(i)}
onSelect={this._onSelect.bind(this)}
/>
);
})}
/* ... */

state selectedKey 는 현재 선택된 컴포넌트의 고유번호 이다.
만약에 선택된 Contact 가 없을 시에는 -1 로 설정된다.
_onSelect() 메소드는 컴포넌트가 클릭 될 때 실행 할 메소드 입니다. 선택 할 컴포넌트가 이미 선택되어있다면 선택을 해제한다.
이 메소드는 child 컴포넌트의 onSelect prop 으로 전달된다.
_isSelect(key) 메소드는 child 컴포넌트에게 해당 컴포넌트가 선택된 상태인지 아닌지 알려준다.
이 메소드를 실행 한 결과 값이 child 컴포넌트의 isSelected prop 으로 전달 된다.

c. ContactInfo: 렌더링 시 선택된 상태라면 특정 스타일 적용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ContactInfo extends React.Component {
/* ... */
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>
);
}
}

5번 줄에서는 getStyle 이라는 함수를 선언했다. arrow function 이 사용되었는데, 매개변수가 오직 하나라면 괄호가 생략 될 수 있다.
이 함수는 매개변수가 참이면 배경색이 아쿠아색인 스타일을 반환하며 거짓이면 비어있는 스타일을 반환한다.
전에 JSX 에서 언급했었던 inline styling이 사용되었다.

2.3 ContactRemover 컴포넌트 만들기

선택 기능이 구현 되었으니, 이 컴포넌트를 구현하는건 간단하다.

a. ContactRemover: 컴포넌트 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
class ContactRemover extends React.Component {
handleClick() {
this.props.onRemove();
}

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

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

b. Contact: 렌더링 할 컴포넌트에 추가

1
2
3
4
5
6
class Contacts extends React.Component {
/* ... */
<ContactCreator onInsert={this._insertContact.bind(this)} />
<ContactRemover />
/* .. */
}

렌더링 부분에 ContactCreator 하단에 <ContactRemover /> 를 추가한다

c. Contact: _removeContact() 메소드 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Contacts extends React.Component {
/* ... */
_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(...) 가 실행 되면 state contactData 에서 selectedKey번째 데이터를 제거하고 아무것도 선택하지 않은 상태로 설정한다.
참고: jsfiddle을 사용한다면 React.addons.update 를 사용해야한다.

d. Contact: ContactRemover 컴포넌트에 삭제 메소드 prop onRemove 으로 전달

1
2
3
4
class Contacts extends React.Component {
/* ... */
<ContactRemover onRemove={this._removeContact.bind(this)}/>
/* ... */

2.4 ContactEditor 만들기

ContactEditor 에서 구현하고자 하는 기능은 다음과 같다.

  • Contact를 선택하면 Contact 의 name 과 phone 데이터가 인풋박스로 복사됨
  • Edit 버튼을 누르면 Contacts 의 데이터를 수정함.

이 컴포넌트에서 사용 할 prop 들은 다음과 같다.

  • isSelected: parent 컴포넌트에서 Contact가 선택 되어있는지 안되어있는지 알려준다.
  • onEdit(): parent 컴포넌트에서 전달 받을 메소드로서, 데이터 수정 작업을 처리한다.
  • contact: parent 컴포넌트에서 선택된 Contact의 name 과 phone 정보를 갖고있는 객체이다.

a. ContactEditor: 컴포넌트 초기 작성
이 컴포넌트의 코드 형태는 위에서 만든 ContactCreator 와 매우 비슷하다.

ContactCreator 를 copy & paste 하고 우선 필요한 부분만 수정하도록 하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class ContactEditor extends React.Component {
constructor(props) {
super(props);
// Configure default state
this.state = {
name: '',
phone: ''
};
}

handleClick() {

}

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

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>
);
}
}

코드를 붙여 넣은 후, 클래스 이름을 변경하고 handleClick() 에서 처리할 내용이 다르니 코드를 비워두자.
이 컴포넌트는 수정하는 컴포넌트니까 버튼 캡션을 Edit 으로 변경해할 것이다.

b. ContactEditor: handleClick() 메소드 작성

1
2
3
4
5
6
7
8
9
10
11
12
class ContactEditor extends React.Component {
/* ... */
handleClick(){
if (!this.props.isSelected) {
console.log('contact not selected');

return;
}

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

선택 된 Contact가 없다면 작업을 취소한다.
onEdit() 은 parent 컴포넌트에서 전달 받을 메소드 이다.

c. Contacts: _editContact 메소드 초기작성 렌더링 할 컴포넌트 추가

1
2
3
4
5
6
7
8
9
class Contacts extends React.Component {
/* .. */
_editContact(name, phone){

}
/* ... */
<ContactRemover onRemove={this._removeContact.bind(this)} />
<ContactEditor onEdit={this._editContact.bind(this)} isSelected={(this.state.selectedKey !=-1)} />
/* ... */

_editContact() 메소드는 오류가 나지 않도록 초기 작성만하고 구현은 나중에 하도록 하겠습니다.
ContactsRemover 컴포넌트 하단에 <ContactEditor... />를 작성하세요.
prop isSelected 은 JavaScript 표현식을 사용하여 selectedKey가 -1이 아니라면 true를, 맞다면 false를 반환합니다.

d. 선택된 내용을 인풋박스로 복사하는 기능 구현하기

Contact 를 선택 하였을 때 내용을 ContactEditor 의 input 으로 복사되는 기능을 구현해보겠다.
일단은 선택된 Contact 의 정보를 ContactEditor 로 전달을 해줘야할 것 이다.
우선 선택된 Contact의 정보를 Contacts 의 state selected 에 저장하도록 하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 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');
}
/* ... */
<ContactEditor
onEdit={this._editContact.bind(this)}
isSelected={(this.state.selectedKey !=-1)}
contact={this.state.selected}
/>
/* ... */

새로운 state 를 사용 할 땐, 언제나 초기 값을 설정해줘야한다. (그렇지 않으면 오류가 발생하기 쉽상이다.)
Contact를 선택하였을 때 prop selected 에 값을 저장 하게하고, 선택을 취소 하였을 때, 값을 공백으로 설정하도록 하였다.
그리고 이 prop selected 값을 ContactEditor 에 prop contact로 전달해준다.

이제 ContactEditor 에서 선택된 Contact의 값을 받아와서 렌더링해줘할 것이다.
하지만, 인풋박스의 value 부분은 유동적이기에 그 부분에 {this.props.contact.name} 을 할 수는 없다.
prop값이 바뀔 때마다 state를 업데이트 해줄 필요가 있는데요, 이는 Component Lifecycle API 중 하나인 componentWillReceiveProps()를 사용하면 된다. 이 컴포넌트 내장메소드는, prop 값을 받게 될 때 실행되는 메소드 이다.

1
2
3
4
5
6
7
8
9
10
// ContactEditor:
class ContactEditor extends React.Component {
/* ... */
componentWillReceiveProps(nextProps) {
this.setState({
name: nextProps.contact.name,
phone: nextProps.contact.phone
});
}
/* ... */

e. Contacts: _editContact 메소드 구현하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Contacts extends React.Component {
/* ... */
_editContact(name, phone) {
this.setState({
contactData: update(
this.state.contactData,
{
[this.state.selectedKey]: {
name: {$set: name},
phone: {$set: phone}
}
}
),
selected: {
name: name,
phone: phone
}
});
}
/* ... */

출력물

1
2
3
<div id="container">
<!-- This element's contents will be replaced with your component. -->
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
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'}
],
selectedKey: -1,
selected: {
name: '',
phone: ''
}
};
}

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

this.setState(newState);
}

_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');
}

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

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

return;
}

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

_editContact(name, phone) {
this.setState({
contactData: React.addons.update(
this.state.contactData, {
[this.state.selectedKey]: {
name: {$set: name},
phone: {$set: phone}
}
}
),
selected: {
name: name,
phone: phone
}
});
}

render() {
return (
<div>
<h1>Contacts</h1>
<ul>
{this.state.contactData.map((contact, i) => {
return (
<ContactInfo
key={i}
name={contact.name}
phone={contact.phone}
contactKey={i}
isSelected={this._isSelected.bind(this)(i)}
onSelect={this._onSelect.bind(this)}
/>
);
})}
</ul>
<ContactCreator onInsert={this._insertContact.bind(this)} />
<ContactRemover onRemove={this._removeContact.bind(this)} />
<ContactEditor onEdit={this._editContact.bind(this)} contact={this.state.selected} />
</div>
);
}
}

class ContactInfo extends React.Component {
handleClick() {
this.props.onSelect(this.props.contactKey);
}

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>
);
}
}

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

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

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

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>
);
}
}

class ContactRemover extends React.Component {
handleClick() {
this.props.onRemove();
}

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

class ContactEditor extends React.Component {
constructor(props) {
super(constructor);
// 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>
);
}
}

ReactDOM.render(<App />, document.getElementById('container'));

3. CPU 자원낭비 줄이기

원하고자 하는 기능은 모두 구현하였지만, 사실 위에서 작성한 코드는 CPU 자원을 낭비하고있다.
비록 큰데이터를 다루는게 아니기 때문에 성능에 큰 영향을 끼치지는 않고 있지만 코드의 완성도를 위하여 컴포넌트를 최적화해보겠다.

3.1 무엇이 문제인가..

데이터가 수정 될 때 마다, 상태에 변동이 없는, 즉 리렌더링 할 필요가 없는 컴포넌트들도 리렌더링이 되고있다.
한번 ContactInfo 컴포넌트의 render() 메소드에 코드 console.log('rendered: ' + this.props.name); 를 추가해보자.

1
2
3
4
5
6
// ContactInfo:
class ContactInfo extends React.Component {
/* ... */
render() {
console.log("rendered: " + this.props.name);
/* ... */

그리고 Contact들을 선택해보고, 추가해보고 수정해보자.

리렌더링으로 인한 CPU 자원낭비 출력결과

보다시피, 쓸데없는 렌더링을 하고있는걸 확인할 수 있다.

3.2 해결하기

자, 그럼 이제 해결해보도록 하자. 해결법은 매우 간단하다.
Component Lifecycle API 중 하나인 shouldComponentUpdate() 메소드를 컴포넌트 클래스 안에 작성해주면 된다.
이 메소드는 컴포넌트를 다시 렌더링 해야 할 지 말지 정의를 해준다.

1
2
3
4
5
6
class ContactInfo extends React.Component {
/* ... */
shouldComponentUpdate(nextProps, nextState) {
return (JSON.stringify(nextProps) !== JSON.stringify(this.props));
}
/* ... */

CPU 자원낭비 최적화 출력결과

자, 이제 필요한 컴포넌트만 렌더링하게 된다.
이와 같이, 컴포넌트를 Mapping 하고 데이터를 수정 할 떄 코드를 최적화 하는것을 잊지 않도록 하자.

참조

공유하기