PhoneForm Component에 있는
안에 들어 있는 값을 App 부모 컴포넌트가 관리하고 있는 배열에 데이터를 삽입하는 예제 코드를 작성해보았습니다.import React, { Component } from 'react';
import PhoneForm from './components/PhoneForm';
class App extends Component {
id = 1;
state = {
information: [],
}
handleCreate = (data) => {
const { information } = this.state;
this.setState({
information: information.concat({
...data,
id: this.id++,
})
});
}
render() {
return (
<div>
<PhoneForm onCreate={this.handleCreate} />
{JSON.stringify(this.state.information)}
</div>
);
}
}
export default App;
위 코드에서 render() 함수에서 PhoneForm 컴포넌트의 onCreate에 App 컴포넌트의 handleCreate 함수를 바인딩 하였습니다. 그리고 PhoneForm에 <button>
을 넣고, 안에서 버튼을 submit
하면 새로고침이 되기 때문에 이벤트 호출을 막도록 handleSubmit() 함수를 작성하였습니다.
import React, { Component } from 'react';
class PhoneForm extends Component {
state = {
name: '',
phone: '',
}
handleChange = (e) => {
this.setState({
[e.target.name] : e.target.value
});
}
handleSubmit = (e) => {
// submit 이벤트 호출을 막습니다.
e.preventDefault();
this.props.onCreate(this.state);
this.setState({
name: '',
phone: '',
});
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input
name="name"
placeholder="이름"
onChange={this.handleChange}
value={this.state.name}
/>
<input
name="phone"
placeholder="전화번호"
onChange={this.handleChange}
value={this.state.phone}
/>
<button type="submit">등록</button>
</form>
);
}
}
export default PhoneForm;
PhoneForm 컴포넌트에서 input들을 입력하고 버튼을 클릭하면 this.handleSubmit() 함수가 호출되어서 부모 컴포넌트에서 넘겨받은 this.props.onCreate(this.state)
함수를 호출하여 부모 컴포넌트에게 자식 컴포넌트의 state 객체를 전달합니다. 그리고 { name: '', phone: '',}
로 다시 state 값을 빈 값으로 초기화 시켜줍니다.
이제 자식 컴포넌트에서 전달받은 state 객체를 부모 컴포넌트가 가지고 있는 클래스 필드인 state.information 배열 객체에 넣어 줍니다. 이 때 주의할 점은 아래와 같이 값을 넣는 것은 절대 하면 안됩니다.
this.setState({
information: this.state.information.push(data)
});
state는 불변의 값이기 때문에 변경이 되어서는 안됩니다. 따라서 새로운 객체를 생성 후 할당해주도록 내장 메서드인 concat() 함수를 사용해야 합니다.
handleCreate = (data) => {
const { information } = this.state;
this.setState({
information: information.concat({
...data,
id: this.id++,
})
});
}
여기에 자식 컴포넌트에서 전달받은 state 객체 이외에도 배열이 참조하고 있는 객체들의 식별값을 위해 id라는 별도의 프로퍼티 값도 함께 넣어줬습니다. 여기서 id 값을 자식 컴포넌트가 아닌 부모 컴포넌트에서 가지고 있는 점인데, id는 식별값이기 때문에 컴포넌트에서 보여줄 필요가 없는 클래스 필드입니다. 그리고 setState() 함수를 호출하는 이유는 상태 값을 수정하면 리액트는 리렌더링을 하도록 설계되어 있기 때문입니다. 그리고 id 값은 배열에 값을 넣을 때 참조 역할을 하는 프로퍼티이기 때문에 굳이 state 객체에 넣을 필요가 없습니다.
information key 값에 자식 컴포넌트에서 받은 객체를 새롭게 넣는 방법은 아래와 같은 방법도 있습니다.
this.setState(Object.assign({}, data, {
id: this.id++
}))
PhoneForm 컴포넌트에서 작성한 form 엘리먼트에서 작성한 상태 값을 부모 컴포넌트인 App Component에서 관리하는 배열 객체에 넣은 후 PhoneInfo 리스트를 보여주는 예제를 작성해보겠습니다. PhoneInfoList 컴포넌트는 자식 컴포넌트로 전화번호 정보를 보여줄 PhoneInfo 컴포넌트를 가지고 있습니다.
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 info={info} key={info.id} />)
);
return (
<div>
{list}
</div>
);
}
}
export default PhoneInfoList;
render() 함수에서 react 엘리먼트를 자바스크립트 표현식을 이용해서 만든 다음에 {list}로 리턴해서 렌더링 합니다. 여기서 주목할 점은 Arrays.proptotype 객체가 제공하는 내장 메소드인 map() 함수를 사용하여 ES6 화살표 함수로 배열 객체 안에 있는 객체들을 <PhoneInfo> react 엘리먼트로 매핑해주는 것이 핵심입니다.
static defaultProps = {
data: []
}
만약 App 컴포넌트에서 전달받은 props 값이 배열이 아닐 경우에는 에러가 발생할 수 있으니 defaultProps 객체를 정의하여 기본 값을 설정해주는 것이 좋습니다.
import React, { Component } from 'react';
class PhoneInfo extends Component {
constructor(props) {
super(props);
console.log(this.props);
}
render() {
const { name, phone, id} = this.props.info;
const style = {
border: '1px solid black',
padding: '8px',
margin: '8px',
}
return (
<div style={style}>
<div><b>{name}</b></div>
<div><b>{phone}</b></div>
<div><b>{id}</b></div>
</div>
);
}
}
export default PhoneInfo;
PhoneInfo는 information 배열 객체를 PhoneInfoList 부모 컴포넌트에서 전달 받은 후에 비구조식으로 변수에 할당 후 각각의 전화번호부 정보를 렌더링 해주는 컴포넌트입니다.
최종적으로 이제 최상위 컴포넌트인 App에서 PhoneForm 컴포넌트에서 작성한 form 엘리먼트의 정보들을 받아서 또 다른 자식 컴포넌트인 PhoneInfoList 컴포넌트에 information 배열 객체를 전달하여 전화번호 부 정보를 렌더링 하게 됩니다.
import React, { Component } from 'react';
import PhoneForm from './components/PhoneForm';
import PhoneInfoList from './components/PhoneInfoList';
class App extends Component {
id = 1;
state = {
information: [],
}
handleCreate = (data) => {
const { information } = this.state;
this.setState({
information: information.concat({
...data,
id: this.id++,
}) // concat은 인자로 주어진 배열이나 값들을 기존의 배열에 합쳐서 새 배열을 반환힘.
});
}
render() {
return (
<div>
<PhoneForm onCreate={this.handleCreate} />
<PhoneInfoList data={this.state.information} />
</div>
);
}
}
export default App;
여기서 가장 궁금한 점은 App 컴포넌트에서 PhoneInfoList 컴포넌트에서 PhoneInfo 컴포넌트를 렌더링 할 때 props.info 객체 말고, 배열 객체의 식별 값을 가지고 있는 id를 props.key를 통해 전달해주고 있습니다.
const list = data.map(
info => (<PhoneInfo info={info} key={info.id} />)
);
그 이유는 아래와 같이 배열 객체의 요소를 렌더링 했다고 합시다.
<div>a</div>
<div>b</div>
<div>c</div>
<div>d</div>
b와 c 값을 가진 엘리먼트 사이에 z 값을 가지고 있는 엘리먼트가 추가될 경우 c 엘리먼트가 사라지고 z 엘리먼트가 들어옵니다. 그리고 c는 d가 있는 위치로 이동합니다. 그리고 d 엘리먼트가 다시 생성됩니다.
<div>a</div>
<div>b</div>
<div>z</div> // c가 d 엘리먼트 위치로 이동하고, z가 들어옴.
<div>c</div>
<div>d</div> // d 엘리먼트가 다시 생성됨.
삭제의 경우도 동일합니다. 만약 a 엘리먼트가 삭제되면 b, z, c, d가 한칸 이동하고 맨 아래에 있는 d가 삭제가 되는 비효율적인 방식으로 처리됩니다.
리액트에서 배열 객체의 정보를 가지고 있는 엘리먼트들을 제거, 추가, 업데이트를 할 때 작업을 효과적으로 하기 위해서 id 값을 사용합니다.
<div key={0}>a</div>
<div key={1}>b</div>
<div key={5}>z</div>
<div key={2}>c</div>
<div key={3}>d</div>
위의 key 값은 유니크하기 때문에 만약 z 엘리먼트를 추가하게 되면 나머지는 가만히 있고, 단순히 z만 추가됩니다. 삭제도 마찬가지로 동일합니다.
PhoneInfo 컴포넌트를 삭제하는 예제를 살펴보겠습니다.
먼저, App 최상위 부모 컴포넌트 코드는 아래와 같습니다.
import React, { Component } from 'react';
import PhoneForm from './components/PhoneForm';
import PhoneInfoList from './components/PhoneInfoList';
class App extends Component {
id = 1;
state = {
information: [],
}
handleCreate = (data) => {
const { information } = this.state;
this.setState({
information: information.concat({
...data,
id: this.id++,
}) // concat은 인자로 주어진 배열이나 값들을 기존의 배열에 합쳐서 새 배열을 반환힘.
});
}
handleRemove = (id) => {
const { information } = this.state;
this.setState({
information: information.filter(info => info.id !== id)
});
}
render() {
return (
<div>
<PhoneForm onCreate={this.handleCreate} />
<PhoneInfoList
data={this.state.information}
onRemove={this.handleRemove}
/>
</div>
);
}
}
export default App;
handleRemove 함수를 작성하여 자식 컴포넌트인 PhoneInfoList 컴포넌트의 props로 전달해줍니다.
import React, { Component } from 'react';
import PhoneInfo from './PhoneInfo';
class PhoneInfoList extends Component {
static defaultProps = {
data: []
}
render() {
const { data, onRemove } = this.props;
const list = data.map(
info => (<PhoneInfo onRemove={onRemove} info={info} key={info.id} />)
);
return (
<div>
{list}
</div>
);
}
}
export default PhoneInfoList;
PhoneInfoList 컴포넌트에서는 다시 자식 컴포넌트인 PhoneInfo 컴포넌트에 props로 전달해줍니다.
import React, { Component } from 'react';
class PhoneInfo extends Component {
handleRemove = () => {
const { info, onRemove } = this.props;
onRemove(info.id);
}
render() {
const { name, phone } = this.props.info;
const style = {
border: '1px solid black',
padding: '8px',
margin: '8px',
}
return (
<div style={style}>
<div><b>{name}</b></div>
<div><b>{phone}</b></div>
<button onClick={this.handleRemove}>삭제</button>
</div>
);
}
}
export default PhoneInfo;
삭제할 대상은 PhoneInfo 컴포넌트이기 때문에 <button onClick={this.handleRemove}>
엘리먼트를 추가합니다.
handleRemove 함수에서는 props를 비구조식으로 { info, onRemove }에 할당하여 삭제 버튼 클릭 시 handleRemove() 함수를 호출하여 최종적으로 onRemove 함수변수가 참조하고 있는 App 컴포넌트의 handleRemove() 함수를 호출하여 해당 id를 가진 PhoneInfo 컴포넌트를 삭제하게 됩니다.
PhoneInfo 컴포넌트에서 보여주는 전화번호부 정보를 수정하는 예제를 작성해보겠습니다.
먼저, App 최상위 컴포넌트에 전화번호부 정보를 삭제하기 위해 이벤트 함수를 전달하는 것처럼 수정을 위한 이벤트 함수를 정의하고, 자식 컴포넌트인 PhoneInfoList 컴포넌트에 props로 전달합니다.
import React, { Component } from 'react';
import PhoneForm from './components/PhoneForm';
import PhoneInfoList from './components/PhoneInfoList';
class App extends Component {
id = 1;
state = {
information: [],
}
handleCreate = (data) => {
const { information } = this.state;
this.setState({
information: information.concat({
...data,
id: this.id++,
}) // concat은 인자로 주어진 배열이나 값들을 기존의 배열에 합쳐서 새 배열을 반환힘.
});
}
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 => {
if (info.id === id) {
return {
id,
...data,
};
}
return info;
})
});
}
render() {
return (
<div>
<PhoneForm onCreate={this.handleCreate} />
<PhoneInfoList
data={this.state.information}
onRemove={this.handleRemove}
onUpdate={this.handleUpdate} // handleUpdate 함수 바인딩
/>
</div>
);
}
}
export default App;
handleUpdate 함수는 id, data({ name: '', phone: ''})를 파라미터로 받아서 내장 메서드인 map을 이용하여 배열 객체 요소가 가지고 있는 요소들을 순회하면서 해당 요소가 가지고 있는 id 값이랑 PhoneInfo 컴포넌트가 전달한 id 값이 같은 것만 배열 객체의 react 엘리먼트 요소를 변경하고, 그 외에는 그대로 리턴하도록 코드를 수정하였습니다.
import React, { Component } from 'react';
import PhoneInfo from './PhoneInfo';
class PhoneInfoList extends Component {
static defaultProps = {
data: []
}
render() {
const { data, onRemove, onUpdate } = this.props;
const list = data.map(
info => (
<PhoneInfo
onRemove={onRemove}
onUpdate={onUpdate}
info={info}
/>)
);
return (
<div>
{list}
</div>
);
}
}
export default PhoneInfoList;
PhoneInfoList 컴포넌트는 마찬가지로 부모 컴포넌트인 App으로 전달받은 props를 그대로 onUpdate만 추가해서 전달해주면 역할이 끝납니다.
import React, { Component, Fragment } from 'react';
class PhoneInfo extends Component {
state = {
editing: false,
name: '',
phone: '',
}
handleRemove = () => {
const { info, onRemove } = this.props;
onRemove(info.id);
}
handleToggleEdit = () => {
// true -> false
const { info, onUpdate } = this.props;
if (this.state.editing) {
onUpdate(info.id, {
name: this.state.name,
phone: this.state.phone
});
} else {
this.setState({
name: info.name,
phone: info.phone,
});
}
this.setState({
editing: !this.state.editing,
});
// onUpdate
// false -> true
// state에 info 값들 넣어주기
}
handleChange = (e) => {
this.setState({
[e.target.name]: e.target.value
});
}
render() {
const { name, phone, id } = this.props.info;
const { editing } = this.state;
const style = {
border: '1px solid black',
padding: '8px',
margin: '8px',
}
return (
<div style={style}>
{
editing ? (
<Fragment>
<div>
<input
name="name"
onChange={this.handleChange}
value={this.state.name}
/>
</div>
<div>
<input
name="phone"
onChange={this.handleChange}
value={this.state.phone}
/>
</div>
</Fragment>
) : (
<Fragment>
<div><b>{id}</b></div>
<div><b>{name}</b></div>
<div><b>{phone}</b></div>
</Fragment>
)
}
<button onClick={this.handleRemove}>삭제</button>
<button onClick={this.handleToggleEdit}>
{ editing ? '적용' : '수정' }</button>
</div>
);
}
}
export default PhoneInfo;
PhoneInfo 컴포넌트의 state 객체에 editing key 값을 추가하고, 해당 값에 따라서 버튼 문구를 수정 or 적용으로 보여주도록 하였습니다.
여기서 editing 값이 true이면 Fragment 태그를 사용하여 JSX를 리턴하도록 하였습니다.
맨 처음에 PhoneInfo 컴포넌트가 랜더링이 되면 기본적으로 editing 상태 값이 false이기 때문에 수정 버튼을 보여주도록 되어 있습니다.
수정 버튼 클릭 시 toggle을 시켜서 editing 값을 false로 반전시키는 메서드를 호출합니다. 그리고 기존에 보여주었던 state의 name과 phone 프로퍼티 값을 동기화 시켜줍니다.
또 한 state 값이 바뀌었기 때문에 PhoneInfo 컴포넌트는 다시 리렌더링이 되고 수정 버튼이라고 보였던 문구는 적용 버튼으로 보이게 됩니다.
if (this.state.editing) {
onUpdate(info.id, {
name: this.state.name,
phone: this.state.phone
});
} else {
this.setState({
name: info.name,
phone: info.phone,
});
}
업데이트를 하고 싶은 값을 input 엘리먼트에 입력하고 적용 버튼을 클릭하면 handleToggleEdit 메서드가 호출되어 editing 값이 true이면 props로 받은 이벤트 함수인 onUpdate를 호출하게 됩니다.
State 업데이트는 병합됩니다. setState()를 호출할 때 React는 제공한 객체를 현재 state로 병합합니다.
예를 들어, state는 다양한 독립적인 변수를 포함할 수 있습니다.
state = {
editing: false,
name: '',
phone: '',
}
handleChange = (e) => {
this.setState({
[e.target.name]: e.target.value
});
}
위 코드를 살펴보면 state 객체는 다양한 프로퍼티를 가지고 있지만 setState() 함수 호출 시에는 한 개의 프로퍼티만 변경하고 있습니다.
병합은 얕게 이루어지기 때문에 this.setState({name})는 this.state.phone에 영향을 주진 않지만 this.state.name는 완전히 대체됩니다.