All Articles

ReactJS 기본2

ReactJS 번역 및 정리한 내용. 언어 기능이 생겨 현재는 일부 한글로 번역되어 있는 상태이다. 사내 발표용으로 작성하였다.

  • Component와 Props
  • State와 생명주기
  • 이벤트 처리하기

Component와 Props

  • Component는 UI를 재사용 가능한 독립적인 조각으로 분리시켜줍니다.
  • 개념상 Component는 JS의 function과 같습니다.

함수형 컴퍼넌트와 클래스 컴퍼넌트

Componenet를 정의하는 가장 간단한 방법

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

// ES6 lambda
const welcome = props => <h1>Hello, {props.name}</h1>;

이 함수는 데이터를 가진 하나의 props(props는 속성을 나타내는 데이터) 객체 인자를 받은 후 React 엘리먼트를 반환하므로 유효한 React 컴퍼넌트입니다. 이러한 컴퍼넌트는 JS 함수이기 때문에 말 그대로 함수 컴퍼넌트라고 호칭합니다.

또한 ES6 class를 사용하여 컴퍼넌트를 정의할 수 있습니다.

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

컴퍼넌트 렌더링

이전까지는 React 엘리먼트를 DOM 태그로 나타냈습니다.

const element = <div />;

React 엘리먼트는 사용자 정의 컴포넌트로도 나타낼 수 있습니다.

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

React가 사용자 정의 컴포넌트로 작성한 엘리먼트를 발견하면 JSX 어트리뷰트를 해당 컴포넌트에 단일 객체로 전달한다. 이 객체를 props라고 합니다.

다음은 페이지에 “Hello, Sara”를 렌더링하는 예시입니다.

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

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

이 예시에서는 다음과 같은 일들이 일어납니다.

  1. <Welcome name="Sara" /> 엘리먼트로 ReactDOM.render()를 호출한다.
  2. React는 {name: 'Sara'}를 props로 하여 Welcome 컴포넌트를 호출한다.
  3. Welcome 컴포넌트는 결과적으로 <h1>Hello, Sara</h1> 엘리먼트를 반환한다.
  4. React DOM은 <h1>Hello, Sara</h1> 엘리먼트와 일치하도록 DOM을 효율적으로 업데이트한다.

컴퍼넌트 합성

컴퍼넌트는 자신이 렌더링 될 때 다른 컴퍼넌트를 참조할 수 있다. React 앱애서는 버튼, 폼, 다이얼로그, 화면 등의 모든것들이 컴퍼넌트로 표현된다.

const Welcome = (props) => (<h1>Hello, {props.name}</h1>);

const App = (props) => (
  <div>
    <Welcome name="Sara" />
    <Welcome name="Cahal" />
    <Welcome name="Edite" />
  </div>
);

컴퍼넌트 추출

  • Component를 더 작은 Component로 나누는 것을 두려워 하지맙시다.
  • 아래 예에서는 어떻게 나누는게 좋을까요?
const Comment = (props) => (
  <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>
);
  • props로 author(객체), text(문자열), date(Date)를 받고, Comment, 소셜 미디어 사이트 정보까지 받고 있다.
  • 이 Component는 중첩된 구조로 수정하기 어렵고, 각 부분을 재활용 하기도 어렵다.

먼저 Avatar를 분리해보죠.

const Avatar = (props) => (
  <img className="Avatar"
    src={props.user.avatarUrl}
    alt={props.user.name}
  />
);
  • Avatar는 Comment가 어떻게 렌더링 되는지 알 필요가 없음.
  • author 보다 일반적인 user를 할당한 이유.
  • 필요한 만큼만 알면 된다.

이제 Comment가 일부 단순해졌습니다.

const Comment = (props) => (
  <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>
);

다음으로 Avatar 옆에 사용자의 이름을 렌더링 하는 UserInfo를 추출합시다.

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

Comment가 더욱 단순해졌습니다.

const Comment = (props) => (
  <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 )
  • 그 자체로도 이미 복잡한( App, FeedStory, Comment )
  • 것들은 재사용 가능한 컴퍼넌트로 만드는 것이 좋음.

Props는 읽기 전용이다.

다음 sum 함수를 봅시다.

const sum = (a, b) => a + b;

이 함수는 순수함수이다. 입력값을 바꾸지 않고, 동일한 입력에 동일한 결과를 반환하기 때문이다.

반면 아래 함수는 자신의 입력값을 변경하기 때문에 순수 함수가 아니다.

const withdraw = (account, amount) => {
  account.total -= amount;
}

모든 React 컴퍼넌트는 자신의 props를 다룰 때 반드시 순수 함수처럼 동작해야 한다.

React 컴포넌트는 state를 통해 위 규칙을 위반하지 않고 사용자 액션, 네트워크 응답 및 다른 요소에 대한 응답으로 시간에 따라 자신의 출력값을 변경할 수 있습니다.

State를 보러 갑시다.


State와 생명주기

전에 본 시계 예시 기억나시나요? 다시 한번 살펴봅시다.

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

rendering을 다시 콜 해서 Component를 갱신 시키는 방법으로 해결 했었죠? 이번에는 Clock Component가 재사용, 캡슐화 가능하게 만들어 볼게요. 아마 스스로 업데이트 할거에요.

캡슐화부터 해볼게요.

const Clock = (props) => (
  <div>
    <h1>Hello, world!</h1>
    <h2>It is {props.date.toLocaleTimeString()}.</h2>
  </div>
);

const tick = () => {
  ReactDOM.render(
    <Clock date={new Date()} />,
    document.getElementById('root')
  );
}

setInterval(tick, 1000);

그러나 중요한게 빠졌죠? Clock이 타이머를 설정하고 매초 UI를 업데이트 하는 것이 Clock의 구현 세부사항이 되어야 합니다.

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);
  • 이걸 구현하려면 우리는 state라는 녀석을 Clock Component에 추가해 주어야 해요.
  • 앞에서도 설명 했지만 state를 사용하려면 Class를 써야 합니다.
  • 업데이트로 인해 함수형 컴퍼넌트에서도 state를 다룰 수 있게 되었습니다.
  • 그렇지만, 일단 클래스로 진행하도록 하겠습니다.

클래스로 바꾸기

  1. React.Component를 확장하는 동일한 이름의 class를 생성합니다.
  2. render()라고 불리는 빈 메서드를 추가합니다.
  3. 함수의 내용을 render() 메서드 안으로 옮깁니다.
  4. render() 내용 안에 있는 propsthis.props로 변경합니다.
  5. 남아있는 빈 함수 선언을 삭제합니다.
class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

Clock은 이제 함수가 아닌 클래스로 정의됩니다.

render 메서드는 업데이트가 발생할 때마다 호출되지만, 같은 DOM 노드로 <Clock />을 렌더링하는 경우 Clock 클래스의 단일 인스턴스만 사용됩니다. 이것은 로컬 state와 생명주기 메서드와 같은 부가적인 기능을 사용할 수 있게 해줍니다.

클래스에 로컬 State 추가하기

  1. render() 함수에서 this.props.datethis.state.date로 바꿀게요.
class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}
  1. class에 생성자를 추가해주고 this.state를 초기화 시킬게요.
class Clock extends React.Component {
  constructor(props) {
    super(props);

    this.state = {date: new Date()};
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

어떻게 props를 생성자에 전달하죠?

// 이렇게 props를 인자로 받고
constructor(props) {
  // 이렇게 super에 props를 전달해주어야 합니다.
  super(props);

  this.state = {date: new Date()};
}

클래스 컴퍼넌트는 항상 props로 기본 constructor를 호출해야 합니다.

  1. date props를 에서 삭제할게요.
ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

완성된 코드:

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

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

잠시만요. 타이머 코드는 어디에 갔죠? 이제 Clock이 혼자 업데이트 하도록 해볼게요.

생명주기 메서드를 클래스에 추가하기

많은 Component가 있는 응용프로그램에서는 Component가 릴리즈 될 때 사용된 리소스를 확보 하는 것이 중요합니다.

  • Clock이 처음 DOM에 렌더링 될 때 타이머를 설정해야 합니다.( mount )
  • Clock에 의해 생성된 DOM이 삭제될 때 타이머를 해제해야 합니다.( unmount )
class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
    /// Component가 mount 되면 실행됨.
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    /// Component가 unmount 되면 실행됨.
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
      date: new Date()
    });
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

componentDidMount에서 타이머를 설정했고, componentDidUnmount에서 타이머를 해제했습니다.

근데 타이머 아이디를 어떻게 저장한거죠?

componentDidMount() {
  this.timerID = setInterval(
    () => this.tick(),
    1000
  );
}

this.props는 React에 의해 설정되고, this.state는 특수한 의미가 있습니다. 그런데, 타이머ID 같이 데이터 흐름 안에 포함되지 않는 어떤 항목을 보관할 필요가 있으면 클레스에 부가적인 필드를 추가해도 됩니다.

state를 업데이트 하기위해 tick() 함수 내에서 this.setState()를 사용합니다.

요약:

  • <Clock />ReactDOM.render()에 전달되면 React는 Clock의 생성자를 호출한다. 그럼 현재 시간이 포함된 객체로 state를 초기화 한다.
  • React는 Clock컴퍼넌트의 render() 함수를 호출. 이 render 함수가 React가 DOM에 무엇을 표시해야 하는지 결정하는 방법이다. 그 다음 React는 DOM을 업데이트 해서 Clock 렌더링 출력과 일치시킨다.
  • Clock의 출력이 DOM에 삽입되면, React는 componentDidMount() 생명주기 메서드를 호출한다. 그 안에서 Clock 컴퍼넌트는 매초 컴퍼넌트의 tick() 메서드를 호출하기 위한 타이머를 설정하도록 브라우저에 요청함.
  • 매 초 마다 브라우저가 tick()메서드를 호출함. 그 안에서는 setState()로 현재 시간을 계속 넘겨줘서 UI 업데이트를 진행한다. setState()를 호출하는 덕분에 React는 상태가 변경되었다는 것을 감지하고, render() 함수를 다시 호출한다. this.state.date가 달라졌기 때문에 render()의 결과값이 달라지고, 렌더링에 업데이트된 시간이 포함된다. React는 이에 따라 DOM을 업데이트 한다.
  • componentWillUnmount() 덕분에 페이지 이동등의 액션으로 component가 DOM에서 제거되면 타이머도 같이 제거된다.

State를 올바르게 사용하기

직접 State를 수정하지 마세요

// Wrong
this.state.comment = 'Hello';

대신에 setState()를 사용합니다.

// Correct
this.setState({comment: 'Hello'});

this.state를 지정할 수 있는 유일한 공간은 바로 생성자입니다.

State 업데이트가 비동기적일 수 있어요

React는 여러 setState() 호출을 하나의 업데이트로 일괄처리하여 성능을 높입니다.

따라서 this.propsthis.state가 즉시 업데이트 되는 것이 아니라 비동기 적으로 업데이트 될 수 있으므로 다음 상태를 계산하기 위해 이 값에 의존하면 안됩니다.

예를 들어, 다음 코드는 카운터 업데이트에 실패할 수 있습니다.

// wrong
this.setState({
  counter: this.state.counter + this.props.increment,
});

이를 수정하기 위해 객체보다는 함수를 인자로 사용하는 다른 형태의 setState()를 사용합니다. 그 함수의 첫 번째 인자는 이전 state이고, 두 번째 인자는 업데이트가 적용된 시점의 props 입니다.

// Correct
this.setState((prevState, props) => ({
  counter: prevState.counter + props.increment
}));

State 업데이트는 병합됩니다

아래 state를 봐주세요. posts, comments가 있습니다.

constructor(props) {
  super(props);
  this.state = {
    posts: [],
    comments: []
  };
}

이제 이걸 2번에 걸쳐서 업데이트를 해볼게요.

componentDidMount() {
  fetchPosts().then(response => {
    this.setState({
      posts: response.posts
    });
  });

  fetchComments().then(response => {
    this.setState({
      comments: response.comments
    });
  });
}

2번 째 this.setState({ comments })가 호출될 때 this.state.posts의 데이터는 온전히 남아있고, this.state.comments의 데이터는 완전히 대체 됩니다.

데이터는 아래로 흐릅니다

부모 컴포넌트나 자식 컴포넌트 모두 특정 컴포넌트가 유상태인지 또는 무상태인지 알 수 없고, 그들이 함수나 클래스로 정의되었는지에 대해서 관심을 가질 필요가 없습니다.

그래서 State는 로컬변수라고 불리거나, 캡슐화 되었다고 불립니다. 그 이유는 다른 Component에서 접근이 안되기 때문입니다.

그리고 부모 Component의 state를 자식 Component의 prop으로 전달할 수 있습니다.

<h2>It is {this.state.date.toLocaleTimeString()}.</h2>

이것 또한 사용자 정의 컴퍼넌트에도 적용 가능합니다.

<FormattedDate date={this.state.date} />
const FormattedDate = (props) =>
  (<h2>It is {props.date.toLocaleTimeString()}.</h2>);

이걸 일반적으로 “top-down(하향식)” 혹은 “단방향” 데이터 흐름이라고 부릅니다. 모든 state는 특정 Component가 소유하고, 해당 State에서 파생된 모든 데이터는 아래에만 영향을 미칩니다.

트리구조가 props들의 폭포라고 상상하면 각 컴포넌트의 state는 임의의 점에서 만나지만 동시에 아래로 흐르는 부가적인 수원(water source)이라고 할 수 있습니다.


이벤트 처리하기

HTML과 React 이벤트 처리 방식의 차이:

  • React 이벤트는 소문자보다 camelCase를 사용합니다.
  • JSX에서는 문자열보다 이벤트핸들러 함수를 전달해서 사용합니다.

HTML:

<button onclick="activateLasers()">
  Activate Lasers
</button>

React:

<button onClick={activateLasers}>
  Activate Lasers
</button>

또 다른 차이점으로는 이벤트에 false를 리턴해서 이벤트를 정지할 수 없습니다. 반드시 명시적으로 preventDefault를 호출해야 합니다.

HTML:

<a href="#" onclick="console.log('The link was clicked.'); return false">
  Click me
</a>

React:

const ActionLink = () => {
  const handleClick = (e) => {
    e.preventDefault();
    console.log('The link was clicked.');
  }

  return (
    <a href="#" onClick={handleClick}>
      Click me
    </a>
  );
}

여기서 e는 합성 이벤트입니다. React는 W3C 명세에 따라 합성 이벤트를 정의하기 때문에 브라우저 호환성에 대해 걱정할 필요가 없습니다.

React를 쓸 때 일반적으로 addEventListener를 사용할 필요가 없습니다. 대신 Element가 처음 렌더링 될 때 리스너를 제공해주세요.

ES6 클래스를 사용할 때 일반적인 패턴으로 event handler가 클래스의 메서드가 되도록 하는 것입니다:

class Toggle extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isToggleOn: true};

    // 콜백에서 `this`가 작동하려면 아래와 같이 바인딩 해주어야 합니다.
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState(prevState => ({
      isToggleOn: !prevState.isToggleOn
    }));
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.isToggleOn ? 'ON' : 'OFF'}
      </button>
    );
  }
}

JSX 콜백에서 this의 의미에 대해 주의해야 합니다. JS에서는 클래스 메서드가 기본적으로 바인딩 되지 않습니다. 생성자에서 바인딩 해주지 않으면 onClick 메서드가 undefined 일거에요.

이건 React의 행동이 아닙니다. JS에서 함수가 작동하는 방식의 일부입니다. 일반적으로 onClick={this.handleClick}과 같이 ()가 없는 함수콜을 하려면 메서드를 를 바인딩 해야합니다.

이렇게 바인딩 하는것이 귀찮으세요? 아래처럼 실험적인 신택스를 사용해보세요.

class LoggingButton extends React.Component {
  // 이 문법은 `this`가 handleClick 내에서 바인딩되도록 합니다.
  // 주의: 이 문법은 *실험적인* 문법입니다.
  handleClick = () => {
    console.log('this is:', this);
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    );
  }
}

CRA(create-react-app)을 사용하시면 기본적으로 활성화 됩니다.

만약 클래스 필드 문법을 사용하고 있지 않다면, 이렇게 화살표 함수를 사용할 수 도 있어요.

class LoggingButton extends React.Component {
  handleClick() {
    console.log('this is:', this);
  }

  render() {
    // 이 문법은 `this`가 handleClick 내에서 바인딩되도록 합니다.
    return (
      <button onClick={(e) => this.handleClick(e)}>
        Click me
      </button>
    );
  }
}

그런데 이렇게 하게되면 이 Component가 생성될 때 마다 다른 콜백이 생성됩니다. 대부분의 경우 문제가 되지 않으나, 콜백이 하위 컴퍼넌트에 prop으로서 전달된다면 그 컴퍼넌트들은 추가로 다시 렌더링을 수행할 수도 있습니다. 이러한 종류의 성능 문제를 피하고자, 생성자 안에서 바인딩하거나 클래스 필드 문법을 사용하는 것을 권장합니다.