본문 바로가기
Front-end Framework/React (Reactjs)

4. 컴포넌트

by kellis 2020. 10. 19.

이 글에서는 컴포넌트에 대해 다루어보겠습니다. 컴포넌트와 속성(state, props)에 대한 내용으로 구성됩니다.

 

1. 컴포넌트

 

대부분의 UI는 여러 개의 HTML 요소로 이루어져 있습니다. 계층적으로 더 복잡한 구조를 만드는 방법은 엘리먼트를 중첩하는 것입니다. 그런데 엘리먼트를 중첩하다 보면, 곧 입력할 엘리먼트가 굉장히 많다는 문제를 발견하게 될 것입니다. 그래서 React는 컴포넌트 기반 아키텍처를 활용합니다. 컴포넌트 클래스를 이용하면 기능을 느슨하게 결합된 부분으로 분리하여 코드를 재사용할 수 있습니다. 여기서 컴포넌트 클래스는 컴포넌트라고 부르기도 합니다. (웹 컴포넌트와 혼동하지 말아야 합니다)

이 컴포넌트 클래스의 인스턴스인 사용자 정의 엘리먼트를 생성하면, 이식 가능한 클래스(구성할 수 있고 재사용할 수 있는 컴포넌트)에 논리를 추상화하고 캡슐화할 수 있습니다. 이런 추상화는 거대하고 복잡한 애플리케이션에 UI를 재사용할 수 있도록 만들어주며, 심지어 다른 프로젝트에서도 재사용할 수 있도록 해줍니다. 

  

앞에서 사용했던 APP 예제를 다시 한 번 보겠습니다. 

 

[src/App.js]

import React, { Component } from 'react';
   
class App extends Component {
  render() {
    return (
        <div>
            <h1> 
                Hello, world!
            </h1>
        <div>
    );
  }
}
   
export default App;

참고. 앞의 예제에서와 달리 중첩 엘리먼트로 만들기 위해 <h1>을 <div>로 감싸주었습니다.


ES6 문법을 이용하면 React.Component 클래스를 상속받아서 React 컴포넌트 클래스를 생성할 수 있습니다. 새로운 컴포넌트 클래스를 구현할 때는 반드시 render() 메서드를 작성해야 합니다. 이 메서드는 다른 사용자 정의 컴포넌트 클래스나 HTML 태그로 만든 React 엘리먼트를 반환해야 합니다. 앞서 JSX 장에서 보았듯이 중첩하는 것 역시 가능합니다.  index페이지에서 ReactDOM.render()와 유사하게 컴포넌트 클래스의 render() 메서드 역시 엘리먼트 하나만을 반환합니다. 여러 개의 동일 계층 엘리먼트를 반환하는 방법에 대해서는 앞장의 단일 루트 노드에서 설명했습니다.  그럼 이제 index.js를 조금 수정해 보겠습니다. 

 

[src/index.js]

import React, {Fragment} from 'react';
import ReactDOM from 'react-dom';
import App from './App';
 
ReactDOM.render(
    <Fragment>
        <App />
        <App />
        <App />
    </Fragment>,
    document.getElementById('root')
);

App이라는 컴포넌트를 생성하였기 때문에 위와 같이 재사용이 가능해집니다. 이것이 앞선 장에서 h1태그의 내용을 곧바로 index.js에서 render()하는 것을 권장하지 않은 이유입니다.

컴포넌트를 사용하게 되면 재사용성뿐민 아니라, 컴포넌트가 제공하는 라이프사이클 이벤트(lifecycle), 상태(state), DOM 이벤트 등 여러 가지 기능을 활용할 수 있습니다.


2. React 컴포넌트의 속성(Props)

 

(1) props?

바로 위의 예제에서 App 엘리먼트를 세 번 출력했습니다. 그렇다면 이 APP 엘리먼트 내부의 내용이나 동작을 각 엘리먼트 별로 다르게 변경하고 싶다면 어떻게 해야 될까요? 컴포넌트의 속성(properties)을 통해 구현할 수 있습니다.  속성은 엘리먼트 내의 변경할 수 없는 값이라고 생각하면 될 것입니다. 여기서 중요한 것은 속성이 컴포넌트 내부에서는 변경할 수 없는 값이라는 점입니다.  부모 컴포넌트는 자식의 생성 시점에 속성을 할당합니다. 자식 엘리먼트에서는 속성을 수정하지 않아야 합니다. 

<TAG_NAME PROPERTY_NAME=VALUE/>

위와같이 속성명(PROPERTY_NAME)에 값(VALUE)을 입력하는 방식으로 속성을 전달할 수 있습니다.

React의 속성은 HTML속성을 작성하는 것과 비슷합니다. React의 속성을 사용하는 이유는 다음과 같습니다.

  • 일반적인 HTML 요소의 속성에 값을 할당 : href, title, style, class 등
  • React 컴포넌트 클래스의 자바스크립트 코드에서 this.props의 값을 사용. 예: this.props.PROPERTY_NAME(PROPERTY_NAME을 임의의 값으로 정할 수 있음)
Object.freeze()와 Object.isFrozen()
내부적으로 React는 ES5 표준인 Object.freeze()를 사용하여 this.props 객체를 불변 객체로 만듭니다. 객체에 Object.freeze()가 적용되었는지 확인하려면 Object.isFrozen() 메서드를 사용할 수 있습니다. 이에 대한 자세한 내용은
Object.freeze() : https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze
Object.isFrozen() : https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/isFrozen

에서 확인하실 수 있습니다.

정리하면 같은 컴포넌트에 다른 속성값을 입력하면 컴포넌트가 렌더링 한 엘리먼트의 모습을 다르게 할 수 있습니다. 좀 더 이해를 돕기 위해 App 예제를 수정하여 보겠습니다.

 

[src/index.js]

import React, {Fragment} from 'react';
import ReactDOM from 'react-dom';
import App from './App';
 
ReactDOM.render(
  <Fragment>
  <App id="React" title="React" frameworkName="React.js"/>
  <App id="Vue" title="Vue" frameworkName="vue.js" />
  <App id="Angular" title="Angular" frameworkName="Angular2"/>
  </Fragment>,
  document.getElementById('root')
);

[src/App.js]

import React, { Component } from 'react';
 
class App extends Component {
  render() {
    return (
      <div>
        {
            //<h1 title={this.props.title} id={this.props.id}>
        }
        <h1 {...this.props}>
          Hello, {this.props.frameworkName} world react!
        </h1>
      </div>
    );
  }
}
export default App;

h1태그안에 세 가지 속성이 추가되었습니다.

  • id : HTML 표준 속성인 id와 일치. React가 자동 렌더링
  • framework : h1의 표준 속성은 아니지만, 제목 텍스트로 표시할 때 사용하는 값
  • title : HTML 표준속성인 title과 일치. React가 자동 렌더링

 

주석으로 기재한 속성 전달 방법은 안티 패턴입니다. HTML 표준 속성의 값에 대해서 React는 자동 렌더링을 수행합니다. 따라서 모든 속성을 전달해야 한다면, 생략 부호(...)처럼 생긴 펼침 연산자를 사용하여 속성을 전달할 수 있습니다. (전달된 속성 중 일부만 렌더링해야 한다면, 개별 속성을 따로 전달해야 할 것입니다)

속성이 제대로 전달된 것을 볼 수 있습니다. 위 예제는 속성이 전달되는가를 확인하기 위해 만든 것으로 다소 어색하게 보일 수 있습니다. 실제 애플리케이션에서 어떤 식으로 이 속성이 적용되는지는 React의 마지막 장인 예제 애플리케이션에서 확인해보실 수 있습니다.

 

참고. 사용자 정의 속성의 렌더링

위 예제를 돌려보면, 브라우저의 개발자 도구 콘솔에서 아래와 같은 warning이 출력되는 것을 볼 수 있습니다.

React 버전 16 이전에는 frameworkName과 같은, HTML 표준 속성이 아닌 사용자 정의 속성(비표준 속성)을 렌더링 하지 못하였습니다. 그래서 data-를 속성 이름 앞에 붙여주어야만(data-frameworkName) 렌더링이 되었는데 16 버전부터는 위의 Warning(비표준 속성을 적용하려면 소문자로만 작성하여야 하며, 부모 컴포넌트에 작성해야 하는 속성을 실수로 작성했다면 제거하라)과 함께 자동으로 렌더링 하도록 변경되었습니다. 결과 화면에서 frameworkName이 frameworkname으로 변경되어 적용된 것을 확인하실 수 있을 것입니다. 그러므로 만약 비표준 속성을 태그에서 사용하고자 할 때에는 data- 를 사용하여 네이밍 컨벤션을 지켜줄 것을 권고합니다.

 


(2) 기본값 지정

defaultProps를 이용하여 props의 디폴트 값을 지정해 줄 수 있습니다. 컴포넌트 인스턴스가 만들어질 때 호출되는 것이 아닌, 컴포넌트가 정의될 때만 호출되므로 유의해야 합니다.

 

[getDefaultProps()]

import React, {Component} from 'react'
  
class DefaultProps extends Component{
    render(){
        return <div>Hello {this.props.name}</div>
    }
}
  
DefaultProps.defaultProps = {
    name: 'React'
};
  
export default DefaultProps;

참고. static defaultProps = { ... }로 클래스 내부에 작성하여도 무관합니다. 

setProps & replaceProps

이전의 버전에서는 위 두가지 메서드를 통해 props를 변경할 수 있었습니다. 하지만 최신 버전의 React에서는 더 이상 이 메서드들을 사용할 수 없고, 컴포넌트의 수명주기 동안 props가 변경되어서도 안됩니다.

(3) prop-types

props는 외부로부터 들어오는 값이므로 검증(validation) 작업이 필요합니다. 이를 위해서 react에서는 prop-types라는 npm을 사용할 수 있습니다. (v15.5부터 적용되었으며 그 이전에는 propTypes를 사용하였습니다. 15.4 이전 버전의 propTypes를 사용하는 경우 별도의 설치가 필요 없습니다. 페이스북에서 개발한 Flow를 사용하거나, MS의 TypeScript를 사용하는 것도 방법이 될 수 있습니다)

 

이를 사용하기 위해서는 먼저 설치를 해주어야 합니다. 

npm install --save prop-types
yarn add prop-types
참고. 별도의 설치 없이, html에 script로 추가하여 사용하는 방법도 있습니다. 
React팀은 난독화하지 않은 개발 모드와 난독화를 거친 프로덕션 모드 두 가지를 제공합니다.
  1. 개발 모드 : <script src="https://unpkg.com/prop-types@15.5.4/prop-types.js"></script>
  2. 운영 모드 : <script src="https://unpkg.com/prop-types@15.5.4/prop-types.min.js"></script>

설치가 끝나면 컴포넌트에서 아래와 같이 사용이 가능합니다.(기존 propTypes와 사용방법이 동일하며 import문이 필요하다는 차이점만 있을 뿐입니다)

 

[src/App.js]

import React, { Component } from 'react';
import PropTypes from 'prop-types'
 
class App extends Component {
  render() {
    return (
      <div>
        <h1 {...this.props}>
          Hello, {this.props.frameworkName} world!
        </h1>
      </div>
    );
  }
}
App.propTypes = {
  title: PropTypes.string,
  id: PropTypes.string,
  frameworkName: PropTypes.string.isRequired,
  onSubmit: PropTypes.func
}
export default App;

앞의 장에서 다루었던 App.js 예제에 추가해주었습니다. 직관적으로 세 속성 모두 string을 받는 것을 검증하며, frameworkName은 반드시 입력받아야 함을 검증합니다. 
PropTypes에 대한 자세한 내용은 여기에서 확인해 보시길 바랍니다.

 

위 예제 코드의 테스트를 위해서 index.js에서 frameworkName을 props로 넘겨주지 않으면 콘솔에 아래와 같이 출력됩니다. 

 

검증에 실패한 경우에도 화면은 제대로 출력되며, 에러가 발생하는 것이 아닌 console.warn으로 출력됩니다.

 

더보기

사용자 정의 유효성 검사

바로 위에서 살펴 본 타입 유효성 검사는 직접 정의하는 것도 가능합니다. 이를 구현하기 위해 Error 인스턴스를 반환하는 표현식을 생성해야 합니다. 간단한 예제는 아래와 같습니다.

...
propTypes = {
    email: function(props, propName, componentName){
        var emailRegularExpression = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i
        if(!emailRegularExpression.test(props[propName])){
            return new Error('Email validation failed!')
        }
    }
}
...

 


3. React 컴포넌트 메서드 생성

 

당연한 이야기겠지만, React 컴포넌트에 애플리케이션을 위한 메서드를 자유롭게 추가할 수 있습니다. React 컴포넌트는 클래스이기 때문입니다. 간단한 예제로 살펴보겠습니다. 

 

[src/InvokeMethod.js]

import React, {Component} from 'react';
class InvokeMethod extends Component{
  getUrl(){
    return 'http://google.com';
  }
  render(){
    return (
      <div>
        Google : <a href={this.getUrl()}>{this.getUrl()}</a>
      </div>
    )
  }
}
export default InvokeMethod;

간단한 코드지만, render()외에도 임의의 메서드를 직접 만들 수 있다는 사실을 알 수 있습니다.  getUrl의 반환 값을 출력하려면 변수를 출력할 때와 마찬가지로 {}를 사용합니다. 즉, {}에서 컴포넌트 메서드를 직접 호출할 수 있는 것입니다. 너무 당연한 것이 아니냐고 생각할 수도 있지만, 컴포넌트 메서드는 React의 이벤트 핸들러를 이해하기 위한 기초로서 아주 중요하기 때문에 짚고 넘어가는 것입니다. 

 


4.  React 컴포넌트의 상태(State)

 

리액트 컴포넌트에서 다루는 데이터는 두 가지로 나뉩니다. props와 state입니다. props는 위에서 살펴보았으니 이번에는 state, 상태에 대해 살펴보도록 하겠습니다.  React에서 이 상태 객체는 아주 핵심적인 역할을 수행합니다.  React 팀에서는 "컴포넌트는 상태 머신(state machine)입니다."라고 말할 정도입니다. 

 

위에서 살펴 본 속성(props)은 현재 컴포넌트 내부에서는 수정할 수 없습니다. 속성은 컴포넌트 생성 시에 전달받는 값이기 때문입니다. 그렇다면 만약 뷰에서 어떠한 값을 입력받아, 이 값을 서버에서 조회하고 결괏값을 받았다고 가정해봅시다. 도대체 어디에 이 정보를 저장하여 뷰에 출력할 수 있을까요? 속성을 변경할 수 없다면, 어떤 방법으로 뷰를 갱신할 수 있을까요? 물론 서버에서 응답을 받을 때마다 새로운 속성으로 엘리먼트를 렌더링 하는 방법을 사용할 수도 있을 것입니다. 하지만 그런 경우 이에 대한 로직을 컴포넌트 외부에 작성해야 하므로 컴포넌트는 독립적으로 동작할 수 없게 됩니다. 그래서 사용하는 것이 바로 상태(state)입니다. 

상태(state)는 컴포넌트의 변경 가능한 데이터 저장소입니다. React 컴포넌트에 데이터를 저장하고 데이터의 변경에 따라 자동으로 뷰를 갱신하도록 하는 핵심 개념입니다. 본질적으로 상태를 변경하면 뷰에서 변경한 상태에 관련된 부분만 갱신됩니다. 이는 개요에서 설명했듯이 가상 DOM 덕분인데 React가 보정(reconciliation) 과정을 통해 변경할 부분을 결정하는 방식입니다. 

 

그럼 이제 예제를 통해 상태 객체를 다루어 보겠습니다. 아래 예제는 입력된 값에 따라 화면에 렌더링되는 문자를 변경하는 코드입니다. 

 

[src/ChangeState.js]

import React, {Component} from 'react';
 
class ChangeState extends Component{
  constructor(props){
    super(props)
    this.state = {input: ""}
  }
  changeInput(e){
    this.setState({input: e.target.value});
  }
  render(){
    return (
      <div>
        <input type="text" value={this.state.input} onChange={this.changeInput.bind(this)}/>
        &nbsp; <span>input : {this.state.input}</span>
      </div>
    )
  };
}
export default ChangeState;

상태 객체는 컴포넌트의 멤버 변수로, this를 통해 접근할 수 있습니다. 상태 객체에 접근하는 방식은 props와 크게 다르지 않습니다. 이 예제에서는 constructor와 setState에 대해 설명할 것이며, onChange의 이벤트 동작에 대해서는 이후의 글에서 설명하겠습니다.

  • constructor : render()에서 상태 데이터를 사용하려면 먼저 상태를 초기화시켜주어야 합니다. 초기 상태를 설정하기 위해 ES6 클래스의 생성자에서 this.state를 선언해주었습니다. 
    constructor 없이 setState를 사용하면 input이 null이므로 속성을 읽을 수 없다는 에러가 발생하게 됩니다. (그렇다고 render메서드 안에서 setState를 실행해서는 안됩니다. setState->render->setState...로 끊임없이 반복되어 오류를 발생시킵니다)

getInitialState() ?
getInitialState()와 constructor()는 상호 교환적입니다. constructor()는 ES6의, getInitialState()는 ES5의 문법으로 마찬가지로 각각은 React.Component와 React.createClass와 함께 사용됩니다. 무엇을 사용하건 개발자의 판단이며, ES6와 ES5의 장단점으로 귀결될 것이지만 React는 ES6에 더 가까워지고 있으므로 constructor를 사용하는 것을 권장합니다.
  • setState : 클래스 메서드인 this.setState(data,callback)를 사용하면 상태를 변경할 수 있습니다. this.state = {...} 혹은 this.state.input = ... 과 같이 사용할 수 없습니다. 이 메서드는 객체 리터럴 방식으로 객체를 받아 현재 state에 병합(merge)한 뒤 render()를 호출합니다. (이후에 React가 callback함수를 실행합니다) 

 

참고. constructor에서 super(props)를 작성하지 않으면 에러가 발생합니다. 그 이유는 Component를 상속한 상태에서 constructor를 작성하게 되면, 기존이 상속받은 생성자를 덮어쓰게 되기 때문입니다. 그러므로 React가 지니고 있던 생성자를 super를 통해 먼저 실행해 준 후 필요한 작업을 수행해야 정상적으로 동작하게 됩니다. 

 

더보기

setState()

state의 업데이트는 비동기로 실행됩니다. setState에 전달된 객체가 현재 상태에 병합되고 나면 reconciliation이 시작됩니다. 새로운 리액트 엘리먼트 트리를 만들고, 새 트리를 이전의 트리와 비교합니다. setState()에 전달된 객체를 기반으로 변경된 부분을 파악하여 DOM에 최종적으로 업데이트하게 됩니다. 즉, setState라는 이름처럼 단순하게 상태를 세팅하는 것만 하는 것이 아니라는 뜻입니다.

setState()는 데이터를 병합한다고 하지만 아예 객체가 바뀌어버리는 경우도 존재합니다.

 

----- 기존 state 데이터 -----
state = {
    number:0,
    foo: {
        bar: 0,
        count: 1
    }
}
----- state 변경 -----
this.setState({
    foo:{
        count:2
    }
})
----- 변경된 state 데이터 -----
state = {
    number:0,
    foo: {
        count: 2
    }
}

그러므로 이러한 경우에는 전개 연산자(기존 객체 안에 있는 내용을 해당 위치에 풀어준다는 의미)를 사용하여 주어야 합니다.

this.setState({
    number:0,
    foo:{
        ...this.state.foo,
        count:2
    }
})

이러한 작업이 매우 귀찮기 때문에 immer.js 또는 immutable.js를 사용하여 작업을 더 간단하게 만들 수도 있습니다.

 

참고. setState()는 메서드를 인자로 받을 수도 있습니다.

이 컴포넌트를 ReactDOM.reader에서 렌더링 해주면 아래와 같은 결과가 나타나게 됩니다. 

참고. props vs state

 


5. 상태 비저장 컴포넌트 

상태비저장 컴포넌트(stateless component)는 상태 객체가 없고, 컴포넌트 메서드나 다른 React의 라이프사이클 이벤트를 가지지 않습니다.(이에 대해서는 다음장에서 자세히 다루겠습니다) 즉 뷰를 렌더링 하는 것이 목적인 컴포넌트입니다. 다른 말로는 함수형 컴포넌트, 퓨어 컴포넌트라고 부르기도 합니다. 

상태 비저장 컴포넌트를 사용하는 이유는 다음과 같은 이유때문입니다. 

  • 예측가능성 : 출력을 결정하는 입력이 한 가지뿐이기 때문에  예측할 수 있다는 장점. 코드를 이해하기 쉽고, 유지보수와 디버깅이 편리.
  • 더 선언적이며 잘 작동한다 : 별도의 인스턴스를 생성하거나, 라이프사이클 메서드를 사용하지 않아도 됨.
  • 중복을 줄일 수 있다 : 더 나은 문법을 바탕으로 더 간결하게 컴포넌트 작성.

 

그래서 React 팀은 상태비저장 컴포넌트는 많이 사용할수록, 상태저장 컴포넌트는 더 적게 사용할 수록 좋다고 합니다. 


앞의 장에서 보았던 예제를 다시 한번 살펴보도록 하겠습니다. 

import React, {Component} from 'react'
 
class Stateless extends Component{
  render(){
    return <h1{...this.props}>Hello {this.props.frameworkName} world!</h1>
  };
}
export default Stateless;

이 예제는 상태 비저장 컴포넌트로, React는 함수형 스타일을 사용하여 더 간결하게 표현할 수 있도록 합니다. 

const Stateless = function(props){
    return <h1{...props}>Hello {props.frameworkName} world!</h1>
}
----- 화살표 함수 사용가능 -----
const Stateless = (props) => return <h1{...props}>Hello {props.frameworkName} world!</h1>

이 방식을 사용할 경우 기존과 동일하게 컴포넌트처럼 사용할 수도 있고, 아니면 index.js파일에서 ReactDOM.render() 위에서 선언해주어 사용할 수 도 있습니다.

 

React는 클래스를 사용해서 상태비저장 컴포넌트를 만드는 방식을 추천하지 않습니다. 상태를 추가할 수 있는 여지가 있기 때문입니다. 상태비저장 컴포넌트는 단순하게 유지해야 하므로 상태 객체나 메서드는 추가하지 않아야 하며, 특히 외부 메서드나 함수를 호출하지 않도록 해야 합니다. 

 


Next...

이 글에서는 컴포넌트란 무엇인지, 컴포넌트의 속성과 상태란 무엇인지 살펴보았습니다. 다음 장에서는 컴포넌트의 라이프사이클 이벤트에 대해 살펴보도록 하겠습니다. 

 

 

[references]

 

「리액트 교과서」 - 아자트 마르단

state

 

State and Lifecycle – React

A JavaScript library for building user interfaces

reactjs.org

stateless component

 

Components and Props – React

A JavaScript library for building user interfaces

reactjs.org

constructor

 

React.Component – React

A JavaScript library for building user interfaces

reactjs.org

props vs state

propTypes

 

Typechecking With PropTypes – React

A JavaScript library for building user interfaces

reactjs.org

 

facebook/prop-types

Runtime type checking for React props and similar objects - facebook/prop-types

github.com

stateless component

 

https://hackernoon.com/react-stateless-functional-components-nine-wins-you-might-have-overlooked-997b0d933dbc%EF%BB%BF

 

hackernoon.com

props

 

http://ww25.reactjstutorial.net/props.html?subid1=20201019-1642-2946-a308-b3b81a0b60f6

 

ww25.reactjstutorial.net

 

 

'Front-end Framework > React (Reactjs)' 카테고리의 다른 글

6. React의 이벤트 처리  (0) 2020.10.19
5. 컴포넌트 라이프사이클 이벤트  (0) 2020.10.19
3. 리액트의 문법 (+JSX)  (0) 2020.10.19
2. React 개발 설정  (0) 2020.10.19
1. React?  (0) 2020.10.19

댓글