티스토리 뷰
데이터 검색(필터링)
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 = [1, 2, 3, 4]; 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
'프로그래밍 > React' 카테고리의 다른 글
[예제]간단한 가계부3 - 배열관련 처리2 (0) | 2019.02.10 |
---|---|
[예제]간단한 가계부2 - 배열관련 처리1 (0) | 2019.02.08 |
[예제]간단한 가계부1 - input 상태 관리 (0) | 2019.02.06 |
[문법]Life Cycle API (0) | 2019.02.04 |
[문법]props와 state (0) | 2019.01.31 |
- Total
- Today
- Yesterday
- #예제 #example #가계부 #Account Book
- 목표
- flaMap
- flatMapToLong
- 토이 프로젝트
- 개발자
- java
- IntelliJ
- mapToObj
- 스트림
- 자바
- java14
- 다짐
- import문
- 계획
- 충북 콕! 콕!
- Stream API
- 중간 연산
- modern java
- flatMapToDouble
- 람다
- #배열 #array #map 함수
- jdk14
- 회고
- #React #ReactJS #리액트
- flatMapToInt
- lambda
- Java8
- 익명 클래스
- 변경사항
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |