옵시디언이나 타이포라 같은 잘 만들어진 마크다운 편집기는 어딘지 모르게 편하다. 여러 이유가 있겠지만 캐럿 이동이 아주 잘 설계되었다는 점이 특히 눈에 띈다. 두 편집기의 캐럿 이동을 살펴보며 나름대로의 규칙을 정리해봤다. 옵시디언에만 해당하는 내용도 있고 타이포라에만 해당하는 내용도 있고 둘 다 해당되지 않는 내용도 있다.
레이아웃 쉬프트 최소화
첫번째로 떠오르는 건 캐럿 이동으로 인한 레이아웃 쉬프트를 최소화한다는 원칙이다. 예를 들어 다음 예시에서는 캐럿이 인라인 코드 영역에 진입할 때 백틱이 나타나면서 콘텐츠가 우측으로 밀린다.
Inline `code` and backticks.
Inline `code` and backticks.
인라인 코드 영역에는 보통 회색 배경 표시를 하곤 하기 때문에 글자 좌우에 약간의 공간이 이미 확보되어 있는 경우가 많다. 이 점을 활용하여 백틱이 이 공간에 나타나도록 하면 레이아웃 쉬프트가 일어나지 않는다. 다음은 이 방법을 적용한 예시이다.
Inline `code` and backticks.
Inline `code` and backticks.
같은 문제가 제목(## Heading)에도 존재한다. 아래처럼 처리하면 레이아웃 쉬프트가 일어난다.
Some paragrah
Level 2 Heading
Another paragraph
Some paragrah
## Level 2 Heading
Another paragraph
Some paragrah
Level 2 Heading
Another paragraph
반면, 다음과 같이 좌측의 여백을 이용하여 #을 작은 글자로 표현한다면 레이아웃 쉬프트도 일어나지 않고 필요하면 자연스럽게 캐럿을 좌측으로 옮겨서 제목 수준을 변경할 수 있다.
Some paragrah
## Level 2 Heading
Another paragraph
Some paragrah
## Level 2 Heading
Another paragraph
Some paragrah
## Level 2 Heading
Another paragraph
Some paragrah
## Level 2 Heading
Another paragraph
Some paragrah
## Level 2 Heading
Another paragraph
즉각적인 연결
Bret Victor의 Inventing on principle은 나에게 많은 영감을 준 강연 중 하나이다. 여기에서 B. Victor는 “Creators need an immediate connection to what they’re creating”이라는 자신의 원칙을 소개한다. 편집기에 이 원칙을 적용해보자면, 내가 뭘 고쳤으면 그 결과가 즉각적으로 보여야 한다는 뜻으로 해석할 수 있다.
WYSIWYG 편집기는 대체로 이 원칙에 부합하지만, Result Oriented UI 같은 걸 도입하여 더 개선할 여지도 얼마든지 있다. 이 글의 주제는 WYSIWYG 편집기와 소스 편집기의 중간인 하이브리드 편집기이므로, 하이브리드 편집기에서 즉각적인 연결을 더 잘 구현하는 방법에 대해 생각해보자.
예를 들어 LaTeX 수식 블록은 어떤 식으로 처리하면 좋을까? 대부분의 하이브리드 편집기는 이렇게 작동한다.
Here's the equation:
$$
E = mc^2
$$
And some sentences after the equation.
Here's the equation:
$$
E = mc^2
$$
And some sentences after the equation.
코드를 편집하는 동안에는 렌더링된 결과물을 볼 수 없다. 아래와 같이 사이드-바이-사이드로 병치해보면 어떨까?
Here's the equation:
$$
E = mc^2
$$
And some sentences after the equation.
Here's the equation:
$$
E = mc^2
$$
And some sentences after the equation.
원래 있던 콘텐츠가 옆으로 부드럽게 이동하고, 수식 블록이 펼쳐질 때 아래에 있는 문장이 밀려나지 않는다. 게다가 수식을 편집하면서 결과물을 실시간으로 볼 수 있다.
캐럿이 엉뚱한 곳으로 튀지 않기
렌더링된 결과물이 차지하는 높이보다 코드가 차지하는 높이가 더 크다면 어떻게 해야할까? 좌우로 길게 렌더링되는 Mermaid 다이어그램들이 대체로 그렇다. 이 경우에는 레이아웃 쉬프트를 막기 어려워보인다.
레이아웃 쉬프트의 문제 중 하나는 사용자가 기대했던 위치가 아닌 곳으로 캐럿을 옮겨버리곤 한다는 점이다. 레이아웃 쉬프트를 막을 수 없더라도, 그로 인해 캐럿이 엉뚱한 위치로 튀는 문제가 일어나지 않는다면 적어도 불필요한 눈동자의 움직임을 줄일 수는 있을 것이다.
다음과 같이 캐럿이 위에서 아래로 진입하는 경우에는 코드가 아래로 펼쳐지며 하단의 콘텐츠를 밀어낸다. 그러면 캐럿은 원래 기대했던 그 위치에 머무르기 때문에 사용자를 당황하게 만들지 않는다.
A wide diagram and a long line of code.
```mermaid
flowchart LR
A[Input]
B[Check]
C[Transform]
D[Output]
A --> B
B --> C
C --> D
```
A sentence below the diagram.
A wide diagram and a long line of code.
```mermaid
flowchart LR
A[Input]
B[Check]
C[Transform]
D[Output]
A --> B
B --> C
C --> D
```
A sentence below the diagram.
A wide diagram and a long line of code.
```mermaid
flowchart LR
A[Input]
B[Check]
C[Transform]
D[Output]
A --> B
B --> C
C --> D
```
A sentence below the diagram.
A wide diagram and a long line of code.
```mermaid
flowchart LR
A[Input]
B[Check]
C[Transform]
D[Output]
A --> B
B --> C
C --> D
```
A sentence below the diagram.
반대로, 캐럿이 아래에서 위로 진입하는 경우에는 코드가 윗쪽 콘텐츠를 밀고 나타나야 한다. 그래야 캐럿의 위치가 튀지 않는다.
A wide diagram and a long line of code.
```mermaid
flowchart LR
A[Input]
B[Check]
C[Transform]
D[Output]
A --> B
B --> C
C --> D
```
A sentence below the diagram.
A wide diagram and a long line of code.
```mermaid
flowchart LR
A[Input]
B[Check]
C[Transform]
D[Output]
A --> B
B --> C
C --> D
```
A sentence below the diagram.
A wide diagram and a long line of code.
```mermaid
flowchart LR
A[Input]
B[Check]
C[Transform]
D[Output]
A --> B
B --> C
C --> D
```
A sentence below the diagram.
A wide diagram and a long line of code.
```mermaid
flowchart LR
A[Input]
B[Check]
C[Transform]
D[Output]
A --> B
B --> C
C --> D
```
A sentence below the diagram.
다음과 같이 캐럿이 위에서 진입한 뒤에 아래로 빠져나가는 경우에는? 진입할 땐 아래쪽을 밀며 펼쳐지고, 나갈 땐 위쪽을 당기며 접혀야 한다. 그래야 캐럿의 위치가 튀지 않는다.
A wide diagram and a long line of code.
```mermaid
flowchart LR
A[Input]
B[Check]
C[Transform]
D[Output]
A --> B
B --> C
C --> D
```
A sentence below the diagram.
A wide diagram and a long line of code.
```mermaid
flowchart LR
A[Input]
B[Check]
C[Transform]
D[Output]
A --> B
B --> C
C --> D
```
A sentence below the diagram.
A wide diagram and a long line of code.
```mermaid
flowchart LR
A[Input]
B[Check]
C[Transform]
D[Output]
A --> B
B --> C
C --> D
```
A sentence below the diagram.
A wide diagram and a long line of code.
```mermaid
flowchart LR
A[Input]
B[Check]
C[Transform]
D[Output]
A --> B
B --> C
C --> D
```
A sentence below the diagram.
A wide diagram and a long line of code.
```mermaid
flowchart LR
A[Input]
B[Check]
C[Transform]
D[Output]
A --> B
B --> C
C --> D
```
A sentence below the diagram.
A wide diagram and a long line of code.
```mermaid
flowchart LR
A[Input]
B[Check]
C[Transform]
D[Output]
A --> B
B --> C
C --> D
```
A sentence below the diagram.
A wide diagram and a long line of code.
```mermaid
flowchart LR
A[Input]
B[Check]
C[Transform]
D[Output]
A --> B
B --> C
C --> D
```
A sentence below the diagram.
A wide diagram and a long line of code.
```mermaid
flowchart LR
A[Input]
B[Check]
C[Transform]
D[Output]
A --> B
B --> C
C --> D
```
A sentence below the diagram.
A wide diagram and a long line of code.
```mermaid
flowchart LR
A[Input]
B[Check]
C[Transform]
D[Output]
A --> B
B --> C
C --> D
```
A sentence below the diagram.
A wide diagram and a long line of code.
```mermaid
flowchart LR
A[Input]
B[Check]
C[Transform]
D[Output]
A --> B
B --> C
C --> D
```
A sentence below the diagram.
A wide diagram and a long line of code.
```mermaid
flowchart LR
A[Input]
B[Check]
C[Transform]
D[Output]
A --> B
B --> C
C --> D
```
A sentence below the diagram.
A wide diagram and a long line of code.
```mermaid
flowchart LR
A[Input]
B[Check]
C[Transform]
D[Output]
A --> B
B --> C
C --> D
```
A sentence below the diagram.
A wide diagram and a long line of code.
```mermaid
flowchart LR
A[Input]
B[Check]
C[Transform]
D[Output]
A --> B
B --> C
C --> D
```
A sentence below the diagram.
A wide diagram and a long line of code.
```mermaid
flowchart LR
A[Input]
B[Check]
C[Transform]
D[Output]
A --> B
B --> C
C --> D
```
A sentence below the diagram.
편집 의도가 없으면 펼치지 않기
처음에 살펴봤던 백틱 예시는 레이아웃 쉬프트 전혀 없이 빈 공간에 백틱을 렌더링하는 식으로 해결할 수 있었다.
Inline `code` and backticks.
Inline `code` and backticks.
하지만 링크는 어떻게 하면 좋을까? 레이아웃 쉬프트를 없앨 수 없다. 다만 글자 크기를 조절해서 되도록 쉬프트가 덜 일어나게 할 수는 있다.
Let's visit W3C - The World-Wide Web Consortium.
Let's visit [W3C](https://w3.org) - The World-Wide Web Consortium.
이게 최선일까? 약간 더 개선할 방법이 있다. 키를 누른채로 빠르게 지나갈 땐 내용을 펼치지 않도록 하는 방법이다. 즉, 편집 의도가 없고 그냥 지나가던 길이었다는 게 확실하면 굳이 레이아웃 쉬프트를 일으킬 필요가 없다.
Let's visit W3C - The World-Wide Web Consortium.
Let's visit W3C - The World-Wide Web Consortium.
Let's visit W3C - The World-Wide Web Consortium.
Let's visit W3C - The World-Wide Web Consortium.
Let's visit W3C - The World-Wide Web Consortium.
Let's visit W3C - The World-Wide Web Consortium.
Let's visit W3C - The World-Wide Web Consortium.
중의적 상황이라면 피해가 최소화되도록
사용자가 기대하는 바가 명확하지 않고 정답이 없는 상황이라면 어떻게 하는 게 좋을까? 예를 들어 아래와 같은 상황에서 사용자가 ⌘→ / End 키를 누르면 캐럿이 어디로 가야할까?
Let's visit W3C
어떤 사람은 다음과 같이 캐럿이 “C” 뒤에 놓이길 기대할 것이다. 이걸 A안이라고 하자.
Let's visit [W3C](https://w3.org)
또다른 사람은 다음과 같이 문장 끝에 놓이길 기대할 것이다. 이걸 B안이라고 하자.
Let's visit [W3C](https://w3.org)
누구의 기대가 옳은가? 정답이 없다. 이 경우에는 기대와 다르게 움직였을 때 문제 해결에 드는 노력이 적어지는 방향으로 움직이도록 한다.
A안을 기대했던 사람이 B안처럼 움직이는 시스템을 사용한다면 이 사람은 굳이 마우스를 움직여서 “C” 옆을 클릭하거나 왼쪽 화살표를 여러번 눌러서 캐럿을 “C” 옆으로 옮겨놔야 한다. 고생스럽다. 반면, B안을 기대했던 사람이 A안처럼 움직이는 시스템을 사용한다면 단순히 좀 전에 눌렀던 ⌘→ / End 키를 한 번 더 누르면 된다.
따라서 이 경우 캐럿은 A안처럼 움직이도록 했다.
Let's visit W3C
Let's visit [W3C](https://w3.org)
Let's visit [W3C](https://w3.org)
⌘A / Ctrl + A 키를 눌러서 전체 선택을 하는 경우도 마찬가지다. 아래와 같이 코드 블록 내부에서 전체 선택을 하면 코드 블록만 선택되는 게 좋을까 전체 문서가 선택되는 게 좋을까? 같은 원칙에 의거하여 코드 블록 내부만 선택되고 한 번 더 누르면 전체가 선택되도록 했다. (별로 좋아하는 소프트웨어는 아니지만, 블록 단위 편집기인 노션 역시 이와 같은 방식으로 작동한다.)
Before the code block.
```js
const x = 2
const y = 3
console.log(x + y)
```
After the code block.
Before the code block.
```js
const x = 2
const y = 3
console.log(x + y)
```
After the code block.
Before the code block.
```js
const x = 2
const y = 3
console.log(x + y)
```
After the code block.
캐럿 이동의 가역성
마지막 원칙은 가역성이다. 쉽게 말해서 위로 한 칸 움직였다가 아래로 한 칸 움직이면 캐럿이 되도록 원래 위치로 정확히 되돌아오는 게 좋다는 원칙이다. 이 원칙이 잘 안지켜지는 사례와 잘 지켜지는 사례를 비교해보자.
예를 들어 아래 상황에서 캐럿을 오른쪽으로 한 칸 옮기면 캐럿이 어디로 가는 게 좋을까?
Before the code block.
```js
const x = 2
const y = 3
console.log(x + y)
```
다음과 같이 블록 시작 부분에 놓는 방법이 있다. 이걸 A안이라고 하자.
Before the code block.
```js
const x = 2
const y = 3
console.log(x + y)
```
다음과 같이 실제 코드가 시작되는 부분에 놓는 방법도 있다. 이걸 B안이라고 하자.
Before the code block.
```js
const x = 2
const y = 3
console.log(x + y)
```
A안에 비해 B안에 키보드를 조금 덜 누를 수 있기 때문에 더 좋다고 생각할 수도 있다. 하지만 캐럿 이동의 가역성을 생각해보면 사용자의 습관 형성을 해치는 안좋은 방법일 수 있다.
A안의 경우 왼쪽 화살표를 한 번 누르면 캐럿이 정확히 기대한 위치로 돌아온다. 즉, 오른쪽 화살표를 한 번 누르고 왼쪽 화살표를 한 번 누르면 캐럿이 원래 위치에 놓이게 된다. 반면 B안의 경우 이 원칙이 성립하지 않는다. 다음은 B안의 가역성이 성립하지 않는 문제를 보여준다.
Before the code block.
```js
const x = 2
const y = 3
console.log(x + y)
```
Before the code block.
```js
const x = 2
const y = 3
console.log(x + y)
```
Before the code block.
```js
const x = 2
const y = 3
console.log(x + y)
```
반면 A안의 경우, 가역성이 잘 지켜진다.
Before the code block.
```js
const x = 2
const y = 3
console.log(x + y)
```
Before the code block.
```js
const x = 2
const y = 3
console.log(x + y)
```
Before the code block.
```js
const x = 2
const y = 3
console.log(x + y)
```
결론
여기에서 제시한 원칙들은 정답이라기보다는 일관성 있는 편집 경험을 제공하기 위한 고민의 기록에 가깝다. 실제로 이 원칙이 지켜지는 편집기를 만들고 지속적으로 써보면서 다듬어갈 생각이다.