TRENDSHIFT #15 · 2026.05.23 · MarkEdit-app/MarkEdit

MarkEdit 딥다이브
TextEdit처럼 가벼운데, 안엔 웹 에디터가 산다

Mac의 기본 텍스트 에디터인 TextEdit을 떠올려보자. 그걸 마크다운에만 바친 4MB짜리 네이티브 앱이 MarkEdit이다. 그런데 신기한 점은 — 에디터 코어가 Swift가 아니라 CodeMirror 6, 즉 웹 기술이라는 사실이다. 왜 그렇게 만들었는지, 코드는 어떻게 엮여 있는지 한 번 풀어본다.

0먼저, MarkEdit이 뭔가?

한 줄로 — Mac을 위한 가볍고 빠른 오픈소스 마크다운 에디터.

MarkEdit은 macOS 전용 오픈소스 마크다운 에디터다. MIT 라이선스, 별 약 3,800개, 1,100여 커밋. 만든 사람은 Ying Zhong이라는 개발자고, 직접 만든 이유는 README에 이렇게 적혀 있다 — "We make it mostly because we need it, and we ship it just in case you need it too." (자기가 필요해서 만들었고, 혹시 당신도 필요할까 싶어 풀어둔다는 뜻.)

핵심 캐릭터를 짚어보자.

기본 사실
MarkEdit 핵심 스펙
플랫폼: macOS Sequoia / Tahoe (최신 2개 메이저 버전만 지원) · 설치 크기: 약 4MB · 사용 언어: Swift 60% + TypeScript 37% + Mustache/HTML/Objective-C/JS/CSS 소량 · 라이선스: MIT · 핵심 의존성: CodeMirror 6, Vite 7, ts-gyb (Microsoft) · 설치: GitHub Releases의 .dmg 또는 brew install --cask markedit · 데이터 수집 0 (앱이 외부로 보내는 요청은 GitHub Releases API 1개뿐, 업데이트 체크용).

비유하자면 — 워드 프로세서가 한글이나 MS Word라면, MarkEdit은 '메모장에 가까운, 그러나 마크다운 문법만 예쁘게 입혀주는' 도구다. WYSIWYG(보이는 그대로 결과가 나오는 편집 방식)도 아니고, 미리보기 패널도 기본 탑재가 아니다. 별도 확장으로 켤 수는 있지만, 기본은 어디까지나 "원본 마크다운 문법을 그대로 보면서 쓰는" 에디터다.

용어
WYSIWYG (What You See Is What You Get, "보이는 그대로 얻는다")
제목이라고 쓰면 화면에 즉시 굵은 큰 글씨로 보이는 편집 방식. Notion, Typora, 한글, MS Word가 이쪽. 반대로 MarkEdit은 # 제목이라는 원본 마크다운 문법을 그대로 보여주고, 그 위에 색·굵기만 살짝 입혀준다. "키 한 번 누르면 무슨 문자가 들어갔는지 정확히 보고 싶다"는 철학.

다른 마크다운 에디터들과 위치를 비교하면 이렇게 그려진다.

   네이티브 ◄─────────────── 크로스플랫폼/Electron
   |                                          |
   TextEdit  MarkEdit ───────► Typora ─► Obsidian ─► Notion
   (간단)    (간단,            (WYSIWYG)  (지식관리)  (모든 것)
              마크다운
              전용)
   ──────────────────────────────────────────────────────►
                         기능 풍부

왼쪽으로 갈수록 "한 가지만 잘하기", 오른쪽으로 갈수록 "이것저것 다 하기". MarkEdit은 의도적으로 왼쪽 끝 가까이에 자리잡았다.

1왜 주목받는가

한 줄 요약 — 가볍고, 사용자 데이터를 만지지 않고, 그래도 진짜 빠르다.

핵심 메시지

"중요한 건 무엇을 더 넣었는가가 아니라,
무엇을 빼고 견딜 수 있었는가다."

MarkEdit이 트렌드에 오른 이유는 새로운 기능이 있어서가 아니라, 거꾸로 최신 마크다운 앱들이 당연하게 넣는 것을 일부러 빼고 있어서다. 미리보기 패널, WYSIWYG, 동기화 서버, 플러그인 마켓플레이스, AI 기능 — 전부 없다. 그 결과 설치 크기 4MB, 10MB짜리 파일도 깜빡임 없이 편집되는 성능, 그리고 "내 데이터는 그냥 디스크의 .md 파일일 뿐"이라는 안도감이 남았다.

경쟁 제품들과 비교하면 자리가 더 분명해진다.

비교 1

Obsidian / Notion 대비

Obsidian과 Notion은 "지식 관리 시스템"이다. 노트끼리 링크하고, 그래프 뷰를 그리고, 데이터베이스 뷰까지 지원한다. MarkEdit은 그런 야망이 없다. 그냥 .md 파일 하나 열어서 쓰고 닫는 일을 한다. "이걸로 두 번째 뇌를 만들고 싶다"가 아니라 "그냥 마크다운 파일 좀 빨리 편집하고 싶다"인 사람에게 맞다.

비교 2

Typora 대비

Typora는 유료(약 $15)고 WYSIWYG 위주다. MarkEdit은 무료/오픈소스고 정반대로 마크다운 문법을 그대로 보여준다. 또 Typora는 Electron 기반이라 설치 시 100MB 이상인데, MarkEdit은 4MB. "마크다운 문법을 매번 보고 싶다"는 취향에 정확히 맞물린다.

비교 3

VSCode + 마크다운 확장 대비

VSCode로도 마크다운을 쓸 수 있지만, 개발자 도구라 메뉴가 너무 많고 설치 크기도 350MB+. MarkEdit은 "Mac 사용자가 TextEdit에서 마크다운만 쓰고 싶을 때 쓰는 앱"으로 포지셔닝이 다르다. 키 바인딩이나 메뉴 구조가 macOS 표준 그대로라 학습 곡선이 거의 없다.

여기까지가 "어디에 있는 앱인가". 다음 섹션부터는 본격적으로 코드 속으로 들어간다. 정말 흥미로운 건 이 작은 Mac 앱 안에 웹 기술이 어떻게 박혀 있는지다.

2기술 스택 전체 지도

Swift + AppKit + WebKit + CodeMirror — Mac과 웹이 한 앱 안에 공존한다.

레포 언어 비율부터 보자. Swift 60.5%, TypeScript 37.2%, 나머지는 Mustache/HTML/Objective-C/JS/CSS 소량. 이 두 언어가 거의 5:3 비율로 섞여 있다는 점이 이미 힌트다 — 절반은 macOS 네이티브, 절반은 웹 에디터다.

네이티브 macOS 쪽 (Swift, ~60%)

Swift 코드는 크게 세 영역에 살고 있다.

Swift 패키지 3종
MarkEditMac / MarkEditKit / MarkEditCore
MarkEditMac — 실제 .app 번들. EditorWindow, EditorWindowController, 메뉴/툴바, 설정 화면(SwiftUI), Shortcuts/AppleScript 연동 등 "macOS 앱 그 자체"를 담당.
MarkEditKit — WKWebView를 감싸서 JS와 Swift가 메시지를 주고받게 만드는 브릿지 레이어. WKWebView 래퍼 + Swift↔JS 양방향 브릿지(ts-gyb 자동생성 코드 포함)가 핵심.
MarkEditCore — Foundation에만 의존하는 가장 작은 모듈. 텍스트 처리, 모델 등 UI 없이 검증 가능한 핵심 로직. 단위 테스트(MarkEditCoreTests)도 여기에 있다.

이런 식의 "UI 계층 ◄ Kit 계층 ◄ Core 계층" 3단 분리는 Apple의 표준 권장 아키텍처다. 가장 안쪽 Core는 UIKit/AppKit 안 쓰니 어디서든 재사용·테스트가 가능하다.

UI 프레임워크 선택도 흥미롭다. README에 직접 적힌 원칙은 — "WebKit for text editing, SwiftUI for settings, AppKit for all the rest." 즉 텍스트 편집은 웹, 설정 화면은 SwiftUI, 나머지 메뉴·창·툴바는 AppKit. 한 가지 도구를 모든 곳에 강요하지 않고, 시나리오마다 가장 잘 맞는 도구를 골랐다.

용어
AppKit / SwiftUI / WebKit
AppKit: Mac에 NSWindow가 있었던 시절부터 쓰던 macOS의 전통적인 UI 프레임워크 (1989년 NeXTSTEP 시절 기원).
SwiftUI: 2019년 Apple이 발표한 신세대 선언형 UI 프레임워크 — "이런 화면을 그려달라"고 설명문처럼 작성.
WebKit: Safari를 만든 그 브라우저 엔진. macOS에는 WKWebView라는 컴포넌트로 들어 있어 앱 안에 작은 웹뷰를 띄울 수 있다. MarkEdit의 편집 영역은 사실 이 WKWebView다.

웹 에디터 쪽 (TypeScript, ~37%)

편집기 코어는 CoreEditor/ 폴더 안에 통째로 들어 있다. package.json을 까보면 의존성 윤곽이 한눈에 들어온다.

// CoreEditor/package.json — 핵심 dependencies (요약)
"dependencies": {
  "@codemirror/autocomplete":   "^6.0.0",   // 자동완성
  "@codemirror/commands":       "^6.0.0",   // 기본 키 명령
  "@codemirror/lang-markdown":  "^6.0.0",   // 마크다운 파서
  "@codemirror/lang-yaml":      "^6.0.0",   // YAML front-matter
  "@codemirror/language":       "^6.0.0",   // 언어 추상화
  "@codemirror/search":         "^6.0.0",   // 찾기/바꾸기
  "@codemirror/state":          "^6.0.0",   // 에디터 상태
  "@codemirror/view":           "^6.0.0",   // 렌더링
  "@lezer/markdown":            "^1.0.0",   // Lezer 파서 (마크다운)
  "@lezer/highlight":           "^1.0.0"    // 구문 강조
}

핵심은 CodeMirror 6. 그리고 그 안에서 마크다운을 파싱하는 건 Lezer라는 또 다른 라이브러리(CodeMirror와 같은 저자 Marijn Haverbeke). Lezer는 "에러에 강한 LR 파서 제너레이터"로, 코드 한 글자 칠 때마다 트리 일부만 다시 파싱해서 실시간 신택스 하이라이트가 가능하게 한다.

용어
CodeMirror 6 (코드미러 식스)
웹에서 동작하는 코드 에디터 라이브러리. 모듈식(@codemirror/view, @codemirror/state, ...)으로 잘게 쪼개진 v6는 트리 셰이킹과 커스터마이즈가 쉽다. VSCode가 쓰는 Monaco와 함께 웹 에디터 두 거장 중 하나. Replit, Sourcegraph, Jupyter, 그리고 MarkEdit이 이걸 코어로 쓴다. 비유하자면 — 자동차로 치면 "엔진+변속기" 같은 동력 모듈만 따로 사다가 자기 차체에 올린 셈.

빌드 도구 쪽

웹 코어를 빌드하는 도구도 모던하다. Vite 7 + vite-plugin-singlefile + esbuild + rollup. 빌드의 결과물은 단일 HTML 파일이다 — 자바스크립트와 CSS가 전부 한 .html 안에 인라인으로 박힌다. 왜? 그래야 macOS 앱 번들 안에 그대로 넣고 WKWebView가 loadFileURL로 한 번에 불러올 수 있어서다.

그리고 가장 흥미로운 도구가 하나 더 있다 — Microsoft가 만든 ts-gyb다.

용어
ts-gyb (TypeScript → Swift/Kotlin 브릿지 코드 자동생성기)
TypeScript 인터페이스를 정의하면, 같은 시그니처를 가진 Swift/Kotlin/ObjC 코드를 자동으로 만들어준다. MarkEdit이 이걸로 해결하는 문제: "JS에서 호출하는 함수와 Swift에서 받는 함수가 타입까지 일치해야 한다." 손으로 일일이 양쪽 작성하면 깨지기 쉬운 부분을, 한쪽(TS)만 적으면 다른 쪽(Swift)이 자동 생성되게 묶어주는 역할. README도 "MarkEdit uses ts-gyb to generate lots of boilerplate code"라고 명시.

여기서 처음 보이는 큰 그림: 웹 기술(TS+CodeMirror)이 텍스트 편집을, 네이티브 기술(Swift+AppKit)이 윈도우·메뉴·파일 시스템을 맡고, 둘 사이는 ts-gyb로 자동 생성된 JSON 메시지로 통신한다. 다음 섹션에서 이 그림을 더 자세히 본다.

3아키텍처 심화 분석

WKWebView 브릿지 + 양방향 메시지 — Mac과 웹이 만나는 지점.

먼저 시스템 전체 구조를 한 장으로.

 ┌─────────────────────────────────────────────────────────────────┐
 │                       MarkEdit.app (macOS)                       │
 │                                                                  │
 │  ┌──────────── MarkEditMac ─────────────┐                       │
 │  │  EditorWindowController              │   AppKit              │
 │  │  ├─ NSToolbar  / NSMenuItem          │   (창, 메뉴,           │
 │  │  ├─ Settings  (SwiftUI)              │    툴바)              │
 │  │  └─ Shortcuts / AppleScript 핸들러    │                       │
 │  └──────────────┬────────────────────────┘                       │
 │                 │ 메서드 호출                                    │
 │  ┌──────────────▼─ MarkEditKit ─────────┐                       │
 │  │  EditorWebView : WKWebView           │   WebKit 브릿지       │
 │  │  ├─ ScriptMessageHandler             │   (Swift ↔ JS         │
 │  │  ├─ Bridge / Bridge+Generated.swift  │    메시지 통로)        │
 │  │  └─ MarkEditCore (모델)              │                       │
 │  └──────────────┬────────────────────────┘                       │
 │                 │ postMessage / evaluateJavaScript               │
 │  ┌──────────────▼─ WKWebView 내부 (CoreEditor) ──┐              │
 │  │   index.html  (Vite로 한 파일에 인라인됨)       │              │
 │  │   ┌──────────────────────────────────────┐    │              │
 │  │   │  CodeMirror 6  (EditorView, State)   │    │              │
 │  │   │   + @codemirror/lang-markdown        │    │              │
 │  │   │   + Lezer 파서 트리                  │    │              │
 │  │   │   + ~30개 자체 modules/ (commands,   │    │              │
 │  │   │     completion, frontMatter, ...)    │    │              │
 │  │   └──────────────────────────────────────┘    │              │
 │  │   window.nativeModules.core.notify…()         │              │
 │  │   ▲  ▲  ▲                                     │              │
 │  └──┼──┼──┼─────────────────────────────────────┘              │
 │     │  │  └─ notifyContentHeightDidChange                       │
 │     │  └──── notifyViewDidUpdate (선택·포커스 변경)              │
 │     └─────── notifyContentOffsetDidChange (스크롤)                │
 │                                                                  │
 │  ┌────────────────────────────────────────────────────────────┐ │
 │  │  FinderExtension  (NSFinderSync / FinderSyncExtension)       │ │
 │  │  PreviewExtension (QuickLook Preview / com.apple.quicklook.preview) │ │
 │  └────────────────────────────────────────────────────────────┘ │
 │                                                                  │
 │      ts-gyb 자동 생성 ──► Swift/TS 양쪽에 동일 인터페이스 보장   │
 └─────────────────────────────────────────────────────────────────┘

핵심 설계 패턴 ① — WKWebView 브릿지 (Native ↔ Web 통신)

이 앱의 가장 큰 디자인 결정은 "편집기를 웹뷰 안에 둔다"였다. 그러면 macOS 메뉴에서 'Bold' 버튼을 눌렀을 때 그 메시지가 어떻게 웹뷰 안의 CodeMirror까지 전달될까? 정답은 두 방향이다.

방향 1

Swift → JS (네이티브에서 웹뷰 호출)

Swift 측에서 WKWebView.evaluateJavaScript("MarkEdit.replaceText(...)")를 실행. WebKit이 그 JS를 웹뷰 안의 글로벌 컨텍스트에서 평가하고 결과를 콜백으로 돌려준다. 예: 사용자가 메뉴에서 "Bold" 클릭 → Swift가 insertText('**bold**', ...)을 JS로 평가 → CodeMirror가 텍스트 삽입.

방향 2

JS → Swift (웹뷰에서 네이티브 알림)

JS 측에서 window.webkit.messageHandlers.<name>.postMessage(payload)를 호출. Swift 측 WKScriptMessageHandler가 그 메시지를 받는다. core.ts를 보면 실제로 window.nativeModules.core.notifyContentHeightDidChange(...) 같은 호출이 곳곳에 있는데, 이게 전부 이 통로다.

// core.ts에서 발췌 — 스크롤 위치가 바뀌면 Swift에 알린다
scrollDOM.addEventListener('scrollend', () => {
  window.nativeModules.core.notifyContentOffsetDidChange();
});

이 두 방향을 손으로 짜면 양쪽 시그니처가 어긋나기 쉽다. 그래서 등장하는 게 앞서 본 ts-gyb. TypeScript에서 인터페이스 한 번 적으면 Swift 쪽 호출 코드를 자동 생성해준다.

비유

국제 회의에서 동시통역사가 양옆에 한 명씩 앉아 있는 그림. 한국 대표가 한국어로 발화하면 영어로 즉시 번역되고, 영어권 대표가 영어로 답하면 한국어로 즉시 돌아온다. WKWebView 브릿지가 통역사고, ts-gyb는 그 통역사들에게 "오늘 회의에서 쓸 단어집"을 사전에 양쪽 언어로 만들어주는 도구다. 그래서 통역 중 단어가 헷갈릴 일이 없다.

핵심 설계 패턴 ② — CodeMirror Compartment (런타임에 설정 갈아끼기)

사용자가 설정에서 '줄 번호 표시'를 켜고 끌 때, 일반적인 에디터라면 전체를 다시 생성해야 한다. CodeMirror 6는 Compartment라는 메커니즘으로 일부 확장만 런타임에 갈아끼울 수 있다. MarkEdit은 이걸 적극 활용한다.

// CoreEditor/src/extensions.ts에서 발췌
const theme       = new Compartment;
const readOnly    = new Compartment;
const gutters     = new Compartment;
const invisibles  = new Compartment;
const activeLine  = new Compartment;
const selectedLines = new Compartment;
const lineWrapping  = new Compartment;
// ... 12개 컴파트먼트

window.dynamics = {
  theme, readOnly, gutters, invisibles, activeLine, selectedLines,
  lineWrapping, lineEndings, indentUnit, selectionHighlight,
  extensionConfigurator, markdownConfigurator,
};

window.dynamics에 다 박아둔 게 인상적. Swift 쪽에서 "테마를 dark로 바꿔라"라는 메시지가 오면 window.dynamics.theme.reconfigure(newThemeExtension)만 호출하면 끝. 전체를 다시 만들 필요가 없다.

핵심 설계 패턴 ③ — Vite의 단일 파일 번들링

웹뷰 안에서 동작하는 모든 코드(JS + CSS + 모듈)는 빌드 후 HTML 파일 하나에 인라인으로 박힌다. 이걸 가능하게 하는 게 vite-plugin-singlefile. macOS 앱 번들에 .js 파일 수십 개를 넣고 경로 관리하는 일이 없어지고, WKWebView가 loadFileURL로 한 번에 띄울 수 있다.

설계 효과
왜 이 모든 게 4MB로 끝나는가

① CodeMirror는 트리 셰이킹이 잘 되도록 모듈 분리가 잘 되어 있어, 안 쓰는 기능은 번들에 안 들어감. ② Vite + esbuild로 압축된 단일 HTML. ③ Swift 코드도 사용 안 하는 거대 프레임워크(예: 자체 마크다운 파서)를 안 들고 있음. ④ 미리보기·동기화·플러그인 마켓 같은 무거운 기능을 의도적으로 빼서 4MB 안에 들어맞은 결과.

4디렉토리 구조 해부

레포 루트의 폴더가 무슨 역할을 하는지.

MarkEdit/
├── CoreEditor/                    ← 웹 에디터 (TypeScript + CodeMirror)
│   ├── package.json               ← Yarn 4, Vite 7, CodeMirror 6
│   └── src/
│       ├── core.ts                ← 에디터 진입점, resetEditor()
│       ├── extensions.ts          ← 12개 Compartment 정의
│       ├── config.ts              ← 사용자 설정 타입
│       ├── api/                   ← MarkEdit-api (외부 스크립트용)
│       ├── bridge/                ← Swift와 주고받는 메시지 인터페이스
│       ├── common/                ← 유틸·스토어
│       ├── modules/               ← selection, commands, completion,
│       │   │                        events, frontMatter, lezer,
│       │   │                        lineEndings, textChecker, toc, ...
│       │   └── ... (30+ 서브모듈)
│       ├── styling/               ← 테마·노드 스타일 (gutter, line, link)
│       ├── @codegen/              ← ts-gyb 설정
│       ├── @vendor/               ← 직접 패치한 외부 코드 (예: history)
│       ├── @res/                  ← 정적 리소스
│       └── @types/                ← 글로벌 타입 선언
│
├── MarkEditMac/                   ← macOS 앱 본체 (Swift + AppKit + SwiftUI)
│   ├── Info.plist                 ← 번들 메타데이터
│   ├── Info.entitlements          ← 샌드박스 권한
│   ├── AppShortcuts.xcstrings     ← Shortcuts 앱 통합
│   ├── Sources/
│   │   ├── Main/                  ← AppDelegate, 앱 라이프사이클
│   │   ├── Editor/                ← EditorWindow / WindowController /
│   │   │                            ChunkLoader / ImageLoader …
│   │   ├── Extensions/            ← Swift 표준 타입 확장
│   │   ├── Panels/                ← 다이얼로그·검색 패널
│   │   ├── Settings/              ← SwiftUI 설정 화면
│   │   ├── Shortcuts/             ← Shortcuts 인텐트
│   │   ├── Scripting/             ← AppleScript 핸들러
│   │   └── Updater/               ← 업데이트 체크
│   ├── Modules/                   ← Previewer, DiffKit 등 보조 모듈
│   ├── Base.lproj / mul.lproj     ← 다국어 리소스
│   └── Resources/                 ← 아이콘, 사운드 등
│
├── MarkEditCore/                  ← Foundation만 의존하는 모델·로직
├── MarkEditKit/                   ← WebView 호스팅 + 공유 UI 로직
├── MarkEditTools/                 ← SwiftLint 등 빌드 도구 플러그인
├── FinderExtension/               ← Finder 통합 (.md 미리보기 등)
├── PreviewExtension/              ← QuickLook 썸네일
├── MarkEdit.xcodeproj/            ← Xcode 프로젝트 정의
├── Build.xcconfig                 ← 버전 번호 등 빌드 설정
├── .swiftlint.yml                 ← Swift 코드 스타일 규칙
└── README.md / LICENSE (MIT)

핵심 포인트 몇 가지.

관전 포인트 1

CoreEditor와 MarkEditMac이 완전히 분리됨

웹 개발자가 cd CoreEditor && yarn dev로 에디터만 따로 띄워서 브라우저에서 개발할 수 있다. macOS 앱을 빌드하지 않고도 CodeMirror 부분만 만지는 게 가능. 개발 자전거(웹 dev server) + 배포 자동차(macOS .app) 두 가지 모드를 한 레포 안에 보유.

관전 포인트 2

FinderExtension / PreviewExtension은 별도 번들

macOS에서 Finder 안에서 .md 파일을 미리보거나 컨텍스트 메뉴를 띄우려면 "앱 본체와 분리된 작은 번들"을 따로 만들어야 한다(샌드박스 격리). 이 두 폴더가 각각 그 작은 번들이다. 작은 앱이지만 macOS 시스템 확장 점까지 챙긴 점이 인상적.

관전 포인트 3

@vendor 폴더의 의미

CoreEditor/src/@vendor/commands/history처럼 @vendor/ 안엔 npm에서 받아온 코드를 손으로 수정한 버전들이 들어 있다. 외부 라이브러리에 버그가 있거나 동작이 미세하게 다르면, fork 대신 vendor 디렉토리에 박제해서 직접 패치하는 패턴. "의존성을 fork하지 말고 vendor에 박아라"는 작은 팀에 잘 맞는 전략.

5학습 포인트 (기술별)

이 레포에서 무엇을 배울 수 있는가, 그리고 어디부터 손대볼 수 있는가.

① Swift + AppKit/SwiftUI 혼합 데스크탑 앱

대부분의 Swift 튜토리얼은 iOS+SwiftUI에 치우쳐 있는데, MarkEdit은 macOS + AppKit + SwiftUI 혼용을 실제로 어떻게 하는지 보여주는 살아 있는 표본이다. 메뉴·툴바·창은 AppKit, 설정 화면은 SwiftUI — "필요한 자리에 맞는 도구를 쓴다"의 본보기.

실습 아이디어

MarkEditMac/Sources/Settings/ 안 SwiftUI 코드를 보고, 거기에 새로운 토글 옵션 1개를 추가해보기. 예: "포커스 모드(현재 줄만 진하게)" 옵션을 SwiftUI에 추가하고, 그 값을 CoreEditor 쪽으로 메시지로 보내는 흐름을 따라가보기.

② CodeMirror 6 모듈식 구조

CodeMirror 6는 v5까지의 "꽉 차고 모놀리식"에서 벗어나 잘게 쪼개진 모듈식이 됐다. 이걸 실전에서 어떻게 조립하는지 알고 싶다면 CoreEditor/src/extensions.ts가 거의 교과서. 12개 Compartment 패턴, keymap.of([...])의 우선순위, EditorState.transactionFilter로 입력 가로채기 등이 한 파일에 다 있다.

실습 아이디어

CodeMirror 6 단독으로 npm create vite한 빈 페이지에 마크다운 에디터를 만들어보기. @codemirror/state + @codemirror/view + @codemirror/lang-markdown 3개만 install 하면 시작할 수 있다. 그러고 나서 MarkEdit이 어떤 옵션을 추가로 어떻게 켰는지 비교해보면 학습 효과가 배.

③ WKWebView 양방향 브릿지

Mac/iOS에서 웹뷰를 앱에 박고 native ↔ JS 통신을 만들어야 하는 경우는 의외로 많다. (예: 인앱 결제 페이지, 차트 라이브러리, 코드 에디터…) MarkEdit은 그 양방향 흐름을 깔끔히 정리한 예. MarkEditKitBridge+Generated.swiftCoreEditor/src/bridge/를 같이 펴놓고 보면 한 함수가 양쪽에 어떻게 대응되는지 한눈에 들어온다.

실습 아이디어

아주 작은 Mac 앱(SwiftUI WebView) 안에 HTML 파일을 띄우고, 버튼 클릭 → JS 함수 호출, JS의 fetch 결과 → Swift로 알림 받기를 손으로 한 번 구현해보기. ts-gyb 없이 손으로 짠 뒤, 이후 ts-gyb 도입을 시도하면 그 가치가 체감된다.

④ Vite로 "단일 HTML 번들" 만들기

요즘 웹앱은 분할 번들이 표준인데, 임베디드 환경(WKWebView, Electron, 시놀로지 NAS 앱 등)에서는 거꾸로 "단일 HTML 한 파일"이 더 깔끔할 때가 많다. vite-plugin-singlefile이 그 정확한 도구. MarkEdit의 vite.config.mts(여러 개로 분기되어 있음 — 다크/라이트 빌드)는 좋은 레퍼런스다.

⑤ macOS 시스템 통합 (Shortcuts·AppleScript·QuickLook·Finder)

요즘은 잘 다뤄지지 않는 영역이지만 Mac 네이티브 앱에서 강력한 차별화 포인트. MarkEdit은 이 네 가지를 모두 챙긴다. MarkEditMac/Sources/Scripting(AppleScript), Shortcuts(인텐트), FinderExtension(파일 매니저 통합), PreviewExtension(QuickLook 미리보기) 각각이 독립 학습 단원이 될 수 있다.

⑥ "기능 빼는 용기" — 제품 의사결정 모범

기술적인 학습 외에도 이 레포가 보여주는 큰 교훈이 하나 있다 — 제품을 단순하게 유지하기 위한 의사결정 그 자체. README와 Wiki의 "Philosophy", "Why MarkEdit" 페이지를 읽어보면 "왜 이 기능을 안 넣는지"를 거의 모든 항목마다 설명하고 있다. 신규 기능 PR은 거부하면서 버그 수정 PR은 환영하는 정책도 명문화. 작은 앱을 작게 유지하기 위해 필요한 사회·문서 측면의 장치까지 챙긴 점이 학습할 만하다.

6하드웨어/시스템 요구사항

Mac 사용자 한정, 그것도 최신 두 버전만.

MarkEdit의 시스템 요구사항은 단순하지만 매우 엄격하다.

필요 환경
개발 / 사용 환경
사용자(End User): macOS Sequoia(15) 또는 Tahoe(26) 중 하나. 그 이전 macOS는 별도 태그(macos-12, macos-13, macos-14)로 마지막 호환 버전을 받아 쓸 수 있지만 더 이상 업데이트되지 않음. Intel/Apple Silicon 둘 다 지원(Universal 바이너리).
개발자: 최신 안정 버전 Xcode + macOS, Node 18+, Yarn 4. CoreEditor만 만질 거면 Node + Yarn만 있어도 됨(yarn dev로 브라우저에서 작업 가능).

"최신 2개 메이저 macOS만 지원"은 큰 결정이다. 보통 OSS는 호환성 유지를 더 길게 가져가는데, MarkEdit은 일부러 잘라낸다. 이유는 Philosophy 페이지에 적혀 있다 — "To be focused, we only support the latest two major macOS releases." 핵심에 집중하기 위해 호환성 부담을 줄인 것.

주의
Mac App Store 없음

예전엔 App Store에 있었지만 2023년 10월 이후 GitHub Releases로만 배포(이슈 #368). 자동 업데이트는 앱 내장 기능이 처리하지만, App Store 대비 약간의 추가 신뢰 단계(개발자 인증 + 노터라이즈)를 거친다. 처음 실행 시 "확인되지 않은 개발자" 같은 경고가 안 뜨도록 notarytool로 사전 노터라이즈된 상태.

퍼포먼스 측면 — README 본인 주장으로 8년 된 MacBook Pro에서 10MB 마크다운 파일이 깜빡임 없이 열린다. 이건 CodeMirror 6의 가상화 렌더링(화면 밖 줄은 DOM에 안 그림) 덕분이고, 일반적인 마크다운 앱이 2MB 파일조차 못 여는 경우와 큰 격차.

7직접 해볼 수 있는 실습 과제

난이도별로 5단계.

난이도 ★ — 워밍업

CoreEditor만 브라우저에서 띄워보기

Mac 없어도 가능. 레포 클론 후 cd CoreEditor && yarn install && yarn dev. Vite dev server가 뜨고, 브라우저에서 CodeMirror 기반 마크다운 에디터가 그대로 동작한다. window.webModules로 노출된 내부 API를 콘솔에서 호출해보면 에디터 내부가 어떻게 짜였는지 빨리 감 잡을 수 있다.

난이도 ★★ — 가벼움

나만의 CodeMirror 6 마크다운 에디터 만들기

빈 Vite 프로젝트에 @codemirror/state, @codemirror/view, @codemirror/lang-markdown을 설치하고 30줄 정도로 동작하는 에디터를 만들기. 그다음 MarkEdit의 extensions.ts에서 마음에 드는 옵션 하나(예: highlightSelectionMatches)를 골라 내 에디터에도 적용해보기.

난이도 ★★★ — 본격

MarkEdit-api 확장 1개 만들기

v1.19.0부터 도입된 MarkEdit-api를 사용하면 macOS 앱 빌드 없이도 사용자 스크립트(JavaScript)로 기능을 추가할 수 있다. 예: "선택된 텍스트의 마크다운 표를 정렬해주는 명령"을 1~2시간 작업으로 만들어보기. 이미 있는 MarkEdit-previewMarkEdit-theming이 코드 참고용 좋은 본보기.

난이도 ★★★★ — Mac 개발

MarkEdit을 직접 빌드해서 작은 기능 1개 더하기

Xcode 열고 MarkEditMac 스킴 빌드 → 본인 머신에서 동작하는 .app 생성. 그다음 메뉴에 "Reverse text" 같은 작은 기능 1개를 추가. AppKit 메뉴 등록 → Swift 핸들러 → WKWebView로 메시지 → CodeMirror에서 텍스트 변형까지 한 번 손으로 끝까지 따라가는 게 핵심 학습. 새 기능 PR은 받지 않으니 본인 fork에만 적용.

난이도 ★★★★★ — 챌린지

MarkEdit-clone-on-Windows 시도

같은 발상(웹 에디터를 네이티브 셸 안에 박기)을 다른 OS에 가져가본다. Windows라면 WebView2(Edge의 임베디드 컴포넌트) + WinUI 3 또는 .NET MAUI. Linux라면 GTK + WebKitGTK. 핵심 학습: 네이티브 ↔ JS 메시지 브릿지가 플랫폼마다 어떻게 다른지, 그리고 단일 HTML 번들 전략이 어디서도 통하는지. 완성된 결과물보다 "왜 어렵나"를 체득하는 게 목적.

8관련 기술 심화 학습 로드맵

주차별 4주 코스 — 가볍게 한 주 한 분야씩.

1주차

CodeMirror 6 입문

공식 가이드 codemirror.net/docs의 "Examples"부터. 특히 State / View / Extensions의 분리 모델을 이해. 목표: 빈 페이지에 마크다운 신택스 하이라이트 + 기본 키 바인딩 + 검색 기능까지 동작하는 ~80줄 에디터. 옆에서 MarkEdit의 extensions.ts를 같이 펴두고 "내가 만든 것과 뭐가 다른가" 대조.

2주차

WKWebView 양방향 브릿지

Apple 공식 문서의 "Communicating with Your Webpage" 챕터. 목표: SwiftUI macOS 앱에 WKWebView를 박고, 버튼 한 번에 JS 함수를 호출(Swift→JS), JS의 fetch 결과를 Swift로 받기(JS→Swift). 처음엔 손으로 짜고, 두 번째 단계에서 ts-gyb 도입을 시도. ts-gyb 없는 버전 → 있는 버전으로 진화 과정에서 "왜 자동 생성이 필요한가"가 몸으로 체감됨.

3주차

Vite / esbuild로 단일 HTML 번들 만들기

Vite의 plugin-singlefile 또는 esbuild의 인라인 옵션. 목표: React 또는 Svelte 미니 앱을 한 .html 파일로 빌드해서 더블클릭으로 동작하게. 이 기술은 MarkEdit 외에도 임베디드 화면(전자종이, 키오스크), 이메일 첨부 가능한 작은 도구 등에 광범위하게 쓸 수 있다.

4주차

macOS 시스템 통합 — Shortcuts·AppleScript·QuickLook

Apple 공식 App Intents 문서 + AppleScript Language Guide. 목표: 본인이 자주 쓰는 작은 Mac 앱(또는 직접 만든 앱)에 Shortcuts 인텐트 1개 + QuickLook 미리보기를 붙여보기. 이건 학습 곡선이 가파른 분야지만 일단 익혀두면 Mac 앱의 "체감 완성도"가 단숨에 올라간다.

4주 후엔 "내가 만드는 Mac 앱"이 더 이상 SwiftUI 튜토리얼 수준에 머물지 않고, MarkEdit 정도의 통합 깊이를 흉내낼 수 있는 상태가 된다.

9핵심 키워드 사전

레포 곳곳에 등장하는 용어 한 번씩 정리.

키워드 1
CodeMirror 6 / Lezer
웹 코드 에디터 라이브러리(CodeMirror)와 그 안에서 마크다운·다른 언어를 파싱하는 LR 파서(Lezer). 같은 저자(Marijn Haverbeke). v6의 특징은 모듈식 — 필요한 패키지만 골라 조립.
키워드 2
WKWebView
macOS/iOS의 임베디드 웹 브라우저 컴포넌트. Safari와 같은 WebKit 엔진을 쓴다. 앱 안에 작은 웹 페이지를 띄울 때 사용. JS와 Swift는 messageHandlers / evaluateJavaScript로 양방향 통신.
키워드 3
AppKit vs SwiftUI
AppKit은 1989년부터 이어진 macOS의 전통 UI 프레임워크(NSWindow, NSMenu …). SwiftUI는 2019년 발표된 선언형 신세대 프레임워크. MarkEdit은 둘을 시나리오별로 섞어 쓴다(메뉴=AppKit, 설정=SwiftUI).
키워드 4
ts-gyb
TypeScript 인터페이스로부터 Swift/Kotlin/ObjC 코드를 자동 생성하는 도구(Microsoft 제작). 양 언어 시그니처를 일치시켜 손으로 짜다가 어긋날 버그를 원천 차단.
키워드 5
CodeMirror Compartment
CodeMirror 6에서 에디터 일부 설정을 런타임에 갈아끼우게 해주는 메커니즘. 전체 에디터를 다시 만들지 않고 "이 부분만 새 값으로"가 가능. MarkEdit은 12개 컴파트먼트로 거의 모든 동적 설정을 관리.
키워드 6
Notarization (노터라이즈)
Apple이 개발자가 배포하는 macOS 앱을 자동 검사해서 "악성 코드 없음" 도장을 찍어주는 절차. App Store 외부 배포 앱이 "확인되지 않은 개발자" 경고 없이 실행되려면 필수. MarkEdit은 xcrun notarytool로 사전 처리 후 .dmg 배포.
키워드 7
QuickLook / Finder Extension
QuickLook = Mac에서 파일 클릭 후 스페이스바 누르면 뜨는 빠른 미리보기. Finder Extension = Finder 안에서 컨텍스트 메뉴·뱃지·동기화 상태 등을 표시. 둘 다 앱 본체와 분리된 작은 번들로 만든다(샌드박스 격리).
키워드 8
GFM (GitHub Flavored Markdown)
GitHub이 표준 마크다운에 표·체크박스·자동 링크·코드펜스 등을 추가해 정리한 사양. MarkEdit은 "100% GFM, not 80% or 120%"를 표방하며 GFM 사양을 그대로 따른다(자체 확장 안 함).
키워드 9
YAML Front Matter
마크다운 파일 맨 위에 ---로 둘러싸인 YAML 메타데이터 영역. 정적 사이트 생성기(Jekyll, Hugo)와 Obsidian 같은 도구에서 흔히 사용. MarkEdit은 @codemirror/lang-yamlyamlFrontmatter 확장으로 이 부분만 YAML로 파싱.
키워드 10
Periphery / SwiftLint
Periphery는 호출되지 않는 Swift 코드를 찾아내는 도구(죽은 코드 사냥꾼). SwiftLint는 코드 스타일·관습 규칙을 강제하는 린터. MarkEdit은 둘 다 CI에 박아 코드 크기와 일관성을 자동 관리.

10참고 링크

더 깊이 파고들 때 갈 곳들.

레포 / 공식

· MarkEdit 본 레포 (MIT, ~3,800 stars)
· Wiki — Philosophy / Why MarkEdit / Development / Customization / Manual
· MarkEdit-api — 사용자 스크립트용 API
· MarkEdit-preview · MarkEdit-theming · MarkEdit-ai-writer
· MarkEdit-skill — AI 에이전트로 MarkEdit 관리

관련 기술 문서

· CodeMirror 6 공식
· Lezer 파서 가이드
· ts-gyb (Microsoft)
· Apple WKWebView 공식 문서
· GitHub Flavored Markdown 사양
· Vite 공식 · vite-plugin-singlefile
· libremac.github.io — 같은 팀의 다른 무료 Mac 앱들

커뮤니티 / 참고 글

· Daring Fireball — WYSIWYG가 아닌 마크다운 에디터 논의 (Philosophy 페이지에서 인용한 글)
· TrendShift의 MarkEdit 페이지
· @MarkEditApp on Mastodon — 공식 업데이트 채널

실용 가이드

지금 시도해볼 만한 것들

  1. 레포 클론하고 CoreEditor만 띄워보기. Mac 안 가져도 된다. cd CoreEditor && yarn install && yarn dev로 5분 안에 브라우저에서 CodeMirror 기반 마크다운 에디터가 동작하는 걸 확인. 콘솔에서 window.dynamicswindow.webModules를 찍어보며 내부 구조 탐색.
  2. Wiki의 Philosophy / Why MarkEdit 두 페이지를 정독. 코드 읽기 전에 "왜 이렇게 만들었나"를 먼저 흡수하면 디렉토리 구조가 훨씬 빠르게 이해된다. "기능을 일부러 안 넣는다"는 문서로 적은 OSS는 드물기 때문에 그 자체가 학습 자료.
  3. CodeMirror 6 빈 페이지부터 한 번 만들어보기. 빈 Vite 프로젝트에 @codemirror/state, @codemirror/view, @codemirror/lang-markdown만 설치해서 30줄짜리 에디터. 그다음 MarkEdit의 extensions.ts에서 마음에 드는 옵션을 하나씩 가져와 비교.
  4. WKWebView 브릿지를 미니 앱으로 손으로 짜보기. SwiftUI WebView에 HTML 1장을 띄우고, Swift→JS 호출과 JS→Swift 알림을 둘 다 동작시키기. 이 패턴은 MarkEdit 같은 풀앱뿐 아니라 인앱 결제·차트·코드 뷰어 등에도 쓰인다.
  5. "기능을 빼는 결정"을 자기 프로젝트에 한 번 적용. 지금 만들고 있거나 유지보수 중인 도구에서 "이 기능은 왜 있는가 / 빼면 누가 곤란한가" 한 번 점검하고, 빼도 무방한 것 1개를 제거. MarkEdit의 작은 앱 미학을 코드가 아니라 의사결정 차원에서 따라해보는 연습.
원문 · MarkEdit-app/MarkEdit (MIT, 2026.05.23 기준) · github.com/MarkEdit-app/MarkEdit · TrendShift #15
딥다이브 정리 · 2026.05.23