React.js Immutability Helper – State 내부 Array 에 원소 삽입/제거/수정
컴포넌트에서 사용할 데이터 배열을 매핑하여 효율적으로 렌더링하는 방법에 이어 이번엔 데이터 배열에 변화를 주는 방법을 알아보도록 하겠다. 이 과정은 생각보다 쉽지만은 않다.
1. state 안의 array 에 원소 삽입/제거/수정
this.state 에 포함된 배열에 원소를 삽입/제거/수정 을 할 때 그 배열에 직접 접근하면 안된다. 예를들어, 원소를 추가 할 때 배열객체의 push() 메소드를 사용하면 원하는대로 되지 않는다. this.state가 변경된다고해서 컴포넌트가 업데이트되지 않기 때문이다. 물론 변경 후 React 컴포넌트 API 인 forceUpdate()를 통하여 컴포넌트가 render()를 다시 실행 하게 끔 하는 방법이 있긴하지만 이건 절대 권장되지 않는 방법이다. React 메뉴얼 에서도 this.state를 직접 수정하지 말고 this.setState()를 사용하여 수정 할 것을 강조하고 있다. (이 메소드가 실행되면 자동으로 re-rendering 이 진행된다.)
concat 을 사용함으로서 현재의 list 배열에 newObj 원소를 추가한 새로운 배열을 생성 한 후, 그 값을 현재의 list 로 설정한다.
배열을 수정 할 땐 원시적인 방법으론 위와 같이 배열 전체를 복사하고 처리 후 기존 값에 덮어씌우는 과정을 거쳐야 한다. 허나, 만약에 배열의 크기가 클 땐 성능이 좀 저하될 것이다.
다른 방법으로는 Immutability Helpers 를 사용하는 방법이 있다. 이는 배열을 더 효율적으로 수정 할 수 있게 해주는 페이스북의 Immutable-js 를 사용한다.
이를 사용하려면 라이브러리를 사전 설치해주어야 한다.
React 구버전에서는 해당 라이브러리가 내장되어 import React from 'react/addons'; 으로 React를 import 하여 React.addons.update() 를 사용 할 수 있었으나, 이제 이 방법은 deprecated 되었다. 아직도 이렇게 사용은 가능 하나, 브라우저 상에서 ‘react-addon-update’ 를 import 하라고 권장하는 오류 메시지가 발생한다.
라이브러리 설치 방법
$ npm install --save react-addons-update 를 통하여 라이브러리를 저장 후, js 파일 상단에 import update from 'react-addons-update' 를 삽입해 준다.
update() 메소드의 첫 파라미터는 처리 할 배열이며 두번째는 처리 할 명령들을 지니고 있는 객체 타입 이다. $push: [newObj, newObj2]는 list 배열에 newObj 와 newObj2 를 추가해 준다. 한 객체를 추가 할 때도 [] 안에 배열형태로 감싸 줘야 한다. Immutable-js 의 syntax 는 MongoDB 쿼리 언어에서 영감을 받았다고 한다. 브라우저상에서 react-with-addons를 불러와서 사용하는 경우에는 update 가 아닌 React.addons.update 를 사용 해야 한다. (jsfiddle이 이에 해당한다.)
1.2 원소 제거하기
원소를 제거 할 때 역시, state 의 배열에 직접 접근하면 안 되고, 배열을 복사한 후 원소 제거 후 기존 값에 덮어씌워져야 한다. JavaScript Array의 내장 함수인 splice()를 사용하면 되지만, 이는 생략하고 더 효율적인 Immutability Helper를 사용하는 예제를 알아 보겠디.
초기 state 값을 지정하고, 렌더링 부분 코드에서 input 의 value를 state를 사용하도록 수정한 후, 인풋박스에 텍스트를 적으려고 시도해보면 값이 고정되서 변경되지 않는다. 이 부분을 해결하기 위하여, onChange 이벤트를 통하여 인풋박스에 텍스트를 입력 시 status 를 업데이트하도록 설정해야 한다.
인풋박스의 값을 변경 할 때 실행 될 handleChange(e) 메소드를 만들었다. 여기서 파라미터 e 는 JavaScript 의 Event 인터페이스이다. e 를 사용함으로서 한 메소드로 여러 인풋박스를 인풋박스의 name 에 따라 처리 할 수 있게된다. 렌더링 부분의 코드를 보기 좋게 하기위해 줄바꿈을 하였으며 onChange={this.handleChange.bind(this)}를 넣어주었다. 인풋박스가 변경 될 때 해당 메소드를 실행한다는 의미 이다. bind 를 통하여 컴포넌트의 this 에 접근 할 수 있게 된다.
Immutability Helpers 를 사용하여 배열에 원소를 추가하였으며, _insertContact(name, phone) 메소드를 ContactCreator 의 prop 으로 전달 해 주었다. 참고: jsfiddle 에선 React.addons.update 를 사용해아한다.
2.2 선택 기능 구현하기
배열에서 데이터를 수정 하거나 제거 할 때 필요할 마우스로 선택하는 기능을 구현해보자.
a. ContactInfo: handleClick() 메소드 및 onClick prop 추가
해당 컴포넌트가 클릭되면 handleClick() 메소드가 실행되며, 이 메소드 내부에선 parent 컴포넌트에서 prop 으로 전달받은 onSelect() 메소드를 실행한다. 여기서 인수 contactKey 는 해당 컴포넌트의 고유 번호이다. 컴포넌트를 매핑할 때 key 를 사용하긴 하였으나, 이는 prop으로 간주되지 않으며 React 내부에서 사용하는 용도이기에 직접 접근이 불가하다.
state selectedKey 는 현재 선택된 컴포넌트의 고유번호 이다. 만약에 선택된 Contact 가 없을 시에는 -1 로 설정된다. _onSelect() 메소드는 컴포넌트가 클릭 될 때 실행 할 메소드 입니다. 선택 할 컴포넌트가 이미 선택되어있다면 선택을 해제한다. 이 메소드는 child 컴포넌트의 onSelect prop 으로 전달된다. _isSelect(key) 메소드는 child 컴포넌트에게 해당 컴포넌트가 선택된 상태인지 아닌지 알려준다. 이 메소드를 실행 한 결과 값이 child 컴포넌트의 isSelected prop 으로 전달 된다.
5번 줄에서는 getStyle 이라는 함수를 선언했다. arrow function 이 사용되었는데, 매개변수가 오직 하나라면 괄호가 생략 될 수 있다. 이 함수는 매개변수가 참이면 배경색이 아쿠아색인 스타일을 반환하며 거짓이면 비어있는 스타일을 반환한다. 전에 JSX 에서 언급했었던 inline styling이 사용되었다.
선택한 Contact 를 제거하는 메소드 입니다. 선택된 Contact 가 없다면 작업을 취소한다. this.setState(...) 가 실행 되면 state contactData 에서 selectedKey번째 데이터를 제거하고 아무것도 선택하지 않은 상태로 설정한다. 참고: jsfiddle을 사용한다면 React.addons.update 를 사용해야한다.
d. Contact: ContactRemover 컴포넌트에 삭제 메소드 prop onRemove 으로 전달
_editContact() 메소드는 오류가 나지 않도록 초기 작성만하고 구현은 나중에 하도록 하겠습니다. ContactsRemover 컴포넌트 하단에 <ContactEditor... />를 작성하세요. prop isSelected 은 JavaScript 표현식을 사용하여 selectedKey가 -1이 아니라면 true를, 맞다면 false를 반환합니다.
d. 선택된 내용을 인풋박스로 복사하는 기능 구현하기
Contact 를 선택 하였을 때 내용을 ContactEditor 의 input 으로 복사되는 기능을 구현해보겠다. 일단은 선택된 Contact 의 정보를 ContactEditor 로 전달을 해줘야할 것 이다. 우선 선택된 Contact의 정보를 Contacts 의 state selected 에 저장하도록 하자.
새로운 state 를 사용 할 땐, 언제나 초기 값을 설정해줘야한다. (그렇지 않으면 오류가 발생하기 쉽상이다.) Contact를 선택하였을 때 prop selected 에 값을 저장 하게하고, 선택을 취소 하였을 때, 값을 공백으로 설정하도록 하였다. 그리고 이 prop selected 값을 ContactEditor 에 prop contact로 전달해준다.
이제 ContactEditor 에서 선택된 Contact의 값을 받아와서 렌더링해줘할 것이다. 하지만, 인풋박스의 value 부분은 유동적이기에 그 부분에 {this.props.contact.name} 을 할 수는 없다. prop값이 바뀔 때마다 state를 업데이트 해줄 필요가 있는데요, 이는 Component Lifecycle API 중 하나인 componentWillReceiveProps()를 사용하면 된다. 이 컴포넌트 내장메소드는, prop 값을 받게 될 때 실행되는 메소드 이다.
원하고자 하는 기능은 모두 구현하였지만, 사실 위에서 작성한 코드는 CPU 자원을 낭비하고있다. 비록 큰데이터를 다루는게 아니기 때문에 성능에 큰 영향을 끼치지는 않고 있지만 코드의 완성도를 위하여 컴포넌트를 최적화해보겠다.
3.1 무엇이 문제인가..
데이터가 수정 될 때 마다, 상태에 변동이 없는, 즉 리렌더링 할 필요가 없는 컴포넌트들도 리렌더링이 되고있다. 한번 ContactInfo 컴포넌트의 render() 메소드에 코드 console.log('rendered: ' + this.props.name); 를 추가해보자.
자, 그럼 이제 해결해보도록 하자. 해결법은 매우 간단하다. Component Lifecycle API 중 하나인 shouldComponentUpdate() 메소드를 컴포넌트 클래스 안에 작성해주면 된다. 이 메소드는 컴포넌트를 다시 렌더링 해야 할 지 말지 정의를 해준다.