React 엘리먼트와 컴포넌트

React의 구성 요소

React 엘리먼트

엘리먼트는 React 앱의 가장 작은 단위로 화면에 표시할 내용을 기술할 수 있는데, 브라우저 DOM 엘리먼트와는 달리 React 엘리먼트는 일반 객체로 쉽게 생성할 수 있고, React DOM은 React 엘리먼트와 일치하도록 DOM을 업데이트합니다.

React 엘리먼트
const element = <h1>Hello, world</h1>;

일반적으로는 “컴포넌트”라는 개념이 더 많이 알려져 있는데, 엘리먼트는 바로 이 컴포넌트의 “구성 요소”라고 할 수 있습니다.

DOM 엘리먼트 렌더링

React는 일반적으로 <div id=”root”>와 같이 특정 ID를 가진 HTML 엘리먼트를 생성한 후 이 엘리먼트에 들어가는 모든 엘리먼트를 React DOM에서 관리하기 때문에, 이 최초의 엘리먼트를 “루트” DOM 노드라고 부릅니다.

루트 DOM 노드
<div id="root"></div>

React로 구현된 애플리케이션은 일반적으로 하나의 루트 DOM 노드가 있지만, React를 기존의 앱에 통합하는 경우에는 원하는 만큼 많은 수의 독립된 루트 DOM 노드가 있을 수 있는데, React 엘리먼트를 루트 DOM 노드에 렌더링하기 위해서는 모두 다음과 같이 루트 DOM 노드를 ReactDOM.render()로 전달해 주어야 합니다.

React 엘리먼트 렌더링하기
<div id="root"></div>

<script>
const element = <h1>Hello, world</h1>;
ReactDOM.render(element, document.getElementById('root'));
</script>

렌더링 된 엘리먼트 업데이트

React 엘리먼트는 불변객체로, 엘리먼트를 생성한 이후에는 해당 엘리먼트의 자식이나 속성을 변경할 수 없습니다.

엘리먼트는 하나의 프레임과 같이 특정 시점의 UI를 보여주는데, UI를 업데이트하기 위해서는 새로운 엘리먼트를 생성하고 이를 ReactDOM.render()로 전달해줘야 합니다.

React 시계 업데이트
function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  ReactDOM.render(element, document.getElementById('root'));
}

setInterval(tick, 1000);

위 코드는 초 단위로 업데이트되는 디지털 시계를 구현한 것인데, 함수에서는 setInterval() 콜백을 이용해 매초마다 ReactDOM.render()를 호출하도록 되어 있습니다.

하지만 실제 작업에서 대부분의 React 앱은 ReactDOM.render()를 한 번만 호출하기 때문에, 이와 같은 코드가 상태 컴포넌트에서 어떻게 캡슐화되는지를 이해할 필요가 있습니다.

변경된 부분만 업데이트

React DOM은 해당 엘리먼트와 그 자식 엘리먼트를 이전의 엘리먼트와 비교하여 필요한 경우에만 DOM을 업데이트합니다.

즉 앞에서 구현했던 React 시계의 코드에서는 매초마다 전체 UI를 다시 그리도록 엘리먼트를 만들었지만, React DOM은 실제로 내용이 변경된 텍스트 노드만 업데이트하는 것이죠.

이렇게 특정 시점에 UI가 어떻게 보일지 고민하는 접근법은 시간의 변화에 따라 UI가 어떻게 변화할지를 고민하는 것보다 더 많은 수의 버그를 없앨 수 있는 방법이 될 수 있습니다.

컴포넌트와 속성

React는 컴포넌트를 통해 UI를 재사용 가능한 개별적인 조각으로 나누고, 각 조각을 개별적으로 살펴볼 수 있습니다. 개념적으로 컴포넌트는 JavaScript 함수와 유사한데, React 컴포넌트는 “props”라고 하는 임의의 입력을 받은 후 화면에 어떻게 표시되는지를 기술하는 React 엘리먼트를 반환합니다.

함수 컴포넌트와 클래스 컴포넌트

컴포넌트를 정의하는 가장 간단한 방법은 JavaScript 함수를 작성하는 것인데, 다음과 같이 함수는 데이터를 가진 하나의 props 객체 인자를 받은 후 React 엘리먼트를 반환하기 때문에 유효한 React 컴포넌트가 될 수 있습니다.

함수 컴포넌트
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

위와 같은 컴포넌트는 JavaScript 함수로 작성된 “함수 컴포넌트”라고 부르는데, 동일한 기능의 컴포넌트를 ES6의 class로 구현할 수도 있습니다.

클래스 컴포넌트
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

위와 같이 class로 구현된 컴포넌트는 “클래스 컴포넌트”라고 하고, 함수 컴포넌트와는 다른 몇 가지 추가적인 기능을 사용할 수 있습니다.

컴포넌트 렌더링

React는 기존의 DOM 태그 뿐만 아니라 사용자 정의 컴포넌트로도 나타낼 수 있습니다. React가 사용자 정의 컴포넌트로 작성한 엘리먼트를 발견하면 JSX 어트리뷰트와 자식을 해당 컴포넌트에 단일 객체로 전달하게 되는데, 이 때 전달되는 객체는 “props”라고 합니다.

사용자 정의 컴포넌트 렌더링
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

const element = <Welcome name="Sara" />;
ReactDOM.render(
  element,
  document.getElementById('root')
);

사용자 정의 컴포넌트의 이름은 항상 대문자로 시작해야 하는데, React는 소문자로 시작하는 컴포넌트를 DOM 태그로 처리하기 때문입니다.<div />는 단순히 HTML의 div 태그를 나타내지만, <Welcome />은 사용자 정의 컴포넌트를 나타내기 때문에, React에는 함수 컴포넌트나 클래스 컴포넌트로 Welcome이 정의되어 있어야 합니다.

위의 코드와 같이 사용자 정의 컴포넌트를 사용하는 경우에는 다음과 같은 과정으로 렌더링 작업이 이루어집니다.

  • <Welcome name="Sara" /> 엘리먼트로 ReactDOM.render()를 호출
  • React는 {name: 'Sara'}props로 하여 Welcome 컴포넌트를 호출
  • Welcome 컴포넌트는 <h1>Hello, Sara</h1> 엘리먼트를 반환
  • React DOM은 <h1>Hello, Sara</h1> 엘리먼트와 일치하도록 DOM을 업데이트

컴포넌트 합성

컴포넌트는 자신의 출력에 다른 컴포넌트를 참조할 수 있는데, 이는 모든 세부 단계에서 동일한 추상 컴포넌트를 사용할 수 있음을 의미합니다.

React 앱에서는 버튼, 폼, 다이얼로그, 화면 등의 모든 것들이 컴포넌트로 표현될 수 있는데, 다음과 같이 동일한 컴포넌트인 Welcome을 여러 번 재사용하여 렌더링하는 앱 컴포넌트를 만들 수도 있습니다.

컴포넌트 재사용
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

function App() {
  return (
    <div>
      <Welcome name="Sara" />
      <Welcome name="Cahal" />
      <Welcome name="Edite" />
    </div>
  );
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

일반적으로 새 React 앱은 최상위에 단일 App 컴포넌트를 가지고 있지만, 기존 앱에 React를 통합하는 경우에는 Button과 같은 작은 컴포넌트부터 시작해서 뷰 계층의 상단으로 올라가면서 점진적으로 작업해야 하는 경우도 있을 수 있습니다.

컴포넌트 추출

컴포넌트는 여러 개의 작은 컴포넌트로 나눌 수 있고, 가능한 작은 컴포넌트로 나누는 것이 더 효율적인 작업을 하는데 도움이 됩니다.

다음은 Comment를 구현한 컴포넌트인데, 이 컴포넌트의 주요 기능은 author, text, dateprops로 받은 후 소셜 미디어 웹 사이트에 코멘트를 표시하기 위한 컴포넌트입니다.

Comment 컴포넌트
function Comment(props) {
  return (
    <div className="Comment">
      <div className="UserInfo">
        <img className="Avatar"
          src={props.author.avatarUrl}
          alt={props.author.name}
        />
        <div className="UserInfo-name">
          {props.author.name}
        </div>
      </div>
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

위 컴포넌트는 Comment에 필요한 모든 기능을 제공하고 있지만, 구성요소들이 모두 중첩 구조로 이루어져 있기 때문에 변경이 어렵고, 각 구성요소를 개별적으로 재사용하기도 힘들다는 단점이 있습니다.

Comment 컴포넌트에서 Avatar, UserInfo와 같이 몇 가지 컴포넌트를 추출하여 새로운 개별 컴포넌트로 만들면 Comment 컴포넌트는 더 단순해지고 효율적인 컴포넌트가 될 수 있습니다.

Avatar 컴포넌트 추출
function Avatar(props) {
  return (
    <img className="Avatar"
      src={props.user.avatarUrl}
      alt={props.user.name}
    />
  );
}

Avatar는 자신이 Comment 내에서 렌더링 된다는 것을 알 필요가 없기 때문에, props의 이름을 author에서 더욱 일반화된 user로 변경했는데, 이렇게 props의 이름은 사용될 context가 아닌 컴포넌트 자체의 관점에서 짓는 것이 권장됩니다.

UserInfo 컴포넌트
function UserInfo(props) {
  return (
    <div className="UserInfo">
      <Avatar user={props.user} />
      <div className="UserInfo-name">
        {props.user.name}
      </div>
    </div>
  );
}

UserInfo 컴포넌트는 Avatar 컴포넌트를 추가하여 새로운 컴포넌트로 합성한 컴포넌트로, AvatarUserInfo 컴포넌트를 추출한 Comment 컴포넌트의 최종 모습은 다음과 같습니다.

컴포넌트 추출 후 새로 구성한 Comment
function Comment(props) {
  return (
    <div className="Comment">
      <UserInfo user={props.author} />
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

이런 식으로 재사용 가능한 컴포넌트를 만들어 놓는 것은 큰 규모의 앱에서 매우 효율적으로 작업할 수 있는 방법입니다. Button, Panel, Avatar와 같이 UI의 일부가 여러 번 사용되거나, App, FeedStory, Comment와 같이 UI의 일부가 자체적으로 복잡한 경우에는 개별적인 컴포넌트로 만드는 것이 좋습니다.

props

함수 컴포넌트나 클래스 컴포넌트는 모두 컴포넌트의 자체 props를 수정해서는 안됩니다. React는 매우 유연하지만 한 가지 엄격한 규칙이 있는데, 바로 모든 React 컴포넌트는 자신의 props를 다룰 때 반드시 순수 함수처럼 동작해야 한다는 것이죠.

순수 함수
function sum(a, b) {
  return a + b;
}

위의 함수는 순수 함수라고 불리는데, 순수 함수는 입력값을 바꾸려 하지 않고 항상 동일한 입력값에 대해 동일한 결과를 반환하는 것이 특징입니다.

반대로 다음의 함수와 같이 자신의 입력 값을 변경하는 경우에는 순수 함수라고 할 수 없습니다.

일반 함수
function withdraw(account, amount) {
  account.total -= amount;
}

답글 남기기