함께 성장하는 프로독학러

5-4. 전화번호부 어플리케이션 만들기 - Contact 데이터 삭제, 수정 기능 구현 본문

Programming/react.js

5-4. 전화번호부 어플리케이션 만들기 - Contact 데이터 삭제, 수정 기능 구현

프로독학러 2018. 4. 20. 13:17

안녕하세요, 프로독학러 입니다.


저번 포스팅에 이어 전화번호부 어플리케이션을 만들어 보도록 하겠습니다.

*velopert 님의 Youtube 강의를 정리한 내용이라고 보시면 될 것 같습니다. (4강)

<Contact application - velopert>


이번 포스팅에서는 전화번호부 어플리케이션의 목록 삭제와 수정 기능을 구현해 보겠습니다.


1) 데이터 삭제


먼저 데이터 삭제기능을 구현해 보도록 하겠습니다.

데이터 삭제는 ContactDetail 컴포넌트에서 remove 버튼을 누르면 동작하도록 만들것입니다.


따라서, 먼저 ContactDetail 컴포넌트에 remove 버튼을 추가하겠습니다.


(./src/components/ContactDetail.js)

1
2
3
4
5
6
7
8
9
...
 
        <div>
          <h2>Detail information</h2>
          {this.props.isSelected ? details:blank}
          <button>remove</button>
        </div>
 
...
cs


ContactDetail 컴포넌트의 렌더 부분에 위의 6번째 줄처럼 button 태그를 추가해 줍니다.

ContactDetail 컴포넌트 안에 있는 이 버튼을 누르면 상위 컴포넌트인 Contact 의 state.contactData 를 수정해야 합니다.

setState 를 통해 state.contactData 를 변경해야 하므로 Contact 컴포넌트에 이를 실행하는 메소드를 정의하고, ContactDetail 컴포넌트로 프롭스로 전달하여 button 태그를 통해 실행하도록 하겠습니다.

(이전 포스팅의 데이터 추가와 같은 맥락입니다. 수정 메소드를 하위컴포넌트에 프롭스로 전달, 하위컴포넌트에서 전달받은 메소드를 이벤드로 사용)


(./src/components/Contact.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...    
 
    _contactRemove = () => {      
      if(this.state.selectedKey===-1){
        return
      }
      let before = this.state.contactData.slice(0, this.state.selectedKey);
      let after = this.state.contactData.slice(this.state.selectedKey+1);
      let removedArr = [...before, ...after];
      this.setState({...this.state,
        contactData:removedArr
      })
    }
 
...
cs


setState 를 실행할 Contact 컴포넌트에 _contactRemove 메소드를 지정했습니다.

다소 코드가 복잡해 보이지만 천천히 뜯어보겠습니다.

먼저 살펴볼 행은 위의 코드의 7번째 줄입니다. 

state.contactData(배열) 를 처음부터 선택된 부분(this.state.selectedKey) 이전까지 잘라내고 그 배열을 before 에 할당했습니다.

그리고 그 아랫줄에서 선택된 다음부분 부터 끝까지 잘라내고 after 에 할당했습니다.

즉, 선택된 부분을 기준으로 앞 부분의 배열은 before, 뒷 부분의 배열은 after 입니다.

그리고 before 와 after 를 합친 배열을 새로 만들어 removedArr 에 할당했습니다.

10번째 줄에서 setState 메소드로 contactData 를 removedArr 로 대체 했습니다.

*splice 메소드를 사용하면 간단히 될 것 같지만 splice 는 원본 배열을 손상시키기 때문에 사용하면 안됩니다.

*slice 메소드에 익숙하지 않으신 분들은 아래의 링크를 참조해 주세요

<배열, 문자열의 slice 메소드>


즉, slice 와 spread operator 를 통해 원본 state 를 훼손하지 않고 선택된 원소가 삭제된 '새로운 배열'(removedArr)을 state.contactData 로 대체했습니다.


위 코드의 4번째 줄은 아무것도 선택되지 않은 상태에서 remove 버튼을 눌러도 동작하지 않도록 조건문을 준 것입니다.


이제 _contactRemove 메소드를 ContactDetail 컴포넌트에 전달하겠습니다.


(./src/components/Contact.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...    
render(){
    ...
        return(
            ...
            <ContactDetail          
              isSelected = {this.state.selectedKey != -1}
              contact = {this.state.contactData[this.state.selectedKey]}
              onRemove = {this._contactRemove}
            />
            ...
        )
}
...
cs


9번째 줄에서 onRemove 프롭스로 _contactRemove 메소드를 전달했습니다.


ContactDetail 에서 onRemove 로 전달받은 프롭스를 사용하겠습니다.


(./src/components/ContactDetail.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
import React, { Component } from 'react';
import PropTypes from 'prop-types';
 
export default class ContactDetail extends Component {
    render() {
      const details = (<div>
                          <p>{this.props.contact.name}</p>
                          <p>{this.props.contact.phone}</p>
                       </div>);
      const blank = (<div>Not selected</div>);
      return (
        <div>
          <h2>Detail information</h2>
          {this.props.isSelected ? details:blank}
          <button onClick={this.props.onRemove}>remove</button>
        </div>
      );
    }
}
 
ContactDetail.defaultProps = {
  isSelected : false,
  contact : {
    name : '',
    phone : ''
  },
  onRemove: ()=>{
    console.log('there is onRemove function');
  }
}
 
ContactDetail.propTypes = {
  isSelected : PropTypes.bool.isRequired,
  contact : PropTypes.object.isRequired,
  onRemove : PropTypes.func.isRequired
};
cs


코드의 15번째 줄에서 button 태그의 onClick 이벤트에 onRemove 로 전달받은 프롭스를 지정했습니다.

21, 32번째 줄에서 기본 프롭스 설정과 프롭타입 검증 객체를 추가했습니다.


그럼 제대로 동작하는지 브라우저를 통해 확인해 보도록 하죠.



목록에서 john 을 클릭하고 삭제버튼을 누르면 목록에서 john 이 사라졌습니다.


2) 데이터 수정


이제 목록을 클릭 했을 때 상세뷰에서 수정버튼을 눌러, 선택된 데이터를 수정하는 기능을 구현해 보겠습니다.

데이터 수정은 ContactDetail 컴포넌트의 state 를 활용해 수정될 데이터의 모양을 만들고, Contact 컴포넌트로 부터 전달받은 메소드를 통해 선택된 항목을 ContactDetail 컴포넌트의 state 정보로 변경할 것입니다.


먼저 ContactDetail 컴포넌트에 edit 버튼을 추가해 줍니다.


(./src/components/ContactDetail.js)

1
2
3
4
5
6
7
8
9
10
11
...
        <div>
          <h2>Detail information</h2>
          {this.props.isSelected ? details:blank}
          <p>
             <button>edit</button>
            <button onClick={this.props.onRemove}>remove</button>            
          </p>          
        </div>
...
 
cs


위의 코드의 6번째 줄에서 edit 버튼을 추가하였습니다.


우리는 수정될 데이터를 ContactDetail 컴포넌트의 state 를 활용해 수정될 모양을 만들것이므로 state 값을 초기화 해 줍니다.


(./src/components/ContactDetail.js)

1
2
3
4
5
6
7
...
    state = {      
      isEdit : false,
      name : '',
      phone : ''
    }
...
cs


위 코드의 4, 5 번째 줄은 수정될 데이터 모양을 만드는데 필요한 state 이고, 3번째 줄의 isEdit은 수정 모드인지 아닌지를 구분하는 state 입니다.


edit 버튼을 눌렀을 때, state.isEdit 이 현재 state.isEdit 의 반대값으로 변경되는 매소드를 만들어 edit 버튼에 onClick 이벤트로 지정하겠습니다.


(./src/components/ContactDetail.js)

1
2
3
4
5
6
7
8
9
10
11
12
...
    _editButtonToggle = () => {      
        this.setState({
        isEdit: !this.state.isEdit
      });
    }
...
    <button onClick={this._editButtonToggle}>
       {this.state.isEdit? 'Ok':'edit'}
    </button>
...
 
cs


위 코드의 2번째 줄에서 버튼을 눌렀을 때 실행될 메소드를 정의했습니다.

메소드의 내용은 4번째 줄에서 볼 수 있듯이, state.isEdit 를 현재의 state.isEdit 의 반댓값으로 변경하는 것입니다.

(false면 true로, true면 false로)

그리고 8번째 줄에서 onClick 이벤트에 지정했습니다.

9번째 줄에서는 isEdit 값이 ture라면 버튼의 내용이 Ok, false 라면 edit 이 나오게 설정했습니다.


위의까지 단계로 수정해야할 상태면 isEdit 이 true 가 되고, 수정해야할 상태가 아니라면 false 가 되도록 만들었습니다.

이제 isEdit 에 따라 수정을 위한 input 태그가 보여지도록 하겠습니다.


(./src/components/ContactDetail.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
...
    const edit = (<div>
                      <p>
                        <input
                          name="name"
                          type="text"
                          placeholder="name"
                          onChange={this._inputChange}
                        />
                        <input
                          name="phone"
                          type="text"
                          placeholder="phone"
                          onChange={this._inputChange}
                        />
                      </p>
                    </div>);
      const view = this.state.isEdit? edit:details;
...
{this.props.isSelected ? view:blank}
...
cs


코드의 2번째 줄에서 수정될 화면을 정의 했습니다. 두 개의 input 태그가 나란히 있는 상태입니다. (<p> 태그를 통해 나란히 위치)

그리고 input 태그에 값이 입력되면 실행될 _inputChange 메소드를 onChange 이벤트에 지정했습니다.


20번째 줄에서 프롭스로 전달받은 isSelected 값이 참이면 view 를 렌더링 합니다.

view 역시 조건문으로(18번째 줄) isEdit 이 참이면 edit 화면을, 거짓이면 details 화면을 렌더링 합니다.

즉, 항목이 선택된 상태(props.isSelected = true)에서 edit 버튼을 누르면(state.isEdit = true) edit 화면이 렌더링 되는 것입니다.


그럼 input 태그의 onChange 이벤트에 지정된 _inputChange 메소드를 살펴봅시다.

이는 ContactCreate 의 _inputChange 와 같습니다.

역할은 값이 입력될 때마다 입력된 값을 state 의 name 과 phone 값에 저장하는 것입니다.


(./src/components/ContactDetail.js)

1
2
3
4
5
6
7
...
    _inputChange = (e) => {
      let nextState = {};
      nextState[e.target.name= e.target.value;
      this.setState(nextState);
    }
...
cs


nextState 로 객체형식을 만들어 입력된 input 태그의 name을 키값으로, 입력된 내용을 값으로 하여 state 를 변경합니다.


여기까지 완료했다면, 항목이 선택된 상태에서 edit 버튼을 누르면 수정화면이 뜨고, 수정화면의 input 태그에 값을 입력하면 해당 내용이 ContactDetail 의 state 로 들어갑니다. 이를 확인하기 위해 _inputChange 메소드에 console.log(this.state); 를 추가해봅시다.

그리고 브라우저의 콘솔창에서 잘 작동 되는지 확인해 봅니다.



Wade 항목을 선택한 뒤, eidt 버튼을 눌러 다음과 같이 치니 state의 변경된 값이 콘솔에 잘 찍히고 있는걸 알 수 있습니다.

하지만 edit 버튼을 눌렀을 때 input 창은 빈 상태로 등장합니다.

input 창이 선택된 항목의 정보를 미리 담고있으면 수정하기 편리하겠죠?

이를 구현해 봅시다.


(./src/components/ContactDetail.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
... 
    _editButtonToggle = () => {
      if(this.state.isEdit===false){
        this.setState({
          name:this.props.contact.name,
          phone:this.props.contact.phone
        })
      }
      this.setState({
        isEdit: !this.state.isEdit
      });
    }
...
cs


_editbuttonToggle 메소드의 앞부분에 조건문을 추가했습니다. (3번째 줄)

edit 버튼을 클릭했을 때 isEdit 속성이 false 라면 state 를 props 로 전달받은 값으로 변경하는 코드입니다.


변경된 state를 input 태그의 value 로 지정해 수정버튼을 입력하면 input 창에 원래의 데이터가 담겨있도록 해 줍니다.


(./src/components/ContactDetail.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
    const edit = (<div>
                      <p>
                        <input
                          name="name"
                          type="text"
                          placeholder="name"
                          onChange={this._inputChange}
                          value={this.state.name}
                        />
                        <input
                          name="phone"
                          type="text"
                          placeholder="phone"
                          onChange={this._inputChange}
                          value={this.state.phone}
                        />
                      </p>
                    </div>);
...
cs


위의 코드의 9, 16번째 줄에서 input 태그의 value 값을 지정해 줬습니다.

*(input 태그의 value 를 바로 this.props.contact.name 등으로 변경하면 될 것 같지만, props 는 사용하는 컴포넌트에서 수정할 수 없으므로 input 창이 수정되지 않습니다. 이러한 이유 때문에 프롭스를 state 값으로 설정하고 state 를 이용하는 것입니다.)



브라우저에서 확인한 결과, 항목을 선택했을 때 원래의 데이터가 input 창에 잘 들어가 있고, 수정하면 state 가 수정한 대로 잘 변경되는 것을 확인했습니다.


이제 마지막 단계로, 수정창에서 수정을 한 뒤에 Ok 버튼을 누르면 Contact 에 반영되는 작업을 하도록 하겠습니다.

Contact 에서 수정하는 메소드를 정의하고, ContactDetail 컴포넌트에게 정의한 메소드를 프롭스로 전달하여 ContactDetail 컴포넌트에서 실행하겠습니다.


Contact 컴포넌트에서 데이터를 수정하는 메소드를 먼저 만들어 줍니다.


(./src/components/Contact.js)

1
2
3
4
5
6
7
8
9
10
... 
  _contactEdit = (editData) => {
    let before = this.state.contactData.slice(0, this.state.selectedKey);
    let after = this.state.contactData.slice(this.state.selectedKey+1);
    let newArr = [...sliced, editData ,...rest];
    this.setState({...this.state,
      contactData:newArr
    })
  }
...
cs


전체적인 맥락은 데이터를 삭제하는 것과 비슷합니다.

선택된 항목을 기준으로 앞, 뒤 배열을 만들고, 선택된 항목은 인자로 들어오는 객체로 대체시켜 줍니다.

(원본 state 를 손상시키지 않기 위함)


이 메소드를 ContactDetail 컴포넌트에 onEdit 프롭스로 전달해 줍니다.


(./src/components/Contact.js)

1
2
3
4
5
6
7
8
... 
    <ContactDetail
       isSelected = {this.state.selectedKey != -1}
       contact = {this.state.contactData[this.state.selectedKey]}
       onRemove = {this._contactRemove}
       onEdit = {this._contactEdit}
    />
...
cs


전달 받은 ContactDetail 컴포넌트에서 수정모드의 Ok 버튼을 눌렀을 때 해당 프롭스를 사용하도록 지정해 줍니다.


(./src/components/ContactDetail.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
... 
    _editFunc = () => {
      this.props.onEdit({name:this.state.name, phone:this.state.phone});
    }
    _editButtonToggle = () => {
      if(this.state.isEdit===false){
        this.setState({
          name:this.props.contact.name,
          phone:this.props.contact.phone
        })
      }else{
        this._editFunc();
      }
      this.setState({
        isEdit: !this.state.isEdit
      });
    }
...
cs


_editButtonToggle 메소드의 조건문에 조건을 만족하지 않는, 즉 state.isEdit 이 참(true)일 때 버튼을 누르면 실행하는 함수를 지정했습니다.

(11~13번째 줄)

실행되는 함수는 2번째 줄에 정의 돼 있는데, 이는 Contact 컴포넌트로 부터 onEdit 프롭스로 전달받은 함수입니다.

함수에 인자로 수정될 내용을 주었습니다.


브라우저를 통해 제대로 작동하는지 확인해 봅시다.



정상적으로 작동하는 것을 알 수 있습니다.


여기까지의 코드는 이렇습니다.


(./src/components/ContactDetail.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
import React, { Component } from 'react';
import PropTypes from 'prop-types';
 
export default class ContactDetail extends Component {
    state = {
      isEdit:false,
      name:'',
      phone:''
    }
    _editFunc = () => {
      this.props.onEdit({name:this.state.name, phone:this.state.phone});
    }
    _editButtonToggle = () => {
      if(this.state.isEdit===false){
        this.setState({
          name:this.props.contact.name,
          phone:this.props.contact.phone
        })
      }else{
        this._editFunc();
      }
      this.setState({
        isEdit: !this.state.isEdit
      });
    }
    _inputChange = (e) => {
      let nextState = {};
      nextState[e.target.name= e.target.value;
      this.setState(nextState);
      console.log(this.state);
    }
    render() {
      const details = (<div>
                          <p>{this.props.contact.name}</p>
                          <p>{this.props.contact.phone}</p>
                       </div>);
      const blank = (<div>Not selected</div>);
      const edit = (<div>
                      <p>
                        <input
                          name="name"
                          type="text"
                          placeholder="name"
                          onChange={this._inputChange}
                          value={this.state.name}
                        />
                        <input
                          name="phone"
                          type="text"
                          placeholder="phone"
                          onChange={this._inputChange}
                          value={this.state.phone}
                        />
                      </p>
                    </div>);
      const view = this.state.isEdit? edit:details;
      return (
        <div>
          <h2>Detail information</h2>
          {this.props.isSelected ? view:blank}
          <p>
            <button onClick={this._editButtonToggle}>
              {this.state.isEdit? 'Ok':'edit'}
            </button>
            <button onClick={this.props.onRemove}>remove</button>
          </p>
        </div>
      );
    }
}
 
ContactDetail.defaultProps = {
  isSelected : false,
  contact : {
    name : '',
    phone : ''
  },
  onRemove: ()=>{
    console.log('there is onRemove function');
  }
}
 
ContactDetail.propTypes = {
  isSelected : PropTypes.bool.isRequired,
  contact : PropTypes.object.isRequired,
  onRemove : PropTypes.func.isRequired
};
cs


(./src/components/Contact.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
import React, { Component } from 'react';
 
import ContactInfo from './ContactInfo';
import ContactDetail from './ContactDetail';
import ContactCreate from './ContactCreate';
 
class Contact extends Component {
  state = {
    keyword : '',
    selectedKey: -1,
    contactData : [{
      name : 'David',
      phone : '010-1234-5678'
    }, {
      name : 'Albert',
      phone : '010-1234-1234'
    }, {
      name : 'John',
      phone : '010-5678-5678'
    }, {
      name : 'Wade',
      phone : '010-4312-5678'
    },]
  }
  _searchContact = (e) => {
    this.setState({
      keyword : e.target.value
    });
  }
  _nameClick = (key) => {
    this.setState({
      selectedKey : key
    });
  }
  _contactCreate = (contactObj) => {
    this.setState({...this.state,
      contactData:[...this.state.contactData, contactObj]
    });
  }
  _contactRemove = () => {
    if(this.state.selectedKey===-1){
      return
    }
    let before = this.state.contactData.slice(0, this.state.selectedKey);
    let after = this.state.contactData.slice(this.state.selectedKey+1);
    let removedArr = [...before, ...after];
    this.setState({...this.state,
      contactData:removedArr
    })
  }
  _contactEdit = (editData) => {
    let before = this.state.contactData.slice(0, this.state.selectedKey);
    let after = this.state.contactData.slice(this.state.selectedKey+1);
    let newArr = [...before, editData ,...after];
    console.log(newArr)
    this.setState({...this.state,
      contactData:newArr
    })
  }
  render(){
    const mapToComponents = (data) => {
      data.sort();
      data = data.filter(
        (contact) => {
          return contact.name.toLowerCase()
          .indexOf(this.state.keyword.toLowerCase()) > -1;
        }
      );
      return data.map(
        (contact, i) => {
          return (<ContactInfo
                    contact={contact}
                    key={i}
                    onClick={()=>this._nameClick(i)}
                 />);
        }
      );
    }
    return(
      <div>
        <h1>Contact</h1>
        <input
          name="keyword"
          placeholder="Search"
          onChange={this._searchContact}
        />
        <div>{mapToComponents(this.state.contactData)}</div>
        <ContactDetail
          isSelected = {this.state.selectedKey != -1}
          contact = {this.state.contactData[this.state.selectedKey]}
          onRemove = {this._contactRemove}
          onEdit = {this._contactEdit}
        />
        <ContactCreate
          onCreate={this._contactCreate}
        />
      </div>
    )
  }
}
 
export default Contact;
cs


데이터의 삭제 및 수정의 핵심내용은 state가 변경될 Contact 컴포넌트에서 삭제 및 수정 메소드를 정의하고, 사용할 하위 컴포넌트에 메소드를 프롭스로 전달하는 것입니다. 


긴 포스팅이 되었네요. 여기까지 따라오느라 수고 많으셨습니다. 다음 포스팅에서는 추가적인 기능을 구현하고, local storage 에 변경된 내용을 저장하는 기능을 구현하도록 하겠습니다.

감사합니다.


**참고 자료 (항상 감사드립니다)

https://velopert.com/reactjs-tutorials


*다녀가셨다는 표시는 공감으로 부탁드릴게요! (로그인 하지 않으셔도 공감은 가능합니다 ㅎㅎ)

Comments