![]() |
![]() |
![]() |
![]() |
|---|---|---|---|
| 1. 온보딩 화면 | 2. 메인 화면 | 3. 상세 화면 | 4. 검색 기능 |
- 마크다운 기반 Q&A 뷰어 📘
- Block·Inline 문법을 직접 파싱해 렌더링 🧩
- 질문/카테고리별 필터링 🔎
| library | description |
|---|---|
| SwiftUI | 전체 UI 렌더링 및 레이아웃 |
| Combine | 문서 업데이트·데이터 흐름 관리 |
| Fastlane | 배포 자동화 |
| Firebase | 문서 관리·실시간 데이터 |
| Custom Markdown Parser | Block/Inline 문법 직접 파싱 |
문제
SwiftUI 기본 Markdown은 Bold/Underline 커스텀 문법 적용, Inline 중첩 구조 표현이 어려웠음.해결
Markdown 문법을 EBNF로 선언형으로 정의했고,
이를 기반으로 Top-Down + 재귀 하강 파서를 직접 구현.Block → Inline 2단계 파싱 구조
- Block: Heading, ListItem, Paragraph
- Inline: Bold, Underline, PlainText
Document ::= Block { Block }
Block ::= Heading | ListItem | Paragraph
Heading ::= '#' TextLine
ListItem ::= '- ' TextLine
Paragraph ::= TextLine
Inline ::= Bold | Underline | Text
Bold ::= '**' Inline '**'
Underline ::= '##' Inline '##'// 전체 파싱 흐름
Markdown 원본
↓
[Block Parser] — 줄 단위로 Heading, ListItem, Paragraph 분류
↓
[Inline Parser] — **굵게**, ##밑줄##, 텍스트 등 문장 구조 분석
↓
[Node Tree] — BlockNode / InlineNode 트리 구조 생성
↓
[Renderer] — SwiftUI View로 변환
↓
UI 출력
성과
🔸 Bold / Underline / 중첩 Inline 지원
🔸 문서가 트리 형태로 유지되어 ForEach 순회로 SwiftUI 렌더링 성능 최적화
🔸 문법 추가 시 확장성 높음
문제
Heading, List, Paragraph, CodeBlock 같은 Block 문법과
Bold/Underline이 섞인 Inline 문법이 뒤엉켜
파싱 구조가 복잡해질 위험이 있었음.해결
- Block 단계는 Top‑Down
- Inline 단계는 **재귀 하강(Recursive‑Descent)**로 분리해 파싱 구조를 안정적으로 설계.
핵심 아이디어:
➡️ “한 줄(Block)을 먼저 결정 → 내부에서 Inline을 재귀로 해석”
func parseBlock() -> MarkdownNode? {
if let heading = parseHeading() { return heading }
else if let code = parseCodeBlock() { return code }
else if let list = parseListItem() { return list }
else if let paragraph = parseParagraph() { return paragraph }
return nil
}private func parseUntil(_ delimiter: String?) -> [InlineNode] {
var result: [InlineNode] = []
var buffer = ""
while !isAtEnd() {
if let d = delimiter, peek(d) {
advance(d.count)
if !buffer.isEmpty { result.append(.text(buffer)); buffer = "" }
return result
}
if match("**") {
if !buffer.isEmpty { result.append(.text(buffer)); buffer = "" }
let inner = parseUntil("**")
result.append(.bold(inner))
} else if match("##") {
if !buffer.isEmpty { result.append(.text(buffer)); buffer = "" }
let inner = parseUntil("##")
result.append(.underline(inner))
} else {
buffer.append(current())
advance(1)
}
}
if !buffer.isEmpty { result.append(.text(buffer)) }
return result
}성과
- 🔸 Markdown의 큰 구조 → 작은 구조 흐름을 코드로 완전히 반영
- 🔸 중첩 Bold/Underline도 정확하게 파싱
- 🔸 Block·Inline이 분리되어 확장성 뛰어남



