문서·출판 시스템
e2605d3 기준입니다.
HowToLiveBetter는 건강·시간·돈·법적 위험 등 생활 의사결정에 관한 중국어 지식 모음입니다. 항목마다 비용, 쉬운 요약, 기대 이득, 증거 등급, 출처, 비고를 적습니다. 기술적으로는 Markdown 원고를 공통 데이터로 삼아 검색 화면과 여러 출판물을 만드는 정적 지식 시스템입니다.
원본은 book/의 Markdown 34개입니다.
index.html이 이 파일들을 읽고 JavaScript 객체로 바꾸어 검색·필터 화면을 만듭니다.
별도의 빌드 도구는 같은 원고를 EPUB, PDF, 오프라인 HTML, Anki 카드로 변환합니다.
따라서 이 저장소를 공부할 때는 원고 → 파싱 → 데이터 객체 → 화면 또는 파일의 흐름을 따라가면 됩니다.
편집자는 book/에서 원고를 고칩니다.
검색 웹페이지는 도서관의 검색대, EPUB은 휴대용 책, PDF는 인쇄본, Anki는 복습용 카드입니다.
매체마다 다시 집필하지 않고, 같은 원고를 각 매체에 맞게 포장합니다.
문제도 출판사와 닮았습니다. 원고 형식이 흐트러지면 변환기가 내용을 놓치고, 항목 번호가 바뀌면 다른 항목을 가리키던 링크나 복습 기록이 어긋납니다.
| 결과 | 담당 파일 | 주로 배우는 것 |
|---|---|---|
| 검색 웹앱 | index.html | 브라우저 파일 요청, 파서, 상태, DOM 갱신 |
| EPUB | tools/epub/build.mjs | Markdown → XHTML → 전자책 패키징 |
tools/pdf/build.mjs | Pandoc 변환과 Typst 조판의 연결 | |
| 오프라인 HTML | tools/offline/build.mjs | 데이터 내장과 분석 스크립트 제거 |
| Anki 카드 | tools/anki/build.mjs | SQLite 파일 생성, ID 설계, ZIP 포장 |
selfcheck.html에는 질문에 답하면 관련 항목을 모아 주는 자기 점검 화면도 있습니다.
이 화면은 원고 전체를 새로 쓰지 않고 기존 항목을 재사용합니다.
skills/life-decision-guide/SKILL.md는 AI 도구가 원고를 찾고 출처를 붙여 답하도록 안내하는 문서입니다.
저장소가 자체 LLM 서버나 추론 엔진을 제공하는 구조는 아닙니다.
검색 입력창 하나를 만드는 데서 끝내지 않습니다. 데이터가 어디서 오는지, 실패하면 어떻게 복구하는지, 다른 출력물과 내용이 같은지, 배포된 파일이 어느 커밋에서 만들어졌는지까지 연결해서 볼 수 있습니다.
2026-10-23 GitHub 기준 스타 68,151개, 포크 5,899개이며 TrendShift Monthly #8에 선정되었습니다. 2026-09-07 생성된 저장소가 10월 하순에 이 규모의 관심을 받았다는 사실은 확인할 수 있습니다. 다만 방문 경로나 소셜 확산 자료 없이 특정 사건이 유행을 만들었다고 단정할 수는 없습니다.
내용과 구현에서 읽히는 매력은 명확합니다. 폭넓은 생활 문제를 짧은 항목으로 정리하고, 비용·이득·출처를 같은 자리에 배치하며, 웹과 오프라인 파일 모두로 접근하게 합니다. 다음 비교는 이런 구성에서 추론한 강점입니다. 제품별 성능이나 실제 이용자 선호를 측정한 결과는 아닙니다.
| 비교 대상 | HowToLiveBetter의 선택 | 그 대신 생기는 제약 |
|---|---|---|
| 일반 블로그 | 항목마다 같은 필드와 등급을 유지해 여러 조건으로 필터링 | 집필자가 엄격한 원고 형식을 지켜야 함 |
| 개인 노트 도구 | 공개 원고·변경 이력·출처를 함께 배포 | 공동 편집은 Git·PR 흐름을 익혀야 함 |
| 일반 위키 | 비용과 기대 이득을 항목별로 비교하기 쉬움 | 정해 둔 분류가 복잡한 맥락을 단순화함 |
| 챗봇 답변 | 원고의 항목 번호와 출처로 결론을 추적 | 책에 없는 질문까지 자동으로 해결하지 않음 |
| PDF 단독 배포 | 검색·필터·전자책·복습 카드를 함께 제공 | 출력별 변환기와 일관성 검사가 필요함 |
주목할 기술은 새 프레임워크가 아니라, 같은 지식을 여러 사용 방식에 맞게 재사용하는 연결 구조입니다.
커밋 e2605d3의 본문 767개는 A등급 495개, B등급 220개, C등급 52개로 README의 표시와 같습니다.
tools/sync-stats.mjs는 이런 숫자를 계산하고 소개 문구와 배지에 반영하는 역할까지 맡습니다.
프로젝트 설명은 학술 논문과 공식 문서를 강조합니다. 그러나 README는 공식적으로 공개되지 않은 일부 사건에 기자 이름이 명시된 자체 취재 기사와 웹 아카이브를 예외로 사용한다고 밝힙니다. 따라서 모든 출처가 논문 또는 공식 문서라고 소개하면 실제 기준을 좁혀 전달하게 됩니다.
README의 읽기 안내는 A등급을 메타분석·대형 시험 중심으로 짧게 소개하지만,
자세한 등급 설명은 관찰 연구와 법령·공식 문서도 A에 포함될 수 있다고 설명합니다.
A라는 글자만 보고 인과관계가 입증되었다고 해석하면 안 됩니다.
개발자는 이 차이를 데이터 모델에서도 보존해야 합니다. grade는 연구 설계 전체를 대신하지 못합니다.
택배의 운송장 번호가 있으면 물건이 어디서 왔는지 확인할 수 있습니다. 그렇다고 내용물의 품질까지 자동으로 보장되지는 않습니다. 출처 추적, 증거 등급, 적용 조건은 서로 다른 확인 항목입니다.
가성비 분류는 COST_W와 ratioOf()로 읽을 수 있고,
문장을 나누는 기준은 splitGain()에 공개되어 있습니다.
결과가 이상하면 입력 원고와 규칙을 대조할 수 있습니다.
초보자에게는 “화면 뒤에 숨어 있던 결정”이 짧은 함수로 드러나는 저장소라는 점이 장점입니다.
원고를 저장하는 데이터베이스나 로그인 API는 없습니다.
정적 호스트가 index.html, README.md, book/*.md, updates.json을 전달합니다.
요청을 받은 뒤 검색하는 쪽은 브라우저입니다.
저장소에서 발견되는 SQLite는 Anki 배포 파일을 만드는 빌드 단계에 쓰이며, 서비스용 DB가 아닙니다.
| 역할 | 실제 기술 | 소스에서 확인할 곳 |
|---|---|---|
| 화면과 스타일 | 정적 HTML, 파일 내부 CSS | index.html, selfcheck.html |
| 원고 읽기 | fetch(), Promise.all(), Map 캐시 | readText(), init() |
| 검색과 필터 | 문자열 포함 검사, Set, DOM의 hidden | parseReadme(), apply() |
| 화면 밖 갱신 연기 | IntersectionObserver, WeakMap | CARD_IO, renderCard() |
| 상태 보존 | URLSearchParams, history, localStorage | readUrl(), writeUrl(), 자기 점검의 save() |
React·Vue·프론트엔드 번들러에 대한 의존성 선언은 없습니다.
EPUB에서 쓰는 marked를 웹 검색 파서에도 쓴다고 오해하기 쉽지만,
검색 화면은 parseReadme()와 자체 텍스트 렌더링 함수를 사용합니다.
| 도구 | 버전·설정값 | 근거 파일과 의미 |
|---|---|---|
| Node.js | CI는 22 | .github/workflows/book.yml의 주 버전 지정. 패치 버전 고정은 아님 |
| marked | 18.0.13 | tools/epub/package.json의 유일한 npm 의존성 |
| lockfile | lockfileVersion: 3 | tools/epub/package-lock.json도 같은 marked 버전 고정 |
| marked의 Node 조건 | >=20 | lockfile의 패키지 engines.node |
| Pandoc | 3.11 | CI의 PANDOC_VERSION. PDF 도구 주석은 최소 3.1 안내 |
| Typst | 0.15.1 | CI의 TYPST_VERSION. PDF 도구 주석은 최소 0.13 안내 |
| Java / EPUBCheck | Temurin 21 / 5.4.0 | 생성한 EPUB의 형식 검증 |
| Actions | checkout·setup-node·upload-artifact·setup-java 모두 @v4 | 전자책 워크플로에서 쓰는 액션 참조 |
| 운영체제·폰트 | ubuntu-latest, fonts-noto-cjk | PDF를 만드는 CI 환경. OS와 폰트 패키지는 완전 고정되지 않음 |
tools/epub/package.json에는 "type": "module"과
"build": "node build.mjs"가 있습니다.
루트 package.json, Cargo.toml, pyproject.toml, go.mod,
Dockerfile은 없습니다.
Python은 README의 간단한 정적 서버 예시에 등장하며 애플리케이션 백엔드 언어는 아닙니다.
“설치 없이 읽을 수 있다”는 말과 “모든 배포 파일을 도구 없이 만들 수 있다”는 말은 다릅니다.
특히 Anki 빌드는 node:sqlite의 DatabaseSync를 사용합니다.
README는 Node 22.13 이상을 안내하며, Node 22.13.0 공식 문서에서도 이 API를 확인할 수 있습니다.
lockfile의 marked 다운로드 주소는 registry.npmmirror.com입니다.
npm ci는 lockfile을 따라 설치하지만,
ubuntu-latest, Node의 주 버전, 액션의 버전 태그, 설치 시점의 시스템 폰트까지 모두 같은 바이트로 고정하지는 않습니다.
또 buildStamp()가 현재 시간을 넣으므로 같은 원고도 빌드 시각에 따라 결과 바이트가 달라질 수 있습니다.
index.html의 parseReadme()는
줄을 순서대로 읽는 파서입니다.
장 제목을 만나면 sec, 항목 제목을 만나면 entry를 바꾸고,
flush()로 직전 항목을 배열에 넣습니다.
함수 이름과 달리 실제 본문 입력은 여러 book/*.md를 합친 문자열입니다.
// parseReadme()에서 발췌: 제목의 모양이 데이터 경계를 결정한다.
const flush = () => {
if (entry && sec) { sec.entries.push(entry); }
entry = null;
};
// 장 제목: # 또는 ## 뒤에 번호와 마침표
/^#{1,2} (\d+)\. (.+)$/
// 항목 제목: ### 뒤에 번호와 마침표
/^### (\d+)\. (.+)$/
문서 속 비용 태그를 읽어 money, time, will, level,
lens, who도 채웁니다.
마지막에는 dispute, todo, 비용 합계 cs, 가성비 ratio,
검색용 문자열 hay를 계산합니다.
사람이 읽는 원고가 기계가 읽는 레코드가 되는 지점입니다.
| 원고의 역할 | 객체 필드 | 설계상 의미 |
|---|---|---|
| 식별과 제목 | sec, n, title | 장·항목 번호로 링크와 맵의 키를 구성 |
| 사람이 읽는 설명 | cost, human, gain | 비용·쉬운 요약·상세 이득을 분리 |
| 검토할 정보 | grade, src, note | 등급·출처·적용 조건을 유지 |
| 기계용 분류 | money, time, will, level, lens | 문장 해석 대신 명시된 태그로 필터링 |
필드는 정해진 접두사와 한 줄 형식을 기대합니다. 설명을 임의로 다음 줄에 이어 쓰면 같은 필드에 자동으로 합쳐 주지 않습니다. 제목 단계나 구두점을 바꾸는 편집도 데이터 추출을 바꿀 수 있습니다. 이 프로젝트에서는 원고 형식 자체가 API 계약처럼 작동합니다.
index.html의 init()은 window.__CORPUS__가 있으면 내장 원고를 쓰고,
없으면 readText()로 정적 파일을 요청합니다.
읽어 온 문자열 뒤의 파싱·화면 생성은 공통입니다.
입력 방식의 차이를 앞단에서 흡수하는 간단한 어댑터 구조입니다.
// init()의 실제 분기
const EMBED = window.__CORPUS__;
const md = EMBED ? EMBED.readme : await readText('README.md');
// 각 장의 내용도 EMBED.parts[f] 또는 readText(f)에서 읽는다.
const sections = parseReadme(parts.join('\n\n'));
build(sections);
tools/offline/build.mjs는 원고·장문·업데이트 정보를 하나의 객체로 묶어 페이지에 내장합니다.
광고 이미지와 후원 QR 이미지도 data URI로 바꾸며, 분석용 GA 구간은 제거합니다.
Google Fonts 링크는 남아 있어 폰트를 불러오려는 외부 요청이 발생할 수 있습니다.
알려진 삽입 지점을 찾는 must()가 실패하면 빌드를 중단합니다.
단순 문자열 치환이지만, 페이지 구조가 바뀌었음을 조용히 무시하지 않습니다.
기본 검색 페이지는 로컬 파일 열기를 감지하면 오류를 안내합니다. 반면 원고가 내장된 오프라인 HTML은 파일 요청 없이 본문을 처리합니다. 다만 자기 점검 화면 링크는 온라인 주소로 바뀌므로 모든 연결 화면까지 오프라인에 포함된 것은 아닙니다.
state는 검색어와 필터별 Set을 갖고,
apply()는 원본 항목을 바꾸지 않은 채 카드의 표시 여부를 계산합니다.
가성비·등급·비용 필터는 같은 축의 선택값 중 하나와 일치하면 통과하고, 서로 다른 축의 조건은 함께 만족해야 합니다.
검색어는 공백으로 나누며 terms.every()로 모든 단어의 포함 여부를 확인합니다.
// apply()에서 발췌: 단어가 모두 들어 있는지 검사
const terms = state.q.toLowerCase().split(/\s+/).filter(Boolean);
if (ok && terms.length && !terms.every(t => e.hay.includes(t))) ok = false;
if (el.hidden === ok) el.hidden = !ok;
검색용 hay는 미리 합쳐 두지만 역색인을 만들지는 않습니다.
필터를 적용할 때 카드들을 순회하는 선형 검색입니다.
입력은 120ms 지연해 처리하고, CARD_IO는 rootMargin: '600px 0px'로
화면 가까이 온 카드의 텍스트 갱신을 준비합니다.
카드의 DOM 뼈대 자체는 build()에서 전체 생성합니다.
따라서 이 방식은 화면에 필요한 요소만 남기는 완전한 가상 목록과 다릅니다.
card.key와 현재 검색어 키가 달라졌을 때만 텍스트를 다시 만들고,
화면 밖 카드는 스크롤로 가까워졌을 때 갱신하는 방식입니다.
도서관의 모든 책꽂이는 그대로 두고, 눈앞에 있는 책의 안내표만 새 검색어에 맞게 바꾸는 셈입니다. 책꽂이 수는 줄지 않지만 안내표를 전부 다시 쓰는 수고는 줄어듭니다. Intersection Observer 문서에서 화면 근처 진입을 관찰하는 원리를 이어 배울 수 있습니다.
tools/lib/split-items.mjs와 index.html에는 긴 문장·출처를 나누는 규칙이 각각 있습니다.
tools/check-split.mjs는 브라우저 쪽 함수 구간을 node:vm으로 읽어,
도구 쪽 splitGain()·splitSrc()와 결과를 비교하도록 작성되어 있습니다.
출력이 같은지만 보지 않습니다.
이득·비고를 나눈 뒤 다시 붙였을 때 원문과 같은지, 굵은 글씨 표시가 조각 사이에서 끊기지 않는지도 검사합니다.
tools/sync-stats.mjs는 비용 규칙의 특정 소스 줄이 유지되는지도 확인합니다.
문자열 모양에 의존하는 검사는 빠르게 만들 수 있지만, 의미가 같아도 형식을 바꾸면 깨질 수 있습니다.
여러 출력물을 유지할 때 중요한 것은 파일을 많이 만드는 능력보다, 출력 사이의 의미가 어긋나지 않게 확인하는 능력입니다. 이 저장소의 중복 규칙과 검사는 “공통 모듈로 합칠지, 복사본을 두고 동등성을 검사할지”를 고민하기 좋은 사례입니다.
docs/核实记录/는 완성된 설명만 보관하는 곳이 아닙니다.
자료를 어떻게 확인하고 수정했는지 남기는 기록입니다.
반면 tools/lib/book.mjs의 readBook()은 README에 연결된 본문·장문 파일을 골라 출력 목록을 만듭니다.
검증 기록 239개가 모두 전자책에 자동으로 들어가는 구조는 아닙니다.
| 구분 | 파일 수 | 줄 수 | 해석 |
|---|---|---|---|
| Markdown | 290 | 25,443 | 본문·장문·검증 기록·개발 지침 포함 |
| HTML | 5 | 2,310 | HTML 안의 CSS·JavaScript 포함 |
| JavaScript 모듈 | 14 | 1,846 | .mjs 도구. HTML 내부 JS와 별도 |
| YAML | 6 | 272 | 워크플로·이슈 양식·후원 설정 |
| JSON | 3 | 435 | 업데이트 데이터·의존성 선언·lockfile |
| Typst / CSS | 1 / 1 | 74 / 16 | PDF 조판 템플릿 / EPUB 스타일 |
위 수치는 커밋 e2605d3의 추적 파일 기준이며, 빈 줄·주석을 포함한 물리적 줄 수입니다.
이미지와 .git/은 줄 수에서 제외합니다.
본문만 보면 book/는 7,360줄, 검증 기록은 14,455줄입니다.
index.html은 1,672줄이며 그 안의 인라인 JavaScript는 약 1,133줄입니다.
HTML 줄 수와 내부 JS 줄 수를 더하면 중복 집계가 됩니다.
GitHub의 주 언어 HTML만 보고 이 저장소를 평가하면, 원고 데이터와 출판 자동화 도구라는 두 축을 놓칩니다. 전체 추적 파일은 331개, 이미지 등을 포함한 파일 내용은 약 6.06MiB입니다. 작은 코드로 비교적 큰 지식 자산을 다룬다는 점이 이 프로젝트의 규모를 읽는 기준입니다.
parseReadme()는 현재 어느 장과 항목을 읽는지 기억하는 작은 상태 기계입니다.
“새 제목이 나오면 이전 항목을 닫는다”는 규칙은 로그 분석, 설정 파일 읽기, CSV 후처리에도 응용할 수 있습니다.
반대로 들여쓰기·여러 줄·인용문까지 허용하려면 정규식 몇 개로 충분한지 먼저 검토해야 합니다.
학습용 변형으로는 원고를 직접 바꾸기 전에 유효성 검사기를 설계해 보세요.
필수 필드가 없을 때 빈 문자열로 둘지 오류로 처리할지, 모르는 비용 태그를 어떻게 표시할지 정합니다.
현재 비용 계산의 ?? 0은 모르는 값을 0점으로 처리하므로,
잘못된 입력이 가장 낮은 비용처럼 보일 수 있다는 점을 생각해 볼 수 있습니다.
tools/lib/book.mjs의 ratioOf(cost, level)는 입력 두 개로 분류를 돌려줍니다.
비용은 돈·시간·의지의 각 0~2점을 합한 0~6점입니다.
아래 표는 원문의 분류값을 한국어로 풀어 쓴 것입니다.
| 이득 규모 | 비용 합계 | 분류 결과 |
|---|---|---|
| 큼 | 0 | 매우 높음 |
| 큼 | 1~2 | 높음 |
| 큼 | 3~6 | 보통 |
| 중간 | 0 | 높음 |
| 중간 / 작음 | 그 밖의 조합 | 보통 |
증거 등급 A/B/C는 이 함수의 인자가 아닙니다. 근거의 신뢰도와 비용 대비 이득을 서로 다른 축으로 보관하기 때문입니다. 분류 기준은 저자의 판단을 코드화한 것이며, 사용자 개인에게 최적인 결정을 수학적으로 증명하는 공식은 아닙니다.
// ratioOf()를 이해하기 위한 학습용 표현: 출력값을 한국어로 바꿨다.
function classify(cost, level) {
if (level === '큼') {
return cost === 0 ? '매우 높음' : cost <= 2 ? '높음' : '보통';
}
return level === '중간' && cost === 0 ? '높음' : '보통';
}
// 실습: cost 0, 2, 3과 level 세 종류의 결과를 표와 대조한다.
index.html의 readText(path, tries = 3)는 성공한 텍스트를 메모리 Map에 저장합니다.
실패한 요청은 캐시에 남기지 않아 다시 읽을 수 있습니다.
파일이 정말 없는 404와 잠시 바쁜 서버의 5xx를 똑같이 재시도하지 않는 것도 배울 점입니다.
| 상황 | 현재 처리 | 학습 포인트 |
|---|---|---|
| 성공 | CORPUS_CACHE에 보관 | 재시도 화면에서 이미 받은 장은 다시 받지 않음 |
| 5xx / 429 | 재시도 대상 | 일시적 장애와 요청 제한을 고려 |
| 기타 HTTP 실패 | 즉시 오류 전달 | 재시도해도 고쳐지지 않을 입력을 구별 |
| 네트워크·시간 초과 | 최대 3회 시도, 대기 0.5초·2초 | 각 요청의 실패 범위와 사용자 복구 흐름 |
지원 브라우저에서는 AbortSignal.timeout(20000)을 연결합니다.
Promise.all()의 장 하나가 끝내 실패하면 전체 로딩도 실패하지만, 성공한 장은 캐시에 남습니다.
네트워크가 약한 상황에서 “전체를 처음부터 다시 받기”를 줄이는 구조입니다.
renderText()는 링크·굵은 글씨 같은 허용한 표현을 골라 DOM 노드로 만듭니다.
mkLink()는 a.textContent에 표시 문구를 넣고,
일반 텍스트는 텍스트 노드와 하이라이트 처리로 전달합니다.
자기 점검의 esc()도 특수문자를 이스케이프합니다.
// mkLink()의 핵심: 표시할 문구를 HTML로 해석하지 않는다.
const a = document.createElement('a');
a.href = href;
a.target = '_blank';
a.rel = 'noopener';
a.textContent = text;
innerHTML을 쓰는 곳도 있으므로 파일 전체가 텍스트 API만 쓴다고 설명하면 틀립니다.
중요한 것은 어떤 값이 외부 입력이며 어느 출력 경로에서 이스케이프되는지를 따라가는 습관입니다.
임의 사용자가 원고를 업로드하는 서비스로 확장한다면 URL과 HTML의 신뢰 경계도 다시 설계해야 합니다.
tools/epub/build.mjs는 new Marked({ gfm: true })를 만들고,
renderer.heading()으로 제목 ID와 목차 정보를 수집합니다.
rewriteHref()는 포함된 문서의 상대 링크를 책 내부 링크로 바꿉니다.
원시 HTML과 이미지는 기본 본문 변환에서 제거하고, 표지는 별도로 추가합니다.
Marked 확장 문서와 나란히 읽으면 렌더러를 바꾸는 지점을 찾기 쉽습니다.
tools/lib/zip.mjs의 zip()는 CRC32, 로컬 헤더, 중앙 디렉터리, 끝 레코드를 직접 만듭니다.
EPUB의 mimetype은 첫 파일이면서 압축하지 않도록 store: true로 넣습니다.
이는 EPUB 표준의 컨테이너 규칙과 연결되는 작은 구현입니다.
직접 ZIP을 만드는 것은 학습 가치가 크지만 범용 압축 도구의 모든 기능을 구현한 것은 아닙니다.
Anki 빌더의 parseSection()은 제목·요약·비용 등을 읽고
DatabaseSync로 collection.anki2를 만듭니다.
이를 빈 미디어 목록과 ZIP으로 묶으면 .apkg가 됩니다.
상세 이득과 모든 출처 원문을 카드에 복제하지 않고, 요약과 온라인 원문 링크를 제공합니다.
hltb-장번호-항목번호입니다.
같은 항목으로 인식되면 내용 갱신과 복습 기록 보존을 연결할 수 있습니다.
그러나 앞 항목을 삭제해 뒤 번호가 당겨지면 다른 내용이 같은 ID를 차지합니다.
순번을 식별자로 쓸 때 생기는 데이터 마이그레이션 문제입니다.tools/check-plain.mjs는 쉬운 요약의 길이를 공백 제외 120자로 제한하고,
통계 약어·연구 전문 표현·추상적인 상투어를 검사합니다.
새 숫자를 검사하는 --numbers는 오탐 때문에 기본 CI에서 제외됩니다.
문장 규칙 검사는 할 수 있지만 출처의 참됨을 증명하는 검사는 아닙니다.
.github/workflows/book.yml의 refs, plain, split,
stats, selfcheck는 build와 독립된 job입니다.
build에 이 검사들을 기다리는 needs가 없으므로,
일부 검사에 실패해 워크플로가 빨갛게 보여도 배포 단계는 별도로 진행될 수 있습니다.
반면 빌드 job 안의 EPUBCheck 실패는 그 뒤 업로드·릴리스 단계의 진행을 막습니다.
정기 링크 검사는 월요일 UTC 01:00, 한국 시간 10:00에 예약됩니다.
tools/check-links.mjs는 동시 작업 수 8, 논리적 대기 제한 20초를 쓰고,
확정된 죽은 링크·접속 실패·TLS 오류를 구분해 보고합니다.
Promise.race()로 대기 시간을 제한하므로 요청 자체를 취소하는 구현과는 구별해야 합니다.
링크 실패는 보고용이며 그 개수로 실패 종료하지 않습니다.
검사 통과 여부를 읽기 전에, 그 검사가 어떤 오류를 찾고 어떤 배포를 막는지부터 확인해야 합니다.
tools/updates.mjs는 Git 이력에서 항목을 추적하므로 완전한 이력을 요구합니다.
CI의 stats job에만 fetch-depth: 0이 있는 이유도 이 요구와 연결됩니다.
저장소는 CPU 코어 수·RAM·디스크의 공식 최소 사양이나 처리 속도 벤치마크를 제시하지 않습니다. 검색은 모델 추론이나 벡터 DB를 사용하지 않으므로 GPU를 요구하는 구성 요소도 없습니다. 아래에서 환경 요구는 소스·CI 설정 기준이고, 자원 여유에 관한 설명은 학습 환경을 위한 추정입니다.
| 하려는 일 | 필요한 환경 | 자원·주의점 |
|---|---|---|
| 검색 웹페이지 읽기 | JavaScript·fetch·IntersectionObserver를 지원하는 브라우저 | 데스크탑·모바일 브라우저에서 읽는 구조. 지원 버전별 실측 표는 없음 |
| 정적 웹페이지 제공 | 정적 HTTP 호스트 | README는 GitHub Pages·Nginx·객체 저장소 등을 안내 |
| 오프라인 파일 읽기 | 릴리스의 단일 HTML과 브라우저 | 기본 index.html을 더블클릭하는 경우와 다름 |
| EPUB 제작 | Node.js, 고정된 marked 의존성 | 읽기에는 Node 불필요. 제작자는 npm 의존성 필요 |
| PDF 제작 | Node.js + Pandoc + Typst + CJK 폰트 | 폰트가 달라지면 글줄·페이지 나눔이 달라질 수 있음 |
| Anki 제작 | README 기준 Node 22.13 이상 | node:sqlite 사용. 별도 SQLite 서버 불필요 |
| CI 수준 EPUB 검사 | Java 21 + EPUBCheck 5.4.0 | EPUB 제작과 형식 검증의 도구를 구별 |
본문 Markdown은 합계 2,385,074바이트, 약 2.27MiB입니다. README까지 합하면 약 2.33MiB이며 전송 압축 전 텍스트 크기입니다. 온라인 초기 로딩은 README에서 장 목록을 얻은 뒤 장 34개를 병렬로 읽고, 업데이트 JSON도 별도로 읽습니다. 이미지·폰트·전송 헤더까지 포함한 실제 다운로드 크기와는 다릅니다.
본문은 작은 편이지만 메모리 사용량이 원고 바이트와 같지는 않습니다. 원문 문자열, 항목 객체, 검색 문자열, 전체 카드 DOM이 함께 존재합니다. 저사양 기기에서는 최초 DOM 생성과 검색어에 따른 텍스트 갱신을 따로 관찰하는 것이 좋습니다. 실제 지연 시간은 측정 자료가 없으므로 숫자로 보장할 수 없습니다.
공식 최소 하드웨어 사양은 없으므로, 기본 웹 실습은 브라우저와 정적 서버의 실행 환경부터 확인하세요. 원본 파일 자체는 약 6.06MiB이며, 제작 도구·폰트·Java·생성물의 저장 공간은 별도로 확보해야 합니다. PDF 전체 제작을 위한 정확한 메모리·용량 수치는 별도 측정이 필요합니다.
아래는 학습자가 별도 실습용 사본이나 새 예제로 진행할 과제입니다. 새 생활 조언을 추가하는 대신 파서·상태·ID 같은 소프트웨어 동작을 다룹니다. 각 과제의 완료 기준은 “화면이 보인다”보다 구체적으로 잡았습니다.
읽을 곳: book/04-不要浪费时间.md와 index.html의 parseReadme().
할 일: 첫 항목 하나를 골라 각 줄이 title, human,
cost, gain, grade, src, note 중 어디에 들어가는지 표로 적습니다.
그 뒤 가상의 한국어 학습 항목 세 개로 같은 형태의 작은 파서를 설계합니다.
완료 기준: 파일 끝에서도 마지막 항목이 빠지지 않아야 합니다.
제목 두 개가 연속으로 나오는 경우, 설명이 다음 줄로 넘어간 경우에 현재 파서가 무엇을 하는지도 설명합니다.
특히 마지막 flush()가 필요한 이유를 말할 수 있으면 성공입니다.
읽을 곳: tools/lib/book.mjs의 COST_W, ratioOf().
할 일: 돈·시간·의지의 각 0~2점과 이득 세 등급을 조합합니다. 원시 조합은 3 × 3 × 3 × 3 = 81개입니다. 비용 합계 0·2·3에서 결과가 어떻게 달라지는지 찾고, 범위를 벗어난 값의 처리도 정합니다.
완료 기준: 정상 입력의 결과가 분류표와 같고, 누락 태그는 “무료”와 구별되어야 합니다. 증거 등급을 바꿔도 현재 함수의 결과가 바뀌지 않는 이유를 설명합니다. 함수의 정확성과 정책 자체의 타당성은 별개라는 점까지 적어 보세요.
읽을 곳: index.html의 state, readUrl(), writeUrl(), apply().
할 일: 가상 항목 열 개로 검색어·분류 두 축을 구현합니다. 같은 축의 여러 값은 OR, 다른 축끼리는 AND로 조합하고, 상태를 URL 쿼리로 저장합니다. 공백으로 나눈 검색어가 모두 포함된 항목만 남기도록 합니다.
완료 기준: 새로고침 후 같은 필터가 복원되어야 합니다.
결과 0개, 한글·공백이 든 검색어, 현재 필터에 가려진 항목으로의 직접 링크도 다룹니다.
소스의 gotoItem()이 필터를 풀고 이동하는 이유와 자신의 정책을 비교합니다.
읽을 곳: selfcheck.html의 qid(), profileDone(),
tools/check-selfcheck.mjs, Anki 빌더의 GUID 생성 부분.
할 일: 질문의 뜻이 바뀌었을 때 기존 답을 재사용할지 결정합니다.
자기 점검은 q.rev를 ID에 붙일 수 있고, 나이 구간 대신 실제 나이를 받도록
ageYears라는 새 필드를 사용합니다. 이를 참고해 새 데이터와 옛 데이터를 구별합니다.
완료 기준: 오래된 나이 구간을 실제 나이로 오인하지 않고, 뜻이 바뀌지 않은 다른 답안은 보존해야 합니다. 별도로 항목 삽입·삭제 시 Anki 순번 ID가 어떻게 어긋나는지 표를 만든 뒤, 순번과 독립된 영구 ID를 제안합니다.
읽을 곳: tools/lib/split-items.mjs, tools/check-split.mjs,
.github/workflows/book.yml.
할 일: 웹·자기 점검·Anki의 파서 차이를 목록으로 정리하고, 공통 중간 JSON을 만들 경우의 장단점을 설계합니다. 원문 보존·필수 필드·안정적인 ID를 검사하는 단계와, 실패 시 배포를 막을 검사도 선정합니다.
완료 기준: 이득·비고는 분리 후 이어 붙이면 원문과 같아야 합니다. 출처는 분리용 구분자를 제외한 내용이 보존되어야 합니다. 쉼표·괄호 속 구분자, 굵은 글씨 경계, 마지막 항목을 담은 예제로 설계의 허점을 찾습니다.
좋은 실습은 기능 하나를 더하는 것에서 끝나지 않고, 잘못된 입력과 기존 사용자의 데이터까지 설명할 수 있어야 합니다. 원본 규칙을 그대로 복사하는 과제보다, 그 규칙이 어떤 상황에서 깨지는지 찾는 과제가 더 오래 남습니다.
다음 일정은 공식 학습 기간이 아닌 제안입니다. JavaScript의 변수·함수·배열을 이미 접했다면 1주차부터 시작하고, 아직 낯설다면 문자열·객체·반복문을 먼저 익힌 뒤 진행하세요. 매주 하나의 작은 결과물을 남기면 서로 다른 도구가 어떻게 연결되는지 확인할 수 있습니다.
| 주차 | 학습 주제·읽을 소스 | 남길 결과물 |
|---|---|---|
| 1주차 | 문자열·정규식·상태 기계. parseReadme(), parseSection() | 항목 하나의 필드 표와 파서 입력 규칙 |
| 2주차 | 순수 함수·Set·검색 조건. ratioOf(), fitsMe(), apply() | 분류 결정표, 검색·필터 조합 표 |
| 3주차 | Promise·fetch·복구·URL 상태. readText(), readUrl(), writeUrl() | 성공·404·429·시간 초과의 처리 흐름도 |
| 4주차 | DOM·이스케이프·지연 갱신. renderText(), CARD_IO | 텍스트·링크·검색 강조의 출력 경로 지도 |
| 5주차 | 문서 변환·ZIP·SQLite. EPUB/PDF/Anki 빌더와 tools/lib/zip.mjs | 각 포맷의 내부 파일 목록과 ID 유지 정책 |
| 6주차 | CI·데이터 품질·변경 추적. .github/workflows/book.yml, tools/updates.mjs, 검사 도구 | 필수 검사와 보고용 검사를 구분한 배포 설계 |
검색 쪽이 재미있다면 단순 문자열 포함 검사와 역색인을 비교해 보세요. 현재 구조는 항목 수를 N, 검색어 수를 T, 검색 문자열 길이를 L로 보면 대략 N × T × L에 비례하는 탐색을 수행합니다. 먼저 데이터 규모별 지연과 메모리를 측정하고, 필요할 때 인덱스나 Web Worker 도입을 판단합니다.
콘텐츠 도구 쪽이 재미있다면 Markdown AST와 스키마 검증으로 이어 가세요. 원고의 자유도를 늘릴수록 줄 단위 규칙은 복잡해집니다. 공통 AST나 중간 JSON을 두면 출력별 파서 중복을 줄일 수 있지만, 생성 단계와 데이터 파일을 관리하는 비용이 추가됩니다.
변경 추적 쪽에서는 tools/updates.mjs의 grams(), dice(), step()이 흥미롭습니다.
제목이 같으면 먼저 연결하고, 남은 항목은 인접 두 글자의 집합 유사도로 대응시킵니다.
후보 기준은 0.5 이상이며 본문 유사도에는 0.9 가중치를 줍니다.
영구 ID가 없는 텍스트에서 변경을 추정하는 휴리스틱의 장단점을 배울 수 있습니다.
프레임워크를 추가하기 전에 현재 데이터 흐름을 설명하고 측정할 수 있으면, 다음 기술을 고를 이유도 분명해집니다. 이 저장소는 브라우저 기본 API와 작은 제작 도구가 어디까지 해낼 수 있는지 확인하는 출발점입니다.
| 용어 | 쉬운 설명 |
|---|---|
| Markdown | 제목·목록 등을 문자 기호로 적는 문서 형식. 여기서는 사람이 읽는 원고와 데이터 입력을 겸합니다. |
| 스키마 | 어떤 필드와 값이 있어야 하는지 정한 약속. 이 저장소에서는 제목 형식·필드 접두사·태그 규칙에 드러납니다. |
| 파서 | 문자열의 구조를 읽어 객체로 바꾸는 프로그램. parseReadme()가 항목 배열을 만듭니다. |
| 상태 기계 | 현재 단계에 따라 같은 입력도 다르게 처리하는 구조. 어느 장·항목 안인지 기억하며 원고를 읽습니다. |
| 파생 데이터 | 원본에서 계산할 수 있는 값. cs, ratio, hay가 해당합니다. |
| 정적 호스팅 | 미리 존재하는 파일을 전달하는 서비스. 별도 검색 API 없이도 브라우저에서 검색할 수 있습니다. |
| DOM | 브라우저가 문서를 요소와 텍스트 노드의 나무로 표현한 것. 카드 생성과 표시 변경의 대상입니다. |
| 디바운스 | 입력이 이어지는 동안 기다렸다가 잠시 멈추면 처리하는 방식. 검색 적용은 120ms 뒤로 미룹니다. |
| 지연 갱신 | 지금 보이지 않는 내용의 재작성을 나중으로 미루는 것. CARD_IO가 가까워진 카드부터 갱신합니다. |
| 캐시 | 한 번 얻은 결과를 다시 쓰려고 보관하는 곳. CORPUS_CACHE는 현재 페이지 메모리에 성공한 원고를 저장합니다. |
| 이스케이프 | 문법으로 오해할 문자를 안전한 표현으로 바꾸는 것. HTML에서 <는 문자 <를 나타냅니다. |
| 중간 표현 / AST | 입력을 바로 화면으로 만들지 않고 구조화해 둔 형태 / 문법 구조를 트리로 표현한 것. 여러 출력물의 공통 기반이 될 수 있습니다. |
| EPUB / XHTML | 전자책 패키지 형식 / XML 규칙을 지키는 HTML. 전자책 빌더가 XHTML 페이지와 목차 등을 묶습니다. |
| ZIP / CRC32 | 여러 파일을 담는 컨테이너 / 데이터 손상을 검사하는 체크섬. CRC32는 보안용 해시가 아닙니다. |
| SQLite / .apkg | 파일 기반 관계형 데이터베이스 / Anki 카드 배포 패키지. 빌드에서 생성하므로 서비스 DB 운영은 필요하지 않습니다. |
| GUID | 대상을 계속 같은 것으로 알아보는 식별자. 이 저장소의 Anki GUID는 장·항목 순번에 의존합니다. |
| 마이그레이션 | 데이터 형식이나 의미가 바뀔 때 기존 데이터를 옮기는 절차. 자기 점검의 새 나이 필드가 사례입니다. |
| CI / job | 변경 때 자동 검사를 수행하는 흐름 / 그 안에서 실행되는 작업 단위. job 간 의존 관계가 배포 순서를 결정합니다. |
| artifact / release | 워크플로가 보관한 산출물 / 사용자에게 배포하는 릴리스. PR에서는 전자를 만들고 릴리스 게시 단계는 건너뜁니다. |
| shallow clone | 일부 최신 이력만 내려받은 저장소. 현재 코드는 읽을 수 있지만 과거 전체를 비교하는 도구에는 부족합니다. |
| 휴리스틱 | 정답을 보장하지는 않지만 실용적인 단서로 판단하는 방법. 제목·본문 유사도로 항목의 변경을 대응시킵니다. |
| 항목 | 확인 내용 |
|---|---|
| 선정 | TrendShift Monthly #8 · 분석일 2026-10-23 |
| 스타 / 포크 | 68,151 / 5,899 · 2026-10-23 GitHub 기준 |
| 생성일 | 2026-09-07 |
| 최근 push | 2026-10-21 · GitHub 기준 |
| 최근 커밋 | e2605d3 · 2026-10-21. PR #149 본문 교정: 표현을 쉽게 고치고 누락된 장소명과 연금 절세 수치를 보완 |
| 최신 릴리스 | epub-latest · GitHub 표시일 2026-09-18. 고정 태그와 파일을 반복 갱신하는 배포 방식 |
| 주 언어 | GitHub 기준 HTML. 실제 자산은 Markdown 원고와 JavaScript 제작 도구를 함께 포함 |
| 본문 규모 | 34개 장 · 767개 항목 · 7,360줄 · 약 2.27MiB |
| 근거·분류 집계 | A 495 / B 220 / C 52. 가성비 매우 높음 123 / 높음 344 / 보통 300 |
| 출처 URL | 본문의 출처·비고 필드에서 2,531회 출현. 중복을 제거한 문헌 수와 다름 |
| 전체 코드·문서 | 추적 파일 331개. Markdown 25,443줄, HTML 2,310줄, 별도 JS 모듈 1,846줄 |
| 라이선스 | LICENSE: CC BY 4.0는 README의 설명상 본문·문서에 적용. LICENSE-CODE: MIT는 명시된 코드 범위에 적용 |
| 수치의 범위 | 파일·설정·본문 수치는 커밋 e2605d3 기준. 실행·빌드 성공 여부나 성능 측정값을 나타내지는 않음 |
생활 조언의 범위: 제도·법률·보험 내용은 README 기준 중국 본토를 대상으로 합니다. 한국에 그대로 적용할 수 없으며, A등급만으로 인과관계나 개인에게 맞는 결론이 보장되지 않습니다. 이 글은 그 조언을 새로 검증한 자료가 아닌 소프트웨어 구조 학습 자료입니다.
버전의 범위: 릴리스 표시일 2026-09-18이 파일 내용의 고정 날짜는 아닙니다. CI는 태그를 갱신하고 같은 이름의 파일을 덮어씁니다. 특정 결과를 재현하려면 산출물의 커밋·생성 시각을 확인해야 합니다.
운영과 데이터: 온라인 검색 화면에는 검색어를 GA 이벤트로 보내는 코드가 있습니다. 오프라인 빌더는 분석 구간을 제거하지만 외부 링크를 누르는 동작까지 없애지는 않습니다. 자기 점검 답안은 브라우저 저장소에 남으며, Anki의 순번 기반 ID는 항목 삭제·재번호 부여에 취약합니다.
검사와 권리: 독립 검사 실패가 배포를 모두 막지는 않으며, 문장 검사로 출처의 참됨을 증명할 수도 없습니다. shallow clone만으로 전체 변경 이력을 재구성할 수는 없습니다. 재사용 시에는 본문과 코드의 라이선스 범위가 다르다는 점도 함께 확인하세요.