Skip to content

숨김 모드 원리: 왜 너비 계산이 중요한가

학습 후 할 수 있는 것

  • OpenCode 숨김 모드의 작동 원리 이해
  • 일반 포맷팅 도구가 숨김 모드에서 정렬이 어긋나는 이유 파악
  • 플러그인의 너비 계산 알고리즘 마스터(3단계)
  • Bun.stringWidth의 역할 이해

현재 겪고 있는 문제

OpenCode로 코드를 작성 중, AI가 멋진 표를 생성했습니다:

markdown
| 필드 | 타입 | 설명 |
|--- | --- | ---|
| **name** | string | 사용자명 |
| age | number | 나이 |

소스 뷰에서는 깔끔하게 정렬되어 보입니다. 하지만 미리보기 모드로 전환하면 표가 어긋납니다:

| 필드     | 타입   | 설명   |
|--- | --- | ---|
| name | string | 사용자명 |    ← 왜 짧아졌지?
| age      | number | 나이   |

문제가 어디에 있나요? 숨김 모드입니다.

숨김 모드란 무엇인가

OpenCode는 기본적으로 **숨김 모드(Concealment Mode)**가 활성화되어 있으며, 렌더링 시 Markdown 구문 기호를 숨깁니다:

소스숨김 모드에서 표시
**굵게**굵게(4개 문자)
*기울임*기울임(4개 문자)
~~취소선~~취소선(6개 문자)
`코드`코드(4개 문자 + 배경색)

숨김 모드의 장점

내용 자체에 집중할 수 있게 해주며, **, * 같은 기호로 인해 시선이 분산되는 것을 방지합니다.

왜 일반 포맷팅 도구는 문제가 발생하는가

일반 표 포맷팅 도구는 너비를 계산할 때 **name**을 8개 문자로 계산합니다:

** n a m e ** = 8 문자

하지만 숨김 모드에서 사용자가 보는 것은 name으로, 4개 문자뿐입니다.

결과: 포맷팅 도구는 8 문자로 정렬하지만, 사용자는 4 문자를 보게 되어 표가 자연스럽게 어긋납니다.

핵심 아이디어: "문자 길이"가 아닌 "표시 너비" 계산

이 플러그인의 핵심 아이디어는: 사용자가 실제로 보는 너비를 계산하는 것이지, 소스 코드의 문자 수가 아닙니다.

알고리즘은 3단계로 나뉩니다:

1단계: 코드 블록 보호(코드 블록 내 기호는 제거하지 않음)
2단계: Markdown 기호 제거(**, *, ~~ 등)
3단계: Bun.stringWidth로 최종 너비 계산

함께 해보기: 3단계 알고리즘 이해

1단계: 코드 블록 보호

왜 필요한가

인라인 코드(백틱으로 감싸진) 내의 Markdown 기호는 "리터럴"이므로, 사용자는 bold라는 4개 문자가 아닌 **bold**라는 8개 문자를 봅니다.

따라서 Markdown 기호를 제거하기 전에 먼저 코드 블록 내용을 "숨겨야" 합니다.

소스 구현

typescript
// 1단계: 인라인 코드 추출 및 보호
const codeBlocks: string[] = []
let textWithPlaceholders = text.replace(/`(.+?)`/g, (match, content) => {
  codeBlocks.push(content)
  return `\x00CODE${codeBlocks.length - 1}\x00`
})

작동 원리

입력처리 후codeBlocks 배열
`**bold**`\x00CODE0\x00["**bold**"]
`a` and `b`\x00CODE0\x00 and \x00CODE1\x00["a", "b"]

\x00CODE0\x00 같은 특수 플레이스홀더로 코드 블록을 대체하면, 나중에 Markdown 기호를 제거할 때 실수로 제거되지 않습니다.

2단계: Markdown 기호 제거

왜 필요한가

숨김 모드에서 **굵게**굵게로 표시되고, *기울임*기울임으로 표시됩니다. 너비를 계산할 때 이 기호들을 제거해야 합니다.

소스 구현

typescript
// 2단계: 비코드 부분의 Markdown 기호 제거
let visualText = textWithPlaceholders
let previousText = ""

while (visualText !== previousText) {
  previousText = visualText
  visualText = visualText
    .replace(/\*\*\*(.+?)\*\*\*/g, "$1") // ***굵은기울임*** → 텍스트
    .replace(/\*\*(.+?)\*\*/g, "$1")     // **굵게** → 굵게
    .replace(/\*(.+?)\*/g, "$1")         // *기울임* → 기울임
    .replace(/~~(.+?)~~/g, "$1")         // ~~취소선~~ → 취소선
    .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, "$1")     // ![alt](url) → alt
    .replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)") // [text](url) → text (url)
}

왜 while 루프를 사용하는가?

중첩 구문을 처리하기 위해서입니다. 예를 들어 ***굵은기울임***:

1라운드: ***굵은기울임*** → **굵은기울임** (가장 바깥쪽 *** 제거)
2라운드: **굵은기울임** → *굵은기울임* (** 제거)
3라운드: *굵은기울임* → 굵은기울임 (* 제거)
4라운드: 굵은기울임 = 굵은기울임 (변화 없음, 루프 종료)
이미지와 링크 처리
  • 이미지 ![alt](url): OpenCode는 alt 텍스트만 표시하므로 alt로 대체
  • 링크 [text](url): text (url)로 표시되며, URL 정보 유지

3단계: 코드 블록 복원 + 너비 계산

왜 필요한가

코드 블록 내용을 다시 넣은 후, Bun.stringWidth로 최종 표시 너비를 계산해야 합니다.

소스 구현

typescript
// 3단계: 코드 블록 내용 복원
visualText = visualText.replace(/\x00CODE(\d+)\x00/g, (match, index) => {
  return codeBlocks[parseInt(index)]
})

return Bun.stringWidth(visualText)

왜 Bun.stringWidth를 사용하는가?

Bun.stringWidth는 다음을 올바르게 계산할 수 있습니다:

문자 타입예시문자 수표시 너비
ASCIIabc33
한글안녕24(각각 2칸 차지)
Emoji😀12(2칸 차지)
제로폭 문자a\u200Bb32(제로폭 문자는 공간 차지 안 함)

일반적인 text.length는 문자 개수만 셀 수 있으며, 이러한 특수 경우를 처리할 수 없습니다.

완전한 예시

셀 내용이 **`code`** and *text* 라고 가정해 봅시다.

1단계: 코드 블록 보호

입력: **`code`** and *text*
출력: **\x00CODE0\x00** and *text*
codeBlocks = ["code"]

2단계: Markdown 기호 제거

1라운드: **\x00CODE0\x00** and *text* → \x00CODE0\x00 and text
2라운드: 변화 없음, 종료

3단계: 코드 블록 복원 + 너비 계산

복원 후: code and text
너비: Bun.stringWidth("code and text") = 13

최종적으로 플러그인은 소스 코드의 22 문자가 아닌 13 문자 너비로 이 셀을 정렬합니다.

확인 포인트

이 수업을 완료한 후 다음 질문에 답할 수 있어야 합니다:

  • [ ] 숨김 모드는 어떤 기호를 숨기나요?(답: **, *, ~~ 등 Markdown 구문 기호)
  • [ ] 왜 먼저 코드 블록을 보호해야 하나요?(답: 코드 블록 내 기호는 리터럴이므로 제거하면 안 됨)
  • [ ] 왜 while 루프로 기호를 제거하나요?(답: ***굵은기울임*** 같은 중첩 구문 처리)
  • [ ] Bun.stringWidthtext.length보다 좋은 점은?(답: 한글, Emoji, 제로폭 문자의 표시 너비를 올바르게 계산)

주의사항

일반적인 오해

오해: 코드 블록 내의 **도 제거됩니다

사실: 아닙니다. 플러그인은 먼저 플레이스홀더로 코드 블록 내용을 보호하고, 다른 부분의 기호를 제거한 후 복원합니다.

따라서 `**bold**`의 너비는 8(**bold**)이지 4(bold)가 아닙니다.

이 수업 요약

단계역할핵심 코드
코드 블록 보호코드 블록 내 기호가 실수로 제거되는 것 방지text.replace(/\(.+?)`/g, ...)`
Markdown 제거숨김 모드에서의 실제 표시 내용 계산다중 정규식 치환
너비 계산한글, Emoji 등 특수 문자 처리Bun.stringWidth()

다음 수업 예고

다음 수업에서는 **표 규격**을 학습합니다.

배우게 될 내용:

  • 어떤 표가 포맷팅될 수 있는지
  • 표 검증의 4가지 규칙
  • "유효하지 않은 표" 오류를 피하는 방법

부록: 소스 코드 참조

클릭하여 소스 코드 위치 확인

업데이트 시간: 2026-01-26

기능파일 경로행 번호
표시 너비 계산 진입점index.ts151-159
코드 블록 보호index.ts168-173
Markdown 기호 제거index.ts175-188
코드 블록 복원index.ts190-193
Bun.stringWidth 호출index.ts195

핵심 함수:

  • calculateDisplayWidth(): 캐시가 있는 너비 계산 진입점
  • getStringWidth(): 핵심 알고리즘, Markdown 기호 제거 및 표시 너비 계산

핵심 상수:

  • \x00CODE{n}\x00: 코드 블록 플레이스홀더 형식