Split View: 웹 에디터에 자가 없는 진짜 이유 — 잴 자가 없는 게 아니라 잴 종이가 없습니다
웹 에디터에 자가 없는 진짜 이유 — 잴 자가 없는 게 아니라 잴 종이가 없습니다
- 워드에서 붙여 넣은 문서가 흐트러지는 지점
- 아무도 안 만든 게 아닙니다
- 웹에는 페이지가 없습니다
- 콘텐츠 폭을 종이로 삼는다는 선택
- 코어와 어댑터로 나눈 구조
- 인쇄가 끼어들면 종이가 다시 생깁니다
- 만들어 보면 걸리는 자잘한 것들
- 이 프로젝트의 현재 상태를 있는 그대로 적으면
- 정리와 출처
워드에서 붙여 넣은 문서가 흐트러지는 지점
사내 문서 도구를 웹으로 옮기는 프로젝트에서 거의 반드시 나오는 요구가 있습니다. 워드처럼 눈금자를 달아 달라는 것입니다. 문단마다 왼쪽 여백을 삼각형으로 끌어 맞추고, 첫 줄만 들여쓰고, 표의 열 경계를 눈금 위에서 잡는 그 도구입니다.
개발 쪽에서는 이 요구가 사소해 보입니다. 자 모양의 눈금을 하나 그리고 드래그 핸들을 얹으면 될 것 같기 때문입니다. 그런데 착수하고 나면 첫날에 막힙니다. 눈금에 3cm라고 쓰려면 화면의 몇 픽셀이 3cm인지 알아야 하는데, 그 답이 없습니다.
아무도 안 만든 게 아닙니다
이건 만드는 사람이 없어서 비어 있는 자리가 아닙니다. 최근 공개된 editor-ruler의 README는 이 상황을 이렇게 정리합니다. 고전적인 웹 위지윅 에디터들, 그러니까 Froala와 TinyMCE와 CKEditor 5와 Quill 같은 것들은 눈금자 없이 나오고, 눈금자가 있는 것은 Syncfusion이나 DevExpress나 ONLYOFFICE처럼 문서 모델을 통째로 들고 있는 무거운 컴포넌트들뿐이라는 것입니다.
이 분류가 힌트입니다. 눈금자가 있는 쪽과 없는 쪽을 가르는 것은 개발 인력이나 제품 성숙도가 아니라 문서 모델을 갖고 있는지 여부입니다.
웹에는 페이지가 없습니다
워드에서 눈금자가 성립하는 이유는 종이가 먼저 정해져 있기 때문입니다. A4를 고르면 폭이 210mm로 확정되고, 좌우 여백이 각각 25mm면 본문 폭은 160mm입니다. 눈금자는 이 160mm를 그리는 자이고, 삼각형을 3cm 위치로 끌면 그것은 종이 왼쪽 끝에서 3cm라는 뜻입니다. 화면 크기를 바꿔도 이 숫자는 변하지 않습니다. 종이는 화면과 무관하게 존재하기 때문입니다.
웹에는 그 종이가 없습니다. 편집 영역의 폭은 브라우저 창 크기와 사이드바 상태와 확대 배율에 따라 계속 변합니다. 그리고 브라우저의 물리 단위는 실제 물리 길이가 아닙니다. CSS에서 1인치는 96픽셀로 정의된 값이고, 그 96픽셀이 화면에서 실제로 몇 밀리미터인지는 모니터마다 다릅니다.
그래서 웹 에디터에 눈금자를 그린다는 것은 없는 종이를 하나 가정하는 일입니다. 문제는 자를 그리는 기술이 아니라, 무엇을 기준으로 잴지 정하는 것입니다.
콘텐츠 폭을 종이로 삼는다는 선택
editor-ruler가 택한 방식은 코어 API에 그대로 드러납니다. 사용하는 쪽이 현재 상태를 알려 주는 함수를 넘기게 되어 있습니다.
import { createRuler } from '@devslab/editor-ruler';
const ruler = createRuler(mountElement, {
unit: 'cm',
getMetrics: () => ({ contentWidth, leftMargin, rightMargin, firstLineIndent }),
onChange(change, phase) {
// 현재 문단에 픽셀 값을 적용한다. 놓는 순간 phase 가 commit 이 된다
},
});
ruler.refresh(); // 선택이나 내용이 바뀔 때마다 호출
여기서 읽어야 할 것은 첫 번째 인자가 아니라 getMetrics입니다. 눈금자는 종이 폭을 스스로 알지 못하고, 매번 호스트에게 물어봅니다. 즉 종이 역할을 하는 것은 편집 영역의 현재 콘텐츠 폭이고, 그 폭이 바뀌면 눈금자의 숫자도 다시 계산됩니다.
이 선택에는 대가가 있습니다. 화면을 좁히면 눈금자의 3cm 위치가 실제로 다른 곳을 가리키게 됩니다. 그래서 이 자는 워드의 자와 이름만 같을 뿐 다른 것을 재고 있습니다. 워드의 자는 인쇄될 종이를 재고, 이 자는 지금 화면에 잡힌 편집 영역을 잽니다. 팀에 도입할 때 사용자에게 반드시 알려야 하는 차이입니다.
코어와 어댑터로 나눈 구조
이 프로젝트는 눈금과 드래그 처리와 단위 환산만 담당하는 의존성 없는 코어를 두고, 에디터별 차이는 얇은 어댑터로 분리했습니다. Froala, Tiptap, CKEditor 5, Summernote 어댑터가 각각 별도 패키지로 나와 있습니다.
이 구조가 필요한 이유는 에디터마다 들여쓰기를 저장하는 자리가 다르기 때문입니다. Tiptap 어댑터는 들여쓰기를 노드 속성으로 저장하고 인라인 CSS로 렌더링합니다. CKEditor 5 어댑터는 모델 속성으로 저장하고 다운캐스트 단계에서 인라인 CSS로 바꿉니다. Froala와 Summernote 어댑터는 DOM을 직접 건드립니다. 눈금자가 하는 계산은 같은데, 그 결과를 어디에 쓸 것인지가 넷 다 다릅니다.
여기서 앞의 논지가 다시 나옵니다. 눈금자를 붙인다는 것은 문단의 여백을 어디에 저장할지 결정하는 일입니다. 노드 속성인지 인라인 스타일인지에 따라 붙여넣기와 내보내기와 협업 편집의 동작이 전부 달라집니다. UI 부품 하나를 얹는 결정이 아닙니다.
인쇄가 끼어들면 종이가 다시 생깁니다
여기서 결정이 갈리는 두 번째 지점이 나옵니다. 이 문서가 화면에서만 읽히면 종이는 끝까지 없습니다. 그런데 대부분의 사내 문서 도구는 결국 인쇄되거나 PDF로 나갑니다. 그 순간 종이가 다시 생깁니다.
인쇄 경로가 있다면 눈금자가 재야 하는 것은 편집 영역의 폭이 아니라 인쇄될 종이의 본문 폭입니다. 웹에서 그 종이를 선언하는 자리는 CSS의 @page 규칙이고, 여기서 용지 크기와 여백이 정해집니다. 즉 눈금자의 기준을 정하는 일은 인쇄 스타일시트의 값과 같은 값을 보게 만드는 일이 됩니다.
이 둘을 섞으면 가장 나쁜 결과가 나옵니다. 사용자가 화면 기준으로 3cm를 맞춰 놓고 인쇄하면 종이에서는 3cm가 아닙니다. 그러면 눈금자가 있어서 오히려 신뢰를 잃습니다. 그래서 선택지는 사실상 둘입니다. 편집 영역을 고정 폭의 가짜 종이로 만들어 화면과 인쇄를 일치시키거나, 눈금자가 재는 것이 인쇄 결과가 아니라는 점을 UI에서 분명히 말하거나. 둘 다 하지 않는 것이 지금 많은 도구가 눈금자를 넣지 않는 이유이기도 합니다.
만들어 보면 걸리는 자잘한 것들
이 프로젝트의 README에는 실제로 만들어 본 사람만 적을 수 있는 항목이 몇 개 있습니다. 두 가지가 특히 실무적입니다.
첫째, 되돌리기의 단위입니다. 드래그 한 번은 수십 번의 값 변경을 만들어냅니다. 그대로 두면 되돌리기를 스무 번 눌러야 원래 위치로 돌아옵니다. 이 프로젝트는 드래그 한 번을 되돌리기 한 단계로 묶는다고 명시합니다. 앞의 API에서 phase가 커밋인지 구분되는 이유가 이것입니다.
둘째, 세로 눈금자를 켜고 끌 때의 레이아웃 흔들림입니다. 세로 자가 나타나면 그만큼 본문이 밀립니다. 이 프로젝트는 자리를 미리 비워 두는 옵션을 두고, 그 동작을 CSS의 scrollbar-gutter: stable에 빗대어 설명합니다. 이 비유는 문제의 성격을 정확히 짚습니다. 나중에 나타날 수 있는 UI는 나타나기 전에 자리를 잡아 두어야 한다는 것입니다.
이 프로젝트의 현재 상태를 있는 그대로 적으면
과장할 수 없는 부분을 분명히 하겠습니다. 이 저장소는 만들어진 지 며칠 되지 않았고, 스타 수는 한 자릿수이며, 개인 또는 아주 작은 팀이 만든 것입니다. 라이선스는 Apache-2.0이고 언어는 TypeScript이며 npm 패키지가 올라와 있습니다. README에는 1.0.0부터 안정적이라는 문구가 있는데, 이것은 제3자의 평가가 아니라 만든 사람 본인의 선언입니다.
그래서 저는 이 글에서 이 라이브러리를 쓰라고 권하지 않습니다. 프로덕션 도입 여부는 직접 검증할 문제입니다. 이 프로젝트가 지금 시점에 볼 만한 이유는 다른 데 있습니다. 문제를 정확히 서술한 문서가 붙어 있기 때문입니다. 저자는 페이지 모델이 없을 때 눈금자가 무엇을 의미할 수 있는지, 만들면서 걸린 것이 무엇인지를 따로 글로 정리해 두었습니다. 직접 만들 계획이 있든 없든, 그 문제 정의는 읽을 값이 있습니다.
정리와 출처
웹 에디터에 눈금자가 없는 것은 기능이 빠진 것이 아니라 전제가 없는 것입니다. 종이를 정하지 않으면 잴 수 없고, 종이를 정하는 순간 그것은 UI가 아니라 문서 모델의 결정이 됩니다. 사내 도구에 이 기능을 요구받았다면, 구현을 견적 내기 전에 무엇을 종이로 삼을지부터 합의하시기 바랍니다.
- devslab-kr/editor-ruler 저장소 — 코어 API, 어댑터별 저장 방식, 되돌리기 처리, 세로 자 자리 예약 옵션. 본문의 이 프로젝트 관련 서술은 모두 README에서 확인했습니다.
- Every web rich-text editor is missing a ruler — 저장소가 배경 글로 연결해 둔 저자의 글
- 저장소 메타데이터로 확인한 사항: 라이선스 Apache-2.0, 언어 TypeScript, 저장소 생성 시점과 스타 수. 성숙도에 대한 서술은 이 수치들에 근거한 것이며 코드 품질을 평가한 것이 아닙니다.
- CSS에서 1인치가 96픽셀로 정의된다는 점은 CSS Values and Units 명세의 절대 길이 단위 정의에 따른 것입니다.
The Real Reason Web Editors Have No Ruler — The Ruler Is Not Missing, the Paper Is
- The point where a document pasted from Word falls apart
- It is not that nobody built it
- The web has no page
- The choice of treating content width as the paper
- The structure split into core and adapters
- When printing enters, the paper comes back
- The small things you only hit by building it
- Writing this project's current state as it is
- Summary and sources
The point where a document pasted from Word falls apart
There is a requirement that comes up in almost every project that moves an internal document tool onto the web: give us a ruler like the one in Word. That tool where you drag triangles to set the left margin per paragraph, indent only the first line, and grab table column boundaries on the scale.
From the development side this requirement looks trivial, because it seems like you draw one ruler-shaped scale and put drag handles on it. But once you start, you get stuck on day one. To write 3cm on the scale you need to know how many screen pixels are 3cm, and there is no answer to that.
It is not that nobody built it
This is not an empty spot because nobody was building it. The README of the recently published editor-ruler sums up the situation like this: the classic web WYSIWYG editors — the likes of Froala, TinyMCE, CKEditor 5, and Quill — ship without a ruler, and the ones that have a ruler are only the heavy components that carry a whole document model, like Syncfusion, DevExpress, and ONLYOFFICE.
That classification is the hint. What separates the side with a ruler from the side without is not development headcount or product maturity but whether it holds a document model.
The web has no page
The reason a ruler works in Word is that the paper is decided first. Choose A4 and the width is fixed at 210mm, and if the left and right margins are 25mm each the body width is 160mm. The ruler is a rule that draws this 160mm, and dragging a triangle to the 3cm position means 3cm from the left edge of the paper. Change the screen size and this number does not change, because the paper exists independently of the screen.
The web has no such paper. The width of the editing area keeps changing with the browser window size, the sidebar state, and the zoom level. And the browser's physical units are not actual physical lengths. In CSS one inch is defined as 96 pixels, and how many millimetres those 96 pixels actually are on a screen differs from monitor to monitor.
So drawing a ruler in a web editor means assuming a piece of paper that does not exist. The problem is not the technique of drawing a rule but deciding what to measure against.
The choice of treating content width as the paper
The approach editor-ruler took shows up directly in its core API. The caller is required to pass a function that reports the current state.
import { createRuler } from '@devslab/editor-ruler';
const ruler = createRuler(mountElement, {
unit: 'cm',
getMetrics: () => ({ contentWidth, leftMargin, rightMargin, firstLineIndent }),
onChange(change, phase) {
// apply the pixel value to the current paragraph. phase becomes commit on release
},
});
ruler.refresh(); // call whenever the selection or the content changes
What you should read here is not the first argument but getMetrics. The ruler does not know the paper width by itself; it asks the host every time. That is, the thing playing the role of paper is the current content width of the editing area, and when that width changes the ruler's numbers are recomputed.
There is a price to this choice. Narrow the screen and the 3cm position on the ruler actually points somewhere else. So this rule shares only a name with the rule in Word; it is measuring something different. The Word rule measures the paper that will be printed; this rule measures the editing area as currently laid out on screen. That is a difference you absolutely have to tell users about when you introduce it to a team.
The structure split into core and adapters
This project keeps a dependency-free core that handles only the scale, the drag handling, and the unit conversion, and separates the per-editor differences into thin adapters. Froala, Tiptap, CKEditor 5, and Summernote adapters each ship as a separate package.
The reason this structure is needed is that every editor stores indentation in a different place. The Tiptap adapter stores indentation as a node attribute and renders it with inline CSS. The CKEditor 5 adapter stores it as a model attribute and turns it into inline CSS at the down-cast stage. The Froala and Summernote adapters touch the DOM directly. The calculation the ruler does is the same; where the result gets written differs in all four.
Here the earlier argument returns. Attaching a ruler means deciding where a paragraph margin is stored. Whether it is a node attribute or an inline style changes the behaviour of pasting, exporting, and collaborative editing entirely. This is not a decision about adding one UI part.
When printing enters, the paper comes back
Here is the second point where the decision splits. If this document is only ever read on screen, there is no paper all the way through. But most internal document tools eventually get printed or go out as PDF. At that moment the paper comes back.
If there is a print path, what the ruler has to measure is not the width of the editing area but the body width of the paper to be printed. The place where you declare that paper on the web is the CSS @page rule, and that is where the sheet size and margins are set. In other words, deciding the ruler's reference becomes a matter of making it look at the same values as the print stylesheet.
Mixing the two produces the worst outcome. The user sets 3cm against the screen, prints, and on paper it is not 3cm. Then the ruler loses trust precisely by existing. So there are effectively two options: make the editing area a fixed-width fake paper so screen and print agree, or say clearly in the UI that what the ruler measures is not the printed result. Doing neither is also why many tools today do not include a ruler.
The small things you only hit by building it
The README of this project has a few entries only someone who actually built it could write. Two are especially practical.
First, the unit of undo. One drag produces dozens of value changes. Leave it as-is and you have to press undo twenty times to get back to the original position. This project states that it groups one drag into one undo step. That is why phase in the API above distinguishes the commit.
Second, the layout shift when toggling the vertical ruler on and off. When the vertical rule appears, the body is pushed over by that much. This project provides an option that reserves the space in advance, and explains that behaviour by analogy with the CSS scrollbar-gutter: stable. The analogy pins down the nature of the problem exactly: UI that may appear later has to take its place before it appears.
Writing this project's current state as it is
Let me be clear about the part that cannot be exaggerated. This repository is only a few days old, the star count is in single digits, and it was made by an individual or a very small team. The licence is Apache-2.0, the language is TypeScript, and npm packages are published. The README carries a line saying it is stable from 1.0.0, but that is the author's own declaration and not a third-party assessment.
So I am not recommending in this post that you use this library. Whether to adopt it in production is something to verify for yourself. The reason this project is worth looking at right now lies elsewhere. It comes with documentation that states the problem precisely. The author wrote up separately what a ruler can mean in the absence of a page model, and what tripped them up while building it. Whether or not you plan to build one yourself, that problem statement is worth the read.
Summary and sources
The absence of a ruler in web editors is not a missing feature but a missing premise. Without settling the paper you cannot measure, and the moment you settle the paper it becomes a document model decision rather than a UI one. If you have been asked for this feature in an internal tool, agree on what counts as the paper before you estimate the implementation.
- devslab-kr/editor-ruler repository — the core API, the per-adapter storage approach, undo handling, the vertical rule space-reservation option. Everything said about this project in the body was confirmed in the README.
- Every web rich-text editor is missing a ruler — the author's background post, linked from the repository
- Confirmed from repository metadata: licence Apache-2.0, language TypeScript, repository creation time and star count. The statements about maturity rest on those figures and are not an assessment of code quality.
- That one inch is defined as 96 pixels in CSS follows from the absolute length unit definitions in the CSS Values and Units specification.