React 개발자가 실수하기 쉬운 몇 가지 (3)

제가 개발 중인 JavaScript 정적 분석 도구 DeepScan은 JavaScript의 일반적인 오류 외에도 최근 핫한 React를 잘 지원하려는 목표를 갖고 있습니다.

ESLint나 주변 React 개발자들의 피드백을 통해 십여 종의 React 검증 규칙을 개발해 왔는데, 이 중에서 React를 처음 배우는 개발자들이 실수하기 쉬운 내용을 추려 시리즈로 연재합니다.

지난 글에서 오픈소스의 사례로 다음과 같은 React의 실수 패턴을 알아봤습니다.

  • render 함수의 반환 값을 직접 사용
  • 이벤트 핸들러 함수에서 이벤트 전파가 중단되지 않는 경우
  • JSX에서 JavaScript 주석을 사용하는 경우
  • length 속성 검사

오늘도 다양한 실수 패턴을 알아보도록 하겠습니다.

이벤트 핸들러를 문자열로 지정한 경우

HTML에서는 이벤트 핸들러를 문자열로 지정합니다. <image src="hello.png" onclick="alert('Hi')"/> 같이 문자열에 스크립트 코드를 넣죠.

하지만 React의 이벤트 핸들러는 HTML과 달리 항상 함수 객체로 지정되어야 합니다. 이벤트 핸들러를 문자열로 지정하면 React에서 예외가 발생합니다.

React의 테스트 코드에서도 문자열로 지정된 이벤트 핸들러에 대한 체크를 볼 수 있습니다.

it('should prevent non-function listeners, at dispatch', () => {
  var node = ReactTestUtils.renderIntoDocument(
    <div onClick="not a function" />,
  );
  expect(function() {
    ReactTestUtils.SimulateNative.click(node);
  }).toThrowError(
    'Expected onClick listener to be a function, instead got type string',
  );
});

__tests__/EventPluginHub-test.js

React의 이벤트 핸들러는 <div onClick="console.log('clicked')"> 같이 쓰지 않고 아래처럼 함수 객체가 지정되어야 함을 기억하세요.

    import React from 'react';

    class Hello extends React.Component {
        handleClick() {
            console.log('clicked');
        }

        render() {
            return (
                <div onClick={this.handleClick}>
                    Hello
                </div>
            );
        }
    }

DeepScan Rule

DeepScan의 BAD_EVENT_HANDLER 규칙은 이벤트 핸들러에 문자열이 지정된 경우를 찾아 개발자의 실수를 방지할 수 있습니다.

Event handler of a React element cannot be a string. Consider specifying a function instead.
<div onClick="not a function" />,

이벤트 핸들러 함수가 올바로 바인딩 되지 않은 경우

이벤트 핸들러 관련해서 하나 더 보도록 하죠.

React는 이벤트 핸들러를 호출할 때 this 객체를 제공하지 않기 때문에 이벤트 핸들러 함수에서 this의 속성에 접근하는 경우 TypeError 예외가 발생합니다.

이를 해결하기 위해서는 Function.prototype.bind()를 이용해 this 객체를 명시적으로 지정하거나 ES6의 화살표 함수(arrow function)를 사용해야 합니다. 참고로 React.createClass()에서는 멤버 함수가 자동으로 this 객체와 바인딩 되므로 이를 클래스로 변환할 때는 특히 주의할 필요가 있습니다.

아래 오픈소스 wp-calypso의 예를 보면 이벤트 핸들러로 지정된 this.trackUpgradeClick 함수가 제대로 바인딩 되어 있지 않아 에러가 발생합니다.

import React, { Component } from 'react';

class SiteSettingsFormGeneral extends Component {
	blogAddress() {
		translate(
			'Buy a custom domain, ' +
			'map a domain you already own, ' +
			'or redirect this site.',
			{
				components: {
					domainSearchLink: (
						<a href={ '/domains/add/' + site.slug } onClick={ this.trackUpgradeClick } />
					),
					mapDomainLink: (
						<a href={ '/domains/add/mapping/' + site.slug } onClick={ this.trackUpgradeClick } />
					),
					redirectLink: (
						<a href={ '/domains/add/site-redirect/' + site.slug } onClick={ this.trackUpgradeClick } />
					)
				}
			}
		)




		return (
			<FormFieldset className="site-settings__has-divider">
				<FormLabel htmlFor="blogaddress">{ translate( 'Site Address' ) }</FormLabel>
				<div className="site-settings__blogaddress-settings">
					<FormInput
						name="blogaddress"
						type="text"
						id="blogaddress"
						value={ site.domain }
						disabled="disabled" />
					{ customAddress }
				</div>
				{ addressDescription }
			</FormFieldset>
		);
	}

	trackUpgradeClick() {
		this.props.recordTracksEvent( 'calypso_upgrade_nudge_cta_click', { cta_name: 'settings_site_address' } );
	}
}

site-settings/form-general.jsx

다행히 이 커밋에서 trackUpgradeClick을 화살표 함수로 변경한 것을 볼 수 있습니다.

DeepScan Rule

DeepScan의 EVENT_HANDLER_INVALID_THIS 규칙은 이벤트 핸들러 함수가 this 객체와 바인딩 되지 않은 경우를 찾아 개발자의 실수를 방지할 수 있습니다.

Function 'this.trackUpgradeClick' is used as a React event handler without 'this' binding. But 'this' object is accessed in the function body at line 312.
<a href={ '/domains/add/' + site.slug } onClick={ this.trackUpgradeClick } />

trackUpgradeClick 함수 내부인 312 라인에서 this 객체가 사용되기 때문에 문제가 된다는 것을 바로 알 수 있네요.

엘리먼트의 스타일 속성을 잘못 지정한 경우

이벤트 핸들러를 문자열로 지정할 수 없는 것과 유사하게 React는 style 속성에 CSS 규칙을 표현하는 문자열을 허용하지 않습니다.

React에서는 HTML과 달리 style 속성을 항상 객체로 지정해야 하고 객체, null 또는 undefined 외의 값을 지정한 경우 예외가 발생합니다.

React의 테스트 코드에서도 이에 대한 체크를 볼 수 있습니다.

    it('should report component containing invalid styles', () => {
      class Animal extends React.Component {
        render() {
          return <div style={1} />;
        }
      }

      expect(function() {
        ReactDOM.render(<Animal />, container);
      }).toThrowError(
        'The `style` prop expects a mapping from style properties to values, ' +
          "not a string. For example, style= " +
          'when using JSX.\n\nThis DOM node was rendered by `Animal`.',
      );
    });

__tests__/ReactDOMComponent-test.js

DeepScan Rule

DeepScan의 BAD_STYLE_PROP 규칙은 style 속성이 객체가 아닌 값으로 지정된 경우를 찾아 개발자의 실수를 방지할 수 있습니다.

'style' prop of a React DOM element cannot be a numeric value. Consider specifying an object instead.
return <div style={1} />;

자식 엘리먼트의 key 속성을 지정하지 않은 경우

React에서는 DOM 트리를 업데이트할 때 key 속성을 활용한 diff 알고리즘을 통해 렌더링을 최적화합니다.

그런데 DOM 노드의 각 자식 엘리먼트에 key 속성이 없으면 해당 diff 알고리즘을 활용하지 못하고 렌더링 성능에 영향을 줄 수 있습니다. React에서 key 속성이 없을 경우 경고하는 이유이기도 하죠.

따라서 엘리먼트가 부모 노드의 자식으로서 컬렉션에 포함되는 경우 key 속성을 지정하는 습관을 가져야 합니다.

아래 오픈소스 react-viskey 속성 없이 사용하는데,

function renderSection(section, index) {
  const content = [(
    <div className={`fcol fg bullet-info ${(index % 2) ? '' : 'bullet-info-reversed'}`}>
      <div className="bullet-point-title">
        {section.title}
      </div>
      <div className="bullet-point-text">
        {section.text}
      </div>
    </div>
  ), (
    <div className="bullet-example">
      {section.component}
    </div>
  )];
  return (
    <div className="bullet-point f">
      {!(index % 2) ? content.reverse() : content}
    </div>
  );
}

components/home.js

향후 커밋에서 key 속성이 추가되었음을 볼 수 있습니다.

DeepScan Rule

DeepScan의 MISSING_KEY_PROP 규칙은 컬렉션 내의 엘리먼트에서 key 속성이 빠진 경우를 찾아 개발자의 실수를 방지할 수 있습니다.

Each child React element in a collection should have a 'key' prop.
[(
    <div className="bullet-example">
      {section.component}
    </div>
)]

Wrap-Up

위에 제시된 코드들은 데모 페이지에서 바로 붙여넣어 체크해 볼 수 있습니다.

오늘 살펴본 React 코드 오류는 다음과 같습니다.

  • 이벤트 핸들러를 문자열로 지정한 경우
  • 이벤트 핸들러 함수가 올바로 바인딩 되지 않은 경우
  • 엘리먼트의 스타일 속성을 잘못 지정한 경우
  • 자식 엘리먼트의 key 속성을 지정하지 않은 경우

특히 React를 처음 배우는 분들에게 도움이 되길 바라며 오늘 글을 마칩니다.

Related Posts

comments powered by Disqus