배경

Quartz의 Explorer는 파일 트리를 사이드바에 렌더링하는 컴포넌트입니다. 폴더 구조를 그대로 반영하여 블로그의 전체 콘텐츠를 탐색할 수 있게 해주는데, 기본 구현만으로는 아쉬운 부분이 있었습니다.

세 가지를 추가하기로 했습니다.

  1. 폴더 옆 파일 개수 표시 - 폴더 안에 글이 몇 개인지 한눈에 파악
  2. 폴더 노트 직접 네비게이션 - 폴더를 클릭했을 때 서브폴더가 펼쳐지지 않고 폴더 노트로 바로 이동
  3. 모바일 블러 오버레이 - 모바일에서 Explorer가 열릴 때 배경을 흐림 처리

각각의 개선이 어떤 문제를 풀고, 코드 수준에서 어떻게 구현되었는지를 정리합니다.

폴더별 파일 카운트

폴더명만 보고는 그 안에 콘텐츠가 얼마나 있는지 알 수 없습니다. 빈 폴더인지, 수십 개의 글이 들어있는지 클릭해보기 전까지 모릅니다. 폴더명 옆에 파일 개수 배지를 붙이면 이 문제가 해결됩니다.

countFiles 메서드

Quartz는 파일 구조를 FileTrieNode라는 트리 자료구조로 관리합니다. 여기에 countFiles() 메서드를 추가했습니다.

quartz/util/fileTrie.ts
countFiles(): number {
  let count = this.isFolder ? 0 : 1;
  for (let child of this.children) {
    count += child.countFiles();
  }
  return count;
}

로직은 단순합니다. 현재 노드가 폴더면 0, 파일이면 1로 시작하고, 자식 노드를 재귀적으로 순회하면서 합산합니다. 트리 자료구조의 자연스러운 확장입니다.

이런 로직 추가하고 작성할 때마다 Static Site Builder이기 때문에 성능 걱정 없이 단순하고 편하게 작성해도 되서 참 좋은 것 같습니다.

Explorer 렌더링

Explorer의 inline 스크립트에서 폴더 노드를 생성할 때, countFiles()의 결과를 <span> 엘리먼트로 추가합니다.

quartz/components/scripts/explorer.inline.ts
// In createFolderNode function:
a.className = "folder-title"
a.textContent = node.displayName
const span = document.createElement("span")
span.className = "folder-title-length"
span.textContent = ` (${node.countFiles()})`
a.appendChild(span)

스타일링

quartz/styles/component/plugins/explorer_override.scss
.folder-title-length {
  font-weight: 500;
  font-size: 0.65rem;
  opacity: 0.7;
}

폴더 노트 네비게이션

Quartz에서 폴더를 클릭하면 두 가지 동작이 동시에 발생합니다. 폴더 노트(index)로 이동하면서, 해당 폴더의 하위 트리를 펼치는 것입니다. 언뜻 합리적으로 보이지만, 사용자가 폴더 노트를 읽으러 갔을 때 의도하지 않은 트리 확장이 일어나는 문제가 있습니다. Jzhao님이 이걸 몰랐을리는 없고, 의도하셨겠지만, 제 입장에서는 불편했기 때문에 수정을 진행했습니다.

핵심은 “현재 보고 있는 페이지가 폴더 노트 자체인지” 구분하는 것입니다. 기존 코드는 폴더 경로가 현재 slug의 접두사인지만 확인했습니다. 여기에 폴더 노트 정확 매칭 조건을 추가했습니다.

quartz/components/scripts/explorer.inline.ts
const folderIsPrefixOfCurrentSlug =
  simpleFolderPath === currentSlug.slice(0, simpleFolderPath.length)
const isFolderNoteOfCurrentSlug = currentSlug === node.slug
 
if (!isCollapsed || (folderIsPrefixOfCurrentSlug && !isFolderNoteOfCurrentSlug)) {
  folderOuter.classList.add("open")
}

기존 조건은 folderIsPrefixOfCurrentSlug뿐이었습니다. 개발 노트/blog/some-post를 보고 있을 때 개발 노트 폴더가 열리는 것은 자연스럽습니다 - 현재 글이 해당 폴더 아래에 있으니까요.

추가된 isFolderNoteOfCurrentSlug는 현재 slug가 폴더 노트의 slug와 정확히 일치하는지 확인합니다. 일치한다면 !isFolderNoteOfCurrentSlugfalse가 되어, 폴더를 펼치지 않습니다. 즉, 폴더 노트 자체를 보고 있을 때는 하위 트리가 접힌 상태를 유지합니다.

모바일 블러 오버레이

모바일에서 Explorer는 화면 좌측에서 슬라이드 형태로 열립니다. 이때 Explorer 뒤의 본문이 그대로 보이면 시각적으로 산만합니다. 블러 오버레이를 추가하여 Explorer에 집중할 수 있도록 했습니다.

동적 엘리먼트 생성

explorer.inline.ts에서 Explorer가 초기화될 때 블러 오버레이 div를 동적으로 생성합니다.

quartz/components/scripts/explorer.inline.ts
const explorerBlur = document.createElement("div")
explorerBlur.className = "explorer-blur"
explorer.appendChild(explorerBlur)
explorerBlur.addEventListener("click", toggleExplorer)

오버레이를 클릭하면 toggleExplorer가 호출되어 Explorer가 닫힙니다. 모바일 UX에서 흔히 사용되는 “배경 클릭으로 닫기” 패턴입니다.

CSS

오버레이는 모바일에서만 활성화됩니다. backdrop-filter로 배경에 블러 효과를 적용하고, 트랜지션으로 부드러운 전환을 줍니다.

quartz/styles/component/plugins/explorer_override.scss
.explorer {
  @media all and ($mobile) {
    .explorer-blur {
      box-sizing: border-box;
      z-index: 99;
      position: absolute;
      top: 0;
      right: 0;
      width: 100%;
      height: 100dvh;
      max-height: 100dvh;
      background: rgba(255, 255, 255, 0.01);
      backdrop-filter: blur(5px) !important;
      -webkit-backdrop-filter: blur(5px);
      transition: opacity 200ms ease;
    }
  }
}

background를 거의 투명한 흰색(rgba(255, 255, 255, 0.01))으로 설정한 이유는, 완전히 투명하면 일부 브라우저에서 backdrop-filter가 동작하지 않기 때문입니다. 100dvh는 모바일 브라우저의 주소창 크기 변화에 대응하는 동적 뷰포트 높이 단위입니다. -webkit-backdrop-filter는 Safari 호환을 위한 접두사입니다.

데스크톱 Explorer 항상 펼침

데스크톱에서는 사이드바 공간이 충분하므로 Explorer를 접을 필요가 없습니다. title-button의 클릭 이벤트를 차단하고, 접힌 상태에서도 레이아웃이 유지되도록 합니다.

quartz/styles/component/plugins/explorer_override.scss
.explorer {
  &.collapsed {
    flex: 0 1 auto;
 
    & .fold {
      transform: none;
    }
  }
 
  button.title-button {
    pointer-events: none;
 
    & > svg {
      display: none;
    }
  }
}

button.title-buttonpointer-events: none을 적용하여 클릭 자체를 비활성화하고, 접기 화살표 SVG를 숨깁니다. .collapsed 상태에서도 flex: 0 1 auto로 레이아웃을 유지하고, .fold 엘리먼트의 transform을 초기화하여 접힘 애니메이션을 무효화합니다. 미디어 쿼리 없이 전역으로 적용되는 규칙이며, JavaScript 수정 없이 CSS만으로 동작을 제어한 것이 포인트입니다.

마무리

세 가지 개선 모두 Quartz의 기존 구조를 크게 변경하지 않는 범위에서 이루어졌습니다. 항상 이런 최소 침습이 포크 레포에서 가장 중요하다고 생각합니다. Explorer의 핵심 로직을 재작성하지 않고, 필요한 지점에만 코드를 끼워 넣어 동작을 확장했습니다. 여기서는 말하지 않은 이전 버전에서 몇가지 회귀가 발생하긴 했지만, 이 문서의 마지막 수정인 25년 말까지 한 번도 upstream이랑 충돌이 발생하지 않았습니다.