본문 바로가기

[번역] React Components, Elements, 그리고 Instances

본 포스트는 Dan Abramov의 React Components, Elements, and Instances를 번역한 글입니다.

많은 사람들이 React에서 components와 컴포넌트의 instance, 그리고 elements를 혼동합니다. 왜 화면에 그려지는 무언가를 의미하는 각각 다른 세 개의 용어가 있을까요?

 

당신이 React를 처음 접했을 때 아마 컴포넌트 클래스와 인스턴스만 사용해서 개발하였을 겁니다. 예를 들어, 당신은 클래스를 생성해서 Button 컴포넌트를 선언했다고 합시다. 프로그램이 실행되었을 때, 아마 이 컴포넌트의 여러 인스턴스가 있고, 각각은 인스턴스만의 고유한 props와 지역 state가 존재할 것입니다. 이것은 전통적인 객체 지향 UI 프로그래밍입니다. 그럼 elements는 무엇일까요?

 

이 전통적인 UI 모델에서, 자식 컴포넌트 인스턴스를 생성하고 파괴하는 것은 전적으로 당신에게 달려있습니다. 만약 Form 컴포넌트에서 Button 컴포넌트를 렌더하고 싶다면, 버튼 인스턴스를 생성하고, 어떤 새로운 정보로 최신 상태를 유지하도록 직접 업데이트해야합니다.

 

class Form extends TraditionalObjectOrientedView {
  render() {
    // Read some data passed to the view
    const { isSubmitted, buttonText } = this.attrs;
    
    if (!isSubmitted && !this.button) {
      // Form is not yet submitted. Create the button!
      this.button = new Button({
        children: buttonText,
        color: 'blue'
      });
      this.el.appendChild(this.button.el);
    }
    
    if (this.button) {
      // The button is visible. Update its text!
      this.button.attrs.children = buttonText;
      this.button.render();
    }
    
    if (isSubmitted && this.button) {
      // Form was submitted. Destroy the button!
      this.el.removeChild(this.button.el);
      this.button.destroy();
    }
    
    if (isSubmitted && !this.message) {
      // Form was submitted. Show the success message!
      this.message = new Message({ text: 'Success!' });
      this.el.appendChild(this.message.el);
    }
  }
}

이것은 가짜코드(pseudocode)이지만 거의 여러분이 결국 UI를 만들고 구성하려고 할 때, Backbone과 같은 프레임워크를 사용하여 객체 지향적인 방법으로 하고 있는 작업입니다.

 

각 컴포넌트는 DOM 노드와 자식 컴포넌트의 인스턴스에 대한 참조를 유지해야 하며, 때가 되면 이들을 생성, 업데이트, 파괴할 수 있어야 합니다. 코드 길이는 부모 컴포넌트가 그들의 자식 컴포넌트 인스턴스에 직접 접근할수록 그리고 가능한 컴포넌트 state 개수의 제곱만큼 길어지며, 결국 나중에는 이들을 분리하기 어려워집니다.

 

리액트에서, elements가 이것을 해결해줄 수 있습니다. element는 컴포넌트 인스턴스 즉 DOM 노드와 이상적인 properties를 묘사하는 일반 객체입니다. 이것은 오직 컴포넌트 타입(예를 들어 Button)과 그것의 properties(예를 들어 버튼의 color), 그리고 컴포넌트 안의 모든 자식 엘리먼트에 대한 정보만을 포함합니다. 

 

엘리먼트는 실제 인스턴스는 아닙니다. 리액트에게 당신이 화면상에서 무엇을 볼 것이지를 알려주는 수단입니다. 엘리먼트에는 어떠한 메소드도 호출할 수 없습니다. 엘리먼트는 그저 두 개의 필드 type: (string | component)과 props: (Object*)로 이루어진 immutable한 설명 객체일 뿐입니다.

 

엘리먼트의 type이 문자열일 때, 엘리먼트는 tag명을 포함한 DOM node를 표현하며, props는 node의 attribute에 해당합니다. 이 설명 객체를 곧 리액트가 렌더링할 것입니다. 예를 들어:

{
  type: 'button',
  props: {
    className: 'button button-blue',
    children: {
      type: 'b',
      children: 'OK!'
    }
  }
}

위의 엘리먼트는 그저 아래의 HTML을 일반 객체로 표현한 방식일 뿐입니다.

<button class='button button-blue'>
  <b>
    OK!
  </b>
</button>

엘리먼트가 중첩되는 방식을 유의하세요. 관례상, 엘리먼트 트리를 생성하고 싶을 때 여러 개의 자식 엘리먼트들을 포함하는 엘리먼트에 chilren prop으로 특정하면 됩니다.

 

중요한 점은 child와 parent 엘리먼트들은 단지 설명일 뿐, 실제 인스턴스는 아니라는 점입니다. 엘리먼트가 생성될 때, 엘리먼트들은 화면의 어떤 것도 참조하고 있지 않습니다. 여러분이 엘리먼트를 생성하고 이들을 없애도 아무 영향도 없습니다.

 

React Elements는 탐사하기 쉬우며, 파싱할 필요도 없습니다. 물론 React Element는 그저 Object이기 때문에 실제 DOM 엘리먼트보다 훨씬 가볍습니다.

 

그러나, 엘리먼트의 type은 React component에 상응하는 함수나 클래스일 수도 있습니다.

{
  type: Button,
  props: {
    color: 'blue',
    children: 'OK!'
  }
}

이것이 React의 핵심 아이디어입니다.

 

컴포넌트를 묘사하는 엘리먼트 또한 DOM node를 묘사하는 엘리먼트와 마찬가지로 엘리먼트입니다. 이들은 서로 중첩되고 섞일 수 있습니다.

An element describing a component is also an element, just like an element describing the DOM node. They can be nested and mixed with each other.

 

이 특징은 여러분이 특정 color 프로퍼티 값을 사용하여 Button 컴포넌트로 DangerButton 컴포넌트를 정의할 수 있게 해줍니다. 물론 Button 컴포넌트가 button, div 혹은 다른 무언가로 렌더링 되는지를 전혀 걱정할 필요 없이 말이죠.

const DangerButton = ({ children }) => ({
  type: Button,
  props: {
    color: 'red',
    children: children
  }
});

여러분은 이것들을 나중에는 mix하고 match할 수 있습니다.

const DeleteAccount = () => ({
  type: 'div',
  props: {
    children: [{
      type: 'p',
      props: {
        children: 'Are you sure?'
      }
    }, {
      type: DangerButton,
      props: {
        children: 'Yep'
      }
    }, {
      type: Button,
      props: {
        color: 'blue',
        children: 'Cancel'
      }
   }]
});

혹은, 여러분이 JSX를 선호한다면,

const DeleteAccount = () => (
  <div>
    <p>Are you sure?</p>
    <DangerButton>Yep</DangerButton>
    <Button color='blue'>Cancel</Button>
  </div>
);

이 특징으로 컴포넌트들은 오직 합성을 통해 is-a와 has-a 관계로 표현할 수 있기 때문에, 컴포넌트는 서로 분리되서 유지됩니다.

 

React 엘리먼트의 type이 함수나 클래스인 경우, 해당 컴포넌트에게 어떤 엘리먼트가 주어진 props로 렌더링되는지를 물어볼 필요가 있습니다.

 

예를 들어

{
  type: Button,
  props: {
    color: 'blue',
    children: 'OK!'
  }
}

React는 Button 컴포넌트에게 무엇을 렌더링하는지를 묻고, 다음의 결과를 얻습니다.

{
  type: 'button',
  props: {
    className: 'button button-blue',
    children: {
      type: 'b',
      children: 'OK!'
    }
  }
}

React는 화면 상의 모든 컴포넌트에서 근간이 되는 DOM tag elements를 알 때까지 이 프로세스를 반복합니다.

 

React는 마치 "Y가 뭔가요?"로 모든 "X 는 Y입니다."를 요청하는 아이와 같습니다. 여러분은 아이들이 세상의 모든 사소한 점까지 알 때까지 설명해야 하는 것처럼, React는 컴포넌트에게 실제 DOM 트리를 알 때까지 계속해서 물을 것입니다.

 

위의 Form 예제를 기억하나요? 위의 예제는 React*로 다음과 같이 쓸 수 있습니다.

const Form = ({ isSubmitted, buttonText }) => {
  if (isSubmitted) {
    // Form submitted! Return a message element.
    return {
      type: Message,
      props: {
        text: 'Success!'
      }
    };
  }
  // Form still visible! Return a button element.
  return {
    type: Button,
    props: {
      children: buttonText,
      color: 'blue'
    }
  };
};

이게 전부입니다! React 컴포넌트 입장에서, props는 input이고, element 트리는 output입니다.

 

반환되는 element 트리는 DOM node를 묘사하는 elements와 컴포넌트를 묘사하는 elements를 포함할 수 있습니다. 이것으로 내부 DOM 구조에 의존하지 않고도 UI 각각의 part를 구성할 수 있습니다.

The returned element tree can contain both elements describing DOM nodes, and elements describing other components. This lets you compose independent parts of UI without relying on their internal DOM structure.

 

React가 인스턴스를 생성, 업데이트, 파괴하게 할 수 있습니다. 우리는 이들을 컴포넌트가 반환하는 elements로 묘사하고, React가 인스턴스들을 관리하는 것을 담당합니다.

 

위의 코드에서, Form, Message 그리고 Button은 React 컴포넌트입니다. 이들은 위의 코드처럼 함수로, 혹은 React.Component를 상속받는 클래스로 쓰일 수 있습니다.

class Button extends React.Component {
  render() {
    const { children, color } = this.props;
    // Return an element describing a
    // <button><b>{children}</b></button>
    return {
      type: 'button',
      props: {
        className: 'button button-' + color,
        children: {
          type: 'b',
          props: {
            children: children
          }
        }
      }
    };
  }
}

컴포넌트를 클래스로 정의하면, 함수형 컴포넌트보다 약간 더 powerful합니다. 클래스 컴포넌트는 지역 state를 선언할 수 있고, DOM node를 생성하거나 파괴하는 custom 로직을 수행할 수 있습니다. 함수형 컴포넌트는 클래스 컴포넌트보다는 덜 powerful하지만 더 간단하고 하나의 render()메소드로 클래스 컴포넌트와 비슷하게 동작할 수 있습니다.

 

그러나 함수형이든 클래스이든, 근본적으로 이들은 모두 React 컴포넌트이며, props를 input으로 받아 elements를 output으로 반환합니다.

However, whether functions or classes, fundamentally they are all components to React. They take the props as their input, and return the elements as their output.

아래의 코드를 호출하면,

ReactDOM.render({
  type: Form,
  props: {
    isSubmitted: false,
    buttonText: 'OK!'
  }
}, document.getElementById('root'));

Form 컴포넌트는 주어진 props로 element tree를 리턴하고, simpler primitives 면에서 단계적으로 컴포넌트 트리를 이해하는 과정을 "정제(refine)"해나갑니다.

// React: 개발자가 이것을 알려주었고...
{
  type: Form,
  props: {
    isSubmitted: false,
    buttonText: 'OK!'
  }
}
// React: ...그리고 Form은 이걸 알려주었고...
{
  type: Button,
  props: {
    children: 'OK!',
    color: 'blue'
  }
}
// React: ...그리고 Button은 이걸 알려줬네! 이제 끝!
{
  type: 'button',
  props: {
    className: 'button button-blue',
    children: {
      type: 'b',
      props: {
        children: 'OK!'
      }
    }
  }
}

이 프로세스의 끝에는 React는 최종 DOM 트리를 알게 되고, ReactDOM 혹은 React Natives같은 renderer가 실제 DOM node를 업데이트할 필요가 있는 최소한의 업데이트 set을 적용합니다.

 

단계적인 정제 프로세스는 React app이 최적화를 더 쉽게하는 이유이기도 합니다. 만약 컴포넌트 트리의 몇몇 부분이 너무 커져버려 React가 효율적으로 방문할 수 없다면, 여러분은 컴포넌트에게 만약 상관관계가 있는 props가 변하지 않았다면 이 "정제" 과정은 하지말고 트리의 특정 부분만 diffing하라고 알릴 수 있습니다. props가 불변하다면 props의 변화 여부를 확인하는 것은 매우 빠릅니다. 따라서 React와 불변성은 최고의 조합이며, 적은 노력으로도 훌륭한 최적화를 제공할 수 있습니다.

 

여러분은 제가 components, elements에 대해 많은 것을 말했지만 instance에 대해서는 그렇지 않다고 생각할지도 모르겠습니다. 사실, React에서 인스턴스는 객체 지향 UI 프레임워크에서만큼 중요하진 않습니다.

 

클래스 컴포넌트로 정의된 컴포넌트만이 인스턴스를 가지기 때문에 여러분이 직접 인스턴스를 생성할 수는 없고, React가 대신 생성합니다. 부모 컴포넌트에서 자식 컴포넌트에 접근하는 매커니즘이 있기 때문에 그들은 단지 field에 focus하는 것과 같은 명령을 내리는데 사용될 뿐입니다. 그러나 일반적으로는 그렇게 하지 말아야합니다.

 

React가 모든 클래스 컴포넌트에서 인스턴스를 생성해줘서 여러분은 객체 지향적인 방법으로 메소드와 지역 state를 가진 컴포넌트를 생성할 수 있습니다. 그러나 이거 말고는 인스턴스는 React 프로그래밍 모델에서 그다지 중요하지 않습니다. 인스턴스는 React에서 알아서 관리될 것입니다.

 

Recap

element는 DOM node 혹은 다른 컴포넌트 면에서 화면에 나타나는 것을 묘사하는 일반 객체입니다. 엘리먼트는 props 안에 다른 엘리먼트를 포함할 수 있습니다.

 

컴포넌트는 두 가지 방법으로 생성됩니다.  render() 메소드가 존재하고, React.Component에서 상속된 클래스 혹은 함수 형태의 컴포넌트가 존재합니다. 이 두 경우 모두, props를 input으로 하여 element 트리를 output으로 반환합니다.

 

컴포넌트가 input으로 어떤 props를 받으면, 부모 컴포넌트는 type과 props를 가진 element를 반환하기 때문에 사람들이 React에서는 props가 한 방향으로 흐른다고 말합니다. 부모에서 자식으로 말이죠.

 

인스턴스는 여러분이 작성한 클래스 컴포넌트에서 this로 참조하고 있는 대상입니다. this에는 지역 state와 lifecycle 이벤트를 저장하는데 유용합니다.

 

함수형 컴포넌트는 인스턴스가 존재하지 않지만, 클래스 컴포넌트는 인스턴스가 존재합니다. 그러나 여러분이 직접적으로 컴포넌트 인스턴스를 생성할 필요는 없습니다. 리액트가 알아서 생성해줄 것입니다.

 

최종적으로, 엘리먼트를 생성하기 위해, React.createElement(), JSX 혹은 element 팩토리 함수를 사용하십시오. 저는 여러분이 실제 코드상에서 일반 객체로 엘리먼트를 작성하는 것을 추천하지 않습니다. 그저 자세히 살펴보면, 엘리먼트는 일반 객체라는 사실만 알아두십시오.

Further Reading

Footnotes

* 모든 리액트 엘리먼트는 보안적인 이유로 객체에 추가적인 $$typeof: Symbol.for('react.element') 필드를 필요로 합니다. 저는 위의 예제들에서는 이것을 생략하였습니다. 일반적으로 여러분은 JSX 혹은 React.createElement()를 사용해야 하므로 걱정하지 마세요. 이 글에서는 raw API에 대한 이해를 돕고자 객체를 inline으로 작성하였습니다. 하지만 예제 코드를 실행하기 위해서는 $$typeof을 모두 추가하십시오.

요약해보기

출처: 내 손

React에서 엘리먼트는 해당 컴포넌트가 어떤 DOM element(혹은 컴포넌트)를 그릴 것인지, 속성은 무엇인지, 어떤 자식 컴포넌트를 포함하는지에 대한 정보를 가지고 있는 신분증? 명세서?같은 개념입니다.

 

즉, 화면에서 해당 컴포넌트로 무엇을 볼 수 있는지를 알 수 있습니다.

 

일반 객체이기 때문에 DOM Element보다 가볍고, 미래에 App 규모가 더 커져서 Component가 많아진 상황에서, 리액트는 일반 객체인 엘리먼트에서 props와 type의 변화 유무를 확인하여 업데이트할 부분만 업데이트할 수 있습니다.