들어가기 전에

리액트 공식문서로 배워보자 #4, 컴포넌트와 Props!

컴포넌트는 우리가 UI를 독립적이고 재사용 가능하게 나눌 수 있게 해줍니다. 그리고 각각을 독립적으로 생각할 수 있게도 만들어줍니다. 이 페이지는 컴포넌트의 메인 아이디어에 대한 소개를 제공할 것입니다. 더욱 상세한 컴포넌트 API 레퍼런스는 여기에 있습니다.

개념적으로, 컴포넌트는 자바스크립트 함수입니다. 컴포넌트는 임의의 props라 불리는 인풋을 받아서 화면에 무엇이 보여질지 기술(describing)하는 리액트 엘리먼트를 반환합니다.

함수와 클래스 컴포넌트

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

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

이 함수는 유효한 리액트 컴포넌트입니다. 왜냐하면 이 리액트 컴포넌트는 프로퍼티를 의미하는 데이터를 가진 하나의 "props" 오브젝트 인자를 받아들이기(accept) 때문입니다. 또한 이 리액트 컴포넌트는 리액트 엘리먼트를 반환합니다. 우리는 이러한 컴포넌트를 "함수형 컴포넌트(function components)"라고 부릅니다. 왜냐하면 말 그대로 이 자체가 자바스크립트 함수이기 때문입니다.

컴포넌트를 정의하기 위해 ES6 함수를 사용할 수도 있습니다.

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

리액트의 관점에서 봤을 때, 위 두 컴포넌트는 동일합니다.

클래스는 사실 추가적인 특성이 더 있는데, 다음 섹션에서 다뤄보도록 하겠습니다. 그때 까지, 간결함을 위해 함수형 컴포넌트를 사용할 것입니다.

컴포넌트 렌더링하기

이전에, 우리는 DOM 태그를 보여주는 리액트 컴포넌트만 다뤘습니다.

const element = <div />;

하지만, 엘리먼트는 유저가 정의한 컴포넌트를 표현할 수도 있습니다.

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

리액트가 유저가 정의한 컴포넌트를 보여주는 엘리먼트를 봤을 때, 리액트는 JSX 속성을 하나의 오브젝트로 이 컴포넌트에게 넘겨줍니다. 우리는 이 오브젝트를 "props"라고 합니다.

예를 들면, 이 코드는 "Hello, Sara"를 페이지에 렌더링해줍니다.

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

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

CodePen에서 직접 해보기

이 예제에서 무슨 일이 일어나는지 다시 한번 정리해봅시다.

  1. ReactDOM.render()<Welcome name="Sara" /> 엘리먼트를 인자로 주어 호출했습니다.
  2. 리액트는 Welcome 컴포넌트를 {name: 'Sara'}를 props로 주어 호출했습니다.
  3. Welcome 컴포넌트는 <h1>Hello, Sara</h1>엘리먼트를 결과로 반환했습니다.
  4. React DOM은 <h1>Hello, Sara</h1>와 매치시키기 위해 DOM을 효율적으로 업데이트 합니다.

알아둬야 할 것: 컴포넌트 이름은 항상 대문자로 시작하세요.
리액트는 소문자로 시작하는 컴포넌트를 DOM 태그로 취급합니다. 예를 들면, <div />는 HTML의 div태그를 보여줍니다. 하지만 <Welcome />은 컴포넌트를 보여주고 Welcome이 스코프 안에 들어있기를 요구합니다.
이 컨벤션이 만ㄷ르어진 더 많은 이유를 알고 싶다면, JSX in Depth를 읽어보시기 바랍니다.

컴포넌트 조립하기(Composing Components)

컴포넌트는 출력(output)에서 다른 컴포넌트를 참조할 수 있습니다. 이러한 점은 어떤 수준의 디테일에 대해서도 같은 컴포넌트 추상화를 사용할 수 있게 해줍니다. 리액트 앱 내부의 버튼, 폼, 다이얼로그, 스크린 모든 것들이 리액트 내부에서 일반적으로 컴포넌트로 표현됩니다.

예를 들면, Welcome이라는 메세지를 많은 횟수 출력하는 App 컴포넌트를 만들 수 있습니다.

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')
);

CodePen에서 직접 해보기

일반적으로 리액트 앱은 가장 상위에 하나의 App 컴포넌트만 가지고 있습니다. 하지만, 이미 존재하는 어플리케이션과 리액트를 통합한다면, Button과 같은 작은 컴포넌트에서 바텀-업 방식으로 수행해야 할 것입니다. 그리고 점차적으로 뷰 계층의 맨 위로 갈 것입니다.

컴포넌트 추출하기

컴포넌트를 더 작은 컴포넌트로 나누는 것을 두려워하지 마세요.

예를 들면, 이 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>
  );
}

CodePen에서 직접 해보기

이 컴포넌트는 author(오브젝트), text(문자열), 그리고 date(날짜)를 prop으로 받습니다. 그리고 SNS 웹 사이트에서의 댓글을 기술(describe)합니다.

이러한 컴포넌트는 변경하기 꽤나 헷갈릴 수 있습니다. 왜냐하면 중첩된 것들 때문입니다. 그리고 각각의 부분을 다시 재사용하기도 어렵습니다. 이 거대한 컴포넌트에서 몇개의 컴포넌트를 추출해봅시다.

먼저, Avatar 컴포넌트를 추출하겠습니다.

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

Avatar 컴포넌트는 자신이 Comment 컴포넌트 내부에서 렌더링 되는 것을 알 필요가 없습니다. 이러한 이유 때문에, 우리는 prop의 이름을 더욱 일반적인 이름으로 주었습니다. author 대신 user라는 이름을 주었습니다.

우리는 props의 이름을 지을 때, 사용되는 컨텍스트를 고려하기보단 컴포넌트 자체의 관점을 고려하는 것을 권장합니다.

이제 Comment 컴포넌트를 더욱 간단하게 만들 수 있습니다.

function Comment(props) {
  return (
    <div className="Comment">
      <div className="UserInfo">
        <Avatar user={props.author} />
        <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>
  );
}

다음으로, 우리는 UserInfo-nameAvatar를 렌더링하는 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>
  );
}

CodePen에서 직접 해보기

컴포넌트를 추출하는 것은 처음에는 힘들고 지루한 일처럼 보일 수 있지만, 재사용할 수 있는 컴포넌트의 팔레트를 가지는 것은 더 큰 앱들을 만들 때 충분히 그 가치를 보여줄 것입니다. 경험에 의해 깨달은 좋은 방법은 만일 당신의 UI의 일부(Button, Panel, Avatar)가 여러번 계속해서 사용되거나 그 자체로 충분히 복잡한 형태를 가지고 있다면(App, FeedStory, Comment), 재사용 가능한 컴포넌트로 만들만한 후보지에 놓는 것이 좋습니다.

Props와 읽기 전용(Read-Only)

컴포넌트를 함수 또는 클래스로 정의하든 상관 없이, 자신이 갖고 있는 props를 절대 수정하지 않습니다. 이 sum 함수를 생각해봅시다.

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

이러한 함수는 순수(pure)함수라 불립니다. 왜냐하면 그들이 인자로 받은 값들의 원본을 건들지 않기 때문입니다. 그리고 항상 같은 입력값에 대해서 같은 출력 값을 반환합니다.

반대로, 다음과 같은 함수는 비순수 함수입니다. 왜냐하면 입력으로 들어온 값의 원본을 수정할 수 있습니다.

function withdraw(account, amount) {
  account.total -= amount;
}
// (역자 주 : JavaScript에서 오브젝트를 인자로 넘길 때는 call-by-reference로 동작한다는 점을 참조하시면 이해하기 쉽습니다.)

리액트는 꽤 유연합니다. 하지만 한가지 엄격한 규칙이 있습니다.

모든 리액트 컴포넌트는 props의 측면에서 봤을 때, 순수함수처럼 동작해야 합니다.

물론, 어플리케이션 UI는 동적이며, 몇번이고 바뀔 수 있습니다. 다음 섹션에서, "state"라는 새로운 개념을 소개할 것입니다. state는 리액트 컴포넌트가 위에서 말한 규칙(순수함수)을 어기지 않고 사용자의 액션에 대한 응답, 네트워크 응답 그리고 다른 요소들에 따라 출력 결과를 몇번이고 수정할 수 있게 해줍니다.