프론트엔드/React

[React] 엘리먼트(Element)와 컴포넌트(Components)와 프로퍼티(Props)

MINJIN's 2024. 1. 5. 11:53

1. 엘리먼트(Element)란?

  엘리먼트는 리액트 앱의 가장 작은 단위이며, 화면에 표시할 내용을 기술한다.

const element = <h1>Hello, world</h1>;

  브라우저 DOM 엘리먼트와 달리 React 엘리먼트는 일반 객체이며(plain object) React DOM은 React 엘리먼트와 일치하도록 DOM을 업데이트한다. 하지만 리액트 엘리먼트는 DOM 엘리먼트의 가상 표현이며 상대적으로 DOM 엘리먼트가 리액트 엘리먼트에 비해 많은 정보를 담고 있어서 크고 무겁다.

 

1) 엘리먼트 렌더링

  엘리먼트를 생성한 이후에 실제로 화면에 보여주기 위해서는 렌더링이라는 과정을 거쳐야 한다.

<div id="root"></div>

  이 코드는 root라는 id를 가진 <div> 태그이다. 이 안에 들어가는 모든 엘리멘트는 리액트 DOM에서 관리하기 때문에 이것을 'root DOM node'라고 부른다. 리액트 엘리먼트를 렌더링 하기 위해서는 우선 DOM 엘리먼트를 ReactDOM.createRoot()에 전달한 다음, React 엘리먼트를 root.render()에 전달해야 한다.

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

 

2) 렌더링 된 엘리먼트 업데이트하기

  리액트엘리먼트는 불변객체이다. 즉 엘리먼트를 생성한 이후에는 해당 엘리먼트의 자식이나 속성을 변경할 수 없다. 그렇다면 한 번 렌더링된 엘리먼트는 어떻게 업데이트 할 수 있을까? 다음 코드를 살펴보자.

const root = ReactDOM.createRoot(
  document.getElementById('root')
);

function tick() {
  const element = (
    <div>
      <h1>안녕, 리액트!</h1>
      <h2>현재 시간 : {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  root.render(element);
}

setInterval(tick, 1000);

  이 코드는 tick()이라는 함수를 정의하고 있고, 이 함수는 현재 시간을 포함하고 있는 엘리먼트를 생성하여 root div에 렌더링하는 역할을 한다. 그리고 자바스크립트의 setInterval() 함수를 이용하여 tick()함수를 1초(1000ms) 마다 호출하고 있다. 이는 내부적으로 tick()함수가 호출될 때마다 기존 엘리먼트를 변경하는 것이 아니라, 새로운 엘리먼트를 생성해서 바꿔치기하는 것이다.
  결국 리액트 엘리먼트의 불변성 때문에 엘리먼트를 업데이트하기 위해서는 새로 만들어야 한다는 사실이 중요하다.

 

2. 컴포넌트(Components)와 프로퍼티(props)

1) 컴포넌트

  리액트는 컴포넌트 기반의 구조를 가지고 있다. 리액트는 모든 페이지가 컴포넌트로 구성되어 있고, 하나의 컴포넌트는 여러 개의 컴포넌트의 조합으로 구성될 수 있다. 컴포넌트는 개념적으로 자바스크립트의 함수와 유사하다. 'props'라고 하는 임의의 입력을 받은 후, 리액트 엘리먼트를 반환한다.

 

2) 프로퍼티

  props는 prop 뒤에 복수형을 나타내는 s를 붙여서 여러 개로 존재하는 property의 약자이다. props는 리액트 컴포넌트의 속성이며 컴포넌트에 전달할 다양한 정보를 담고 있는 자바스크립트 객체이다. 또한 props 읽기 전용으로서 모든 리액트 컴포넌트는 props를 직접 바꿀 수 없고, 같은 props에 대해서는 항상 같은 결과를 보여줘야 한다.

 

3. 컴포넌트 만들기

  컴포넌트를 정의하는 가장 간단한 방법은 자바스크립트의 함수를 작성하는 것이다.

 

1) 컴포넌트의 종류

① 함수 컴포넌트

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

  이 코드에는 Welcome이라는 함수가 나오며 props객체를 받아 인사말이 담긴 리액트 엘리먼트를 리턴하기 때문에 유요한 리액트 컴포넌트라 할 수 있다. 이러한 컴포넌트는 자바스크립트의 함수이기 때문에 말 그대로 함수 컴포넌트라 한다.

 

② 클래스 컴포넌트

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

  이 코드는 위의 함수 컴포넌트와 동일한 역할을 한다. 하지만 함수 컴포넌트와 다르게 클래스 컴포넌트는 React.Component를 상속 받아서 만든다. 위 코드를 보면 React.Component라는 클래스를 상속받아서 Welcome이라는 클래스를 만들었고, 이는 유요한 리액트 컴포넌트이다.

 

2) 컴포넌트 렌더링

const element = <Welcome name="Sara" />;

  위 코드는 Welcome이라는 컴포넌트를 사용한 엘리먼트이다. 즉 엘리먼트는 DOM 태그를 사용하여서 만들 수도 있지만 위와 같이 컴포넌트를 활용하여 만들 수 있다. 다음은 컴포넌트 렌더링 코드이다.

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

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

  위 코드는 Welcome이라는 함수 컴포넌트를 선언하고 있고, <Welcome name="Sara" />라는 값을 엘리먼트로 root.render()를 호출한다. 리액트는 Welcome 컴포넌트에 { name : "Sara"라는 props를 넣어서 호출하고 그 결과로 리액트 엘리먼트가 생성된다. 이렇게 생성된 엘리먼트는 리액트 DOM을 통해 실제 DOM에 업데이트되고 브라우저를 통해 볼 수 있게 된다.

 

3) 컴포넌트 합성

  여러 개의 컴포넌트를 합쳐서 하나의 컴포넌트를 만들 수 있다. 이를 컴포넌트 합성이라 한다.

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

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

  위 코드를 보면 props의 값을 다르게 해서 Welcome 컴포넌트를 여러 번 사용하는 것을 볼 수 있다. 이렇게 하면 App 컴포넌트는 세 개의 Welcome 컴포넌트를 포함하고 있는 것이다.  

 

4) 컴포넌트 추출

  복잡한 컴포넌트는 여러 개의 컴포넌트로 다시 나눌 수 있다. 이를 컴포넌트 추출이라 하며, 큰 컴포넌트에서 일부를 추출하여 새로운 컴퍼넌트를 만들 수 있다.

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이며 이 컴포넌트는 이미지와 이름에 대한 유저 정보, 문자열과 날짜를 props로 받는다. 여기서 Avatar (className="Avatar") 부분을 추출해보자.

function Avatar(props) {
  return (
    <img className="Avatar"
      src={props.user.avatarUrl}
      alt={props.user.name}
    />
  );
}

  Avatar라는 별도의 컴포넌트를 만들었다. props에 사용되었던 author 대신 더 보편적인 user를 사용하였다. 다음은 사용자의 이름을 렌더링하는 UserInfo 컴포넌트를 추출해보자.

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

  추출한 두 가지 컴포넌트를 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>
  );
}

  컴포넌트를 어느 정도 수준까지 추출하는 것이 좋은지에 대한 기준은 없지만, 기능 단위로 구분하는 것이 좋고, 곧바로 재사용이 가능한 형태로 추출하는 것이 좋다. 재사용이 가능한 컴포넌트를 많이 갖고 있을수록 개발 속도는 빨라지게 될 것이다.

 

 

 

- 참고문헌

https://ko.legacy.reactjs.org/docs/rendering-elements.html