9편: 불변성을 지키는 이유와 업데이트 최적화
우리는 지난 섹션에서 배열을 어떻게 다뤄야 하는지에 대해서 알아보았습니다. 데이터를 업데이트하는 과정에서 불변성을 지켜야한다는것을 강조했었는데요, 왜 그렇게 해야하는지 알아보겠습니다.
데이터 필터링 구현하기
우선, 불변성의 중요성을 알아보는 과정에서 이름으로 전화번호를 찾는 데이터 필터링 기능을 구현해보겠습니다.
먼저 App 컴포너트에서 input 하나를 렌더링하고 해당 input 의 값을 state 의 keyword 라는 값에 담겠습니다. 이를 위해서 이벤트 핸들러도 만들어줘야겠지요?
// file: src/App.js
import React, { Component } from 'react';
import PhoneForm from './components/PhoneForm';
import PhoneInfoList from './components/PhoneInfoList';
class App extends Component {
id = 2
state = {
information: [
{
id: 0,
name: '김민준',
phone: '010-0000-0000'
},
{
id: 1,
name: '홍길동',
phone: '010-0000-0001'
}
],
keyword: ''
}
handleChange = (e) => {
this.setState({
keyword: e.target.value,
});
}
handleCreate = (data) => {
const { information } = this.state;
this.setState({
information: information.concat({ id: this.id++, ...data })
})
}
handleRemove = (id) => {
const { information } = this.state;
this.setState({
information: information.filter(info => info.id !== id)
})
}
handleUpdate = (id, data) => {
const { information } = this.state;
this.setState({
information: information.map(
info => id === info.id
? { ...info, ...data } // 새 객체를 만들어서 기존의 값과 전달받은 data 을 덮어씀
: info // 기존의 값을 그대로 렌더링
)
})
}
render() {
const { information, keyword } = this.state;
return (
<div>
<PhoneForm
onCreate={this.handleCreate}
/>
<p>
<input
placeholder="검색 할 이름을 입력하세요.."
onChange={this.handleChange}
value={keyword}
/>
</p>
<hr />
<PhoneInfoList
data={information}
onRemove={this.handleRemove}
onUpdate={this.handleUpdate}
/>
</div>
);
}
}
export default App;
검색어를 입력했을 때 필터링을 하는것은 나중에 구현하도록 하겠습니다. 지금의 상황에선, input 에 입력을 했을때 업데이트가 필요한것은 오직 input 뿐입니다.
하지만, App 컴포넌트의 상태가 업데이트 되면, 컴포넌트의 리렌더링이 발생하게 되고, 컴포넌트가 리렌더링되면 그 컴포넌트의 자식 컴포넌트도 리렌더링됩니다.
한번 확인을 해볼까요? PhoneInfoList 컴포넌트에서 render 함수의 상단에 다음 코드를 넣어보세요.
// file: src/components/PhoneInfoList.js
...
render() {
console.log('render PhoneInfoList');
const { data, onRemove, onUpdate } = this.props;
const list = data.map(
info => (
<PhoneInfo
key={info.id}
info={info}
onRemove={onRemove}
onUpdate={onUpdate}
/>)
);
return (
<div>
{list}
</div>
);
}
...
이렇게 하고 검색어 input 을 수정한다음에 콘솔을 확인해봅시다.
App 이 리렌더링됨에 따라 PhoneInfoList 도 리렌더링이 되고 있죠. 물론, 실제로 변화가 일어나진 않으니 지금은 Virtual DOM 에만 리렌더링 합니다. 지금의 상황에는 별로 큰 문제가 되지 않는데, 리스트 내부의 아이템이 몇백개, 몇천개가 된다면 이렇게 Virtual DOM 에 렌더링 하는 자원은 아낄 수 있으면 아끼는게 좋습니다.
이러한 낭비되는 자원을 아끼기 위해선 우리가 이전에 배웠던 shouldComponentUpdate LifeCycle API 를 사용하면 됩니다.
자, PhoneInfoList 에서 shouldComponentUpdate 를 구현해보세요.
그냥 단순히 다음 받아올 data 가 현재 data 랑 다른 배열일 때 true 로 설정하게 하면 됩니다.
import React, { Component } from 'react';
import PhoneInfo from './PhoneInfo';
class PhoneInfoList extends Component {
static defaultProps = {
data: [],
onRemove: () => console.warn('onRemove not defined'),
onUpdate: () => console.warn('onUpdate not defined'),
}
shouldComponentUpdate(nextProps, nextState) {
return nextProps.data !== this.props.data;
}
render() {
console.log('render PhoneInfoList');
const { data, onRemove, onUpdate } = this.props;
const list = data.map(
info => (
<PhoneInfo
key={info.id}
info={info}
onRemove={onRemove}
onUpdate={onUpdate}
/>)
);
return (
<div>
{list}
</div>
);
}
}
export default PhoneInfoList;
그러면 이제 변화가 필요하지 않을 때는 render 함수가 호출되지 않게 됩니다.
우리는 shouldComponentUpdate 로직을 굉장히 간단하게 작성해주었는데 어떻게 이런게 가능 한 것일까요?
불변성에 대해 알아보자.
그 이유는, 우리가 불변성을 지켜줬기 때문입니다.
만약에 우리가 배열을 직접 건들여서 수정해줬다고 가정해봅시다.. 그럴때는 이렇게 !== 하나로 비교를 끝낼수가 없습니다.
const array = [1,2,3,4];
const sameArray = array;
sameArray.push(5);
console.log(array !== sameArray); // false
우리가 sameArray = array 를 했다고 해서 기존에 있던 배열이 복사되는것이 아니라 똑같은 배열을 가르키고 있는 레퍼런스가 하나 만들어진 것이기 때문에, 우리가 sameArray 에 push 를 하게 된다고 해서 array 와 sameArray 가 달라지지 않습니다.
하지만, 우리가 불변성을 유지하면
const array = [1,2,3,4];
const differentArray = [...array, 5];
// 혹은 = array.concat(5)
console.log(array === differentArray); // true
위 코드와 같이 바로바로 비교가 가능하다는 것이죠.
이는 객체를 다룰때도 마찬가지입니다.
// NO
const object = {
foo: 'hello',
bar: 'world'
};
const sameObject = object;
sameObject.baz = 'bye';
console.log(sameObject !== object); // false
// YES
const object = {
foo: 'hello',
bar: 'world'
};
const differentObject = {
...object,
baz: 'bye'
};
console.log(differentObject !== object); // true
기능 마저 구현하기
그러면, 구현하던 기능을 마저 끝내보겠습니다.
App 컴포넌트에서 keyword 값에 따라서 information 배열을 필터링 해주는 로직을 작성하고, 필터링된 결과를 PhoneInfoList 에 전달해주겠습니다.
// file: src/App.js
import React, { Component } from 'react';
import PhoneForm from './components/PhoneForm';
import PhoneInfoList from './components/PhoneInfoList';
class App extends Component {
id = 2
state = {
information: [
{
id: 0,
name: '김민준',
phone: '010-0000-0000'
},
{
id: 1,
name: '홍길동',
phone: '010-0000-0001'
}
],
keyword: ''
}
handleChange = (e) => {
this.setState({
keyword: e.target.value,
});
}
handleCreate = (data) => {
const { information } = this.state;
this.setState({
information: information.concat({ id: this.id++, ...data })
})
}
handleRemove = (id) => {
const { information } = this.state;
this.setState({
information: information.filter(info => info.id !== id)
})
}
handleUpdate = (id, data) => {
const { information } = this.state;
this.setState({
information: information.map(
info => id === info.id
? { ...info, ...data } // 새 객체를 만들어서 기존의 값과 전달받은 data 을 덮어씀
: info // 기존의 값을 그대로 렌더링
)
})
}
render() {
const { information, keyword } = this.state;
const filteredList = information.filter(
info => info.name.indexOf(keyword) !== -1
);
return (
<div>
<PhoneForm
onCreate={this.handleCreate}
/>
<p>
<input
placeholder="검색 할 이름을 입력하세요.."
onChange={this.handleChange}
value={keyword}
/>
</p>
<hr />
<PhoneInfoList
data={filteredList}
onRemove={this.handleRemove}
onUpdate={this.handleUpdate}
/>
</div>
);
}
}
export default App;
필터링이 잘 돼었나요? 참고로, 지금 상황에서는 키워드 값에 따라 PhoneInfoList 가 전달받는 data 가 다르므로, 키워드 값이 바뀌면 shouldComponentUpdate 도 true 를 반환하게 됩니다.
계속해서 최적화
자, 이번에는 PhoneInfo 컴포넌트도 최적화해주겠습니다.
PhoneInfo 컴포넌트의 render 함수 상단에 다음 코드를 넣어보세요.
render() {
console.log('render PhoneInfo ' + this.props.info.id);
그 다음에, 새 데이터를 등록하고나서 개발자 콘솔을 확인해보세요.
보면 처음 렌더링이 됐을 때 0과 1이 렌더링됐습니다. 그 다음에, 새 데이터가 나타났을때 사실상 맨마지막 데이터만 새로 렌더링해주면 되는데, 그 위에 있는 컴포넌트도 렌더링되었는데요, 이것도 아까전에 다뤘던것과 마찬가지로 실제로 바뀌지 않는 컴포넌트들은 DOM 변화가 일어나지는 않겠지만, Virtual DOM 에 그리는 자원도 아껴주기 위해서 우리는 shouldComponentUpdate 를 통하여 최적화 해줄 수 있습니다.
// file: src/components/PhoneInfo.js
shouldComponentUpdate(nextProps, nextState) {
// 수정 상태가 아니고, info 값이 같다면 리렌더링 안함
if (!this.state.editing
&& !nextState.editing
&& nextProps.info === this.props.info) {
return false;
}
// 나머지 경우엔 리렌더링함
return true;
}
...
낭비 렌더링이 사라졌지요?
정리
축하합니다! 여러분은 리액트의 기본 사용법부터 활용법까지 모두 배웠습니다. 다음 섹션에서는, 앞으로 여러분들이 무엇을 더 배워야 할 지에 대해서 다뤄보겠습니다.