Ryu.log

[ React-Tutorial-09 ] 불변성을 지키는 이유와 업데이트 최적화 본문

Prev-content

[ React-Tutorial-09 ] 불변성을 지키는 이유와 업데이트 최적화

류뚝딱 2018. 6. 5. 16:41

지난 섹션에서 배열을 어떻게 다뤄야 하는지에 대해서 알아보았다. 데이터를 업데이트하는 과정에서 불변성을 지켜야한다는 것을 강조 하였는데,

왜 그래야 하는지 알아보자.





01. 데이터 필터링 구현하기

우선, 불변성의 중요성을 알아보는 과정에서 이름으로 전화번호를 찾는 데이터 필터링 기능을 구현해보자.
먼저 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}
          : 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 함수의 상단에 아래코드를 넣어보자.

// 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로 설정하게 하면 된다.

// src/components/PhoneInfoList.js
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 로직을 굉장히 간단하게 작성해주었는데 어떻게 이런게 가능한 것일까?





02. 불변성에 대해 알아보자

그 이유는, 우리가 불변성을 지켜줬기 때문이다.
만약에 우리가 배열을 직접 건들여서 수정해줬다고 가정해보자. 그럴 때는 이렇게 !== 하나로 비교를 끝낼 수 가 없다.
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





03. 기능 마저 구현하기

구현하던 기능을 마저 끝내보겠다.

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}
          : 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를 반환하게 된다.





04. 계속해서 최적화

자, 이번에는 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;
  }
...


낭비 렌더링이 사라졌다!




이 글은 Velopert님의 블로그에서 React 포스팅을보며 실습하며 공부한 자료입니다.

Comments