함께 성장하는 프로독학러

5-3. 전화번호부 어플리케이션 만들기 - Contact 데이터 추가 기능 구현 본문

Programming/react.js

5-3. 전화번호부 어플리케이션 만들기 - Contact 데이터 추가 기능 구현

프로독학러 2018. 4. 20. 08:52

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


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

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

<Contact application - velopert>


1) 개념 - 원본 state 를 훼손해서는 안되는 작업


이번 포스팅에서 구현할 기능은 Contact 컴포넌트의 state.contactData 에 새로운 전화번호부를 추가하는 내용입니다.

이번 포스팅에서는 전화번호부를 추가, 다음포스팅에서는 수정 및 삭제 기능을 구현 할 예정인데, 놓치지 말아야 할 중요한 개념이 있습니다.

추가, 수정, 삭제 모두 state 를 변경하는 작업인데요, 이 작업들은 모두 원본 state 를 직접변경해서는 안됩니다.

이전 포스팅에서 selectedKey 나 keyword 와 같은 state 를 수정했는데, 이와 앞으로 할 state 수정의 다른점은 무엇일까요?

selectedKey 나 keyword 는 user 의 동작에 따라 시시각각 변하는 데이터입니다. 이를 통해 어플리케이션의 동작을 다르게 해 주는 프로그래밍적인 역할을 하는 state 들이죠.

반면 contactData 는 이전의 state 와 현재의 state 를 비교할 수 있어야 합니다. 

(이전의 state 와 현재의 state 를 비교해 달라진 값이 있으면 localStorage 에 저장할 예정입니다.)

즉, 이전의 state 와 현재의 state 의 비교를 활용하는 어떤 작업을 하려면 state 의 원본을 손상하지 않고, 새로운 state 를 만들어 대체 시켜 줘야한다는 것입니다.

(이전스테이트와 현재스테이트의 비교가 가능한 것은 Component Lifecycle API 중 componentDidUpdate)

*<리액트 Component Lifecycle>


이를 위해 우리는 ES6 의 spread 연산자를 활용할 것입니다.

(React 의 add-ons 중 하나인 Immutability Helper 를 이용할 수 도 있습니다)

*(ES6 의 spread 연산자에 익숙하지 않으신 분들은 아래의 링크를 참조해 주세요)

<ES6 의 spread 연산자>


spread 연산자를 이용하여 객체를 수정하는 방법을 간단하게 살펴보겠습니다.


1
2
3
4
5
6
7
8
9
10
11
let object = {
    a:'1',
    b:'2',
    c: {
        d:'3',
        e: {
            changeTarget: '0',
            f: '5'
        }
    }
}
cs


위의 코드에서 객체 object 를 정의 했습니다.

우리는 이 object 에서 7번째 줄에 있는 changeTarget 의 값을 4로 바꾼 새로운 객체를 만들어 changed 변수에 할당할 것입니다.


1
2
3
4
5
6
7
8
9
10
let changed = {
...object,
    c: {
        ...object.c,
        e: {
            ...object.c.e,
            changeTarget: '4'
        }
    }
}
cs


수정을 원하지 않는 부분은 spread 연산자를 활용하여 표기하고, 수정을 하고자 부분만 새롭게 지정해 주면 됩니다.

코드의 7번째 줄에서 changeTarget 에 4 를 지정해 주고 나머지 부분에는 spread 연산자를 이용해 그대로 두었습니다.

changed 를 콘솔에 찍어보면



의도한 대로 changedTarget 만 변경된 것을 볼 수 있습니다.


2) Contact 데이터 추가


그럼 본격적으로 전화번호부 어플리케이션에 새로운 데이터를 추가하는 기능을 구현해 보도록 하겠습니다.


먼저 데이터를 추가하기 위해 사용자가 추가할 데이터의 정보를 입력하는 필드가 필요하겠죠?

새로운 컴포넌트를 하나 만들도록 하겠습니다.

컴포넌트의 이름은 ContactCreate 입니다.


(./src/components/ContactCreate.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { Component } from 'react';
 
export default class ContactCreate extends Component {
  render() {
    return (
        <div>
          <h2>Create new user</h2>
          <p>
            <input />
            <input />
          </p>
          <button>Create new user</button>
        </div>
    );
  }
}
cs


ContactCreate 컴포넌트는 Contact 컴포넌트에서 ContactDetail 컴포넌트 아래에서 렌더링되야 하므로 Contact.js 파일을 수정합니다.


(./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
...
import ContactCreate from './ContactCreate';
 
class Contact extends Component {
...
    render(){
        ...
        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]}
                />
                <ContactCreate />
              </div>
        )
    }
}
...
cs


두 번째 줄과 같이 ContactCreate 컴포넌트를 임포트, 21번째 줄과 같이 렌더링 하면 됩니다.



브라우저에서 확인해 보면, 위와 같이 ContactCreate 컴포넌트가 잘 렌더링 된 것을 알 수 있습니다.


이제 만들어진 input 태그의 값이 변할 때 마다 그 값을 ContactCreate state 에 저장하고, 

Create new user 버튼을 누르면 Contact 컴포넌트의 state.contactData 에 추가되는것을 구현해 봅시다.


input 태그에 타입을 지정, 이름을 지어주고, placeholder 값을 줍니다.

그리고 input 태그에 입력될 때 마다(onChange 이벤트) 실행될 함수를 지정해 줍니다.

이 함수는 ContactCreate 의 스테이트를 변경합니다.

(state 를 사용하기 위해 state 를 초기화 해 줘야한다는 것도 잊지 않으셨죠?)


(./src/components/ContactCreate.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';
 
export default class ContactCreate extends Component {
  state = {
    name:"",
    phone:""
  }
  _inputChange = (e) => {
    let nextState = {};
    nextState[e.target.name= e.target.value;
    this.setState(nextState);
    console.log(this.state);
  }
  render() {
    return (
        <div>
          <h2>Create new user</h2>
          <p>
            <input
              type="text"
              name="name"
              placeholder="name"
              onChange={this._inputChange}
            />
            <input
              type="text"
              name="phone"
              placeholder="phone"
              onChange={this._inputChange}
            />
          </p>
          <button>Create new user</button>
        </div>
    );
  }
}
cs


위의 코드에서 20-22, 26-28 은 input 태그에 타입지정, 이름지정, placeholder 지정한 코드입니다.

그리고 8번째 줄에서 input 태그가 onChange 될 때 사용할 함수를 지정했습니다.

함수에 들어오는 인자는 이벤트 객체입니다.

9번째 줄에서 해당 스코프에서 사용하는 객체를 지정하고,

10번째 줄에서 input 태그로 입력되는 값을 객체의 값으로 전달합니다.

그리고 11번째 줄에서 nextState 객체로 state를 변경하고 있습니다.

*12번째 줄은 스테이트의 변경이 제대로 되는지 확인하기 위한 코드입니다.

(실제로 콘솔창을 확인해 보면, 입력한 것 보다 한 문자 적게 표시되는데 이는 setState 메소드가 비동기이기 때문입니다.)


input 창에 입력이 될 때마다 state 가 제대로 변경됩니다.

그럼 이제 Create new user 버튼을 누르면 Contact 의 state.contactData 에 입력된 값이 추가되는 기능을 구현해 보겠습니다.

이를 위해서 먼저 Contact 컴포넌트에서 state.contactData 에 데이터를 추가하는 메소드를 만들어 줍니다.

(이 메소드를 ContactDetail 에 프롭스로 전달한 뒤, ContactDetail 에서 실행할 예정)

Contact 컴포넌트의 state.contactData 는 원본이 수정되면 안되는 state 입니다.

따라서 다음과 같이 정의 합니다.


(./src/components/Contact.js)

1
2
3
4
5
6
7
...
    _contactCreate = (contactObj) => {
      this.setState({...this.state,
        contactData:[...this.state.contactData, contactObj]
      });
    }
...
cs


setState 를 할 때 spread 연산자를 이용해 contactData 부분만 수정하고(3번째 줄),

contactData에 배열리터럴([ ])을 이용해 새로운 배열을 할당했습니다. 

할당된 배열의 내용은 기존의 state.contactData 에 인자로 들어온 객체를 추가 한 배열입니다.


이제 _contactCreate 메소드를 ContactCreate 컴포넌트에 프롭스로 전달합니다.


(./src/components/Contact.js)

1
2
3
4
5
...
    <ContactCreate
        onCreate={this._contactCreate}
    />
...
cs


ContactCreate 컴포넌트에서는 button 을 클릭했을 때 전달받은 _contactCreate 메소드 (ContactCreate 입장에서는 this.props.onCreate) 를 자신의 스테이트를 인자로 주어 실행해야 합니다.

따라서 다음과 같이 코딩합니다.


(./src/components/ContactCreate.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
import React, { Component } from 'react';
import PropTypes from 'prop-types';
 
export default class ContactCreate extends Component {
  state = {
    name:"",
    phone:""
  }
  _inputChange = (e) => {
    let nextState = {};
    nextState[e.target.name= e.target.value;
    this.setState(nextState);
    console.log(this.state);
  }
  _buttonClick = () => {
    const newContact = {
      name : this.state.name,
      phone : this.state.phone
    };
    this.props.onCreate(newContact);
    this.setState({
      name:'',
      phone:''
    });
  }
  render() {
    return (
        <div>
          <h2>Create new user</h2>
          <p>
            <input
              type="text"
              name="name"
              placeholder="name"
              onChange={this._inputChange}
            />
            <input
              type="text"
              name="phone"
              placeholder="phone"
              onChange={this._inputChange}
            />
          </p>
          <button onClick={this._buttonClick}>Create new user</button>
        </div>
    );
  }
}
 
ContactCreate.defaultProps = {
  onCreate: () => { console.error('error') }
};
 
ContactCreate.propTypes = {
  onCreate : PropTypes.func
};
cs


코드의 15번째 줄에서 44번째 줄의 button이 클릭되었을 때 실행될 메소드를 정의 했습니다.

16번째 줄의 newContact 객체는 20번째 줄에서 인자로 들어갈 내용입니다.

Contact 컴포넌트의 state.contactData 에 들어가야할 데이터 형식에 맞춘 것 입니다.

그리고 20 번째 줄에서 props.onCreate 메소드를 실행해 Contact 컴포넌트의 state.contactData 에 데이터를 추가합니다.

(엄밀히 말하면 데이터를 추가한 새로운 배열로 Contact 컴포넌트의 state.contactData 를 대체해 주는것입니다.)

그리고 21번째 줄에서 ContactCreate 의 state를 초기화 시켜줍니다.


프롭스가 들어왔으므로 기본값 설정, 타입검증 객체가 들어옵니다. (50, 54 번째 줄)


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



input 태그에 입력할 때마다 state 가 콘솔창에 찍힙니다.(하나 모자라게 찍히는 이유는 setState 가 비동기이기 때문)


버튼을 누르면 위와 같이 목록에 peter 가 추가된 것을 볼 수 있습니다.

*버튼을 눌러 추가한 다음에 input 창이 그대로 남아있지 않고 비워지게 하려면 각각 input 태그에 value={this.state.name}, value={this.state.phone} 프로퍼티를 추가해 줍니다.

(_buttonClick 메소드에서 데이터를 추가한 다음 state 를 초기화 하기 때문)


최종적인 코드는 다음과 같습니다.


(./src/components/ContactCreate.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
import React, { Component } from 'react';
import PropTypes from 'prop-types';
 
export default class ContactCreate extends Component {
  state = {
    name:"",
    phone:""
  }
  _inputChange = (e) => {
    let nextState = {};
    nextState[e.target.name= e.target.value;
    this.setState(nextState);
    console.log(this.state);
  }
  _buttonClick = () => {
    const newContact = {
      name : this.state.name,
      phone : this.state.phone
    };
    this.props.onCreate(newContact);
    this.setState({
      name:'',
      phone:''
    });
  }
  render() {
    return (
        <div>
          <h2>Create new user</h2>
          <p>
            <input
              type="text"
              name="name"
              placeholder="name"
              onChange={this._inputChange}
              value={this.state.name}
            />
            <input
              type="text"
              name="phone"
              placeholder="phone"
              onChange={this._inputChange}
              value={this.state.phone}
            />
          </p>
          <button onClick={this._buttonClick}>Create new user</button>
        </div>
    );
  }
}
 
ContactCreate.defaultProps = {
  onCreate: () => { console.error('error') }
};
 
ContactCreate.propTypes = {
  onCreate : PropTypes.func
};
cs

*(13번째 줄의 콘솔로그는 개발중 확인의 용도이므로 빼도 상관없습니다.)


(./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
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]
    });
  }
  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]}
        />
        <ContactCreate
          onCreate={this._contactCreate}
        />
      </div>
    )
  }
}
 
export default Contact;
cs


정리하자면, 데이터 추가 기능은 input 태그를 입력하면 ContactCreate 의 state가 수정되고, 버튼을 누르면 ContactCreate 의 state 를 Contact  컴포넌트의 state.contactData 로 추가하는 것 입니다.


다음 포스팅에서는 데이터를 삭제, 수정하는 기능을 추가해보겠습니다.

감사합니다.


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

https://velopert.com/reactjs-tutorials


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

Comments