함께 성장하는 프로독학러

5-2. 전화번호부 어플리케이션 만들기 - Contact 선택기능 구현 본문

Programming/react.js

5-2. 전화번호부 어플리케이션 만들기 - Contact 선택기능 구현

프로독학러 2018. 4. 19. 22:15

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


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

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

<Contact application - velopert>


이번 포스팅에서는 저번 포스팅에서 구현한 Contact 컴포넌트의 목록(ContactInfo 컴포넌트)을 클릭하면 아래에서 세부 정보를 표현해 주는 기능을 구현하겠습니다.


먼저 Contact 클래스의 state 에 selectedKey 속성을 초기화 하겠습니다.

state.selectedKey 는 선택된 ContactInfo 컴포넌트가 어떤 컴포넌트인지 구분하는 역할을 합니다.


(./src/components/Contact.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
...
 
class Contact extends Component {
     state = {
        keyword : '',
        selectedKey : -1,
        contactData : [
            {...}, {...}, ...
        ]
    }
}
 
...
cs


위의 코드의 6번째 줄에서 앞으로 사용할 selectedKey 를 -1로 초기화 했습니다.

(ContactInfo 컴포넌트를 만든 배열의 index 값을 사용할 예정이므로 0부터 시작한다. 아무것도 선택되어 있지 않은 상태로 초기화 하려면 -1.)


state 에 selectedKey 를 초기화 했으면, Contact 클래스 안에 _nameClick 메소드를 정의해 줍니다.

_nameClick 메서드는 ContactInfo 컴포넌트를 클릭했을 때 실행되는 메소드로, 인자로 전달받은 값을 state.selectedKey 값으로 변경해 주는 역할을 하는 메소드입니다.


(./src/components/Contact.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
...
 
class Contact extends Component {
...
    _nameClick = (key) => {
        this.setState({
              selectedKey : key
        });
      }
...
}
 
...
cs


_nameClick 메소드의 인자로 key 값을 주었습니다. _nameClick 메소드는 onClick 이벤트의 함수로 지정될 메소드 인데 인자가 들어올 수 있을까요? 없습니다.

onClick 이벤트는 함수를 실행하는 코드를 넣는것이 아닌 실행될 함수를 지정해야하는 속성입니다.

(함수를 지정하지 않고 실행하는 코드를 넣으면 렌더링이 될 때마다 함수가 실행됩니다. 만약 _nameClick 처럼 state 를 변경하는 메소드라면 무한실행되게 됩니다. 리액트는 state 가 변경될 때마다 컴포넌트를 다시 렌더링 하기 떄문이죠.)

그렇다면 _nameClick 에는 어째서 인자가 들어왔을까요? 

인자가 필요한 함수를 onClick 과 같은 이벤트에서 사용할 수 없는 걸까요?

다음 코드를 통해 이 궁금증을 해결해 보도록하죠.


(./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
...
 
class Contact extends Component {
...
    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)}
                     />);
            }
          );
        }
    ...
    }
...
}
 
...
cs


위의 코드의 19번째 줄에 ContactInfo 컴포넌트에 onClick 이벤트를 정의 했습니다.

겉으로 보기에는 컴포넌트에 이벤트를 지정한 것 같지만 아쉽게도 리액트 컴포넌트에는 이벤트를 지정할 수 없습니다.

따라서 위의 onClick은 이벤트가 아닌 프롭스로 전달된 것입니다.

따라서 ContactInfo 컴포넌트에서 이를 HTML 태그를 이용해 이벤트로 연결시켜 줘야 합니다.


여기서 주목해야 할 점은 onClick 의 값으로 들어가는 _nameClick 메소드가 인자를 가지고 실행되야 하는 메소드 라는 점입니다.

하지만 onClick 과 같은 이벤트의 값으로는 함수를 실행하는 것이 아니라 함수를 지정해줘야 합니다.

(여기선 프롭스로 전달 됐지만 ContactInfo 에서 이벤트로 사용할 것이기 때문에 함수를 지정해 줘야하는건 마찬가지다)

만약 함수를 지정하는 것이 아닌

onClick={this._nameClick(i)}

와 같이 함수를 실행하면 어떻게 될까요?

우리가 만든 _nameClick 메소드는 state 를 변경하는 메소드 입니다.

리액트는 state 가 변경되면 리랜더링을 하게되죠.

만약 onClick={this._nameClick(i)} 처럼 이벤트에 함수를 실행한다면, 이는 랜더링이 될 때 함수가 실행되는 것을 의미합니다.

함수가 실행되면 스테이트가 변경되고, 스테이트가 변경되면 리랜더링되고, 랜더링되면서 함수가 실행되고...

무한 루프에 빠지게 됩니다.

이런 상황을 방지하기 위해 위 코드의 19번째 줄과 같이 arrow function 을 이용해 익명함수를 '지정'해 줍니다.

익명함수를 지정함으로 인자가 필요한 함수를 이벤트가 발생할 때 실행시킬 수 있습니다.


이제 ContactInfo 컴포넌트에 onClick 프롭스로 전달된 익명함수를 ContactInfo 컴포넌트의 div 태그에 이벤트로 전달해 주도록 하겠습니다.


(./src/components/ContactInfo.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { Component } from 'react';
import PropTypes from 'prop-types';
 
export default class ContactInfo extends Component {
    render() {
        return (
            <div onClick={this.props.onClick}>{this.props.contact.name}</div>
        );
    }
}
 
ContactInfo.defaultProps = {
  onClick: ()=>{
    console.log('there is onClick function');
  }
}
 
ContactInfo.propTypes = {
  onClick : PropTypes.func.isRequired
};
cs


코드의 7번째 줄에 div 태그에 onClick 이벤트로 onClick 프롭스로 전달된 익명함수를 지정했습니다.

프롭스가 들어왔으므로 기본값과 타입검증하는 객체를 만들어줬습니다.

(프롭스가 들어오면 기본값객체와 타입검증 객체를 만드는 것을 생활화 합시다!)


여기까지 Contact 의 목록을 클릭하면 state.selectedKey 가 변경되는 것을 구현했습니다.

그럼 이제부터 Contact 컴포넌트의 state.selectedKey 를 이용하여 상세정보를 보여주는 컴포넌트를 만들어 보도록하겠습니다.


components 폴더에 ContactDetail.js 파일을 만듭니다.


(./src/components/ContactDetail.js)

1
2
3
4
5
6
7
8
9
10
11
import React, { Component } from 'react';
 
export default class ContactDetail extends Component {
    render() {
        return (
          <div>
            <h2>Detail information</h2>
          </div>
        );
    }
}
cs


일단은 위의 코드처럼 간단하게 컴포넌트를 정의해 줍니다.

그리고 Contact 컴포넌트의 렌더에서 ContactDetail 컴포넌트를 추가시켜줍니다.


(./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
...
import ContactDetail from './ContactDetail';
 
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 />
              </div>
        )
    }
..
}
...
cs


위의 코드에서 두 번째 줄과 같이 ContactDetail 컴포넌트를 임포트해 주고, 16번째 줄에서 컴포넌트를 렌더링 합니다.


이제 상세정보를 보여주기 위한 props 를 ContactDetail 에 전달해보도록 하겠습니다.

먼저 선택된 state.contactData 를 전달해 줍니다.

목록에서 한 사람을 클릭하면 그 사람에 해당하는 컴포넌트의 key 값이 Contact 컴포넌트의 state.selectedKey 값으로 변경되므로, 전달될 상세 정보는 this.state.contactData[this.state.selectedKey] 가 됩니다.

그리고 ContactDetail 컴포넌트에서 selectedKey 가 -1이 아닐경우에만 상세정보를 보여줄 것이기 때문에 isSelected 프롭스도 지정해 줍니다.

조건문으로 사용할 것이고, 조건문의 값이 true 일 때 상세뷰를 보여줄 것이므로 this.state.selectedKey != -1 를 전달합니다.

(ContactInfo 가 클릭된 상황이라면 selectedKey 의 값은 -1 이 아니다)


따라서 다음과 같이 프롭스를 전달합니다.


(./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
...
import ContactDetail from './ContactDetail';
 
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]}
                 />
              </div>
        )
    }
..
}
...
cs


ContactDetail 컴포넌트에서 프롭스를 전달받았으므로 전달받은 isSelected 값에 따라 상세뷰를 보여줄지 안 보여줄지 정하는 코드를 작성해 봅시다.


(./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
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}
        </div>
      );
    }
}
 
ContactDetail.defaultProps = {
  isSelected : false,
  contact : {
    name : '',
    phone : ''
  }
}
 
ContactDetail.propTypes = {
  isSelected : PropTypes.bool.isRequired,
  contact : PropTypes.object.isRequired
};
cs


코드의 6번째 줄에서 details 라는 상수로 ContactInfo 가 선택 됐을 때(props.isSelected 가 -1 이 아닌 상태) 렌더링할 요소를 정의했습니다.

10번째 줄에서는 선택되지 않은 상태에서 보여줄 요소를 정의 했습니다.


그리고 14번째 줄에서 삼항 조건 연산자를 이용해 선택 됐을 때 details 를, 선택되지 않았을 때 blank 를 렌더링 하도록 했습니다.

(삼항 조건 연산자는 if 조건문의 축약형입니다. 이에 익숙하지 않으신 분은 아래의 링크를 참고해 주세요)

<ES6 의 삼항 조건 연산자>


코드의 20번째 줄에서 들어온 프롭스의 기본값을 설정해 주었습니다. 이를 설정하지 않으면 선택되지 않은 상태에서 오류가 발생하기 때문에 기본값을 설정해 줘야 합니다.

프롭스가 들어왔기 때문에 28번째 줄에서는 프롭타입 검증 객체를 만들어 줍니다.


여기까지 완료됐으면 브라우저에서 제대로 작동하는지 확인해 봅시다.



아무것도 선택하지 않은 상태에서는 위에서 정의한 blank 가 렌더링 됩니다.

(Not selected)



항목이 선택된 상태라면 해당 항목의 정보를 보여줍니다.


정리하자면, ContactInfo 가 선택되면 Contact 컴포넌트의 state.selectedKey 가 선택된 항목의 key 값으로 변경되고, Contact 컴포넌트는 ContactDetail 컴포넌트에 selectedKey이 -1이 아니라는 조건문(참이 되는 것은 선택된 상태, 프롭스명은 isSelected)과 해당 항목의 contactData 객체를 프롭스(contact)로 전달합니다. ContactDetail 컴포넌트는 전달 받은 isSelected 프롭스(selectedKey이 -1이 아니라는 조건문)를 검사하여 true 이면  contact 프롭스를 뿌려주고 false 라면 Not selected 를 뿌려주는 것 입니다.


복잡한 듯 보이지만 결국엔 선택된 정보를 의미하는 Contact 컴포넌트의 state.selectedKey 로 제어하는 방식입니다.


여기까지 Contact 의 선택기능 구현을 해 보았습니다.

다음 포스팅에서는 ContactDetail 컴포넌트 안에서 contactData를 수정하거나 삭제하는 기능을 구현해 보도록하겠습니다.

감사합니다.


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

https://velopert.com/reactjs-tutorials


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

Comments