블로그 내 검색

2015. 7. 7.

React 별도 사전 컴파일 없이 써보자. ( + with RequireJS )

React?

여기서 이런저런 설명하는 것보다 공식 사이트에서 보는게 정확하다.
그래도 그냥 넘어가긴 아쉬우니 요약해보면,
A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES
사용자 인터페이스라는건 보다 실무적으로 말하면 화면을 구성하는 UI 컴포넌트를 말한다.

예를 들어 포탈 사이트(네이버같은...)를 보면, 크게

  • 검색박스
  • 서비스 네비바
  • 광고 롤러
  • 로그인 박스
  • 기사 박스
  • ...

이런 UI 컴포넌트의 조합으로 만들어진다.

React 는 저 컴포넌트들을 커스텀 태그 형식으로 만들수 있는 라이브러리라고 요약할 수 있겠다.

<SearchBox lang='ko'/>
<Navigation />
<section>
    <Advertise rolling='hour'/>
    <LoginBox />
    <News />
</section>

이런 식.

사이트의 3가지 헤드라인을 보면 다음과 같이 말하고 있다.

View 만을 담당 (JUST THE UI)

Model, Controller 등은 지원하지 않고 오로지 UI만 담당한다.

Virtual DOM 으로 성능 향상 (VIRTUAL DOM)

Virtual DOM 으로 성능 향상 및 다양한 플랫폼 및 서버 및 클라이언트 환경 모두를 지원할 수 있다. (동형 자바스크립트 참고)

단방향 데이터 흐름(DATA FLOW)

AngularJS등에서 강조하던 양방향 데이터 바인딩이 아닌 단방향 데이터 바인딩만을 지원한다. 상태 (state) 기반으로 컴포넌트가 갱신되는 방식이다.

보다 자세한 건 문서를 읽거나, 직접 React를 사용해보면서 익히는게 더 빠르다.
(공식 사이트에 들어가면 나오는 ToDo Example 예제가 꽤 훌륭하게 되어 있다)

여기까지 보면 한번 써 볼까... 하는 마음이 들다가도 다음 이러한 기능을 구현하기 위한 Transform 이슈에 걸리면 살짝 망설여진다.

뭐지 Transform 이라는건...

Transform 이슈

React 문법 자체만 두고 보면 상당히 가독성이 떨어진다.

Virtual DOM 구현체라 직접 태그를 쓸 수 없고, DOM API의 document.createElement 비슷한 React.createElement('DIV') 이런식의 문법을 써야 한다.

<form action="/articles">
    <input type="text" name="id">
</form>

라는 DOM을 표현하려면, React 문법으로는

var input = React.createElement('input', { type: 'text', name: 'id' }),
    form = React.createElement('form', { action: '/' }, input);

이런 식으로 표현한다.
더 어렵고 알기 힘들다. 기존의 DOM API 가 아닌 Virtual DOM 방식이기에 이질적인건 어쩔수가 없다.

이렇게는 아무래도 쓰기 힘들기에 JSX 형식의 코딩을 지원하는데, 이 형식으로 코딩하면

var form = (
    <form action='/'>
        <input type='text' name='id' />
    </form>
);

이렇게 JavaScript 코드 안에 마크업을 넣을 수 있다!

HTML과 같네? 라고 생각할 수 있지만 관대한 HTML과는 다소 다른 문법이다. (닫는 태그가 반드시 필요하다거나)
물론 이러한 코드가 브라우저에서 바로 동작하진 않기에 JS 변환이 필요한데, 별도의 파서컴파일러로 처리하는 단계가 필요하다.

그냥 React.createElement 를 사용해서 코딩하면 이런 작업이 필요없지만, 아무래도 괴롭고 가독성 떨어지기 마련이라 여러 컴파일러중 하나를 택해 빌드 혹은 사전 로딩 절차에 포함시켜 두는게 일반적이다. (Grunt, Gulp, webpack 등등)

공식 React-Tool 도 지원하고 있다.

https://www.npmjs.com/package/react-tools

nodejs 설치가 필요하며, jsx > js 의 컴파일을 수행해준다.
watch 기능도 있어서 실시간 컴파일도 가능하다.

사전작업 없이 JSX로 쓸 수는 없나...


있다. (하지만 추천하지 않는다.)

React는 기본적으로 패키지를 받아볼 경우 JSXTransformer.js 를 제공하고 있고 이걸 사용해서 코딩을 할 수 있다.

먼저 간단한 Navigation Component 를 만들어보자

HTML을 만들자.
<!DOCTYPE html>
<html>
<head lang="ko">
    <meta charset="UTF-8">
    <title>React Test</title>
</head>
<body>

    <div class="nav"></div>

    <script src="//cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/react/0.13.3/JSXTransformer.js"></script>
    <script type="text/jsx" src="Navigation.jsx"></script>

</body>
</html>

Navigation.jsx 를 만든다.

// @file Navigation.jsx
(function(React) {

    // 보여줄 링크
    var linkList = [
        {
            title: "쿠팡", href :"http://www.coupang.com/"
        },
        {
            title: "아마존", href :"http://www.amazon.com/"
        },
        {
            title: "11번가", href :"http://www.11st.co.kr/"
        },
        {
            title: "Yes24", href :"http://www.yes24.com/"
        }
    ];

    var Navigation = React.createClass({
        links: function() {
            var self = this;
            return linkList.map(function(link) {
                return (
                    <li><a href={link.href} target={link.target || "_self"}>{link.title}</a></li>
                );
            });
        },
        render: function() {
            return (
                <ul classname="nav">
                    {this.links()}
                </ul>
            );
        }
    });

    React.render( <Navigation/>, document.querySelector(".nav") );

})(React);

파일을 실행해보면 정상적으로 링크가 그려지는걸 확인할 수 있다.

HTML 코드 부분을 잘 보면 Navigation.jsx 파일을 include 할 때 type 을 text/jsx 로 지정했다. 이렇게 하면 JSXTransformer 에서 이 파일을 JavaScript 변환한 뒤 실행시켜 주게 된다.

여기까지 하니 뭔가 뿌듯하다. 그렇지만 모던한 개발자로서 모듈화가 없는 이런 코딩은 뭔가 아쉽다.

그렇다고 모듈화를 바로 하기엔 JSX 형식이라 일반적인 방법으로는 힘들다.

Grunt나 Gulp 같은걸로 Compile 하면 간단하지만, 앞서 밝힌대로 사전작업 없이 순수하게 JavaScript 로만 처리하고 싶다면 어떻게 해야 할까?

with RequireJS


순수하게 브라우저의 힘으로 모듈화를 할 수 있는 RequireJS로 한번 모듈화를 시도해보자.

먼저 설정파일을 준비한다.

//@ Main.js
require.config({

    baseUrl: '.',

    waitSeconds: 1000,

    urlArgs: '_v_=1',

    deps: [
        'jquery',
        'react',
        'App'
    ],

    paths: {      
        'react': 'path/react/react.min',
        'JSXTransformer': 'path/react/JSXTransformer',
        'jquery': 'path/jquery/dist/jquery.min'
    },
    shim: {
        'JSXTransformer': {
            'exports': 'JSXTransformer'
        }
    }
});

다음에 할 일은 Navigation.jsx 모듈 코딩이다.

// @Navigation.jsx
define([
    'react'
], function(React) {

    var linkList = [
        {
            title: "쿠팡", href :"http://www.coupang.com/"
        },
        {
            title: "아마존", href :"http://www.amazon.com/"
        }
    ];

    var Navigation = function() {
        this.component = React.createClass({
            links: function() {
                return linkList.map(function(link) {
                    return (
                        <li><a href={link.href} target={link.target || '_self'}>{link.title}</a></li>
                    );
                });
            },
            render: function() {
                return (
                    <ul className="nav">
                        {this.links()}
                    </ul>
                );
            }
        });
    };

    Navigation.prototype.render = function(renderArea) {
        React.render(
            React.createElement(this.component, {
                linkList: linkList
            }),
            renderArea
        );
    };

    return Navigation;

});

이제 엔트리 파일 App.js, html 을 만든다.

// @App.js
define([
    'Navigation'
], function(Navigation) {

    'use strict';

    var view = document.querySelector('[data-view]');

    (function initialize() {

        var nav = new Navigation();
        nav.render(view);

    })();

});

<!DOCTYPE html>
<html>
<head lang="ko">
    <meta charset="UTF-8">
    <title>React-Test</title>
</head>
<body>

    <div data-view></div>
    <script type="text/javascript" src="path/to/require.js" data-main="Main"></script>

</body>
</html>

실행해보면...



아차, requirejs는 기본적으로 .js 형식만 require 할 수 있다;
다행히 requirejs 플러그인을 써서 다른 포맷의 파일도 의존성을 걸수 있다.

https://github.com/millermedeiros/requirejs-plugins 에서 여러 플러그인을 볼 수 있는데 그 중 'noext' 를 사용해서 jsx 파일을 인클루드하자.

설치했다면 Main.js 에 다음 설정을 추가하고
 ...
paths: {

    ...

    'noext': 'path/to/requirejs-plugins/src/noext',

    ...
}

App.js 의 의존성 부분도 다음과 같이 변경한다

define([
    'jquery',
    'noext!Navigation'
], function($, Navigation) {
...

! 은 requirejs 의 플러그인 문법이다.
이제 다시 시도해보자


아... jsx 는 js 파일이 아니기에 문법 오류가 나버린다.

앞서 말한 JSXTransformer.js 를 적용해야할 것 같다.

JSXTransformer 에서는 두가지 메서드를 지원하는데
  • transform : 문자열을 js 로 변환한다
  • exec : 문자열을 받아 js 로 변환하고 실행한다.
둘다 문자열을 받기에 jsx 를 문자열로 로딩한 뒤 Transformer 로 처리하는 순서로 진행해야 한다.

멋지게도 requirejs 에는 파일을 text 로 읽을 수 있는 플러그인도 지원한다.

이 플러그인을 사용하여 텍스트로 읽은 뒤 transform 해보자.

먼저 text 플러그인 설정을 추가한다.
 ...
paths: {

    ...

    'text': 'path/to/requirejs-text/text',

    ...
}

다음은 문제의 .jsx 문법이 포함된 부분을 text 파일로 분리한다.
파일 이름은 적당히 Navigation.jsx.text 정도로 해 두자.

React.createClass({
    links: function() {
        return this.props.linkList.map(function(link) {
            return (
                <li><a href={link.href} target={link.target || '_self'}>{link.title}</a></li>
            );
        });
    },
    render: function() {
        return (
            <ul className="nav">
               {this.links()}
            </ul>
        );
    }
});

이제 Navigation.js 파일을 생성한다. jsx 가 아니라 js 다.
앞서 만든 Navigation.jsx 파일에서 jsx 문법을 제거하고 약간 변형했다.

중간에 transform 한 뒤 변환된 소스를 evel 해서 React 객체를 얻어내는 부분이 포인트

// @Navigation.js
define([
    'jquery',
    'react',
    'JSXTransformer',
    'text!./Navigation.jsx.text' // text 플러그인 사용
], function($, React, JSXTransformer, sourceCode) {

    var navigationSource = JSXTransformer.transform(sourceCode);

    var linkList = [
        {
            title: "쿠팡", href :"http://www.coupang.com/"
        },
        {
            title: "아마존", href :"http://www.amazon.com/"
        }
    ];

    var Navigation = function() {
        this.component = eval(navigationSource.code); // 변환된 소스코드를 eval해서 Raact 객체를 얻는다.
    };

    Navigation.prototype.render = function(renderArea) {
        React.render(
            React.createElement(this.component, {
                linkList: linkList
            }),
            renderArea
        );
    };

    return Navigation;

});

이제 다시 실행해보면 잘 나온다.

번거로워!

React와 jsx 하나 쓰자고 굉장히 번거로운 절차를 거쳤다.
이대로 작업하는건 생산성을 떠나 참 삽질 비슷한 것 같다.

사실 이 과정을 생략해주고 더 간단하게 jsx 를 requirejs에서 쓸 수 있게 해주는 플러그인이 이미 존재한다.
이 플러그인을 쓰면 아주 손쉽게 jsx 를 쓸 수 있다.

requirejs-react-jsx 라는 플러그인이다.

이 플러그인으로 변경해보자

전에 만든 Navigation.jsx 파일을 그대로 사용하며 변경해야 할 것은 Main.js 와 App.js 파일이다.
먼저 Main.js 에 requirejs-react-jsx 설정을 추가한다.

paths: {
    'react': 'path/to/react/react.min',
    'JSXTransformer': 'path/to/react/JSXTransformer',
    'jsx': 'path/to/requirejs-react-jsx/jsx', // 이녀석이다.
    'jquery': 'path/to/jquery/dist/jquery.min'
}

App.js 파일에 jsx 플러그인을 사용하여 Navigation.jsx 를 사용할 수 있게 만든다.

define([
    'jquery',
    'jsx!Navigation'
], function($, Navigation) {
... 

이제 다시 돌려보면 아주 잘 나올 것이다.

완전한 소스코드는 여기 에.

결론

RequireJS+ReactJS 의 조합의 장점은 일단 별도 사전 준비없이 Client 단에서 바로 적용해볼 수 있다는 점이다.

다만 별도의 트랜스폼 비용이 드는건 어쩔수가 없고, 번거로운 AMD (RequireJS) 문법과 더불어 간소화 했다고 하지만 그래도 복잡한 설정 ( + Plug-In) 이 마음에 걸리는 편이다.

자신의 프로젝트 상황이 React를 위해 사전 컴파일이 가능하다면 Grunt, Gulp 등의 사전 컴파일을 사용하는게 좋고 그렇지 않을 경우에 차선으로 해볼만한 방법인 것 같다.

공식 사이트에서도 Client-Side의 변환에 대해서는 경고하고 있다

https://facebook.github.io/react/docs/tooling-integration.html

어쨌든 React 참 매력적인건 사실인듯.