SmartEditor로 살펴보는 웹 에디터의 발전

지난 FEConf 2022, DEVIEW 2023에서도 이 글에서 다루는 SmartEditor 역사와 기술을 소개하였습니다. 이 글의 주제 외에도 재미있는 이야기들을 해당 발표에서 함께 다루고 있으니 발표 영상도 시청해 보시길 추천드립니다.

웹 에디터를 한 번도 사용해 보지 않으신 분은 없을 겁니다. 간단한 이메일 작성부터 댓글, 블로그, 그리고 코딩까지. 웹 에디터는 알게 모르게 많은 곳에서 쓰이고 있습니다.

현대의 웹 에디터는 풍부한 콘텐츠 제작을 가능하게 합니다. 더 이상 과거의 에디터처럼 텍스트를 입력하고 서식을 지정하는 도구에 그치지 않습니다. 사진, 동영상, 스티커, 수식 등 다양한 요소를 손쉽게 추가하고 수정할 수 있습니다. 이렇게 다양한 요소를 통해 사용자들은 정보를 보다 풍부하고 생동감 있게 전달할 수 있게 되었습니다.

이 글에서는 이러한 웹 에디터가 어떻게 발전해 왔는지 SmartEditor의 발전을 통해 살펴보고자 합니다. 세대별로 에디터를 나누어 각 세대별 에디터의 특징과 한계를 살펴보겠습니다.

SmartEditor(SE)는 네이버에서 개발한 에디터입니다. 2008년 SE 1.0을 시작으로, SE 2.0, SE 3.0, SE ONE까지 발전해 왔습니다. 네이버의 서비스 곳곳에서 쉽게 만나볼 수 있습니다.

1세대

현재도 지식인에서 쓰이고 있는 1세대 에디터

첫 시작은 SmartEditor 1.0과 2.0이었습니다. 이때의 에디터는 contenteditable 기술을 기반으로 하고, 문서 데이터를 HTML 그대로 저장한다는 특징을 가집니다.

이 세대의 에디터를 예로 들자면 네이버 메일 등의 서비스를 들 수 있습니다.

contenteditable

contenteditable은 HTML 엘리먼트 속성입니다. 이 속성을 적용하게 되면 해당 요소가 편집 가능(말 그대로 content editable)한 상태가 됩니다. 이를 통해 사용자가 웹 페이지를 직접 수정할 수 있게 됩니다.

다음은 contenteditable의 사용 예시입니다. 다음 코드를 실행하고 동작을 확인해 보시길 바랍니다:

위와 같이 contenteditable을 사용하면 HTML 요소를 마치 입력 영역처럼 사용자가 다루고 편집할 수 있게 됩니다. 또한 사용자가 편집하는 내용은 즉시 HTML 콘텐츠가 됩니다.

그렇다면 스타일은 어떻게 지정할 수 있을까요? 1세대의 에디터는 contenteditable을 사용했기에 브라우저 API인 document.execCommand를 활용하여 콘텐츠에 스타일을 입힐 수 있습니다. 한 가지 예로 다음과 같이 document.execCommand를 사용하여 문서의 특정 영역에 bold와 글 색상을 변경하면:

document.execCommand("bold");
document.execCommand("foreColor", false, "rgba(0,0,0,0.5)");

다음과 같이 HTML이 수정되며 본문에 스타일이 적용되게 됩니다:

<div contenteditable="true">
  <b>안녕하세요</b>
  <font color="rgba(0, 0, 0, 0.5)">반가워요!</font>
</div>

이때 스타일이 별도 CSS가 아닌 엘리먼트에 직접 적용되는 것도 확인하실 수 있습니다.

HTML로서 저장

두 번째 특징은 문서 데이터를 HTML로서 그대로 저장한다는 것입니다. 즉 별도 가공 없이 사용자가 contenteditable을 통해 수정한 문서를 HTML 그대로 DB에 저장합니다.

실제 사례로 네이버에서 메일 전송 시 네트워크 요청을 보면 다음과 같이 HTML 데이터가 그대로 담겨 있는 것을 보실 수 있습니다:

1세대의 한계

1세대 에디터가 등장한 시절, 사용자들은 주로 PC에서 작업을 하였습니다. 글을 작성하는 환경과 읽는 환경이 동일하게 PC였습니다. PC에서 작성된 콘텐츠(HTML)가 다른 PC에서 보이기에 어색함이 없었습니다.

하지만 시간이 지나 기술이 발전하여 태블릿, 스마트폰 등 다양한 디바이스가 등장하였습니다. 동일한 콘텐츠를 사용자의 PC, 태블릿, 스마트폰 등 다양한 환경에서 보여줘야 했습니다.

PC에서 작성된 콘텐츠를 다른 환경에서도 자연스럽게 보여주기엔 무리가 있었습니다. 글을 작성하는 환경인 PC 환경에 맞춰 HTML로 문서가 저장되었기 때문입니다. PC에 맞춰 생성된 HTML이 다른 디바이스 환경에서도 자연스럽게 보이는 것은 불가능했습니다.

이렇게 여러 디바이스가 등장함에 따라 1세대 에디터는 한계를 맞이하게 됩니다.

Q. 아직 1세대 에디터를 쓰는데 모바일에서도 잘 보이는 건 어떻게 가능한가요?
A. 이는 모바일에서 잘 보이도록 스타일 정보를 서버에서 지우거나 정제하는 복잡한 작업을 거치기에 가능합니다. 하지만 다양한 콘텐츠를 지원하는 데에 한계가 있습니다.

1.5세대

1.5세대 에디터인 SmartEditor 3.0
사진은 네이버 Post 서비스

1세대 에디터의 한계를 극복하고자 1.5세대 에디터를 만들게 되었습니다. 즉, PC에서 작성된 글이 여러 디바이스 환경에서도 자연스럽게 보일 수 있도록 하는 것이 목표였습니다.

글을 작성하는 환경과 읽는 환경이 다를 수 있다는 것을 고려해야 했습니다. 즉, 읽는 환경을 고려하지 않고 작성하는 환경에만 의존하여 HTML을 생성하고 저장해서는 안 됩니다. 그러기 위해서는 HTML이 아닌 다른 형식으로 문서를 저장해야 했습니다.

JSON 형태로 모델링

우리가 선택한 방법은 문서를 JSON으로 모델링 하는 것이었습니다. 이는 HTML 그 자체로 데이터를 저장하고 관리하는 이전 방식과는 다릅니다. 사용자가 문서를 편집하면 스토어에 그 정보를 저장하고 HTML로 렌더링 하는 방식입니다. 글 작성을 완료하여 문서를 저장할 때 스토어에 저장된 정보를 JSON 형식으로 내보내 저장하게 됩니다.

다음은 JSON으로 표현된 문서 데이터의 한 예입니다:

"components": [{
  "value:" [{
    "nodes": [
      {
        "value": "안녕하세요!",
        "style": { "fontSizeCode": "fs19", "bold": true, "@ctype": "nodeStyle" },
        "@ctype": "textNode"
      },
      {
        "value": "Editor",
        "style": { "italic": true, "underline": true, "@ctype": "nodeStyle" },
        "@ctype": "textNode"
      },
      {
        "value": "를 찾으시나요?",
        "@ctype": "textNode"
      }
    ],
    "@ctype": "paragraph"
  }],
  "@ctype": "text"
}]

이렇게 JSON으로 데이터를 관리하고 저장함으로써 읽는 환경에 맞춰 HTML을 결정할 수 있게 됩니다. 다양한 환경에서도 디바이스에 맞춰 자연스럽게 문서를 보여줄 수 있게 된 것입니다.

동일한 JSON을 읽는 환경에 맞춰 서로 다른 HTML로 변환합니다

블록별 편집

하지만 문서의 모든 영역을 contenteditable로 유지하며 JSON으로 데이터를 관리하는 것은 쉽지 않았습니다.

따라서 JSON 관리를 용이하게 하기 위해 텍스트를 제외한 다른 요소는 contenteditable="false"로 다루기로 하였습니다. 즉, 텍스트는 이전과 동일하게 사용자가 직접 HTML 요소를 편집하지만, 그 외 요소는 JavaScript를 통해 편집하도록 한 것입니다.

1.5세대의 한계

블록 선택을 확장하려 하지만, 블록별로 격리되어 확장되지 못 하는 모습

아쉽게도 contenteditable="true" 요소와 contenteditable="false" 요소는 함께 선택(Selection) 될 수 없습니다. 다시 말해 한 텍스트 요소에서 블록 선택을 한 것이 해당 텍스트 요소를 벗어나서 다른 요소까지 확장될 수 없습니다. 블록별로 갇혀있게 된 것입니다.

여러 디바이스를 지원하고자 JSON으로 데이터를 관리하였지만 이는 블록별 편집으로 인해 어색한 선택 UX를 만들고 사용성을 저해하게 되었습니다.

2세대

목표는 명확했습니다. “어색한 선택 UX를 개선하자. JSON으로 문서를 관리하면서도 선택 UX는 자연스러워야 한다.”

가상 커서

앞서 말했듯 contenteditable="true" 요소와 contenteditable="false" 요소는 함께 선택될 수 없습니다. 브라우저에서 지원하지 않습니다. 편집 가능 요소와 불가능 요소가 함께 선택된다니, 브라우저가 이를 지원하는 것도 어색합니다.

그래서 직접 구현하기로 했습니다. 브라우저의 선택 상태를 HTML, CSS, JavaScript를 이용해서 모사하기로 했습니다. 이렇게 저희가 직접 구현하는 브라우저 선택 상태 표현을 ‘가상 커서’라고 불렀습니다.

SmartEditor는 세 가지 선택 상태를 가지고 있습니다. 글을 입력할 때 나타나는 커서 상태, 블록 지정하였을 때 나타나는 블록 상태, 이미지와 같은 컴포넌트 선택하였을 때 나타나는 컴포넌트 선택 상태입니다.

이미지 컴포넌트 선택 상태
회색 사각형이 이미지를 둘러 싸고 있다

이들 중 어색한 선택 상태 문제가 나타나는, 우리가 직접 구현해야 했던 건 커서 상태와 블록 선택 상태였습니다.

이러한 선택 상태 UI는 HTML과 CSS를 통해 구현했습니다. 커서 상태는 SVG 요소에 적절한 width, height, top, left, 그리고 blink 애니메이션을 주어 모사했습니다. 블록 선택 상태도 동일합니다. 적절한 width, height를 주어 SVG 요소가 마치 브라우저의 블록 선택 UI처럼 보이도록 하였습니다.

블록 선택 상태를 HTML, CSS로 구현한 모습

이러한 가상 커서를 구현하는 것은 사실 상당히 복잡한 일입니다. top, left, width, height와 같은 정보는 JavaScript를 이용해 직접 계산해야만 알 수 있기 때문입니다. 문서 내 어떤 위치에서 마우스 클릭이 발생한다면 이벤트 정보를 정확히 분석해야 정확한 위치에 커서를 표현할 수 있습니다. 키보드 이동도 마찬가지입니다. 좌표 정보뿐만 아니라 클릭이 발생한 HTML 요소가 어떤 요소인지, Shift 등의 키보드 눌림은 없었는지 등 많은 내용을 분석해야만 커서, 블록 등의 선택 상태를 정확히 문서 위에 표현할 수 있게 됩니다.

복잡하지만 이렇게 선택 상태를 직접 표현하게 되면 브라우저 기능만으로는 불가능한 다양한 선택 표현들이 가능해집니다. 즉 1.5세대 에디터의 한계를 극복할 수 있게 됩니다. 다음과 같이 서로 다른 contenteditable 영역을 선택할 수도(정확히는 선택한 것처럼 보이게 할 수도) 있게 됩니다:

텍스트와 이미지가 함께 블록 선택된 모습

입력 버퍼(Input buffer)

가상 커서로 선택 상태를 표현함은 알았습니다. 본문에서 깜빡이는 것은 진짜 커서가 아닌 SVG 요소라고 했습니다. 그렇다면 실제 브라우저 커서는 어디에 있는 걸까요? 다시 말해, 사용자 입력은 어디에서 발생하게 되는 걸까요?

사용자 입력 시 흐름

저희는 이렇게 실제 입력이 발생하는 곳을 ‘입력 버퍼’라고 불렀습니다. 입력 버퍼는 화면 밖에 위치합니다. 실제 커서는 입력 버퍼 안에 존재하여, 화면 밖 보이지 않는 입력 버퍼에서 입력이 발생하고 그 입력들을 JavaScript로 제어하여 본문에 출력하는 것입니다.

본문을 입력 영역으로서 유지하며 가상 커서만 그 위에 띄우면 되지 않나 생각하실 수도 있습니다. 하지만 본문을 모두 입력 영역으로 유지하기엔 contenteditable이 브라우저별로 동작이 모두 달라, 이를 가두어 한곳에서 제어하는 것이 효율적이었습니다. (이전 세대에서도 사실 이슈가 많았습니다.) 따라서 본문이 아닌 입력 버퍼로 입력 영역을 제한하고 제어하게 되었습니다.

2세대의 한계

가상 커서와 입력 버퍼를 통해 1.5세대의 한계를 해결하였지만, 2세대에서도 몇 가지 문제가 있었습니다. 바로 다국어 입력 및 붙여넣기 문제였습니다.

다국어 입력

2세대 에디터가 출시하고 나서 글로벌 서비스를 지원해야 할 일이 있었습니다. 한국어, 영어뿐만 아니라 일본어, 베트남어 등 다양한 언어를 지원해야 했습니다. 즉 다국어 입력이 가능해야 했습니다.

하지만 2세대 에디터의 입력 영역이 별도로 격리되어 있다는 특징이 문제가 되었습니다. 우선 입력 영역이 화면 밖에 있다 보니 IME 영역이 본문과 다른 위치에 나타나게 되었습니다. 또, OS에서 제공하는 IME 관련 액션들(탭, 스페이스 바 등)이 별도로 대응이 되어 있지 않아, 해당 액션을 하려고 하면 이벤트를 빼앗겨 IME 레이어가 닫혔습니다.

IME 관련 액션이 제대로 이뤄지지 않는 모습

물론 몇 가지 언어에 대해서는 대응할 수 있었습니다. 하지만, OS에서 제공하는 언어는 수십 가지이고, 각 언어별로 입력 방식 또한 매우 다양합니다. 예를 들어 한국어도 두벌식, 세벌식 등의 여러 입력 방식이 존재합니다. 이렇게 다양한 언어 및 입력 방식을 코드로서 모두 대응하는 것은 불가능에 가깝습니다.

다양한 언어와 입력 방식

붙여넣기

붙여넣기 또한 문제였습니다. 앞서 말했듯 눈에 보이는 본문 영역은 입력 영역이 아닙니다. 따라서 OS에서 기본으로 제공하는 동작을 수행하는 데에 제한이 있었습니다. 그중 하나가 붙여넣기입니다. 입력 영역이 아니다 보니 PC나 모바일에서 제공하는 기본적인 붙여넣기를 지원할 수가 없었습니다.

이는 PC에서는 확장 프로그램을 설치함으로써 모바일 웹에서는 별도 입력 영역을 가진 레이어를 화면 위에 띄움으로써 우회할 수는 있었습니다. 하지만 이는 기본 OS 동작과 다른 UX를 제공하게 되어 사용자는 불편을 느끼게 되었습니다.

3세대

다국어 입력 문제는 서비스 글로벌화에 발목을 잡았고, 붙여넣기 문제는 특히 모바일 웹에서 사용자에게 큰 불편을 주었습니다.

이 두 문제의 원인은 모두 입력 영역이 본문 밖 별도 영역으로 존재한다는 것이었습니다. 다시 말해 이를 해결하는 방법 중 하나는 본문을 다시 입력 영역으로 되돌리는 것이었습니다.

마침 Microsoft에서 IE 지원 종료를 예고한 상태였습니다. 본문 밖 입력 버퍼로 입력 영역을 옮기게 된 것이 브라우저별로 contenteditable 대응이 어렵기 때문이었는데, 그 브라우저들 중 가장 큰 비중을 차지하는 IE 지원이 종료된다는 것이었습니다. 본문을 다시 contenteditable로 되돌리는 것은 해 볼 만한 도전이 되었습니다.

Upstream/Downstream

3세대 에디터는 2세대 에디터의 모습은 모두 유지한 채 본문만을 다시 입력 영역으로 되돌리는 것을 목표로 삼았습니다. 따라서 본문 입력을 제외한 UI 및 나머지 기능들은 동일하게 유지해야 했습니다.

다시 말해 3세대 에디터는 기존의 2세대 에디터의 React, MobX 기술 위에서 contenteditable이 함께 작동하는 것을 기술적 목표로 삼았습니다.

하지만 contenteditable을 기존 기술에 적용하는 일은 생각보다 복잡한 일이었습니다. React와 contenteditable의 데이터 흐름이 서로 충돌하여 문제를 일으켰기 때문입니다.

React는 Store의 변경에 따라 DOM이 업데이트되는 흐름인 반면, contenteditable은 반대로 DOM의 변경에 따라 Store가 업데이트되는 흐름으로 진행됩니다. 이들은 상충하는 흐름입니다.

상충하는 데이터 흐름

예를 들어 사용자가 본문에 글을 작성한다고 가정해 보겠습니다. 사용자가 본문에 글을 입력하면 contenteditable을 통해 DOM이 변경되고, Store가 업데이트 되게 됩니다. 하지만 이 과정에서 Store가 업데이트 됨에 따라 또다시 React가 반응하여 반대로 DOM 업데이트를 시도하게 됩니다. 그리고 만약 이때 contenteditable로 수정된 DOM 구조가 React가 접근하고자 하는 DOM 구조와 다르다면 React는 DOM을 찾을 수 없다며 오류를 발생시키게 되는 것입니다.

Upstream과 Downstream

이 문제를 해결하기 위해서는 두 데이터 흐름을 제어해야 했습니다. 즉 DOM에서 Store로의 흐름과 Store에서 DOM으로의 흐름이 독립적으로 흘러가도록 분리해야 했습니다. 우리는 이 DOM에서 Store로의 흐름을 ‘Upstream’, Store에서 DOM으로의 흐름을 ‘Downstream’이라 불렀습니다.

Upstream 중에는 Downstream이 반응해서는 안 되고, Downstream 중에는 Upstream이 반응해서는 안 됩니다. Upstream 중에는 Store의 변경사항에 대해 React가 반응하지 않도록 MobX의 untracked API를 사용했습니다. 또한 Downstream 중에는 DOM의 변경 사항이 Store에 흘러가지 않도록 contenteditable과 Store를 이어주는 MutationObserver의 반응을 끊어 두었습니다. (보다 자세한 설명은 FEConf 2022 발표 영상에서 확인해 보시길 바랍니다.)

글 입력 같은 액션은 contenteditable을 이용하여 DOM이 수정된 후 Store가 업데이트되는 Upstream 흐름으로 처리되도록 하였고, 이미지 삽입과 같은 액션은 Store 정보를 업데이트하여 React가 DOM을 그리는 Downstream 흐름으로 처리되도록 하였습니다. 즉, 한 번에 하나의 흐름만이 진행되도록 하였습니다.

React와 contenteditable이 함께 작동하는 3세대 에디터 SE ONE

두 흐름이 나뉘어 제어됨으로써 React와 contenteditable이 함께 작동할 수 있게 되었습니다. 그리고 본문이 실제 입력 영역이기 때문에 OS에서 제공하는 다국어 및 붙여넣기 처리도 가능해졌습니다.

정리

세대특징한계
1세대contenteditable, HTML 그대로 저장다양한 디바이스 대응 어려움
1.5세대JSON 모델링, 블록별 편집어색한 선택 UX
2세대가상 커서, 입력 버퍼다국어 입력, 붙여넣기
3세대Upstream/Downstream

앞으로의 에디터

최근 인공지능 기술의 발전으로 많은 분야에서 변화가 일어나고 있습니다. 작년 말 공개된 ChatGPT는 많은 사람들을 놀라게 하였습니다. 다양한 주제의 질문에도 막힘없이 대답하며 현대 인공지능이 어느 수준에 도달했는지 보여주었습니다.

에디터 분야도 예외는 아닙니다. 몇 개월 전 Microsoft는 Copilot이 적용된 Word 제품을 공개하였습니다:

인공지능이 결합된 에디터는 이전과는 다른 새로운 사용성을 제공하게 됩니다. 사용자가 저장한 데이터(클라우드 등에 올라가 있는)를 기반으로 인공지능이 스스로 글을 작성하고, 글 작성 과정에 인공지능이 개입하여 사용자에게 글의 수정이나 보완을 제안하기도 할 것입니다. 또한, 사용자별로 문체를 학습하여 각각의 사용자 말투로 글을 작성해 줄 수도 있을 것입니다.

이러한 변화는 ‘사용자가 홀로 글을 쓰는’ 기존의 글쓰기 개념을 완전히 바꿔 놓습니다. 이제 글쓰기 과정이 사용자 홀로 하는 과정이 아닌 인공지능과의 협업 과정으로 바뀌게 됩니다.

에디터는 이러한 협업을 보다 효율적으로 지원하도록 발전할 것입니다. 이를 통해 누구나 멋진 글을 쓸 수 있게 되며, 더욱 빠른 시간 내에 글을 작성할 수 있게 될 것입니다.

앞으로의 에디터 발전을 기대해 주시길 바랍니다.

노용구 | SmartEditor

SmartEditor 팀에서 JavaScript 개발을 하고 있습니다.