← 모든 프로젝트

namumark

나무위키 마크업과 렌더링된 html을 하나의 타입 있는 트리로 읽어 들이는 파서, 그리고 그 위에 만든 위키

기간
2026년 3월
대상
개인 프로젝트
역할
1인 개발: 파서, 렌더러, 위키 앱, 배포
기술
typescript, bun, parse5, unist, react, next.js, prisma, postgresql
규모
11일 동안 커밋 71개, 타입스크립트 약 2만 5천 줄 (그중 6,400줄이 파서 라이브러리)
개인 프로젝트

다른 언어: english

어떤 프로젝트인가요

namumark는 한국에서 가장 큰 위키인 나무위키의 마크업 언어예요. 정식 스펙은 없어요. 도움말 페이지가 하나 있고, 나머지는 사이트가 실제로 렌더링하는 결과가 전부예요.

저는 namumark를 제대로 된 구문 트리로 바꾸고, 그 트리를 다시 html로 렌더링하고, namumark로도 다시 써낼 수 있는 라이브러리를 만들고 싶었어요. 그리고 그 위에 작은 위키를 만들었어요. 문서, diff가 보이는 리비전, 문서별 접근 규칙, 토론 스레드, 실시간 미리보기가 되는 에디터까지 있어요.

입구는 두 개, 결과는 트리 하나

까다로운 건 라이브러리 입력이 두 가지라는 점이었어요. 하나는 에디터에서 사용자가 직접 치는 namumark 텍스트예요. 다른 하나는 나무위키가 내려주는 html이에요. 실제 문서를 열어 보면 손에 들어오는 게 이거니까요. 두 입력 모두 결국 같은 트리가 돼야 해요.

'''굵게''' [[나무위키|링크]][* 각주]

이 줄은 어느 쪽에서 들어오든 style 노드(굵게), link 노드(내부 링크, 대상 나무위키, 라벨 링크), footnote 노드를 가진 문단 하나가 돼요.

작업 대부분은 html 쪽에 들어갔어요. 이 마크업은 브라우저를 위해 만들어진 거라 거꾸로 읽어 내기에는 맞지 않았거든요.

  • 이미지가 lazy-load돼요. src는 svg 플레이스홀더이고 실제 url은 data-src에 들어 있어요. 플레이스홀더에는 자기 width와 height가 붙어 있는데, 이건 이미지가 아니라 svg의 크기라서 초반에는 이미지가 엉뚱한 크기로 나왔어요.
  • 각주는 본문 쪽에 링크 하나, 페이지 맨 아래에 목록 항목 하나로 나뉘어 있어요. 임포터는 먼저 한 번 돌면서 각주 본문을 전부 모으고, 두 번째로 돌면서 각 참조를 진짜 각주 노드로 바꿔요. 렌더링된 각주 목록은 버려요. 렌더러가 다시 만들어 주니까요.
  • 목차가 다른 래퍼 안에 중첩돼 있을 수 있어요. 얕게만 확인했더니 놓쳐서, 목차가 두 개 나오는 페이지가 생겼어요.
  • 클래스 이름이 bLnKoY3i처럼 압축된 의미 없는 문자열이에요. 초기의 폴백 처리가 이걸 그대로 넘겨서, 테스트 문서 100개 중 6개에서 제 출력에 섞여 나왔어요.
  • 다크 모드 색상은 별도의 dark-style 속성에 들어 있어요. 나중에 다시 파싱해야 하는 문자열로 두지 않고, 트리에서 따로 필드를 갖게 했어요.

텍스트 쪽에서는 표가 제일 골치 아팠어요. namumark에서는 세 줄을 병합한 셀이 있어도 그 아래 줄들에 빈 채움 셀 ||||이 그대로 남아요. 파서가 어느 위치가 이미 차 있는지 추적해서 채움 셀을 버려야 해요. 안 그러면 rowspan 다음 줄부터 전부 오른쪽으로 밀려요.

끝까지 지킨 규칙

임포터가 못 다루는 부분이 생길 때마다 가장 쉬운 해결책은 원본 html 조각을 트리에 그대로 넣어 두고 페이지에 붙여 넣는 거예요. 저는 그걸 허용하지 않았어요. 초기 버전에는 "preserved element" 폴백이 있었는데, 라이브러리 마지막 변경 중 하나가 이 폴백들을 타입 있는 노드로 바꾸는 작업이었어요. 임포터가 뭔가를 이해하지 못하면 그건 임포터 버그예요. 덩어리째 저장할 이유가 아니에요.

이 규칙 때문에 어려워진 점이 하나 있어요. 수정 하나가 세 계층을 동시에 건드릴 수 있다는 거예요. 임포터, 렌더러, 그리고 이미 데이터베이스에 저장된 트리까지요. 파서를 고쳐도 어제 임포트한 문서에는 아무 효과가 없어요. 그래서 일부 수정에는 예전 트리를 렌더링 시점에 고쳐 주는 처리를 같이 넣었어요. 예를 들면 나무위키 절대 url을 가리키던 링크를 렌더링할 때 내부 링크로 바꿔서, 저장된 문서 8,711개를 하나도 다시 임포트하지 않고 고쳤어요. 저장된 트리가 정말로 잘못된 경우에는 임포터 버전 번호를 올려서 다시 가져와 파싱하게 했어요.

테스트는 이렇게 했어요

테스트 코퍼스는 실제 나무위키 문서 100개를 무작위로 고른 거고, html로 약 9.7mb예요. 코퍼스 테스트는 문서마다 파싱하고, 렌더링하고, namumark로 다시 써내요. 100개 페이지에서 트리에 들어간 건 문단 12,379개, 표 610개, 각주 885개, 링크 13,153개예요. 모든 페이지가 진단 메시지 0개, raw html 노드 0개로 파싱돼요.

왕복 변환의 충실도는 그보다 덜 완벽하고, 이 글을 쓰면서 직접 측정해 봤어요. html에서 트리, namumark, 다시 트리로 가는 과정에서 제목 구조는 100개 문서 모두 유지돼요. 한 번 더 파싱하기 전과 후의 namumark 텍스트를 비교하면 약 90%의 줄이 똑같이 돌아와요. 달라지는 곳은 보통 스타일이 들어간 표 셀이나 이미지 링크에서 처음 차이가 생겨요. 아직 바이트 단위까지 똑같이 돌아오는 문서는 없어요.

위키 앱

앱은 next.js와 postgres로 만들었어요. 문서는 리비전마다 트리를 json으로 같이 저장하고, 역사 페이지에서는 unified diff를 보여 줘요. 접근 제어는 문서 단위, 그다음 이름공간 단위, 마지막으로 전역 기본값 순서로 결정돼요. wysiwyg 에디터와 원본 namumark 탭이 둘 다 있어요. 백그라운드 워커가 같은 html 임포터로 나무위키 문서를 가져왔고, 이미지는 압축된 webp로 저장했어요. 출처 표시를 위해 원본 페이지의 역사 링크도 함께 남겼어요. 나무위키 라이선스가 cc by-nc-sa라서요.

지금 상태

작업은 3월에 11일 동안 몰아서 했고, 그 뒤로는 멈춰 있어요. 라이브러리는 실제 페이지를 상대로 꽤 괜찮은 상태예요. 남은 건 왕복 변환이에요. namumark로 써낸 결과가 바이트 단위까지 똑같이 돌아오게 만드는 건데, 스타일이 들어간 표 셀부터 시작하려고 해요.