본문으로 건너뛰기

Enriched Markdown의 사용자 지정 스포일러 공개를 구현한 호그와트 주문서

I built a Hogwarts spellbook to show off custom spoiler reveals in Enriched Markdown, a native SwiftUI Markdown renderer

docs.swmansion.com

핵심 요약

  1. EnrichedMarkdownText는 md4c로 Markdown을 파싱하고 TextKit으로 그려 선택, VoiceOver, Dynamic Type을 네이티브 텍스트처럼 지원한다.
  2. 스포일러는 기본 오버레이를 선택하거나 SpoilerOverlayProvider와 SpoilerOverlayView를 구현해 공개 애니메이션과 표시 효과를 사용자 정의할 수 있다.
  3. 코드 블록, 링크, 작업 목록, 선택 메뉴, 접근성 레이블과 문단 방향 등은 SwiftUI modifier와 환경 값으로 설정한다.

도입

EnrichedMarkdownText는 Markdown을 네이티브 텍스트로 렌더링하는 SwiftUI 뷰다. md4c가 문법을 파싱하고 TextKit이 결과를 그리므로 텍스트 선택, VoiceOver, Dynamic Type을 지원한다. 문서와 MarkdownParsingOptions를 초기화 인자로 받고, 테마와 링크 처리 같은 설정은 환경과 뷰 modifier로 적용해 하위 렌더러에 전달한다.

상세 요약

렌더링과 파서 설정

EnrichedMarkdownText는 Markdown 문자열과 MarkdownParsingOptions를 받아 문서를 표시한다. 기본 옵션은 .commonMark이며, 밑줄·위첨자·아래첨자·강조 표시, 줄바꿈 처리, 빈 줄 보존, admonition 같은 확장 문법을 개별적으로 설정할 수 있다. permissiveAutolinks는 기본 활성화되어 맨 URL, www 호스트, 이메일 주소를 링크로 처리한다. 일반적인 [text](url) 링크는 이 옵션과 관계없이 동작한다.

테마와 URL 처리는 SwiftUI 환경을 사용한다. 컨테이너에 .markdownTheme 또는 openURL 액션을 지정하면 그 아래의 EnrichedMarkdownText에 적용된다. URL 탭을 직접 처리하거나 시스템 동작으로 넘길 수 있고, 링크 길게 누르기는 별도 핸들러로 가로채 공유 동작 등을 연결할 수 있다.

선택, 복사와 상호작용

텍스트 선택은 .markdownTextSelection으로 켜거나 끌 수 있으며, 선택 강조색과 선택 메뉴의 추가 항목도 설정할 수 있다. 추가 메뉴는 Markdown으로 복사와 이미지 URL 복사다. 기본 시스템 복사는 일반 텍스트와 HTML을 함께 기록한다. HTML을 사용하는 대상은 제목, 목록, 코드 블록, 링크, 이미지 등을 서식과 함께 받는다. 표는 일반 텍스트에서 탭으로 구분되고 HTML에서는 <table>로 복사된다.

작업 목록 체크박스 탭은 .onTaskListItemToggle로 전달된다. 뷰는 변경된 체크 상태를 내부에 보관하지만 입력한 Markdown 문자열 자체를 수정하지 않는다. 따라서 새 소스 문자열에 변경 사항을 유지하려면 핸들러에서 별도로 저장해야 한다. 체크박스 토글을 비활성화하면 시각적 상태 변경과 콜백이 모두 발생하지 않는다.

사용자 정의 스포일러

||spoiler|| 문법은 항상 파싱된다. .markdownSpoilerOverlay에서 점 입자 또는 단색 오버레이를 선택하고, SpoilerOverlayProvider를 구현해 자체 효과를 만들 수도 있다. 제공자는 텍스트 줄 단위로 SpoilerOverlayView를 생성한다. 줄에 걸쳐 표시되는 스포일러는 각 줄마다 별도 뷰를 받는다. SpoilerStyle을 이용하면 테마 색상을 적용할 수 있다.

오버레이 뷰는 불투명해야 한다. 가려진 텍스트는 투명하게 그려지지만 이모지와 인라인 이미지는 오버레이 뒤로 비칠 수 있기 때문이다. 공개 효과를 직접 구현한다면 animateReveal(completion:)에서 완료 콜백을 호출해야 뷰가 제거된다. segmentIndex와 segmentCount는 여러 줄에 걸친 공개 순서를 정할 때 사용할 수 있다. 제공자는 저장 속성을 기준으로 Equatable을 유지해야 하며, 값이나 테마 스타일이 바뀔 때 오버레이가 다시 생성된다.

탭하면 해당 스포일러만 공개되고, 공개 상태는 테마나 Dynamic Type 변경 뒤에도 유지된다. Markdown 문자열이 바뀌면 모두 다시 가려진다. 가려진 스포일러 안의 링크는 공개 전까지 탭이나 VoiceOver 링크 요소로 동작하지 않는다. 반면 시스템 복사는 가려진 텍스트도 일반 텍스트로 복사하므로 주의해야 한다.

코드 블록과 레이아웃

코드 블록은 기본적으로 줄을 감싸 표시하거나 .scrollable 패널로 표시할 수 있다. 패널은 언어 이름과 복사 버튼을 제공하고, 긴 코드는 가로로 스크롤한다. 코드 블록 복사 콜백은 버튼, 길게 누르기, VoiceOver 동작으로 복사할 때 호출되며 Markdown으로 복사할 때는 호출되지 않는다. 구문 강조는 별도의 EnrichedMarkdownSyntaxHighlighting 제품을 추가하고 modifier를 적용한다.

문단 방향은 첫 강한 문자, 자연 방향, 왼쪽에서 오른쪽, 오른쪽에서 왼쪽으로 설정할 수 있다. 목록 표시, 체크박스, 인용 표시줄도 방향 설정을 따른다. 테마는 하위 뷰에 계층적으로 적용되며, 접근성 레이블은 목록·인용·표·이미지·코드 블록·VoiceOver 로터 항목별로 바꿀 수 있다. TextKit 기반 Markdown 렌더링을 다루는 MarkdownEngine: TextKit 2 기반의 macOS용 네이티브 마크다운 에디터 엔진과 렌더링 계층을 비교해 볼 수 있다.

결론

사용자 정의 스포일러 효과는 오버레이 제공자와 뷰를 구현해 지정한다. 오버레이는 불투명해야 하며 공개 애니메이션 완료 시 completion을 호출해야 한다. 작업 목록의 체크 상태는 뷰 내부에서 유지되지만 원본 Markdown을 바꾸지는 않는다. 이와 같은 상태 유지 방식과 복사 동작을 고려해 외부 상태 저장과 사용자 인터랙션을 구성해야 한다.