이번에는 리액트 프로젝트에서 배열을 다루는 방법에 대해 알아보자. 리액트에서는 배열을 다룰 때 평상시에 하던 것처럼 사용하면 안된다. 데이터 추가의 경우 자바스크립트처럼 this.state.array.push('value'); 식으로 하면 되겠지? 하고 생각할 수도 있는데, 리액트에서는 state 내부의 값을 직접적으로 수정하면 절대로 안된다. 이것을 불변성 유지라고 한다. push, splice, unshift, pop과 같은 내장함수는 배열 자체를 직접 수정하게 되므로 적합하지 않다. 대신, 기존의 배열에 기반하여 새 배열을 만들어내는 concat, slice, map, filter과 같은 함수를 사용해야 한다.
리액트에서 불변성 유지가 중요한 이유는, 불변성을 이용해여 리액트에서 모든 것들이 필요한 상황에 리렌더링 되도록 설계할 수 있고, 나중에 성능도 최적화 할 수 있기 때문이다.
데이터 추가
지금은 리액트 기초에 집중하고자 애플리케이션의 상태 데이터는 App 컴포넌트에서 관리하겠다.
App 컴포넌트 state에 information 이라는 배열을 만들고, 그 안에 배열의 기본값들인 샘플 데이터 두개를 추가하겠다. 각 전화번호는 다음과 같은 형식으로 담는다.
{
id: 0,
name: '이름',
phone: '010-0000-0000'
}
여기서 id 값은 각 데이터를 식별하기 위함(수정, 삭제 등)이다. 그리고 이 id값은 데이터를 추가할 때 마다 증가해야 하기 때문에 숫자를 1씩 더해주겠다.
//src/App.js
import React, { Component } from 'react';
import PhoneForm from './components/PhoneForm';
class App extends Component {
id = 2
state = {
information: [
{
id: 0,
name: '김민준',
phone: '010-0000-0000'
},
{
id: 1,
name: '홍길동',
phone: '010-0000-0001'
}
]
}
handleCreate = (data) => {
const { information } = this.state;
this.setState({
information: information.concat({ id: this.id++, ...data })
})
}
render() {
const { information } = this.state;
return (
<div>
<PhoneForm
onCreate={this.handleCreate}
/>
{JSON.stringify(information)}
</div>
);
}
}
export default App;
id 값의 경우에는, 컴포넌트의 일반 클래스 내부 변수로서 선언해주었다. 컴포넌트 내부에서 필요한 값 중에서, 렌더링되는 것과 상관 없는 것들은 굳이 state에 넣어줄 필요가 없다. (그래서 state 상단에 id 값을 따로 빼준 것 !) render 함수에서는 information 값을 문자열로 변환하여 보여주었다. 후에, 이 데이터를 컴포넌트 형태로 렌더링해보겠다.
코드를 저장하고 새로운 데이터를 입력하면
위와 같은 형태로 저장되는 것을 알 수 있다.
데이터 렌더링
이제부터는 위 배열을 컴포넌트로 변환해서 바꾸어주겠다. 초반부에서도 언급했지만, 리액트를 다루는 것은 자바스크립트를 사용하는 것과 매우 비슷하다. 컴포넌트를 여러개 렌더링하기 위해서는 자바스크립트 배열의 내장함수인 map을 사용하면 된다.
map 함수 알아보기
예를 들어 다음과 같은 배열이 있다고 가정해보자.
const a = [1,2,3,4,5];
이 배열을 가지고 내부 원소들에 2를 곱하고 싶다면 어떻게 해야할까?
const a = [1,2,3,4,5];
const b = [];
a.forEach(number => b.push(number * 2));
위와 같이 forEach를 이용해서 구현할 수도 있지만, map을 사용하면 조금 더 쉽게 해결할 수 있다.
const a = [1,2,3,4,5];
const b = a.map(number => number * 2);
map 함수에 대해 더 자세히 알고싶다면 MDN의 Array.prototype.map()을 읽어보자.
컴포넌트 만들기
두 개의 컴포넌트를 만들겠다.
- PhoneInfo : 각 전화번호의 정보를 보여주는 컴포넌트
- PhoneInfoList : 여러개의 PhoneInfo 컴포넌트를 보여주는 컴포넌트
//src/components/PhoneInfo.js
import React, { Component } from 'react';
class PhoneInfo extends Component {
static defaultProps = {
info: {
name: '이름',
phone: '010-0000-0000',
id: 0
}
}
render() {
const style = {
border: '1px solid black',
padding: '8px',
margin: '8px'
};
const {
name, phone, id
} = this.props.info;
return (
<div style={style}>
<div><b>{name}</b></div>
<div>{phone}</div>
</div>
);
}
}
export default PhoneInfo;
info라는 객체를 props로 받아와서 렌더링해줄 것이다. 그런데 실수로 info 값을 전달해주는 것을 잊어버리게 된다면 컴포넌트가 크래시될 것이다. info가 undefined 일 때는 비구조화 할당을 통해 내부의 값을 받아올 수 없기 때문이다. 따라서 defaultProps를 통하여 info의 기본값을 설정해주었고, 크래시를 예방할 수 있다.
그 다음에는 PhoneInfoList 컴포넌트를 만들어보자.
//src/components/PhoneInfoList.js
import React, { Component } from 'react';
import PhoneInfo from './PhoneInfo';
class PhoneInfoList extends Component {
static defaultProps = {
data: []
}
render() {
const { data } = this.props;
const list = data.map(
info => (<PhoneInfo key={info.id} info={info}/>)
);
return (
<div>
{list}
</div>
);
}
}
export default PhoneInfoList;
이 컴포넌트에서는 data라는 배열을 가져와 map을 통하여 JSX로 변환해준다. 이 과정에서 key라는 값도 설정되었는데, 여기서 key는 리액트에서 배열을 렌더링할 때 꼭 필요한 값이다. 리액트는 배열을 렌더링할 때 값을 통하여 업데이트 성능을 최적화하는데 다음 예를 살펴보자.
<div>A</div>
<div>B</div>
<div>C</div>
<div>D</div>
만약에 key를 부여하지 않으면, 배열의 index값이 자동으로 key로 설정되는데, key가 배열의 인덱스로 설정되어있는 상태는 이러하다.
<div key={0}>A</div>
<div key={1}>B</div>
<div key={2}>C</div>
<div key={3}>D</div>
여기서 B와 C 사이에 X를 집어넣는다고 가정해보면
<div key={0}>A</div>
<div key={1}>B</div>
<div key={2}>X</div> [C -> X]
<div key={3}>C</div> [D -> C]
<div key={4}>D</div> [새로 생성됨]
굉장히 비효율적으로 만들어진다. 사실상 중간에 끼워넣기만 하면 되는 것인데, 배열의 index를 key로 사용하게 되어 중간에 값이 들어가면 index도 함께 바뀌어버리기 떄문에 X 아래로 값이 다 바뀌게 되는 것이다.
key를 배열의 index 값으로 사용하는 것이 아니라, 데이터를 추가할 때마다 고정적인 고유 값을 부여해주면 리액트가 변화를 감지해내고 업데이트를 하게 될 때 조금 더 똑똑하게 처리할 수 있게 된다.
<div key={0}>A</div>
<div key={1}>B</div>
<div key={2}>C</div>
<div key={3}>D</div>
이번에 다시 B와 C 사이에 다시 X를 넣어보겠다. 이번에 key 값은 고정된 고유값(id)이다.
<div key={0}>A</div>
<div key={1}>B</div>
<div key={5}>X</div> [새로 생성됨]
<div key={2}>C</div> [유지됨]
<div key={3}>D</div> [유지됨]
결국 새로운 DOM은 하나만 생성되고, 나머지는 그대로 유지된다. key값은 언제나 고유한 값으로 유지해야한다. 실제 프로젝트를 예로들자면, DB에 데이터를 추가하면 주로 해당 데이터를 가리키는 고유 id가 있다. 그러한 데이터를 리액트에서 렌더링하게 된다면 그 고유 id를 가지고 key로 사용하면 된다. 지금 경우네는 전화번호 정보에서 id값을 key값을 사용해줬다.
이제 PhoneInfoList 컴포넌트를 App에서 렌더링하고, data값을 props로 전달하자.
//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'
}
]
}
handleCreate = (data) => {
const { information } = this.state;
this.setState({
information: information.concat({ id: this.id++, ...data })
})
}
render() {
return (
<div>
<PhoneForm
onCreate={this.handleCreate}
/>
<PhoneInfoList data={this.state.information}/>
</div>
);
}
}
export default App;
제대로 했다면 새로운 전화번호를 등록했을 때 아래와 같은 화면으로 잘 동작한다.
가끔씩은 데이터에 고유한 값이 없을 수도 있다. key값이 없는 상태로 렌더링해도 동작은 되지만, 개발자도구 콘솔에서 경고창이 뜨게된다. 그 경고가 보고싶지 않다면
const list = data.map(
(info, index) => (<PhoneInfo key={index} info={info}/>)
);
과 같이 작업하면 되나, 이것은 단순히 경고만 감출 뿐이다.
'Progamming > ReactJS' 카테고리의 다른 글
[벨로퍼트] 불변성과 업데이트 최적화 (0) | 2019.07.12 |
---|---|
[벨로퍼트] 배열 다루기(2) 제거와 수정 (0) | 2019.07.12 |
[벨로퍼트] input 상태 관리하기 (0) | 2019.07.12 |
[벨로퍼트] LifeCycleAPI (0) | 2019.07.12 |
[벨로퍼트] props와 state (0) | 2019.07.11 |
댓글