Wails 실전 강좌 #7 노트 에디터 실전 — 마크다운 미리보기와 목록 UX
여기부터 2부입니다. 1부(#1〜#6)에서 노트 앱을 배포 가능한 상태까지 만들었지만, 정작 사용자가 매일 만지는 앞단은 “가벼운 컴포넌트 프레임워크를 전제한다"며 넘겨 왔습니다. 2부는 이 앱을 다시 열고 싶게 만드는 완성도를 다루고, 그 첫 글이 에디터입니다. 마크다운 노트 앱의 핵심 경험, 곧 편집과 미리보기를 실제로 구현합니다.
이 시리즈는 총 10편, 2부 구성입니다.
- 1부 — 앱을 만들고 배포하기: #1 · #2 · #3 · #4 · #5 · #6
- 2부 — 완성도와 리피터
- #7 노트 에디터 실전 — 마크다운 미리보기와 목록 UX ← 이번 글
- #8 테스트 전략 — 서비스와 저장소를 검증하기
- #9 자동 업데이트 구현 — 새 버전을 안전하게 전달
- #10 마감과 완성도 — 설정·다크모드·데이터 백업
편집을 위한 백엔드: Update 메서드 #
#2에서 만든 저장소에는 생성·조회·검색만 있었습니다. 편집을 하려면 갱신이 필요하므로, 저장소와 서비스에 Update를 더합니다. 서비스가 UpdatedAt을 현재 시각으로 갱신하는 것이 핵심입니다. 이 값이 #3에서 목록을 최신순으로 정렬하는 기준이기 때문입니다.
func (s *NoteService) Update(id int64, title, body string) (Note, error) {
title = strings.TrimSpace(title)
if title == "" {
return Note{}, errors.New("제목을 입력하세요")
}
return s.repo.Update(Note{
ID: id, Title: title, Body: body, UpdatedAt: time.Now(),
})
}바인딩 층에서는 #3과 같은 패턴으로 갱신 후 notes:changed 이벤트를 쏘아, 목록이 자동으로 다시 그려지게 합니다.
미리보기: 마크다운을 안전하게 렌더링 #
마크다운 노트 앱은 편집한 텍스트를 서식 있는 화면으로 보여줘야 합니다. 프론트엔드에서 마크다운을 HTML로 바꾸는 라이브러리(예: marked)를 쓰는데, 여기서 실전의 함정이 하나 있습니다. 노트 본문은 사용자가 쓴 내용이고, 그것을 HTML로 바꿔 그대로 삽입하면 XSS 위험이 생깁니다. 본문에 <script>나 onerror 속성이 들어오면 그대로 실행될 수 있습니다.
혼자 쓰는 로컬 앱이라도 습관을 지킵니다. 마크다운을 HTML로 바꾼 뒤 반드시 정화(sanitize) 라이브러리(예: DOMPurify)를 통과시켜 위험한 태그·속성을 제거하고 나서 삽입합니다.
import { marked } from "marked";
import DOMPurify from "dompurify";
export function renderMarkdown(source) {
const rawHtml = marked.parse(source);
return DOMPurify.sanitize(rawHtml); // 위험한 태그·속성 제거 후 반환
}미리보기는 편집기 옆에 나란히 두는 분할 화면이 흔합니다. 편집 중인 본문을 위 함수로 렌더링해 오른쪽에 그리면, 타이핑하는 대로 서식이 보입니다.
자동저장: 타이핑이 멈추면 저장 #
노트 앱에서 “저장” 버튼을 누르게 하는 것은 낡은 경험입니다. 실전에서는 타이핑이 잠깐 멈추면 자동으로 저장합니다. 다만 키를 칠 때마다 저장을 부르면 DB에 과도한 쓰기가 쏟아지므로, 디바운스로 마지막 입력 후 일정 시간이 지나야 한 번 저장하게 합니다.
import { UpdateNote } from "../wailsjs/go/main/App";
let saveTimer;
function scheduleSave(id, title, body) {
clearTimeout(saveTimer);
saveTimer = setTimeout(async () => {
try {
await UpdateNote(id, title, body);
setStatus("저장됨");
} catch (err) {
setStatus("저장 실패: " + err); // #5의 거부된 Promise가 여기로
}
}, 600); // 마지막 입력 후 600ms
}setStatus로 “저장 중… / 저장됨 / 저장 실패"를 보여주면, 사용자는 버튼 없이도 자기 글이 안전하다는 것을 압니다. 저장 실패는 #5에서 다룬 대로 Go가 돌려준 거부된 Promise로 잡아 화면에 알립니다.
정렬 흔들림 다루기 #
자동저장에는 미묘한 UX 문제가 하나 숨어 있습니다. 저장할 때마다 UpdatedAt이 갱신되고 목록이 최신순이므로, 편집 중인 노트가 자동저장될 때마다 목록 맨 위로 튀어 오릅니다. 화면이 계속 흔들려 거슬립니다.
해법은 두 가지입니다. 하나는 편집 중에는 목록 갱신 이벤트를 무시하고, 편집을 끝내거나 다른 노트로 넘어갈 때만 목록을 다시 그리는 것입니다. 다른 하나는 목록 정렬 기준을 UpdatedAt이 아니라 CreatedAt(고정)으로 두고, 최신 편집순이 꼭 필요하면 별도 정렬 토글을 주는 것입니다. 실전에서는 앞쪽, 곧 편집 중 목록을 얼리는 방식이 자연스럽습니다.
let editing = false;
EventsOn("notes:changed", async () => {
if (editing) return; // 편집 중이면 목록을 흔들지 않는다
notes = await Search(currentQuery);
});목록 UX: 키보드로 넘나들기 #
메모 도구는 마우스 없이도 빠르게 넘나들 수 있어야 합니다. 목록에 위·아래 화살표로 선택을 옮기고, 엔터로 편집기에 포커스를 주는 키보드 내비게이션을 붙입니다. 여기에 빈 상태(노트가 하나도 없을 때, 검색 결과가 없을 때)를 위한 안내 화면까지 갖추면 앱이 비어 있을 때도 길을 잃지 않습니다.
이 앞단 작업은 프레임워크에 크게 기대지 않습니다. 화살표 키 핸들러로 선택 인덱스를 옮기고, 선택된 노트를 편집기에 싣고, 저장은 위의 디바운스로 넘기는 흐름이면 바닐라로도, Svelte·Vue로도 같은 구조입니다. #1에서 정한 원칙(데이터의 진실은 Go에 둔다)을 지키므로, 앞단은 화면과 입력만 책임지면 됩니다.
정리 #
- 편집을 위해 저장소·서비스에
Update를 더하고, 서비스가UpdatedAt을 갱신합니다. 갱신 후notes:changed로 목록을 다시 그립니다. - 마크다운 미리보기는 렌더링한 HTML을 반드시 정화(DOMPurify 등)한 뒤 삽입합니다. 로컬 앱이라도 본문은 사용자 입력이므로 XSS를 막습니다.
- 자동저장은 디바운스로 마지막 입력 후 한 번만 저장하고, “저장됨/실패” 상태를 보여줘 저장 버튼을 없앱니다.
- 자동저장이 목록을 맨 위로 튀게 하는 흔들림은, 편집 중 목록 갱신을 얼리는 방식으로 다룹니다.
- 키보드 내비게이션과 빈 상태 안내로 목록 UX를 마감합니다. 앞단은 화면과 입력만, 데이터의 진실은 Go에 둡니다.
- 다음 글에서 지금까지 쌓은 서비스와 저장소를 실제 테스트로 검증합니다.