티스토리 뷰

데이터 검색(필터링)


 App.js에 input 태그를 하나 붙이고 이 input 태그의 값을 키워드로 검색하는 부분을 만들어보겠습니다.


 App.js를 다음과 같이 수정해주세요.


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
// App.js
import React, { Component } from "react";
import "./App.css";
 
import AccountBookForm from "./components/AccountBookForm";
import AccountBookInfoList from "./components/AccountBookInfoList";
 
class App extends Component {
  // field
  currentId = 1;
 
  state = {
    list: [
      {
        id: 0,
        type: "지출",
        price: 3800,
        usage: "점심 식비",
        date: "2019. 1. 16 오후 1:12:33"
      },
      {
        id: 1,
        type: "수입",
        price: 20000,
        usage: "중고책 판매",
        date: "2019. 1. 18 오전 10:17:21"
      }
    ],
    keyword: ""
  };
 
  change = event => {
    this.setState({
      keyword: event.target.value
    });
  };
 
  add = data => {
    const { list } = this.state;
    this.setState({
      list: list.concat({ id: ++this.currentId, ...data })
    });
  };
 
  remove = id => {
    const { list } = this.state;
    this.setState({
      list: list.filter(info => info.id !== id)
    });
  };
 
  update = (id, data) => {
    const { list } = this.state;
    this.setState({
      list: list.map(
        info =>
          id === info.id // 현재 수정하는 id를 찾음
            ? { ...info, ...data } // 새로운 내용(data)으로 덮어씀
            : info // 기존값 유지
      )
    });
  };
 
  render() {
    const { list, keyword } = this.state;
    const filteredList = list.filter(
      info => info.usage.indexOf(keyword) !== -1
    );
 
    return (
      <React.Fragment>
        <AccountBookForm onAdd={this.add} />
        <p>
          <input
            placeholder="검색어를 입력하세요."
            onChange={this.change}
            value={keyword}
          />
        </p>
        <hr />
        <AccountBookInfoList
          list={filteredList}
          onRemove={this.remove}
          onUpdate={this.update}
        />
      </React.Fragment>
    );
  }
}
 
export default App;
cs


 state에 keyword 변수를 추가하고 render 함수 내에 input 태그를 하나 추가하였습니다. 이 input 태그의 값이 변할 때마다 change라는 함수를 통해 keyword를 갱신해줍니다. 이를 위해 기존에 AccountBookInfoList로 보내는 props에 list 전체를 넣었지만 기존 list에서 filter 함수를 사용해서 필터링한 filteredList를 전달합니다. 이렇게만 만들어도 되긴하지만 하나 고려해야할 사항이 있습니다. App의 state가 변경될 때마다 리렌더링이 발생합니다. 이 때, 자식 component들까지 모두 리렌더링되는 문제가 생깁니다. 이 부분은 나중에 성능 이슈가 될 수 있습니다. 이 이슈를 처리하도록 하겠습니다.


 components/AccountBookInfoList.js 파일을 다음과 같이 수정해 주세요.


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
// components/AccountBookInfoList.js
import React, { Component } from "react";
import AccountBookInfo from "./AccountBookInfo";
 
class AccountBookInfoList extends Component {
  static defaultProps = {
    list: [],
    onRemove: () => console.warn("onRemove is not defined."),
    onUpdate: () => console.warn("onUpdate is not defined.")
  };
 
  shouldComponentUpdate(nextProps, nextState) {
    return nextProps.list !== this.props.list;
  }
 
  render() {
    const { list, onRemove, onUpdate } = this.props;
    const infoList = list.map(info => (
      <AccountBookInfo
        key={info.key}
        data={info}
        onRemove={onRemove}
        onUpdate={onUpdate}
      />
    ));
 
    return <React.Fragment>{infoList}</React.Fragment>;
  }
}
 
export default AccountBookInfoList;
cs


 shouldComponentUpdate API를 통해 다음과 같이 처리할 수 있습니다. 현재 props.list와 다음 props.list가 다를 때만 리렌더링합니다. 리렌더링이 발생하는지 확인하고 싶으면 render 함수 내부에 console.log를 통해 개발자 도구에서 확인할 수 있습니다.(생략) 결과는?



추가적인 최적화


 AccountBookForm에서 새로운 내용을 추가하게 되면 App의 list에 새로운 내용이 추가됩니다. 이 때, list의 값이 변했기 때문에 AccountBookInfoList가 리렌더링됩니다. 하지만 여기서 문제는 기존의 내용(UI)들은 유지된채 새로 추가된 내용만 새로 그려주면 되는데 console.log를 찍어보면 알겠지만 전체 내용을 새로 그리고 있습니다. 이 부분에 관련된 최적화까지 진행해 보겠습니다.


 components/AccountBookInfo.js를 다음과 같이 수정해 주세요.


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
// components/AccountBookInfo.js
import React, { Component } from "react";
 
// 현재시간을 특정 format의 문자열로 반환
const getCurrentTimetoString = () => {
  return new Date().toLocaleString();
};
 
// 천 단위 구분기호를 포함한 문자열을 반환(정규식 이용)
const toCommaString = num => {
  return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
 
class AccountBookInfo extends Component {
  static defaultProps = {
    data: {
      id: 0,
      type: "분류",
      price: 0,
      usage: "-",
      date: "-"
    },
    onUpdate: () => console.warn("onUpdate is not defined.")
  };
 
  // 수정을 할 때, 기존의 내용이 변하므로 state를 정의
  state = {
    editing: false,
    type: "",
    price: "",
    usage: "",
    date: ""
  };
 
  remove = () => {
    const { data, onRemove } = this.props;
    onRemove(data.id);
  };
 
  // 수정/적용 버튼의 토글 기능
  toggleEdit = () => {
    const { editing } = this.state;
    this.setState({
      editing: !editing
    });
  };
 
  // select와 input 태그의 값이 변할 때 이벤트 처리
  changeInput = event => {
    const { name, value } = event.target;
    this.setState({
      [name]: value,
      date: getCurrentTimetoString()
    });
  };
 
  componentDidUpdate(prevProps, prevState) {
    const { data, onUpdate } = this.props;
 
    // 수정 버튼을 클릭한 경우(input 태그가 표시되게 해준다)
    if (!prevState.editing && this.state.editing) {
      this.setState({
        type: data.type,
        price: data.price,
        usage: data.usage,
        date: data.date
      });
    }
 
    // 적용 버튼을 클릭한 경우(App.js에 있는 update 함수를 호출)
    if (prevState.editing && !this.state.editing) {
      onUpdate(data.id, {
        type: this.state.type,
        price: this.state.price,
        usage: this.state.usage,
        date: this.state.date
      });
    }
  }
 
  shouldComponentUpdate(nextProps, nextState) {
    // 현재 수정 중인 상태가 아니고 다음 state 역시 수정 중이지 않고 다음 props.data와 현재 props.data가 같다면
    // 리렌더링 방지
    if (
      !this.state.editing &&
      !nextState.editing &&
      nextProps.data === this.props.data
    ) {
      return false;
    }
 
    return true;
  }
 
  render() {
    const style = {
      border: "1px solid black",
      padding: "5px",
      margin: "5px"
    };
 
    const { editing } = this.state;
 
    // 수정
    if (editing) {
      return (
        <div style={style}>
          <select
            value={this.state.type}
            name="type"
            onChange={this.changeInput}
          >
            <option>지출</option>
            <option>수입</option>
          </select>
          <input
            placeholder="금액"
            type="number"
            name="price"
            value={this.state.price}
            onChange={this.changeInput}
          />
          <input
            placeholder="사용목적"
            name="usage"
            value={this.state.usage}
            onChange={this.changeInput}
          />
          <button onClick={this.toggleEdit}>적용</button>
          <button onClick={this.remove}>삭제</button>
        </div>
      );
    }
 
    // 일반
    const { type, price, usage, date } = this.props.data;
 
    return (
      <div style={style}>
        <div>{type}</div>
        <div>{toCommaString(price)}원</div>
        <div>{usage}</div>
        <div>{date}</div>
        <button onClick={this.toggleEdit}>수정</button>
        <button onClick={this.remove}>삭제</button>
      </div>
    );
  }
}
 
export default AccountBookInfo;
cs


 이 곳도 마찬가지로 shouldComponentUpdate API를 통해 다음과 같이 최적화를 했습니다.


간편한 최적화가 가능한 이유


 우리는 여기서 shouldComponentUpdate에서 기존의 props나 state와 변할 props나 state 값들을 단순히 === 연산자를 통해 비교해서 최적화를 시켰습니다. 이런 간단한 최적화가 가능한 이유는 그동안 우리가 state의 객체나 배열을 변경할 때 불변성을 지켜주었기 때문입니다. 만약 우리가 해왔던 방식이 아닌 직접 배열이나 객체를 변경하는 방법으로 했다면 간단한 최적화가 불가능합니다.


1
2
3
4
5
6
const arr1 = [1234];
const arr2 = arr1;
const arr3 = [...arr1];
 
console.log(arr1 !== arr2);    // false
console.log(arr1 !== arr3);    // true
cs


 위의 코드를 보면 arr1 배열을 각각 arr2와 arr3에 넣었습니다. 이 때, arr2는 단순히 대입 연산을 사용했고 arr3은 기존에 우리가 사용하던 불변성을 지켜주는 전개 연산자를 사욯했습니다. 두 배열과 기존의 arr1과의 차이를 비교해 보겠습니다. 대입 연산을 사용한 arr2는 arr1과 다르지 않습니다. 대입 연산을 사용할 경우, 배열의 값이 복사되는 것이 아니라 기존 배열과 똑같은 레퍼런스가 만들어지기 때문에 기존 배열과 단순 비교연산으로는 차이를 비교할 수가 없습니다. 반면 전개 연산자의 경우, 기존 배열의 레퍼런스뿐만 아니라 배열의 값까지 복사가 되기 때문에 비교연산만으로도 차이를 비교할 수 있습니다.


 간단한 가계부는 완성되었습니다. 코드는 모두 https://github.com/YoungWukJeon/AccountBookExampleWithReact에서 확인할 수 있습니다.


References


[Velopert 블로그 - 불변성을 지키는 이유와 업데이트 최적화] https://velopert.com/3640

댓글