React 기억법(4) - React 필수요소 props, state

November 18, 2017

이전 포스팅을 보시고, 이번 포스팅을 보셔도 좋을 듯 합니다.
React 기억법(1) - 리액트가 뭐지?
React 기억법(2) - 컴포넌트화? 그리고 리액트 설치
React 기억법(3) - React 컴포넌트의 LifeCycle

이번에는 React에 관련되어 중요한 개념중 하나인, props 및 state에 관련되어 살펴볼까 합니다. props 및 state는 React의 컴포넌트 객체에서 DOM 객체를 제어할 때에 꼭 필요한 개념중 하나입니다. 이 두가지 개념을 이용하여 자식컴포넌트에서 부모 컴포넌트 객체에 데이터를 받아와서 출력해주고, 출력해주는 부분들을 수정할 수 있도록 만들어 줄 수도 있습니다.

1. props와 state?

props와 state는 기본적으로 다른 속성을 지니고 있습니다. 컴포넌트에 대해 영향을 미치는 객체인 것은 동일하나, 그 쓰임새가 전혀 다른 쓰임새를 지닙니다. React의 특징에서 살펴보았듯이, React는 데이터가 부모 컴포넌트에서 자식컴포넌트로 데이터흐름이 이루어지는 단방향 데이터흐름이 특징입니다. 부모 객체는 자식객체에 props 값을 전달하며, props 값을 받은 자식객체는 이에 관한 부분들을 렌더링 하며, state라는 자체 값을 포함하여 데이터를 변경해 주고, 다시 렌더링 해줄 수 있습니다.

1) props

props는 개발에서 많이들 사용되는 용어인 프로퍼티(properties)의 줄임말입니다. props는 React에서는 사용자가 컴포넌트에 전달해서 보관하길 원하는 데이터입니다. 즉, 컴포넌트 내에서 데이터가 보관되면, 이 데이터는 수정되지 않고 보존되어야 하는 법칙이 성립됩니다. 만약 props의 값을 변경하고자 할때에는 컴포넌트 내부가 아닌, 부모 컴포넌트에서 이에 대한 부분이 변경되어야 합니다.

간단하게 예제를 살펴보겠습니다.

var post =  { title : '테스트 타이틀' }
<Postdiv post={ post } />

간략하게 부모객체에서 자식객체에 props를 넘겨주는 예시입니다. 이처럼, 부모 객체에서는 자식객체에 post라는 데이터를 props 형태로 전달 해 줄수 있습니다. 이는 부모 객체에서 넘겨주는 데이터이기 때문에, 실제 사용하는 컴포넌트 내에서 props의 변경은 원칙적으로 금지되어 있습니다.

props의 설정은 propTypes를 통해 객체를 설정하거나, getDefaultProps를 통해 props값을 받아올 수 있습니다.

var post = React.createClass({
  propTypes : {
      title : React.PropTypes.String,
      number : React.PropTypes.Integer,
      obj : React.PropTypes.object,
      func : React.PropTypes.func
  }
});

React 컴포넌트는 propTypes를 통해 this.props의 설정객체를 정의할 수 있습니다. props의 설정은 자료형을 구분할 수 있으며, 뒤에 isRequired를 통해 필수 항목인지에 대한 부분도 설정이 가능합니다.

var post = React.createClass({
  getDefaultProps : function () {
      return {
          posts : []
      };
  }
});

또한 props는 getDefaultProps를 통해 기본 props값을 제공할 수 있습니다. 그러나 이 방식은 컴포넌트가 생성될때 필수로 설정해야 하는 값이 아닌 경우에만 사용이 가능합니다.

2) state

React 컴포넌트는 컴포넌트의 상태를 저장할 수 있습니다. props와의 차이점이라면, state는 컴포넌트 내부에 존재하고 있기 때문에, 상태값 변경이 가능하다는 것입니다. 3번째 포스팅에 Life cycle에 관련된 함수에서, getInitialState를 통해서 React 컴포넌트의 상태값을 초기화 할 수 있었습니다.

var post = React.createClass({
    getInitialState : function () {
        return {
            title : '테스트타이틀'
        }
    },
    render : function () {
        return (
            <div>
                <p>{ this.state.title }</p>
            </div>
        )
    }
});

state에 관련된 간략한 예시입니다. 우리는 post라는 컴포넌트에서 getInitialState를 통해서 컴포넌트 내에서 쓰일 state값을 리턴받습니다. getInitialState는 return을 통해 state 초기값을 반환해 주게 되는데, 이렇게 함으로써 컴포넌트 내부에서는 this.state를 통해 상태값을 제어할 수 있게 됩니다.

render 함수에서는 바로 getInitialState에서 받은 state값 중, title을 출력하고 있음을 알 수 있습니다. 이렇게 초기화된 state값을 출력할 수 있지만, 중간에 state값을 바꿔줄 수도 있습니다.

var post = React.createClass({
    getInitialState : function () {
        return {
            title : '테스트타이틀'
        }
    },
 
    handleChange: function () {
        this.setState({title:'테스트타이틀1'});
    },
 
    render : function () {
        return (
            <div>
                <p>{ this.state.title }</p>
                <input type="text" onChange={ this.handleChange } />
            </div>
        )
    }
});

위의 예제에서는 input에서 onChange 이벤트를 받으면, 이 이벤트를 통해서 this.state의 title 값을 변경해주게 됩니다. 이처럼, 컴포넌트 내의 state를 변경해 주기 위해서, 개발자들은 this.setState() 메소드를 통해서 state값을 변경해 줄 수 있습니다. 위의 예제처럼 onChange를 해서 this.setState를 통해 title을 '테스트타이틀1'로 적용하면, render 내의 p태그에서는 state 변경에 따라서 DOM 객체가 변경된 state값으로 렌더링 하게 됩니다.

2. Example

위와 관련된 내용을 좀더 심도깊게 알아보기 위해서, ParentComp와 ChildComp가 있다고 가정합니다. (말 그대로 부모 컴포넌트와 자식 컴포넌트 입니다.) 구현하고자 하는 컴포넌트의 구조는 다음과 같습니다.

Parent & Child 구조

우리가 작업할 내용은 간단합니다. 부모 컴포넌트(ParentComp)는 post라는 데이터를 생성해서 자식 컴포넌트(ChildComp)에 전달해주며, 부모에서 전달해준 post 데이터를 이용하여 자식 컴포넌트는 컴포넌트 내부 상태값(this.state)를 부모에서 받은 post 데이터를 기반으로 생성한 후, input 태그 및 textarea 태그에 설정해 줍니다.

input 태그와 textarea태그는 onChange 이벤트가 발생 시, setState를 통해서 자식 컴포넌트(ChildComp) 내부에서 사용되는 this.state 값들을 변경해 줍니다.

ParentComp와 ChildComp를 함께 사용되는 예제를 확인해 보겠습니다.

var React = require('react');
 
var ParentComp = React.createClass({
  /**
   *  getInitialState : component의 state 값을 초기화합니다.
   */
  getInitialState : function () {
    return {
      post : null
    }
  },
  /**
   *  componentWillMount : render가 실행 되기 전, data를 제어합니다.
   */
  componentWillMount : function () {
    var obj = {
      title : '테스트 타이틀',
      desc : '테스트 컨텐트입니다.'
    }
 
    //this.setState를 통해서 state 값을 변경해 줍니다.
    this.setState({post : obj});
  },
  /**
   * render : 가상 DOM 객체를 복제합니다.
   * @returns {XML}
   */
  render : function () {
    return (
      <div className="ParentComp">
        <childComp post={ this.state.post } />
      </div>
    )
  }
});
 
var childComp = React.createClass({
  /**
   *  propTypes : component의 기본값을 불러옵니다.
   */
  propTypes : {
    post : React.PropTypes.object.isRequired
  },
  /**
   *  getInitialState : component의 state 값을 초기화합니다.
   */
  getInitialState : function () {
    return {
      title : this.props.post.title,
      desc : this.props.post.desc
    }
  },
  /**
   *  title을 변경합니다.
   */
  changeTitle : function (e) {
    this.setState({ title : e.target.value });
  },
  /**
   * 내용을 변경합니다.
   * @param e
   */
  changeDesc : function (e) {
    this.setState({ desc : e.target.value });
  },
  /**
   * render : 가상 DOM 객체를 복제합니다.
   * @returns {XML}
   */
  render : function () {
    return (
      <div className="child_comp">
        <input type="text" value={ this.state.title } onChange={ this.changeTitle }/>
        <textarea value={ this.state.desc } onChange={ this.changeDesc } />
      </div>
    );
  }
});
 
React.render(
  <ParentComp />,
  document.getElementById('app')
);

간략하게 컴포넌트의 Life Cycle 내장함수를 활용하고, 전체 구조에 맞게 구현한 소스코드입니다. 하나하나 살펴보도록 하겠습니다.

1) ParentComp

부모 컴포넌트에서는 render 함수가 실행되기 전에 getInitialState를 통해 this.state에 post라는 데이터를 반환시켜 줍니다. 그다음 componentWillMount 함수를 통해 this.state에 title과 desc가 포함된 object를 setState를 통해 할당해 줍니다.

getInitialState : function () {
    return {
      post : null
    }
},
componentWillMount : function () {
    var obj = {
      title : '테스트 타이틀',
      desc : '테스트 컨텐트입니다.'
    }
 
    //this.setState를 통해서 state 값을 변경해 줍니다.
    this.setState({post : obj});
},

부모컴포넌트는 이렇게 this.state에 할당된 post 데이터를 자식 컴포넌트에 post라는 이름으로 props 데이터를 전달합니다.

render : function () {
    return (
      <div className="ParentComp">
        <childComp post={ this.state.post } />
      </div>
    )
}

2) ChildComp

자식 컴포넌트에서는 부모 컴포넌트에서 받은 post라는 데이터를 propTypes를 통해 설정해줍니다.

propTypes : {
    post : React.PropTypes.object.isRequired
},

이때, propTypes에서는 props데이터로써 설정되어야 할 자료형과, 필수라면 isRequired를 통해 필수 항목을 설정해 줄 수 있습니다.

또한번, 자식컴포넌트 내에서 부모에서 받은 props 데이터를 제어해 주기 위해 this.props를 통해 설정해 줍니다.

getInitialState : function () {
    return {
      title : this.props.post.title,
      desc : this.props.post.desc
    }
},

이렇게 되면, props를 변경시키지 않고도, this.state에 props에 관련된 데이터를 저장함으로써, 자식 컴포넌트 내부에서 변경이 가능해 집니다.

변경 가능한 this.state를 각각의 input 및 textarea에 할당해 줍니다.

render : function () {
    return (
      <div className="child_comp">
        <input type="text" value={ this.state.title } onChange={ this.changeTitle }/>
        <textarea value={ this.state.desc } onChange={ this.changeDesc } />
      </div>
    );
}

각각의 input 및 textarea는 onChange라는 change 이벤트를 통해서 각각의 함수에서 setState를 통해 title과 desc를 변경해 줄수 있도록 설정했습니다.

/**
 *  title을 변경합니다.
 */
changeTitle : function (e) {
  this.setState({ title : e.target.value });
},
/**
 * 내용을 변경합니다.
 * @param e
 */
changeDesc : function (e) {
  this.setState({ desc : e.target.value });
},

전체 코드에 대한 설명이 끝났습니다. 이를 통해서 부모객체에서 받은 post 데이터를 자식컴포넌트에서 state형식으로 저장하고, 각각의 체인지 이벤트를 통해 상태값을 변경해 줄 수 있습니다.

3. 맺음말

컴포넌트 제어를 위해 필요한 props와 state에 대해 알아보았습니다. props는 부모 컴포넌트에서 자식 컴포넌트의 데이터와 설정을 전달해 줄 수 있으며, 따라서 props를 컴포넌트 내에서 변경하는 일은 원칙적으로 금지되어 있습니다. 이를 위해서, 컴포넌트 내부에서 상태값을 제어할 수 있는 state를 사용하는데, state값은 this.state값을 직접 변경하는 것이 아닌, this.setState() 메소드를 통해서 상태값을 변경해 주어야 합니다.

예제나 설명이 부족할 수 있습니다. 궁금하신 사항은 댓글 부탁드리며, 더 많은 정보도 공유 할 수 있었으면 합니다.
감사합니다. :)

...