Markdown Editor를 React로 재작성

codemirror에 한참 빠져있을때 간단히 만든 markdown to html 에디터가 있다. 여기에 tistory api로 글 보내기도 붙이고 글 가져오기, 로컬에 임시 저장하기 등등을 붙여보고 싶었는데 솔직히 말해서 엄두가 나지 않았다. 아무래도 코드가 길어질테니 파일을 분리하는 것이 관리하기 편할텐데 분리한 모듈을 관리하고 merge해서 테스트하고 하는 과정이 굉장히 번거롭게 느껴졌다.

그러다가 react 공부하게 되었고, 부끄럽게도 아직 몰랐던 CommonJS도 알게 되었다. react에 webpack을 적용하면서는 눈이 번쩍 뜨이는 것 같았다. 이런 방법이 있는데 왜 그동안 그렇게 고생했던 것일까 싶다. (사실 CommonJS만 알았어도 더 속도가 붙었을 것 같다.)

지금부터 이전에 만들었던 markdown 에디터를 react와 재작성한 과정을 정리해보려고 한다. 정리를 한번 하면 좀 더 이해한 것이 머리 속에 더 깊이 들어오리라 기대하면서...

구조

구조는 간단하다. 왼쪽에 에디터, 오른쪽에 미리보기가 있고 크기를 조정할 수 있는 칸막이가 있다. 에디터에 markdown으로 내용을 입력하면 미리보기에 이를 html로 변환해서 보여준다. Preview탭에는 html을 렌더링해서 보여주고 HTML탭에는 html 코드로 보여준다.

var Editor = require('./editor');
var Divider = require('./divider');
var Preview = require('./preview');

var App = React.createClass({

    ...

    render: function() {
        return (
            <div className="editor-container">
                <Editor onChange={this.handleChangeEditorValue} />
                <Divider />
                <Preview value={this.state.value} />
            </div>
        );
    }
});


ReactDOM.render(
    <App />,
    document.getElementById("container")
);

react의 좋은 점 중 하나는 이런 식으로 생각한 것을 그대로 코드로 옮기면 된다는 점이다. 이제 EditorDivider, Preview를 각각 구현해주면 된다. 각 모듈은 CommonJS 방식으로 로드하기 때문에 webpack 설정에 필요한 파일들을 넣는다.

module.exports = {
    entry: {
        'editor': [
            path.join(__dirname, 'static/js/editor/app'),
            path.join(__dirname, 'static/js/editor/editor'),
            path.join(__dirname, 'static/js/editor/codemirror-editor'),
            path.join(__dirname, 'static/js/editor/preview'),
        ]
    },
}

webpack에 --watch 옵션을 넣어 실행하면 파일을 수정할때마다 매번 webpack을 실행하는 수고를 하지 않아도 된다.

Editor

var CodeMirrorEditor = require('./codemirror-editor');

var Editor = React.createClass({
    render: function() {
        return (
            <div className="editor">
                <EditorHeader />
                <CodeMirrorEditor placeholder="내용을 입력하세요" onChange={this.props.onChange} />
            </div>
        );
    }
});

에디터는 Header때문에 한번 더 감싼 것이고 실제로는 CodeMirrorEditor를 사용하도록 했다.

var CodeMirror = require('codemirror');

var CodeMirrorEditor = React.createClass({
    componentDidMount: function() {
        this.codeMirror = CodeMirror.fromTextArea(this.refs.textarea, this.props.options);
        this.codeMirror.on('change', this.handleChange);
    },
    componentWillUnmount: function() {
        if (this.codeMirror) {
            this.codeMirror.toTextArea();
        }
    },
    handleChange: function(doc) {
        this.setState({
            value: doc.getValue()
        });
        this.props.onChange(this.state.value);
    },
    render: function() {
        return (
            <div className="editor-content">
                <textarea ref="textarea" placeholder={this.props.placeholder}>{this.props.value}</textarea>
            </div>
        );
    }
});

CodeMirror는 React Class가 아니기 때문에 componentDidMountref를 사용해서 초기화했다. react-codemirror가 있기는 한데 placeholder plugin을 지원하지 않아서 사용하지 않았다. codemirror를 잘 모르는 상태라면 react-codemirror를 사용하는 것도 나쁘지 않을 것 같다.

Preview

var marked = require('marked');
marked.setOptions({
    headerPrefix: "header-"
});

var Preview = React.createClass({
    render: function() {
        var style = {
            left: this.props.position
        };
        var value = marked(this.props.value);
        var html = {
            __html: value
        }

        return (
            <div className="preview" style={style}>
                <ul id="resultTab" className="nav nav-tabs" role="tablist">
                    <li role="presentation" className="active"><a href="#preview" aria-controls="preview" role="tab" data-toggle="tab">Preview</a></li>
                    <li role="presentation"><a href="#html" aria-controls="html" role="tab" data-toggle="tab">HTML</a></li>
                </ul>

                <div className="tab-content">
                    <div ref="preview" id="preview" role="tabpanel" className="tab-pane result_preview active" dangerouslySetInnerHTML={html}></div>
                    <textarea ref="rawPreview" id="html" role="tabpanel" className="tab-pane result_html" readonly="readonly" value={value}></textarea>
                </div>
            </div>
        );
    }
});

미리보기에는 markdown to html을 해주는 라이브러리를 사용했는데 marked를 사용했다. react-marked라는 것도 있던데 그건 결과가 react class라서 marked를 사용했다. html을 render해야해서 어쩔 수 없이 dangerouslySetInnerHTML를 사용했다.

마치며

react는 UI를 구조화할 수 있게 해주고 CommonJS는 소스코드를 구조화할 수 있게 해준다. 이 둘이 있어서 너무 좋다. 그동안 왜 이걸 안썼는지, 아니 왜 몰랐는지... 바보가 되는 것은 참 쉽다는 생각이 들었다. 가만히 있으면 바보가 된다. 늘 공부하며 살아야겠다.

지금까지 정리한 것은 이전에 만들었던 것재작성한 것이고 이제는 추가하려고 했던 기능을 넣어보려고 한다. 오랜만에 두근두근 한다.

반응형