React 이벤트 처리와 조건부 렌더링

React 이벤트 처리 및 렌더링 가이드

이벤트 처리

React 엘리먼트에서 이벤트를 처리하는 방식은 DOM 엘리먼트에서 이벤트를 처리하는 방식과 매우 유사하지만, React 이벤트는 소문자 대신 카멜케이스를 사용하고, JSX를 사용하여 문자열이 아닌 함수로 이벤트 핸들러를 전달한다는 차이가 있습니다.

HTML의 이벤트 처리 방식
<button onclick="activateLasers()">
  Activate Lasers
</button>
React의 이벤트 처리 방식
<button onClick={activateLasers}>
  Activate Lasers
</button>

또 React에서는 false를 반환해도 기본 동작을 방지할 수 없기 때문에 HTML에서 폼을 제출할 때 사용되는 기본 동작을 방지하기 위해서는 반드시 preventDefault를 명시적으로 호출해 주어야 합니다.

HTML 폼 기본동작 막기
<form onsubmit="console.log('You clicked submit.'); return false">
  <button type="submit">Submit</button>
</form>
React 폼 기본동작 막기
function Form() {
  function handleSubmit(e) {
    e.preventDefault();
    console.log('You clicked submit.');
  }

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit">Submit</button>
    </form>
  );
}

위 코드에서 e는 합성 이벤트를 뜻하는데, React는 W3C 명세에 따라 합성 이벤트를 정의하기 때문에 브라우저 호환성에 대해 걱정할 필요가 없지만, React 이벤트는 브라우저의 고유 이벤트와 정확히 동일하게 동작하지는 않는다고 합니다.

React에서는 DOM 엘리먼트가 생성된 후 리스너를 추가하기 위해 addEventListener를 호출할 필요없이 엘리먼트가 처음 렌더링될 때 리스너를 제공하면 됩니다.

ES6 클래스로 컴포넌트 정의하기
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>
    );
  }
}

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

ES6 클래스를 사용하여 컴포넌트를 정의할 때, 일반적인 패턴은 이벤트 핸들러를 클래스의 메서드로 만드는 겁니다. 위 코드에서 Toggle 컴포넌트는 사용자가 ONOFF 상태를 토글 할 수 있는 버튼을 렌더링할 수 있습니다.

JSX 콜백 안에서는 this의 의미에 대해 주의할 필요가 있는데, JavaScript에서 클래스 메서드는 기본적으로 바인딩되어 있지 않습니다. 그래서 this.handleClick을 바인딩하지 않고 onClick에 전달하면 함수가 실제 호출될 때 thisundefined를 가리키게 됩니다.

이런 동작은 사실 React 뿐만 아니라 JavaScript에서 함수가 작동하는 방식이라고 할 수 있는데, 일반적으로 onClick={this.handleClick}과 같이 뒤에 괄호()를 사용하지 않고 메서드를 참조하는 경우에는 해당 메서드를 바인딩 해주어야 합니다.

만약 bind를 호출하는 것이 불편하다면, 이를 해결할 수 있는 두 가지 방법이 있는데, 우선 퍼블릭 클래스 필드 문법을 사용하는 경우라면 클래스 필드를 사용하여 콜백을 올바르게 바인딩할 수 있습니다.

이 방식은 실험적인 문법이기 때문에 많이 사용되지는 않지만, Create React App에서는 이 문법이 기본적으로 설정되어 있습니다.

클래스 필드로 콜백 바인딩하기
class LoggingButton extends React.Component {
  // `this`가 handleClick 내에서 바인딩되도록 하는 문법
  handleClick = () => {
    console.log('this is:', this);
  }

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

클래스 필드 문법 대신 콜백에 화살표 함수를 사용하는 방법도 있습니다.

화살표 함수로 바인딩하기
class LoggingButton extends React.Component {
  handleClick() {
    console.log('this is:', this);
  }

  render() {
    // 화사표 함수로 `this`를 handleClick 내에 바인딩
    return (
      <button onClick={() => this.handleClick()}>
        Click me
      </button>
    );
  }
}

그런데 이 문법은 LoggingButton이 렌더링될 때마다 다른 콜백이 생성된다는 문제가 있습니다.

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

이벤트 핸들러에 인자 전달하기

루프 내부에서는 이벤트 핸들러에 추가적인 매개변수를 전달하는 것이 일반적인데, 다음과 같이 id가 행의 ID일 경우 모두 정상적으로 작동합니다.

화살표 함수를 사용한 경우
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
Function.prototype.bind를 사용한 경우
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>

위의 코드는 같은 동작은 하는 코드로 각각 화살표 함수Function.prototype.bind를 사용하고 있는데, 두 경우 모두 React 이벤트를 나타내는 e 인자가 ID의 뒤에 두 번째 인자로 전달되고 있습니다. 다만 화살표 함수를 사용하면 명시적으로 인자를 전달해야 하는 반면, bind를 사용하는 경우에는 추가 인자가 자동으로 전달된다는 차이가 있습니다.

조건부 렌더링

React에서는 원하는 동작을 캡슐화하는 컴포넌트를 만들 수 있는데, 이렇게 하면 애플리케이션의 상태에 따라서 컴포넌트 중 몇 개만을 렌더링할 수 있습니다.

React에서 조건부 렌더링은 JavaScript에서의 조건 처리와 같이 동작하는데, if조건부 연산자와 같은 JavaScript 연산자를 현재 상태를 나타내는 엘리먼트를 만드는 데에 사용하면 React는 조건에 따라 현재 상태에 맞는 UI를 업데이트할 수 있습니다.

조건부 렌더링을 위한 두 개의 컴포넌트
function UserGreeting(props) {
  return <h1>Welcome back!</h1>;
}

function GuestGreeting(props) {
  return <h1>Please sign up.</h1>;
}

위와 같은 두 개의 컴포넌트가 있다면, 다음과 같이 isLoggedIn이라는 사용자의 로그인 속성의 상태에 맞는 컴포넌트를 보여주는 Greeting 컴포넌트를 만들 수 있습니다.

Greeting 조건부 렌더링 컴포넌트
function Greeting(props) {
  const isLoggedIn = props.isLoggedIn;
  if (isLoggedIn) {
    return <UserGreeting />;
  }
  return <GuestGreeting />;
}

ReactDOM.render(
  // Try changing to isLoggedIn={true}:
  <Greeting isLoggedIn={false} />,
  document.getElementById('root')
);

엘리먼트 변수

변수에도 엘리먼트를 저장할 수 있는데, 변수를 활용하면 출력의 다른 부분은 변하지 않는 상태로 컴포넌트의 일부를 조건부로 렌더링 할 수 있습니다.

로그인과 로그아웃 버튼 컴포넌트
function LoginButton(props) {
  return (
    <button onClick={props.onClick}>
      Login
    </button>
  );
}

function LogoutButton(props) {
  return (
    <button onClick={props.onClick}>
      Logout
    </button>
  );
}

위와 같이 로그아웃로그인 버튼을 나타내는 두 컴포넌트가 있다면, 다음과 같이 LoginControl이라는 상태 컴포넌트를 만들 수 있습니다.

LoginControl 컴포넌트는 현재 상태에 맞게 <LoginButton />이나 <LogoutButton />을 렌더링하고, 이전 예제에서 사용했던 <Greeting /> 컴포넌트를 활용해 상태에 따른 메시지를 렌더링해서 보여줄 수도 있습니다.

LoginControl 컴포넌트
class LoginControl extends React.Component {
  constructor(props) {
    super(props);
    this.handleLoginClick = this.handleLoginClick.bind(this);
    this.handleLogoutClick = this.handleLogoutClick.bind(this);
    this.state = {isLoggedIn: false};
  }

  handleLoginClick() {
    this.setState({isLoggedIn: true});
  }

  handleLogoutClick() {
    this.setState({isLoggedIn: false});
  }

  render() {
    const isLoggedIn = this.state.isLoggedIn;
    let button;
    if (isLoggedIn) {
      button = <LogoutButton onClick={this.handleLogoutClick} />;
    } else {
      button = <LoginButton onClick={this.handleLoginClick} />;
    }

    return (
      <div>
        <Greeting isLoggedIn={isLoggedIn} />
        {button}
      </div>
    );
  }
}

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

인라인에서 논리 연산자로 표현하기

변수를 선언하고 if를 사용해서 조건부로 렌더링 하는 것은 좋은 방법이지만, 연산자를 사용하면 조건부 렌더링을 더 짧은 구문으로 JSX에서 인라인으로 사용할 수 있습니다.

JSX는 중괄호{}를 이용해 표현식을 포함 할 수 있는데, 표현식에 JavaScript의 논리 연산자&&를 사용하면 엘리먼트에 쉽게 조건부 렌더링을 적용할 수 있습니다.

논리 연산자로 조건부 렌더링 구현하기
function Mailbox(props) {
  const unreadMessages = props.unreadMessages;
  return (
    <div>
      <h1>Hello!</h1>
      {unreadMessages.length > 0 &&
        <h2>
          You have {unreadMessages.length} unread messages.
        </h2>
      }
    </div>
  );
}

const messages = ['React', 'Re: React', 'Re:Re: React'];
ReactDOM.render(
  <Mailbox unreadMessages={messages} />,
  document.getElementById('root')
);

JavaScript에서 true && expression은 항상 expression으로 평가되고 false && expression은 항상 false로 평가되기 때문에, && 뒤의 엘리먼트는 조건이 true일때 출력이 되고, 조건이 false인 경우에는 React에서 무시되어 출력되지 않습니다.

그런데 만약 0과 같이 false의 값을 가지는 falsy 표현식을 반환하게 되면, 여전히 && 뒤에 있는 표현식은 건너뛰지만 falsy 표현식이 반환되기 때문에 주의할 필요가 있습니다. 다음 코드와 같이 <div>0</div>render 메서드에서 반환되는 것처럼 의도하지 않은 동작을 할 수도 있습니다.

falsy 데이터에 의해 오동작하는 경우
render() {
  const count = 0;
  return (
    <div>
      {count && <h1>Messages: {count}</h1>}
    </div>
  );
}

인라인에서 조건부 연산자로 if-else 표현하기

condition ? true: false와 같은 조건부 연산자를 사용하면 짧은 구문으로 엘리먼트를 조건부로 렌더링할 수 있습니다.

조건부 연산자를 사용한 엘리먼트 렌더링 1
render() {
  const isLoggedIn = this.state.isLoggedIn;
  return (
    <div>
      The user is <b>{isLoggedIn ? 'currently' : 'not'}</b> logged in.
    </div>
  );
}

다음과 같이 더 큰 표현식에서도 조건부 연산자를 사용할 수 있는데, 가독성은 좀 떨어질 수도 있습니다. 물론 가독성이 큰 문제가 되지 않는다면 편리한 방식을 사용하면 되지만, 렌더링을 위한 조건이 너무 복잡해진다면 컴포넌트를 분리하는 것을 고려해보는 것이 좋습니다.

조건부 연산자를 사용한 엘리먼트 렌더링 2
render() {
  const isLoggedIn = this.state.isLoggedIn;
  return (
    <div>
      {isLoggedIn
        ? <LogoutButton onClick={this.handleLogoutClick} />
        : <LoginButton onClick={this.handleLoginClick} />
      }
    </div>
  );
}

컴포넌트 렌더링 제외처리

간혹 다른 컴포넌트에 의해 렌더링되는 경우 컴포넌트 자체를 숨겨야 하는 경우가 있을 수 있는데, 이때는 렌더링 결과를 출력하는 대신 null을 반환하면 됩니다.

다음의 코드는 <WarningBanner />warnprop 값에 의해 렌더링되는데, propfalse인 경우 컴포넌트는 렌더링되지 않습니다. 이 때 컴포넌트의 render 메서드로부터 null을 반환하는 것은 생명주기 메서드 호출에 영향을 주지 않고, componentDidUpdate는 계속해서 호출하게 됩니다.

컴포넌트 렌더링 제외처리
function WarningBanner(props) {
  if (!props.warn) {
    return null;
  }

  return (
    <div className="warning">
      Warning!
    </div>
  );
}

class Page extends React.Component {
  constructor(props) {
    super(props);
    this.state = {showWarning: true};
    this.handleToggleClick = this.handleToggleClick.bind(this);
  }

  handleToggleClick() {
    this.setState(state => ({
      showWarning: !state.showWarning
    }));
  }

  render() {
    return (
      <div>
        <WarningBanner warn={this.state.showWarning} />
        <button onClick={this.handleToggleClick}>
          {this.state.showWarning ? 'Hide' : 'Show'}
        </button>
      </div>
    );
  }
}

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

답글 남기기