React Basic - (7) 데이터 필터링

React Basic - (7) 데이터 필터링

데이터 필터링 구현하기

  • App.js에 input을 할당하고, input값에 따라 데이터를 필터링하는 예제

App.js

import React, {Component} from 'react';
import PhoneForm from './PhoneForm';
import PhoneInfoList from './PhoneInfoList';

class App extends Component {
  id = 2
  state = {
    // state에는 렌더링되는 값만 넣어줌
    information : [
      {
        id : 0,
        name : 'paige',
        phone : '010-0000-0000'
      },
      {
        id : 1,
        name : 'tiger',
        phone : '010-1111-1111'
      }
    ],
    keyword : ''
  }
  handleCreate = (data) => {
    const {information} = this.state;
    this.setState({
      information : information.concat({
        id:this.id++, ...data
      })
    })
    console.log(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
      )
    })
  }
  // !! 이 부분이 추가됨
  handleChange = (e) => {
    this.setState({
      keyword: e.target.value,
    });
  }
  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 배열을 할당함
          data={information}
          onRemove={this.handleRemove}
          onUpdate={this.handleUpdate}/>
      </div>
    );
  }
}

export default App;

onChange 이벤트로 인한 자식 컴포넌트의 불필요한 rendering 이슈

  • App 컴포넌트에서 input의 onChange 파라미터에 handleChange(e) 함수를 할당하였다.
  • handleChange() 함수는 이벤트가 변경될 때마다 호출되는 함수이기 때문에, input 이벤트가 변경되어 App 컴포넌트의 상태가 업데이트 되면, 컴포넌트의 리렌더링이 발생하게 되고 자식 컴포넌트들도 리렌더링 된다.
  • 하지만 필터링될 data가 변경되지 않는다면 자식 컴포넌트들이 리렌더링될 필요가 없기 때문에, 일반적인 방법으로 구현했을 때 렌더링 횟수가 늘어나므로 자원을 아껴야 할 필요가 있다.
  • 따라서 LifeCycle API 중, shouldComponentUpdate() 함수를 선언하여 현재 props.data와 nextProps.data가 다를 때만 true로 설정하면, 변화가 필요하지 않을 때에는 render()함수가 호출되지 않는다.

    ⇒ shouldComponentUpdate()를 이용하면, 변화가 필요하지 않을 때는 render 함수가 호출되지 않는다.

데이터의 불변성을 유지해야 하는 이유?

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

console.log(array !== sameArray); // false
  • sameArray = array를 한 경우, 레퍼런스가 복사되기 때문에 새로운 배열의 값을 변경하면 원래 배열값도 변경된다. ⇒ 결국 동일한 배열이므로 비교할 수 없음
const array = [1,2,3,4];
const differentArray = [...array, 5];
  // 혹은 = array.concat(5)
console.log(array !== differentArray); // true
  • 원래 배열의 불변성을 유지한 채 배열을 복사하면, 새로운 배열을 변경했을 때 원래 배열과 비교가 가능하다.

새 데이터를 등록할 때, PhoneInfo 컴포넌트의 모든 info의 불필요한 리렌더링 이슈

  • App.js의 PhoneForm 컴포넌트에서 새로운 데이터가 추가되면, App 컴포넌트와 자식 컴포넌트들도 리렌더링된다.
  • 하지만 PhoneInfo 컴포넌트가 리렌더링되면, 포함된 모든 info 데이터가 리렌더링되므로 자원의 낭비가 발생한다.
  • 새로 추가된 info 데이터만 리렌더링되면 되므로, PhoneInfo에서 shouldComponentUpdate()를 작성하여 최적화한다.

전체 Code

App.js

import React, {Component} from 'react';
import PhoneForm from './PhoneForm';
import PhoneInfoList from './PhoneInfoList';

class App extends Component {
  id = 2
  state = {
    // state에는 렌더링되는 값만 넣어줌
    information : [
      {
        id : 0,
        name : 'paige',
        phone : '010-0000-0000'
      },
      {
        id : 1,
        name : 'tiger',
        phone : '010-1111-1111'
      }
    ],
    keyword : ''
  }
  handleCreate = (data) => {
    const {information} = this.state;
    this.setState({
      information : information.concat({
        id:this.id++, ...data
      })
    })
    console.log(data);
  }
  handleRemove = (id) => {
    const {information} = this.state;
    this.setState({
      // information 배열에서 제거할 id와 같은 id만 제거하고 필터링
      information : information.filter(
        info=> info.id !== id)
    })
  }
  handleUpdate = (id, data) => {
    // id : 변경하고자 하는 data의 id
    // data : 변경하고자 하는 새로운 값
    const {information} = this.state;
    this.setState({
      // information 배열에서 수정할 id와 같은 id인 경우, 새로운 info 객체를 만들어서 기존의 값과 새로운 data 값을 할당,
      // 다른 경우 기존 값을 그대로 유지
      information : information.map(info => 
        id === info.id?
          {...info, ...data}
          : info
      )
    })
  }
  handleChange = (e) => {
    this.setState({
      keyword: e.target.value,
    });
  }
  render() {
    const {information, keyword} = this.state;
    // information 배열의 특정 info에서, keyword 문자열이 포함된 index가 있을 경우 남기고 없을 경우는 모두 제외한 새로운 filteredList 배열 생성
    const filteredList = information.filter(
      info => info.name.indexOf(keyword) !== -1
    );
    return (
      <div>
        {/* PhoneForm : input 관리 */}
        <PhoneForm
          onCreate={this.handleCreate}/>
        <p>
          <input
            placeholder="검색할 이름 입력"
            onChange={this.handleChange}
            value={keyword}
          />
        </p>
        <hr />
        <PhoneInfoList
          // data에 filteredList 배열을 할당함
          data={filteredList}
          onRemove={this.handleRemove}
          onUpdate={this.handleUpdate}/>
      </div>
    );
  }
}

export default App;

PhoneForm.js

import React, {Component} from 'react';

class PhoneForm extends Component {
    state = {
        name : '', 
        phone : ''
    }
    handleChange = (e) => {
        this.setState({
            [e.target.name] : e.target.value
            // e.target.value : 이벤트 객체에 담겨있는 현재 text 값.
        });
    }
    handleSubmit = (e) => {
        e.preventDefault();
        // 원래 이벤트가 해야 하는 작업을 방지시킴
        this.props.onCreate(this.state);
        this.setState({
            name:'',
            phone:''
        })
    }
    render() {
        return (
            <form onSubmit={this.handleSubmit}>
                <input
                placeholder="name"
                value={this.state.name}
                // onChange : input의 text값이 바뀔 때마다 발생하는 이벤트
                onChange={this.handleChange}
                name ="name"
                />
                <input
                placeholder="phone"
                value={this.state.phone}
                onChange={this.handleChange}
                name="phone"
                />
                <button type="submit">submit</button>
                {/* <div>{this.state.name} {this.state.phone}</div> */}
            </form>
        );
    }
}
export default PhoneForm;

PhoneInfoList.js

import React, {Component} from 'react';
import PhoneInfo from './PhoneInfo';

class PhoneInfoList extends Component {
    static defaultProps = {
        list : [],
        // onRemove 함수에 아무것도 할당되지 않았으면 경고 문구 출력
        onRemove : () => console.warn('onRemove not defined'),
        onUpdate : () => console.warn('onUpdate not defined'),
    }
    // App 컴포넌트에서 input의 onChange 파라미터에 handleChange(e) 함수를 할당하였다. 
    // handleChange() 함수는 이벤트가 변경될 때마다 호출되는 함수이기 때문에, input 이벤트가 변경되어 App 컴포넌트의 상태가 업데이트 되면, 컴포넌트의 리렌더링이 발생하게 되고 자식 컴포넌트들도 리렌더링 된다. 
    // 하지만 필터링될 data가 변경되지 않는다면 자식 컴포넌트들이 리렌더링될 필요가 없기 때문에, 일반적인 방법으로 구현했을 때 렌더링 횟수가 늘어나므로 자원을 아껴야 할 필요가 있다. 
    // 따라서 LifeCycle API 중, shouldComponentUpdate() 함수를 선언하여 현재 props.data와 nextProps.data가 다를 때만 true로 설정하면, 변화가 필요하지 않을 때에는 render()함수가 호출되지 않는다. 

    shouldComponentUpdate(nextProps, nextState) {
        return nextProps.data !== this.props.data;
    }
    render() {
        console.log('render PhoneInfoList');
        // 부모 컴포넌트 (PhoneInfo)에서 data를 받아옴
        const {data, onRemove, onUpdate} = this.props;
        // 받아온 data를 map을 통하여 컴포넌트로 반환해 list에 저장
        const list = data.map(
            info => (
            <PhoneInfo 
            key={info.id} 
            info={info}
            onRemove={onRemove}
            onUpdate={onUpdate}
            />)
        );
        return (
            <div>
                {list}
            </div>
        );
    }
}
export default PhoneInfoList;

PhoneInfo.js

import React, {Component} from 'react';

class PhoneInfo extends Component {
    // defaultProps을 통하여 info의 기본값 설정 => 컴포넌트가 undefined 되는 것 방지
    static defaultProps = {
        info: {
            name: '이름',
            phone: '010-0000-0000',
            id: 0
        }
    }
    state = {
        editing: false,
        name:'',
        phone:'',
    }
    handleRemove = () => {
        const {info, onRemove} = this.props;
        onRemove(info.id);
    }
    handleToggleEdit = () => {
        const {editing} = this.state;
        // editing 값을 반전시킴
        this.setState({editing:!editing});
    }
    handleChange = (e) => {
        // input에서 onChange 이벤트가 발생될 때 호출됨
        const {name, value} = e.target;
        this.setState({
            [name] : value
        });
    }
    // 업데이트 과정 :     각 data에 수정 버튼을 할당하여, edit 모드가 아닌 경우 클릭 시 state의 editing을 true로
    // 변경     handleToggleEdit() : editing 값을 반전시키는 함수     handleChange() : edit
    // 모드에서 input의 onChange 이벤트가 발생될 때 호출되는 함수

    componentDidUpdate(prevProps, prevState) {
        const {info, onUpdate} = this.props;
        if(!prevState.editing && this.state.editing) {
            // 일반 모드 -> 수정 모드로 변환될 때
            // 선택한 data의 info를 현재 state로 설정해야 함
            this.setState({
                name : info.name,
                phone: info.phone
            });
        }
        if(prevState.editing && !this.state.editing) {
            // 수정 모드 -> 일반 모드로 변환될 때 
            // 현재 state를 info에 update 해야 함
            onUpdate(info.id, {
                name : this.state.name,
                phone : this.state.phone
            });
            // onUpdate() 함수를 호출하여 선택된 id의 현재 상태를 update
        }
    }

    shouldComponentUpdate(nextProps, nextState) {
        if(!this.state.editing && !nextState.editing && nextProps.info === this.props.info) {
        // 현재 수정 상태가 아니고, 다음 상태도 수정 상태가 아니며, 현재 info와 다음 info값이 같다면 리렌더링 안함
            return false;
        }
        return true;
    }

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

        const style = {
            border: '1px solid black',
            padding: '8px',
            margin: '8px'
        };
        const {editing} = this.state;
        if (editing) {
            // 수정 모드
            return (
                <div style={style}>
                    <div>
                        <input              placeholder="이름"
                            value={this.state.name} 
                            onChange={this.handleChange}
                            name="name"/>
                    </div>
                    <div>
                        <input
                            placeholder="전화번호"
                            value={this.state.phone}
                            onChange={this.handleChange}
                            name="phone"/>
                    </div>
                        <button onClick={this.handleToggleEdit}>적용</button>
                        <button onClick={this.handleRemove}>삭제</button>
                </div>
            );
        }
        // 일반 모드 
        else {
            const {name, phone} = this.props.info;
            return (
                <div style={style}>
                    <div>
                        <div>
                            <b>{name}</b>
                        </div>
                        <div>{phone}</div>
                        <button onClick={this.handleToggleEdit}>수정</button>
                        <button onClick={this.handleRemove}>삭제</button>
                    </div>
                </div>
            );
        }
    }
}
export default PhoneInfo;

실행 결과

react_20200320

React 개발의 목표 및 정리

  1. 재사용 가능한 컴포넌트를 만든다.
  2. props 는 부모에게서 전달받는 값이다.
  3. state 는 자기 자신이 지니고 있는 데이터이다.
  4. props 나 state 가 바뀌면 컴포넌트는 리렌더링 한다.
  5. LifeCycle API 를 통해서 컴포넌트 마운트, 업데이트, 언마운트 전후로 처리 할 로직을 설정하거나 리렌더링을 막아줄 수 있다.