본문 바로가기
Progamming/ReactJS

[벨로퍼트] 불변성과 업데이트 최적화

by 동그란 혜주 2019. 7. 12.

데이터 필터링 구현하기

우선, 불변성의 중요성을 알아보는 과정에서 이름으로 전화번호를 찾는 데이터 필터링 기능을 구현해보겠다. 먼저, App 컴포넌트에서 input 하나를 렌더링하고 해당 input의 값을 state의 keyword라는 값에 담겠다. 이를 위한 이벤트 핸들러도 만들고!

//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에 입력했을 때 업데이트가 필요한 것은 오직 input 뿐이다. (input에 키워드를 입력하는 것 자체가 update)

하지만, 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 로직을 !== 연산 하나로 굉장히 간단하게 작성했는데 어떻게 이게 가능한 것일까?

 

불변성

그 이유는, 불변성을 지켜줬기 때문이다. 만약에 배열을 직접 건들여서 수정했다고 가정해보자. 그럴때는 이렇게 !== 연산 하나로 비교를 끝낼 수가 없다.

const array = [1,2,3,4];
const sameArray = array;
sameArray.push(5);

console.log(array !== sameArray); // false

"sameArray = array" 를 했다고 해서 기존에 있던 배열이 복사되는 것이 아니라 똑같은 배열을 가르키고 있는 레퍼런스가 하나 만들어진 것이기 때문에, sameArray에 push를 하게 된다고해서 array와 sameArray가 달라지지 않는다.(sameArray에 push를 하게되면 array에도 동일하게 push되기 때문)

하지만 우리가 불변성을 유지하면

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에 전달해주겠다.

//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 함수 상단에 다음 코드를 넣어보자.

    console.log('render PhoneInfo ' + this.props.info.id);

그 다음에 새 데이터를 등록하고 나서 개발자 콘솔을 확인해보면,

새 데이터가 나타났을 때 사실상 추가한 데이터만 새로 렌더링해주면 되는데, 그 위의 컴포넌트도 렌더링된 것을 볼 수 있다. 이것도 아까와 마찬가지로 실제로 바뀌지 않는 컴포넌트들은 DOM 변화가 일어나지는 않겠지만, Virtual DOM에 그리는 자원도 아껴주기 위해 shouldComponentUpdate를 통해 최적화해줄 수 있다.

//src/components/PhoneInfo.js

...
  shouldComponentUpdate(nextProps, nextState) {
    // 수정 상태가 아니고, info 값이 같다면 리렌더링 안함
    if (!this.state.editing  
        && !nextState.editing
        && nextProps.info === this.props.info) {
      return false;
    }
    // 나머지 경우엔 리렌더링함
    return true;
  }
  
...

낭비 렌더링이 사라진 것을 볼 수 있다.


 

누구든지 하는 리액트: 9편 불변성을 지키는 이유와 업데이트 최적화 | VELOPERT.LOG

이 튜토리얼은 10편으로 이뤄진 시리즈입니다. 이전 / 다음 편을 확인하시려면 목차를 확인하세요. 우리는 지난 섹션에서 배열을 어떻게 다뤄야 하는지에 대해서 알아보았습니다. 데이터를 업데이트하는 과정에서 불변성을 지켜야한다는것을 강조했었는데요, 왜 그렇게 해야하는지 알아보겠습니다. 데이터 필터링 구현하기 우선, 불변성의 중요성을 알아보는 과정에서 이름으로 전화번호를 찾는 데이터 필터링 기능을 구현해보겠습니다. 먼저 App 컴포너트에서 input 하나를 렌

velopert.com

 

댓글