~/blog/tour/ko.mdx

블로그 둘러보기: 다이어그램, 수식, 코드

첫 글입니다. 이 블로그가 글 안에 담을 수 있는 것을 하나씩 보여 드립니다. 읽는 자리를 따라오는 그림, 빌드할 때 그리는 다이어그램, 수식, 코드, 그리고 두 언어.

이 블로그는 제 노트입니다. 대부분 기술 이야기이고, 일하다 생긴 잡다한 것도 올립니다. 모든 글은 한국어와 영어로 함께 씁니다. 첫 글은 이 블로그 자체를 둘러봅니다. 아래의 그림과 수식과 코드는 모두 이 블로그의 빌드가 실제로 만든 결과이고, 숫자는 모두 이 블로그를 만들면서 잰 값입니다.

읽는 자리를 따라오는 그림

설명이 그림을 가리킬 때, 그림은 보통 몇 문단 위에 있습니다. 여기서는 그림이 옆에(휴대폰에서는 위에) 고정되고, 지금 읽는 단계가 가리키는 부분만 밝아집니다. 아래는 글 한 편이 페이지가 되는 경로입니다.

그 밖의 종류 ko.mdx remark rehype-katex rehype-mermaid Expressive Code Astro 페이지 브라우저의 mermaid

글은 MDX 파일입니다. remark가 마크다운을 구문 트리로 바꾸고, 이때 $로 감싼 수식과 mermaid 코드 블록도 각자 트리의 노드가 됩니다.

rehype-katex가 수식을 KaTeX HTML로 바꿉니다. 읽는 사람은 수식 엔진이 아니라 글꼴만 내려받습니다.

rehype-mermaid가 다이어그램 블록을 beautiful-mermaid로 그려 SVG로 끼워 넣습니다. 색은 CSS 변수로 남겨 두어서, 테마를 바꾸면 다시 그리지 않고 색만 바뀝니다.

그림이 된 블록은 더 이상 코드 블록이 아니므로, Expressive Code는 남은 코드만 하이라이트합니다.

Astro가 페이지로 묶습니다. 이 글의 그림과 수식은 모두 이 경로로 왔습니다.

beautiful-mermaid는 flowchart, state, sequence, class, ER, xychart 여섯 가지를 그립니다.1 gantt나 pie 같은 나머지는 그 종류가 있는 페이지에서만 브라우저가 mermaid를 받아서 그립니다.

다이어그램

이 블로그를 만들면서 배포 폴더의 크기를 du -sh로 쟀습니다. 처음에는 브라우저용 mermaid가 번들에 통째로 들어가 12MB였습니다. mermaid를 필요한 페이지에서만 CDN으로 받게 바꾸고, 하네스 페이지를 배포에서 빼자 5.3MB가 됐습니다. 수식용 KaTeX 글꼴과 이 글을 더한 지금은 5.9MB입니다.

mermaid 번들 mermaid CDN lab 제외 KaTeX와 첫 글 0 2 4 6 8 10 12 MB 배포 폴더 크기 (MB, du -sh)

지금 배포되는 파일을 종류별로 나누면 다음과 같습니다. 대부분은 글꼴인데, 한글 글꼴은 글자 범위별로 잘게 나뉘어 있어서 브라우저는 페이지에 실제로 나온 글자가 든 조각만 내려받습니다. 이 원 그래프는 beautiful-mermaid가 그리지 않는 종류라서, 이 페이지에서만 브라우저가 mermaid를 받아 그렸습니다.

pie title 배포 파일 구성 (KB)
  "글꼴" : 5111
  "자바스크립트" : 15
  "CSS" : 173
  "HTML과 기타" : 283

글 한 편이 공개되기까지의 과정도 그림 하나로 그립니다.

npm run new ko.mdx 완성 en.mdx 완성 npm run check 실패 draft false deploy:site, push 초안 번역 검사 공개

수식

수식은 $…$로 쓰면 문장 안에, $$…$$로 쓰면 한 줄을 따로 차지합니다. 예를 들어 Redis의 HyperLogLog는 레지스터를 m=16384m = 16384개 쓰고, 표준 오차는 다음과 같습니다.

σ≈1.04m=1.0416384=1.04128≈0.81%\sigma \approx \frac{1.04}{\sqrt{m}} = \frac{1.04}{\sqrt{16384}} = \frac{1.04}{128} \approx 0.81\%

이 블로그의 읽기 시간도 식으로 셉니다. 한국어는 공백을 뺀 글자 cc를 분당 500자로, 영어는 단어 ww를 분당 230단어로 읽고, 코드는 ℓ\ell줄을 분당 40줄로 훑는다고 봅니다.

tko=c500+ℓ40ten=w230+ℓ40\begin{aligned} t_{\text{ko}} &= \frac{c}{500} + \frac{\ell}{40} \\ t_{\text{en}} &= \frac{w}{230} + \frac{\ell}{40} \end{aligned}

화면 아래 상태 줄의 남은 시간은 이 tt를 절마다 나눠서 셉니다. 절마다 같은 방식으로 무게 mim_i를 구하고(그림 하나에 0.3분을 더합니다), 그 절을 읽은 비율 rir_i만큼 덜어 냅니다. rir_i는 화면 위에서 30% 지점에 있는 읽는 선이 그 절을 얼마나 지났는지입니다. 그래서 글 맨 위에서는 남은 시간이 제목 아래의 읽기 시간과 같습니다.

tleft=t⋅∑imi (1−ri)∑imi,ri=min⁡ ⁣(1, max⁡ ⁣(0, 0.3 H−topibottomi−topi))t_{\text{left}} = t \cdot \frac{\sum_i m_i\,(1 - r_i)}{\sum_i m_i}, \qquad r_i = \min\!\left(1,\ \max\!\left(0,\ \frac{0.3\,H - \mathrm{top}_i}{\mathrm{bottom}_i - \mathrm{top}_i}\right)\right)

코드

코드 블록은 Expressive Code가 그립니다. 파일 이름, 줄 강조, diff, 터미널 창, 접기를 씁니다. 아래 diff는 이 블로그를 만들다가 실제로 고친 한 줄입니다.

src/lib/diagram.js
.replace(/\bid="([^"]*)"/g, `id="${id}-$1"`)
.replace(/(?<=\s)id="([^"]*)"/g, `id="${id}-$1"`)

\bid=는 data-id=의 -와 i 사이도 단어 경계로 보기 때문에 노드 이름까지 바꿔 버렸고, 그래서 위의 워크스루가 밝힐 노드를 찾지 못했습니다. 지금은 앞에 공백이 있는 id=만 고릅니다. 이 함수 전체도 워크스루로 읽을 수 있습니다.

src/lib/diagram.js
export function drawDiagram(src, id) {
return renderMermaidSVG(src, { bg: 'var(--bg)', fg: 'var(--fg)', transparent: true })
.replace(/<style>[\s\S]*?<\/style>/g, '')
.replace(/^(<svg[^>]*?) style="[^"]*"/, '$1')
.replace(/(?<=\s)id="([^"]*)"/g, `id="${id}-$1"`)
.replace(/url\(#([^)]+)\)/g, `url(#${id}-$1)`)
}

그리는 쪽은 한 줄입니다. 색 자리에 실제 색 대신 var(--bg)를 넘깁니다.

SVG마다 들어 있는 <style>을 뺍니다. 같은 규칙이 그림 수만큼 반복되고, Google Fonts에서 Inter를 불러오는 @import도 들어 있기 때문입니다. 규칙은 diagram.css에 한 번만 둡니다.

SVG 자신에 붙은 --bg: var(--bg)를 지웁니다. 자기 자신을 참조하는 변수는 순환이 되어 값이 사라집니다.

화살촉 marker의 id에 그림마다 다른 접두사를 붙입니다. 접두사가 없으면 모든 그림이 #arrowhead 하나를 나눠 쓰는데, 그 첫 그림이 숨겨진 언어 쪽에 있으면 나머지 그림의 화살촉이 함께 사라집니다.

글을 쓰고 공개하는 일은 터미널 명령 네 개로 끝납니다.

Terminal window
$ npm run new -- tour "블로그 둘러보기" "A tour of this blog"
$ npm run dev
$ npm run check
$ npm run deploy:site

코드 글꼴은 IBM Plex Mono에 IBM Plex Sans KR의 한글을 정확히 두 칸 폭으로 합친 Monoplex KR입니다. 한글이 두 칸이 아니면 아래 상자가 어긋납니다.

┌──────────────┬──────────┐
│ 단계 │ 결과 │
├──────────────┼──────────┤
│ 마크다운 │ ko.mdx │
│ 수식 │ HTML │
│ 그림 │ SVG │
└──────────────┴──────────┘

원본 글꼴은 굵기마다 2.7MB입니다. 빌드는 이 저장소에 쓰인 한글만 남깁니다. 이 글을 쓴 시점에는 한글 406자, 굵기마다 25.5KB였습니다.

읽는 사람을 위한 것

넓은 화면에서 왼쪽 목차는 절마다 그 길이만큼 막대를 그리고, 읽은 만큼 채웁니다. 화면 아래 상태 줄은 지금 읽는 절과 남은 시간을 보여 주고, 왼쪽의 파일 이름을 누르면 이 글의 원문 마크다운이 열립니다. 그림은 누르면 크게 볼 수 있습니다.

키하는 일
t밝은 테마와 어두운 테마
l한국어와 영어, 읽던 자리 유지
[ ]이전 절과 다음 절
?단축키 목록

l을 눌러 보세요. 한 페이지에 두 언어가 다 들어 있어서 다시 불러오지 않고, 지금 읽는 절의 같은 위치로 옮겨 갑니다.

l 키 지금 어디까지 읽었나 절 번호와 읽은 비율 data-lang 전환 같은 자리로 다른 언어의 같은 절 독자 chrome.js reader.js

l 키는 chrome.js가 받습니다. 모든 페이지에 들어 있는, 언어와 테마를 맡은 스크립트입니다.

언어를 바꾸기 전에 reader.js에 지금 위치를 묻습니다. 답은 절 번호와 그 절을 읽은 비율입니다.

html의 data-lang을 바꾸면 숨어 있던 언어가 보이고 보이던 언어가 숨습니다. 아무것도 다시 불러오지 않습니다.

reader.js가 새 언어의 같은 절, 같은 비율 지점으로 스크롤합니다. 그래서 두 언어의 절 수가 다르면 빌드가 실패합니다.

넓은 화면에서는 각주가 본문 옆 여백에 놓입니다.2 새 글은 RSS(한국어, 영어)로 받아 볼 수 있고, 에이전트에게 읽힐 때는 llms.txt에서 시작하면 됩니다.

각주

  1. xychart는 xychart-beta 문법입니다. 첫 번째 계열은 이 사이트가 측정값에 쓰는 테라코타 색을 받습니다. 다음 절의 배포 크기 그래프가 그 예입니다. ↩

  2. 이 각주처럼요. 좁은 화면에서는 글 끝으로 갑니다. ↩

~/blog/tour/en.mdx

A tour of this blog: diagrams, math, code

The first post. Everything this blog can put inside a post, one thing at a time: a figure that follows your place, diagrams drawn at build time, math, code, and two languages.

This blog is my notebook: mostly technical, plus whatever else comes up along the way. Every post is written in both Korean and English. The first one is a tour of the blog itself. Every figure, formula and code block below is real output of this blog’s build, and every number was measured while building it.

A figure that follows your place

When an explanation points at a figure, the figure is usually a few paragraphs up. Here it pins beside the text (above it, on a phone), and only the part the current step talks about lights up. Below is the path a post takes to become a page.

other types en.mdx remark rehype-katex rehype-mermaid Expressive Code Astro page mermaid in the browser

A post is an MDX file. remark turns the markdown into a syntax tree, and the math wrapped in $ and the mermaid code blocks each become a node of that tree.

rehype-katex turns the math into KaTeX HTML. The reader downloads fonts, not a math engine.

rehype-mermaid draws each diagram block with beautiful-mermaid and puts the SVG into the tree. Colours stay CSS variables, so switching the theme recolours the figure without drawing it again.

A block that became a picture is no longer a code block, so Expressive Code highlights only the code that is left.

Astro assembles the page. Every figure and formula in this post came this way.

beautiful-mermaid draws six types: flowchart, state, sequence, class, ER and xychart.1 The rest, gantt or pie for example, are drawn by mermaid in the browser, fetched only on a page that has one.

Diagrams

While building this blog I measured the deploy folder with du -sh. At first the browser build of mermaid was bundled whole, and the folder was 12 MB. Fetching mermaid from a CDN only on pages that need it, and keeping the harness pages out of the deploy, brought it to 5.3 MB. With the KaTeX fonts for math and this post added, it is now 5.9 MB.

mermaid bundled mermaid via CDN lab excluded KaTeX + this post 0 2 4 6 8 10 12 MB Deploy folder size (MB, du -sh)

Split by kind, the files deployed today look like this. Most of it is fonts, but the Korean fonts are cut into small ranges of characters, so a browser downloads only the pieces holding characters that appear on the page. This pie chart is a type beautiful-mermaid does not draw, so on this page alone the browser fetched mermaid to draw it.

pie title Deployed files by kind (KB)
  "Fonts" : 5111
  "JavaScript" : 15
  "CSS" : 173
  "HTML and the rest" : 283

How a post gets published fits in one figure too.

npm run new ko.mdx done en.mdx done npm run check fails draft false deploy:site, push Draft Translating Checking Published

Math

Math written as $…$ sits inside a sentence; $$…$$ takes a line of its own. Redis’s HyperLogLog, for example, uses m=16384m = 16384 registers, which gives this standard error:

σ≈1.04m=1.0416384=1.04128≈0.81%\sigma \approx \frac{1.04}{\sqrt{m}} = \frac{1.04}{\sqrt{16384}} = \frac{1.04}{128} \approx 0.81\%

Reading time on this blog is a formula too. Korean is read at 500 characters cc a minute, not counting spaces; English at 230 words ww a minute; code is skimmed at 40 lines ℓ\ell a minute.

tko=c500+ℓ40ten=w230+ℓ40\begin{aligned} t_{\text{ko}} &= \frac{c}{500} + \frac{\ell}{40} \\ t_{\text{en}} &= \frac{w}{230} + \frac{\ell}{40} \end{aligned}

The time left in the status line at the bottom splits this tt across sections. Each section gets a weight mim_i worked out the same way (plus 0.3 minutes per figure), less the share rir_i of it already read. rir_i is how far the reading line, 30% down the screen, has passed through that section. At the top of a post, then, the time left equals the reading time under the title.

tleft=t⋅∑imi (1−ri)∑imi,ri=min⁡ ⁣(1, max⁡ ⁣(0, 0.3 H−topibottomi−topi))t_{\text{left}} = t \cdot \frac{\sum_i m_i\,(1 - r_i)}{\sum_i m_i}, \qquad r_i = \min\!\left(1,\ \max\!\left(0,\ \frac{0.3\,H - \mathrm{top}_i}{\mathrm{bottom}_i - \mathrm{top}_i}\right)\right)

Code

Code blocks are drawn by Expressive Code: file names, marked lines, diffs, terminal frames, collapsed sections. The diff below is a line actually fixed while building this blog.

src/lib/diagram.js
.replace(/\bid="([^"]*)"/g, `id="${id}-$1"`)
.replace(/(?<=\s)id="([^"]*)"/g, `id="${id}-$1"`)

\bid= also treats the gap between - and i in data-id= as a word boundary, so it rewrote node names too, and the walkthrough above could not find the nodes it was meant to light. Now only an id= with whitespace before it matches. The whole function reads as a walkthrough as well.

src/lib/diagram.js
export function drawDiagram(src, id) {
return renderMermaidSVG(src, { bg: 'var(--bg)', fg: 'var(--fg)', transparent: true })
.replace(/<style>[\s\S]*?<\/style>/g, '')
.replace(/^(<svg[^>]*?) style="[^"]*"/, '$1')
.replace(/(?<=\s)id="([^"]*)"/g, `id="${id}-$1"`)
.replace(/url\(#([^)]+)\)/g, `url(#${id}-$1)`)
}

Drawing is one line. Where a colour goes, it passes var(--bg) instead of a colour.

Drop the <style> each SVG carries. The same rules would repeat once per figure, and they include an @import of Inter from Google Fonts. The rules live once, in diagram.css.

Remove --bg: var(--bg) from the SVG element itself. A variable that refers to itself is a cycle, and its value is lost.

Give each figure’s arrowhead markers their own prefix. Without one, every figure shares a single #arrowhead, and when the first figure sits in the hidden language, the arrowheads of all the others disappear with it.

Writing and publishing a post takes four commands.

Terminal window
$ npm run new -- tour "블로그 둘러보기" "A tour of this blog"
$ npm run dev
$ npm run check
$ npm run deploy:site

Code is set in Monoplex KR, which combines IBM Plex Mono with the Hangul of IBM Plex Sans KR at exactly two columns. The box below breaks if a Hangul syllable is anything other than two columns wide; the Korean version of this post labels it in Korean.

┌──────────────┬──────────┐
│ Stage │ Output │
├──────────────┼──────────┤
│ Markdown │ en.mdx │
│ Math │ HTML │
│ Figure │ SVG │
└──────────────┴──────────┘

The source font is 2.7 MB per weight. The build keeps only the Hangul used in this repository: when this post was written, 406 syllables, 25.5 KB per weight.

For the reader

On a wide screen, the contents on the left draw a bar as long as each section and fill it as you read. The status line at the bottom shows the section you are in and the time left, and the file name at its left opens this post’s source markdown. Click a figure to see it large.

KeyWhat it does
tLight or dark theme
lKorean or English, keeping your place
[ ]Previous or next section
?The list of keys

Try l. Both languages are already on this page, so nothing reloads, and you land at the same point of the same section.

presses l where is the reader? section and share read flip data-lang same place, please same section, other language Reader chrome.js reader.js

The l key goes to chrome.js, the script on every page that looks after language and theme.

Before switching, it asks reader.js where the reader is. The answer is a section number and how much of that section has been read.

Changing data-lang on the html element shows the hidden language and hides the visible one. Nothing reloads.

reader.js scrolls to the same share of the same section in the new language. That is why the build fails when the two languages have a different number of sections.

On a wide screen, footnotes sit in the margin beside the text.2 New posts arrive by RSS (Korean, English), and an agent can start from llms.txt.

Notes

  1. xychart uses the xychart-beta syntax. Its first series takes the terracotta this site gives measured numbers; the deploy size chart in the next section is one. ↩

  2. Like this one. On a narrow screen it moves to the end of the post. ↩

ko.mdx

단축키Keyboard

/ ⌘K
글 검색Search posts
t
밝은/어두운 테마Light or dark theme
l
한국어/영어, 읽던 자리 유지Korean or English, keeping your place
[ ]
이전/다음 절Previous or next section
?
이 목록This list

↑↓ 이동move↵ 열기openesc 닫기close