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

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

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

React는 페이스북이 개발과 마케팅을 주도하는 UI 개발용 JavaScript 라이브러리입니다.

컴포넌트 구조, DOM과 분리된 상태 관리, 빠른 렌더링, JavaScript 중심의 구현 같은 특징으로 많은 인기를 얻고 있는데 기존에 HTML과 함께 DOM을 JavaScript로 직접 처리하는 데 익숙한 프론트엔드 개발자들에게는 조금 낯선 것도 사실입니다.

이 글에서는 React를 배우는 개발자들이 실수하기 쉬운 내용을 패턴1으로 정리해서 오픈소스 wp-calypso의 코드와 함께 설명합니다.

React API의 오타

매우 단순한 성격의 오류로서 React 컴포넌트의 lifecycle 메소드 이름이나 PropTypes를 잘못 쓰는 경우입니다.

컴포넌트의 lifecycle 메소드

React 컴포넌트는 생성되거나 상태 변경이 완료되었거나 하는 생명주기에 따른 lifecycle 메소드를 갖고 있습니다.

  • componentWillMount()
  • componentDidMount()
  • componentWillUnmount()
  • componentWillReceiveProps()
  • shouldComponentUpdate()
  • componentWillUpdate()
  • componentDidUpdate()
  • render()

그런데 이 이름이 조금 길고 camel case여서 개발자가 실수하는 경우가 왕왕 있습니다.

아래 예를 보죠.

import { Component } from 'react';

class SyncReaderFollows extends Component {
    check() {
        if ( this.props.shouldSync ) {
            this.props.requestFollows();
        }
    }
 
    componentDidMount() {
        this.check();
    }
 
    componentDidUpate() {
        this.check();
    }
 
    render() {
        return null;
    }
}

sync-reader-follows/index.js

상태 변경 완료 후, 즉 componentDidUpdate에서 check 함수의 호출을 의도했지만 실제로는 componentDidUpate 오타 때문에 원하는 대로 동작하지 않습니다.

PropTypes

React의 PropTypes는 컴포넌트가 가진 속성들의 타입(필수 여부, 값의 형식 등)을 지정할 수 있게 합니다.

import React from 'react';
import PropTypes from 'prop-types';

export default class Item extends React.Component {
    static propTypes = {
        id: PropTypes.number.isRequired,
        comment: PropTypes.string,
    }
}

위와 같이 propTypes라는 camel case로 정의하는데, 개발자들이 PropTypes라고 그대로 사용하는 경우가 있습니다.

import React, { PureComponent, PropTypes } from 'react';
import { localize } from 'i18n-calypso';
import { truncateArticleContent } from '../helpers';

export class GooglePlusSharePreview extends PureComponent {
    static PropTypes = {
        articleUrl: PropTypes.string,
        externalProfilePicture: PropTypes.string,
        externalProfileUrl: PropTypes.string,
        externalName: PropTypes.string,
        imageUrl: PropTypes.string,
        message: PropTypes.string,
    };
}

google-plus-share-preview/index.js

PropTypes라는 오타에 의해 GooglePlusSharePreview 컴포넌트에 대한 PropTypes 검사가 동작하지 않게 됩니다.

DeepScan Rule

DeepScan의 REACT_API_TYPO 규칙은 이런 오타를 찾아 개발자의 실수를 방지할 수 있습니다. 또 단순한 오타 지적 외에 적절한 메소드 이름을 제안해 개발자가 쉽게 코드를 수정할 수 있습니다.

'componentDidUpate' could be a typo. Did you mean 'componentDidUpdate'?
componentDidUpate() {
'PropTypes' could be a typo. Did you mean 'propTypes' instead?
static PropTypes = {

render 함수에서 잘못된 값을 반환

React의 render 함수는 UI에 표시할 DOM tree(React element)를 반환합니다. 표시할 정보가 없으면 null 혹은 false를 반환할 수 있고요. 이 외의 값을 반환할 경우 에러가 발생합니다.

var React = require( 'react' );

module.exports = React.createClass( {
    render: function() {
        if ( ! this.props.site || ! this.props.plugin ) {
            return;
        }
        if ( this.props.site.canUpdateFiles &&
                ( ( this.props.site.plugin.update && ! this.props.site.plugin.update.recentlyUpdated ) || this.isUpdating() ) ) {
            if ( ! this.props.expanded ) {
                /* eslint-disable wpcalypso/jsx-gridicon-size */
                return <Gridicon icon="sync" size={ 20 } />;
                /* eslint-enable wpcalypso/jsx-gridicon-size */
            }
     
            return this.renderUpdate();
        }
        return null;
    }
} );

plugin-site-update-indicator/index.jsx

개발자는 props를 체크해 값이 없으면 렌더링을 하지 않을 목적으로 바로 return하였을 것입니다. 하지만 React의 render 함수는 React element, null 그리고 false만 반환할 수 있어서 위와 같이 undefined가 반환되면 예외가 발생하고 렌더링 이후의 다른 lifecycle 메소드가 수행되지 않습니다.

헷갈리기 쉬운 부분이죠?

DeepScan Rule

DeepScan의 BAD_REACT_API_RETURN_VALUE 규칙은 render 함수의 반환 값을 체크하고 적절한 반환 값에 대한 가이드를 제공해 개발자의 실수를 방지할 수 있습니다.

The 'render()' function of a React component returns an undefined value at this point. Consider returning false or null.
return;

이벤트 핸들러 함수를 잘못 지정한 경우

이벤트 핸들러로 함수 객체가 지정되어야 하는데 잘못된 값이 지정된 경우입니다.

import React, { Component } from 'react';

const EditUserForm = React.createClass( {
    recordFieldFocus( fieldId ) {
        analytics.ga.recordEvent( 'People', 'Focused on field on User Edit', 'Field', fieldId );
    },

    handleChange( event ) {
        this.setState( {
            [ event.target.name ]: event.target.value
        } );
    },

    renderField( fieldId ) {
        let returnField = null;
        switch ( fieldId ) {
            case 'roles':
                returnField = (
                    <RoleSelect
                        id="roles"
                        name="roles"
                        key="roles"
                        siteId={ this.props.siteId }
                        value={ this.state.roles }
                        onChange={ this.handleChange }
                        onFocus={ this.recordFieldFocus( 'roles' ) }
                    />
                );
                break;
        }
    }
} );

edit-team-member-form/index.jsx

onFocus 이벤트 핸들러에 함수를 지정해야 하는데 함수를 호출한 결과인 undefined 값이 지정되고 있습니다. 그 결과 Google Analytics에서 해당 focus 이벤트에 대한 기록이 빠질 것입니다.

이 경우는 다음과 같이 문제를 해결할 수 있습니다.

ES5 bind 함수:

onFocus={ this.recordFieldFocus.bind( this, 'roles' ) }

ES6 arrow 함수:

onFocus={ () => this.recordFieldFocus( 'roles' ) }

단, 위 방식은 렌더링할 때마다 새로운 함수를 생성하므로 성능 이슈가 있고, 생성자에서 한 번 바인딩하거나 별도 컴포넌트로 분리하는 것이 권장되고 있습니다.

DeepScan Rule

DeepScan의 MISSING_RETURN_VALUE 규칙은 함수의 반환 값을 체크해 잘못된 이벤트 핸들러 지정을 방지할 수 있습니다. 이벤트 핸들러에 지정된 함수의 정의 위치도 메시지에서 알려주기 때문에 개발자는 문제 원인이 되는 함수를 바로 확인해 볼 수 있습니다.

No value is returned from function 'recordFieldFocus' defined at line 133.
onFocus={ this.recordFieldFocus( 'roles' ) }

DOM 엘리먼트에서 잘못된 속성 지정

React의 DOM 엘리먼트(element)에서 React 속성이 아닌 기존 DOM 속성을 사용하는 경우입니다. React의 DOM 엘리먼트 속성은 camel case이므로 기존 속성을 사용할 경우 동작하지 않을 수 있습니다.

대표적인 예로 다음 두 가지를 들 수 있습니다.

  • React에서는 클래스 지정을 위해 className을 사용해야 하는데 기존 DOM 속성인 class를 사용한 경우
  • React의 클릭 이벤트 핸들러는 onClick인데 onclick으로 사용한 경우. 대소문자 구분이 없는 HTML에 익숙한 기존 프론트엔드 개발자에게 특히 혼동되기 쉬운 부분인 것 같습니다.
import React, { PropTypes } from 'react';

export default React.createClass( {
    getImportError: function() {
        return this.translate(
            '%(errorDescription)sTry again or contact support.', {
                args: {
                    errorDescription: this.props.description
                },
                components: {
                    a: <a href="#" onclick={ this.retryImport }/>,
                    br: <br />,
                    cs: <a href="#" onclick={ this.contactSupport } />
                }
            }
        );
    }
} );

importer/error-pane.js

위 코드에서는 클릭 이벤트가 실행되지 않습니다.

오픈소스 react-native-macos에서도 frameBorder 속성을 frameborder로 사용하는 오류를 발견할 수 있네요.

var React = require('React');

var Modal = React.createClass({
  render: function() {
    return (
      <div>
        <div className="modal">
          <div className="modal-content">
            <button className="modal-button-close">×</button>
            <div className="center">
              <iframe className="simulator" src={url} width="256" height="550" frameborder="0" scrolling="no"></iframe>
              <p>Powered by <a target="_blank" href="https://appetize.io">appetize.io</a></p>
            </div>
          </div>
        </div>
        <div className="modal-backdrop" />
      </div>
    );
  }
});

layout/AutodocsLayout.js

DeepScan Rule

DeepScan의 BAD_UNKNOWN_PROP 규칙은 DOM element의 속성 이름을 체크하고 적절한 속성 이름에 대한 가이드를 제공해 잘못된 속성이 지정되지 않도록 합니다.

'onclick' is not a valid prop for React DOM element. Did you mean 'onClick' event handler instead?
a: <a href="#" onclick={ this.retryImport }/>,
'frameborder' is not a valid prop for React DOM element. Did you mean 'frameBorder' DOM property instead?
<iframe className="simulator" src={url} width="256" height="550" frameborder="0" scrolling="no"></iframe>

Wrap-Up

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

가볍고 단순하지만, 또 바로 익숙해지지는 않는 React!

React 개발에 도움이 될 만한 내용을 앞으로도 계속 공유하겠습니다.

comments powered by Disqus