1-5 Editor 페이지 UI 구현
Editor 페이지는, 기존에 만들었던 List 페이지와 Post 페이지와 달리, PageTemplate 을 사용하지 않습니다. 이 페이지를 구현하기에 앞서, 어떻게 생겼는지 미리 살펴볼까요?

에디터 페이지는 상단의 헤더 부분에 로고가 사라지고, 좌측엔 뒤로가기 버튼, 우측엔 작성하기 버튼이 있습니다.
그리고, 그 아래에는, 화면이 두개로 분할되어있으며, 가운데 영역을 드래그하여 리사이즈를 할 수 있습니다.

이 에디터는, 마크다운을 작성 할 수 있는 에디터입니다. 마크다운은 텍스트기반 마크업언어로서, 특수문자들을 이용하여 텍스트의 스타일을 쉽고 직관적으로 설정 할 수 있습니다.
좌측에는 CodeMirror 를 사용하여 마크다운 에디터에 작성된 텍스트에 색상을 입혀주었고, 우측엔 Marked 와 Prismjs 라는 라이브러리를 사용하여 마크다운을 HTML 형태로 변환시켜주고 코드에 색상을 입혀줍니다.
이번 섹션에서는, 에디터 페이지의 레이아웃과 리사이즈 기능만 구현하고, 다음 섹션에서 라이브러리들을 연동하겠습니다.
이 페이지에서 필요한 컴포넌트는 총 4개입니다. 다음 컴포넌트들을 src/components/editor 경로에 Generate new component 를 사용하여 만드세요.
- EditorHeader: 에디터의 상단에 위치하는 파란색 헤더입니다
- EditorTemplate: 에디터 페이지를 위한 템플릿입니다. 리사이즈 기능을 여기서 구현하게 됩니다.
- EditorPane: 글을 작성하는 영역입니다.
- PreviewPane: 마크다운이 html 로 렌더링되는 영역입니다.
EditorTemplate 과 EditorPane 은 상태관리가 필요한 컴포넌트들이니, 컴포넌트를 생성 할 때 클래스형으로 생성하세요.

EditorTemplate 컴포넌트
레이아웃 구성
이번에 만들 EditorTemplate 컴포넌트는 지금까지 다룬 컴포넌트들과 살짝 다릅니다. 기존에, 컴포넌트의 props 로 JSX 형태를 받아와야 하는 경우엔 children 값을 사용했었습니다.
예를 들어, 다음과 같은 Parent 컴포넌트가 있다면:
import React from 'react';
const Parent = ({children}) => {
return (
<div>
{children}
</div>
);
};
export default Parent;
이렇게 사용하면, 컴포넌트 JSX 태그 사이의 내용들이 children 으로 전달되지요:
<Parent>
<div>Hello World</div>
</Parent>
하지만, 지금의 경우엔 children 처럼 JSX 형태로 전달받아 사용 할 내용이 3종류가 됩니다. 3 종류의 JSX 가, 하나의 블록 형태로 붙어있는 것이 아니라 각자 다른 곳에 렌더링 되야 하기 때문에, children 을 사용하지 않고, header, editor, preview 라는 props 를 받아서 알맞는 곳에 렌더링해줍니다.

src/components/editor/EditorTemplate/EditorTemplate.js
import React, { Component } from 'react';
import styles from './EditorTemplate.scss';
import classNames from 'classnames/bind';
const cx = classNames.bind(styles);
class EditorTemplate extends Component {
render() {
const { header, editor, preview } = this.props;
return (
<div className={cx('editor-template')}>
{ header }
<div className={cx('panes')}>
<div className={cx('pane', 'editor')}>
{editor}
</div>
<div className={cx('pane', 'preview')}>
{preview}
</div>
</div>
</div>
);
}
}
export default EditorTemplate;
이제 이 컴포넌트를 위한 스타일링을 해봅시다. 일부 스타일은 이 컴포넌트의 레이아웃이 제대로 작동하는지 확인하기 위한 임시 코드입니다.
src/components/EditorTemplate/EditorTemplate.scss
@import 'utils';
.editor-template {
.panes {
height: calc(100vh - 4rem); // 페이지 높이에서 EditorHeader 크기 빼기
display: flex;
background: $oc-gray-1; // 임시
.pane {
display: flex;
flex: 1; // 임시
}
}
}
이 컴포넌트를 EditorPage 에서 렌더링 해볼까요?
src/pages/EditorPage.js
import React from 'react';
import EditorTemplate from 'components/editor/EditorTemplate';
const EditorPage = () => {
return (
<EditorTemplate
header="헤더"
editor="에디터"
preview="프리뷰"
/>
);
};
export default EditorPage;
지금은, JSX 대신에 텍스트를 props 로 설정했습니다. 이제 http://localhost:3000/editor 에 들어가서 페이지가 어떻게 나타나는지 확인해보세요.

각 영역에 우리가 설정한 값이 잘 나타났나요?
리사이즈 기능 구현하기
컴포넌트 템플릿이 잘 작동하는것을 확인했다면, 리사이즈 기능을 구현하겠습니다. 각 영역의 사이에 separator 를 렌더링 한 후, 이 DOM 을 클릭 하게 될 때 이벤트를 등록하여, 커서의 위치에 따라 state 를 변경하고, 이 state 에 따라 각 영역의 사이즈를 변경하여 리렌더링 하도록 설정해보겠습니다.
src/components/EditorTemplate/EditorTemplate.js
import React, { Component } from 'react';
import styles from './EditorTemplate.scss';
import classNames from 'classnames/bind';
const cx = classNames.bind(styles);
class EditorTemplate extends Component {
state = {
leftPercentage: 0.5
}
// separator 클릭 후 마우스를 움직이면 그에 따라 leftPercentage 업데이트
handleMouseMove = (e) => {
this.setState({
leftPercentage: e.clientX / window.innerWidth
});
}
// 마우스를 땠을 때 등록한 이벤트 제거
handleMouseUp = (e) => {
document.body.removeEventListener('mousemove', this.handleMouseMove);
window.removeEventListener('mouseup', this.handleMouseUp);
}
// separator 클릭시
handleSeparatorMouseDown = (e) => {
document.body.addEventListener('mousemove', this.handleMouseMove);
window.addEventListener('mouseup', this.handleMouseUp);
}
render() {
const { header, editor, preview } = this.props;
const { leftPercentage } = this.state;
const { handleSeparatorMouseDown } = this;
// 각 섹션에 flex 값 적용
const leftStyle = {
flex: leftPercentage
};
const rightStyle = {
flex: 1 - leftPercentage
};
// separator 위치 설정
const separatorStyle = {
left: `${leftPercentage * 100}%`
};
return (
<div className={cx('editor-template')}>
{ header }
<div className={cx('panes')}>
<div className={cx('pane', 'editor')} style={leftStyle}>
{editor}
</div>
<div className={cx('pane', 'preview')} style={rightStyle}>
{preview}
</div>
<div
className={cx('separator')}
style={separatorStyle}
onMouseDown={handleSeparatorMouseDown}/>
</div>
</div>
);
}
}
export default EditorTemplate;
이 과정에서, mousedown 을 제외한 이벤트는, document.body 와 window 에 적용이 되었습니다. 그 이유는, mouseup과 mousemove 이벤트를 separator 쪽에만 넣어준다면, 마우스 커서가 separator 영역을 벗어났을 때 이벤트가 제대로 실행되지 않기 때문입니다.
위 코드가 제대로 작동하려면, separator 를 위한 스타일도 작성을 해주어야 합니다. 기존에 작성했던 임시코드들도 제거하세요.
src/components/EditorTemplate/EditorTemplate.scss
@import 'utils';
.editor-template {
.panes {
height: calc(100vh - 4rem); // 페이지 높이에서 EditorHeader 크기 빼기
display: flex;
position: relative; // separator 의 위치설정을 위하여 relative 로 설정
.pane {
display: flex;
min-width: 0; // 내부의 내용이 커도 반대편 영역을 침범하지 않게 해줌
overflow: auto; // 너무 많이 줄이면 스크롤바가 나타나게 해줌
}
.separator {
width: 1rem; // 클릭 영역을 넓게 설정하기 위함
height: 100%;
position: absolute;
transform: translate(-50%); // 자신의 50% 만큼 왼쪽으로 이동
cursor: col-resize; // 리사이즈 커서
background: black; // 임시
}
}
}
다시 페이지를 열면, 다음과 같이 검정색 막대가 보여질것입니다. 이를 드래그하여 리사이즈가 잘 되는지 확인해보세요.

작동이 잘 되는것을 확인했다면, 방금 수정한 EditorTemplate 에서 임시 코드라고 주석으로 명시된 background: black 부분을 제거하세요.
EditorHeader 컴포넌트
에디터 페이지의 상단에 위치할 EditorHeader 컴포넌트를 만드세요. 이 컴포넌트엔, 좌측엔 뒤로가기 버튼이 있고 우측엔 작성하기 버튼이 있습니다.

src/components/editor/EditorHeader/EditorHeader.js
import React from 'react';
import styles from './EditorHeader.scss';
import classNames from 'classnames/bind';
import Button from 'components/common/Button';
const cx = classNames.bind(styles);
const EditorHeader = ({onGoBack, onSubmit}) => {
return (
<div className={cx('editor-header')}>
<div className={cx('back')}>
<Button onClick={onGoBack} theme="outline">뒤로가기</Button>
</div>
<div className={cx('submit')}>
<Button onClick={onSubmit} theme="outline">작성하기</Button>
</div>
</div>
);
};
export default EditorHeader;
우리가 이전에 만든 버튼 컴포넌트를 여기서 재사용 하였습니다. 이 헤더 컴포넌트에서는, onGoBack, 그리고 onSubmit 이라는 props 를 전달받게 됩니다, 각 함수를 양쪽 버튼에 넣어주세요.
그 다음엔, 컴포넌트를 스타일링 하세요.
src/components/editor/EditorHeader/EditorHeader.scss
@import 'utils';
.editor-header {
background: $oc-blue-6;
height: 4rem;
padding-left: 1rem;
padding-right: 1rem;
display: flex;
align-items: center;
.submit { // 우측 정렬
margin-left: auto;
}
}
다 작성하셨다면, EditorPage 파일을 열어 EditorTemplate 의 header props 에서 방금 만든 컴포넌트를 렌더링하세요.
src/pages/EditorPage.js
import React from 'react';
import EditorTemplate from 'components/editor/EditorTemplate';
import EditorHeader from 'components/editor/EditorHeader';
const EditorPage = () => {
return (
<EditorTemplate
header={<EditorHeader/>}
editor="에디터"
preview="프리뷰"
/>
);
};
export default EditorPage;
EditorHeader 컴포넌트가 보여졌나요?
EditorPane 컴포넌트
이번엔 EditorPane 컴포넌트를 만들어봅시다.

이 컴포넌트에는 총 3가지의 인풋이 있습니다 (제목, 내용, 태그). 내용 부분의 경우엔 우리가 나중에 CodeMirror 라이브러리를 연동하여 구현하게 되니, 해당 부분은 div 엘리먼트를 사용하겠습니다.
src/components/editor/EditorPane/EditorPane.js
import React, { Component } from 'react';
import styles from './EditorPane.scss';
import classNames from 'classnames/bind';
const cx = classNames.bind(styles);
class EditorPane extends Component {
render() {
return (
<div className={cx('editor-pane')}>
<input className={cx('title')} placeholder="제목을 입력하세요" name="title"/>
<div className={cx('code-editor')}></div>
<div className={cx('tags')}>
<div className={cx('description')}>태그</div>
<input name="tags" placeholder="태그를 입력하세요 (쉼표로 구분)"/>
</div>
</div>
);
}
}
export default EditorPane;
이 컴포넌트에서는, 나중에 라이프사이클 메소드와, 커스텀 메소드를 사용해야되기 때문에 클래스 형태로 작성되었습니다.
이제 컴포넌트를 스타일링 해보세요.
src/components/editor/EditorPane/EditorPane.scss
@import 'utils';
.editor-pane {
flex: 1; // 자신에게 주어진 영역을 다 채우기
// 세로방향으로 내용 나열
display: flex;
flex-direction: column;
.title {
background: $oc-gray-7;
border: none;
outline: none;
font-size: 1.5rem;
padding: 1rem;
color: white;
font-weight: 500;
&::placeholder {
color: rgba(255,255,255,0.75);
}
}
.code-editor {
flex: 1; // 남는 영역 다 차지하기
background: $oc-gray-9;
}
.tags {
padding-left: 1rem;
padding-right: 1rem;
height: 2rem;
background: $oc-gray-7;
display: flex;
align-items: center;
.description {
font-size: 0.85rem;
color: white;
font-weight: 600;
margin-right: 1rem;
}
input {
font-size: 0.85rem;
border: none;
flex: 1;
background: none;
outline: none;
font-weight: 600;
color: rgba(255,255,255,0.9);
&::placeholder {
color: rgba(255,255,255,0.75);
}
}
}
}
자 이제 이 컴포넌트를 EditorPage 에서 렌더링해보세요.
src/pages/EditorPage.js
import React from 'react';
import EditorTemplate from 'components/editor/EditorTemplate';
import EditorHeader from 'components/editor/EditorHeader';
import EditorPane from 'components/editor/EditorPane';
const EditorPage = () => {
return (
<EditorTemplate
header={<EditorHeader/>}
editor={<EditorPane/>}
preview="프리뷰"
/>
);
};
export default EditorPage;

EditorPane 이 잘 나타났나요?
PreviewPane 컴포넌트
에디터 페이지의 마지막 컴포넌트인 PreviewPane 을 만들어봅시다.
src/components/editor/PreviewPane/PreviewPane.js
import React from 'react';
import styles from './PreviewPane.scss';
import classNames from 'classnames/bind';
const cx = classNames.bind(styles);
const PreviewPane = ({markdown, title}) => (
<div className={cx('preview-pane')}>
<h1 className={cx('title')}>
제목
</h1>
<div>
내용
</div>
</div>
);
export default PreviewPane;
이 컴포넌트는 꽤 간단하지요? 이 컴포넌트는 title 과 markdown 을 props 로 받아와서 보여줍니다. 받아온 값을 렌더링 하는건 나중에 하고, 지금은 텍스트를 바로 렌더링 해주겠습니다.
컴포넌트를 스타일링도 해주세요.
src/components/editor/PreviewPane/PreviewPane.scss
@import 'utils';
.preview-pane {
flex: 1;
padding: 2rem;
overflow-y: auto; // 사이즈 초과시 스크롤바 나타나게 설정
font-size: 1.125rem;
.title {
font-size: 2.5rem;
font-weight: 300;
padding-bottom: 2rem;
border-bottom: 1px solid $oc-gray-4;
}
}
그 다음에, PreviewPane 도 EditorPage 에서 렌더링하세요.
src/pages/EditorPage.js
import React from 'react';
import EditorTemplate from 'components/editor/EditorTemplate';
import EditorHeader from 'components/editor/EditorHeader';
import EditorPane from 'components/editor/EditorPane';
import PreviewPane from 'components/editor/PreviewPane';
const EditorPage = () => {
return (
<EditorTemplate
header={<EditorHeader/>}
editor={<EditorPane/>}
preview={<PreviewPane/>}
/>
);
};
export default EditorPage;

양쪽 컴포넌트가 잘 렌더링 되었나요? 현재 상태에선, 브라우저의 가로길이가 짧을땐 조금 이상하게 나옵니다.

이렇게 화면이 2분할로 나뉘어지면 모바일에선 사용하기가 힘들겠죠? 우측의 미리보기 부분은 모바일에서 숨겨줍시다. EditorTemplate 의 스타일을 다음과 같이 수정하세요
src/components/editor/EditorTemplate/EditorTemplate.scss
@import 'utils';
.editor-template {
.panes {
(...)
@include media("<medium") {
.editor {
flex: 1!important;
}
.preview, .separator {
display: none;
}
}
}
}
display: none 속성을 사용해서 preview 와 separator 클래스를 숨겨주었습니다.

드디어 프로젝트에서 필요한 대부분의 유저인터페이스가 완성되었습니다. 프로젝트가 어떻게 보여질지, 준비가 어느정도 끝났으니, 이제 실제 기능들을 넣어봅시다!