2026-05-31 NodeBB / Quartz 운영 설정 반영 기록

오늘은 goodtek 커뮤니티와 공개 개발노트 사이트의 기본 운영 설정을 정리했습니다.

작업은 크게 세 가지였습니다.

  1. NodeBB 커뮤니티 업그레이드
  2. NodeBB Google Analytics 등록
  3. Quartz 기반 notes.goodtek.xyz favicon, OG 이미지, Google Analytics 등록

처음에는 단순히 favicon 바꾸고 GA 코드 넣는 작업이라고 생각했습니다. 그런데 실제로 해보니 운영 환경에서는 늘 그렇듯이, 작은 설정 하나에도 컨테이너, 캐시, Git 관리 대상, Cloudflare Pages 빌드 산출물까지 같이 따라왔습니다.


1. NodeBB 업그레이드

NodeBB는 Podman Compose 기반으로 운영 중입니다.

구성은 대략 다음과 같습니다.

goodtek-nodebb-postgres   postgres:16
goodtek-nodebb            ghcr.io/nodebb/nodebb:latest

NodeBB 컨테이너는 외부에 직접 열지 않고, 로컬 포트로 바인딩해 리버스 프록시 뒤에서 사용하고 있습니다.

127.0.0.1:4567 -> 4567

데이터는 컨테이너 내부가 아니라 호스트 디렉터리에 마운트되어 있습니다.

./data/postgres
./data/uploads
./data/config

이 구조 덕분에 컨테이너를 지우고 다시 만들어도 데이터는 유지됩니다. 다만 운영 작업 전에 백업은 항상 먼저 해야 합니다.


2. NodeBB 현재 버전 확인

처음에는 아래 명령으로 버전을 확인하려고 했습니다.

podman exec -it goodtek-nodebb ./nodebb --version

그런데 이 명령이 단순히 버전만 보여주는 게 아니라 web installer를 띄우려고 하면서 포트 충돌이 발생했습니다.

Launching web installer on port 4567
EADDRINUSE: address already in use :::4567

이미 실행 중인 NodeBB가 4567 포트를 사용하고 있는데, 같은 컨테이너 안에서 installer가 다시 4567 포트를 열려고 해서 생긴 문제였습니다.

이후부터는 package.json을 직접 읽는 방식으로 버전을 확인했습니다.

podman exec goodtek-nodebb node -p "require('./package.json').version"

최종 확인 결과는 다음과 같습니다.

4.12.0

오늘 작업으로 NodeBB는 다음 버전으로 올라갔습니다.

NodeBB 4.11.3 → 4.12.0

3. NodeBB 컨테이너 이름 충돌

업그레이드 과정에서 podman-compose up -d를 실행했을 때 컨테이너 이름 충돌이 발생했습니다.

the container name "goodtek-nodebb-postgres" is already in use
the container name "goodtek-nodebb" is already in use

원인은 기존 컨테이너가 이미 존재하는 상태에서 Podman Compose가 같은 이름으로 새 컨테이너를 만들려고 했기 때문입니다.

해결은 기존 컨테이너를 중지하고 제거한 뒤, 다시 생성하는 방식으로 진행했습니다.

podman stop goodtek-nodebb
podman stop goodtek-nodebb-postgres
 
podman rm goodtek-nodebb
podman rm goodtek-nodebb-postgres
 
podman-compose pull
podman-compose up -d

여기서 중요한 점은 컨테이너 삭제와 데이터 삭제는 다르다는 것입니다.

데이터는 아래처럼 호스트 디렉터리에 남아 있습니다.

./data/postgres
./data/uploads
./data/config

그래서 컨테이너를 삭제해도 DB 데이터, 업로드 파일, NodeBB 설정은 유지되었습니다.


4. NodeBB 업그레이드 확인

업그레이드 후 로그를 확인했습니다.

podman logs --tail=80 goodtek-nodebb

최종적으로 다음 로그를 확인했습니다.

Initializing NodeBB v4.12.0 https://community.goodtek.xyz
🎉 NodeBB Ready
📡 NodeBB is now listening on: 0.0.0.0:4567
🔗 Canonical URL: https://community.goodtek.xyz

버전도 다시 확인했습니다.

podman exec goodtek-nodebb node -p "require('./package.json').version"

결과:

4.12.0

이로써 NodeBB 업그레이드는 완료되었습니다.


5. NodeBB Google Analytics 등록

NodeBB 커뮤니티에도 Google Analytics를 붙였습니다.

커뮤니티용 GA4 측정 ID는 공개용 기록에서는 일부만 남깁니다.

G-X467H****

Google Analytics에서 제공하는 코드는 다음과 같은 형태입니다.

<!-- Google tag (gtag.js) - goodtek community -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-X467H****"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
 
  gtag('config', 'G-X467H****');
</script>

NodeBB에서는 이 코드를 관리자 화면의 사용자 정의 헤더에 넣었습니다.

NodeBB Admin
→ 외형
→ 사용자 정의
→ 사용자 정의 헤더

처음에는 코드가 들어가 있었는데도 curl로 확인되지 않았습니다.

원인은 단순했습니다.

사용자 정의 헤더 활성화가 꺼져 있었음

NodeBB에서는 코드를 넣는 것만으로는 부족하고, 반드시 사용자 정의 헤더 활성화를 켜야 합니다.

활성화 후 저장하고 NodeBB를 재시작했습니다.

podman restart goodtek-nodebb

확인 명령:

curl -sL -H "Cache-Control: no-cache" "https://community.goodtek.xyz/?ga_test=$(date +%s)" \
| grep -i "G-X467H\|googletagmanager\|gtag"

정상 반영 후 다음처럼 확인되었습니다.

<!-- Google tag (gtag.js) - goodtek community -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-X467H****"></script>
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'G-X467H****');

NodeBB 쪽 GA4 등록도 완료되었습니다.


6. Quartz favicon 등록

notes.goodtek.xyz는 Quartz v5 기반으로 운영 중입니다.

로컬에서 빌드했습니다.

npx quartz build

빌드 결과:

Quartz v5.0.0
 
Cleaned output directory `public`
Found 7 input files from `content`
Parsed 7 Markdown files
Emitted 82 files to `public`
Done processing 7 files

favicon 생성도 확인했습니다.

ls -al public/favicon.ico

Quartz에서는 public/favicon.ico를 직접 관리하기보다, 아래 파일을 기반으로 빌드 과정에서 favicon을 생성합니다.

quartz/static/icon.png

오늘 반영한 주요 이미지 파일은 다음입니다.

quartz/static/icon.png
quartz/static/og-image.png

goodtek 기준으로 favicon은 텍스트까지 넣기보다 심볼 중심으로 두는 것이 더 낫다고 판단했습니다. 작은 탭 아이콘에서는 goodtek 텍스트가 거의 보이지 않기 때문입니다.


7. Quartz OG 이미지 변경

공유용 이미지도 함께 변경했습니다.

사용한 파일:

quartz/static/og-image.png

Quartz 빌드 후 페이지별 OG 이미지가 생성되고, 운영 HTML에서는 다음과 같은 형태로 확인됩니다.

<meta property="og:image" content="https://notes.goodtek.xyz/index-og-image.webp">
<meta property="og:image:url" content="https://notes.goodtek.xyz/index-og-image.webp">
<meta name="twitter:image" content="https://notes.goodtek.xyz/index-og-image.webp">

favicon은 브라우저 탭에서 보이는 작은 아이콘이고, OG 이미지는 링크를 공유했을 때 보이는 대표 이미지입니다.

역할이 다르기 때문에 둘을 분리해서 관리하는 게 좋습니다.

favicon → 심볼 중심
OG image → 브랜드와 사이트 성격이 보이도록 구성

8. Quartz Google Analytics 설정

Quartz 노트 사이트에도 Google Analytics를 붙였습니다.

노트 사이트용 GA4 측정 ID는 공개용 기록에서는 일부만 남깁니다.

G-SRGFZ****

Google Analytics에서 제공하는 원본 스크립트는 다음과 같은 형태입니다.

<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-SRGFZ****"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
 
  gtag('config', 'G-SRGFZ****');
</script>

하지만 Quartz에서는 이 스크립트 전체를 직접 붙여넣지 않습니다.

quartz.config.yaml에 측정 ID만 넣습니다.

기존 설정은 Plausible이었습니다.

analytics:
  provider: plausible

Google Analytics로 변경했습니다.

analytics:
  provider: google
  tagId: G-SRGFZ****

전체 설정 일부는 다음과 같습니다.

# yaml-language-server: $schema=./quartz/plugins/quartz-plugins.schema.json
configuration:
  pageTitle: "● goodtek"
  pageTitleSuffix: ""
  enableSPA: true
  enablePopovers: true
  analytics:
    provider: google
    tagId: G-SRGFZ****
  locale: en-US
  baseUrl: notes.goodtek.xyz
  ignorePatterns:
    - private

설정 후 로컬 빌드를 다시 실행했습니다.

npx quartz build

로컬 빌드 결과에서 GA 코드가 생성되는지 확인했습니다.

grep -R "G-SRGFZ\|googletagmanager\|gtag" -n public | head -20

결과는 다음과 같은 형태였습니다.

public/static/scripts/script-12-341c4442.js:2:      const gtagScript = document.createElement('script');
public/static/scripts/script-12-341c4442.js:3:      gtagScript.src = 'https://www.googletagmanager.com/gtag/js?id=G-SRGFZ****';
public/static/scripts/script-12-341c4442.js:10:        gtag('js', new Date());
public/static/scripts/script-12-341c4442.js:11:        gtag('config', 'G-SRGFZ****', { send_page_view: false });
public/static/scripts/script-12-341c4442.js:12:        gtag('event', 'page_view', { page_title: document.title, page_location: location.href });

여기서 중요한 점은 Quartz가 GA 코드를 HTML에 직접 넣지 않는다는 것입니다.

Quartz는 SPA 라우팅을 고려해서 기본 page view를 끄고 직접 page view 이벤트를 보냅니다.

send_page_view: false
gtag('event', 'page_view', ...)

9. Git 관리 대상 정리

처음에는 .gitignore가 너무 단순했습니다.

# Obsidian local settings
.obsidian/

이 상태에서 git add .를 실행하면 아래 항목들이 모두 잡혔습니다.

node_modules/
public/
.DS_Store
.quartz-cache/

이 파일들은 Git에 올리면 안 됩니다.

반면, 이 프로젝트에서는 Cloudflare Pages 빌드를 위해 Quartz 플러그인 파일이 필요했습니다.

.quartz/plugins

예전에 Cloudflare Pages 빌드에서 다음과 비슷한 문제가 난 적이 있었습니다.

Could not resolve "../../.quartz/plugins"

원인은 .quartz/plugins가 Git에 올라가지 않아 Cloudflare 빌드 환경에서 필요한 플러그인 파일을 찾지 못한 것이었습니다.

그래서 이번에는 .quartz/plugins는 Git 관리 대상으로 유지하고, 빌드 산출물과 의존성만 제외했습니다.

최종 .gitignore는 다음과 같습니다.

# Obsidian local settings
.obsidian/
 
# dependencies
node_modules/
**/node_modules/
 
# Quartz build output / cache
public/
.quartz-cache/
quartz/.quartz-cache/
 
# macOS
.DS_Store
**/.DS_Store

정리하면 다음과 같습니다.

커밋해야 하는 것:
- quartz/static/icon.png
- quartz/static/og-image.png
- quartz.config.yaml
- .quartz/plugins
 
커밋하면 안 되는 것:
- node_modules/
- public/
- .DS_Store
- .quartz-cache/

10. Quartz 플러그인 내부 .git 제거

.quartz/plugins 안에 각 플러그인별 .git 디렉터리가 남아 있었습니다.

확인 명령:

find .quartz/plugins -name .git -type d -prune

처음에는 여러 플러그인 폴더에서 .git 디렉터리가 발견되었습니다.

.quartz/plugins/footer/.git
.quartz/plugins/crawl-links/.git
.quartz/plugins/og-image/.git
.quartz/plugins/favicon/.git
...

이 상태로 커밋하면 Git이 플러그인 폴더를 일반 디렉터리가 아니라 별도 Git 저장소나 서브모듈처럼 인식할 수 있습니다.

그래서 내부 .git 디렉터리만 제거했습니다.

find .quartz/plugins -name .git -type d -prune -exec rm -rf {} +

삭제 확인:

find .quartz/plugins -name .git -type d -prune

결과가 비어 있으면 정상입니다.

그 다음 다시 Git에 추가했습니다.

git add .

서브모듈로 잡히지 않는지 확인했습니다.

git ls-files --stage .quartz/plugins | head -30

정상은 다음처럼 100644로 표시됩니다.

100644 ...

문제가 있는 경우는 160000으로 표시됩니다.

160000 ...

이번에는 100644로 표시되어 일반 파일로 정상 스테이징되었습니다.


11. 커밋 및 Cloudflare Pages 배포

변경 사항을 커밋했습니다.

git add .
git status
git commit -m "Add Quartz favicon, OG image, and analytics"
git push

Cloudflare Pages에서는 다음 흐름으로 빌드되었습니다.

clone repo
install dependencies
npx quartz build
emit files to public
deploy assets
site deployed

빌드 로그에서 중요한 부분은 다음입니다.

Executing user command: npx quartz build
Quartz v5.0.0
Emitted 82 files to public
Assets published
Site deployed

실제 커밋 해시는 공개용 기록에서는 짧게만 남깁니다.

e5ebd42

12. Cloudflare Pages 운영 반영 확인

배포 후 favicon은 바로 반영되었습니다.

curl -I https://notes.goodtek.xyz/favicon.ico

처음에는 아래 명령으로 GA 코드를 찾으려고 했습니다.

curl -sL -H "Cache-Control: no-cache" "https://notes.goodtek.xyz/?ga_test=$(date +%s)" \
| grep -i "G-SRGFZ\|googletagmanager\|gtag"

그런데 아무것도 나오지 않았습니다.

처음에는 “GA가 안 들어갔나?”라고 생각했지만, 원인은 Quartz의 빌드 구조였습니다.

Quartz는 GA 코드를 HTML에 직접 박지 않고, JS 파일을 통해 동적으로 로드합니다.

운영 HTML에서 script 태그를 확인했습니다.

curl -sL -H "Cache-Control: no-cache" "https://notes.goodtek.xyz/?ga_test=$(date +%s)" \
| grep -i "<script"

운영 HTML에는 다음 스크립트들이 있었습니다.

<script src="./prescript-2bfc6315.js" type="application/javascript" data-persist="true"></script>
<script src="./postscript-89e90487.js" type="module" data-persist="true"></script>

로컬에서 GA 파일 연결 구조를 확인했습니다.

grep -R "script-12-341c4442.js" -n public | head -20

결과:

public/postscript-89e90487.js:14:  import("./static/scripts/script-12-341c4442.js")

운영에서도 postscript가 GA 스크립트 파일을 import하는지 확인했습니다.

curl -sL -H "Cache-Control: no-cache" "https://notes.goodtek.xyz/postscript-89e90487.js?cache=$(date +%s)" \
| grep -i "script-12-341c4442.js\|static/scripts\|G-SRGFZ\|googletagmanager\|gtag"

결과:

import("./static/scripts/script-12-341c4442.js")

마지막으로 운영 GA JS 파일 자체를 확인했습니다.

curl -sL "https://notes.goodtek.xyz/static/scripts/script-12-341c4442.js?cache=$(date +%s)" \
| grep -i "G-SRGFZ\|googletagmanager\|gtag"

결과:

const gtagScript = document.createElement('script');
gtagScript.src = 'https://www.googletagmanager.com/gtag/js?id=G-SRGFZ****';
gtagScript.defer = true;
gtagScript.onload = () => {
gtag('js', new Date());
gtag('config', 'G-SRGFZ****', { send_page_view: false });
gtag('event', 'page_view', { page_title: document.title, page_location: location.href });
document.head.appendChild(gtagScript);

이로써 Quartz + Cloudflare Pages 환경에서 Google Analytics가 정상 반영된 것을 확인했습니다.


13. 오늘 최종 상태

오늘 반영된 내용은 다음과 같습니다.

NodeBB 4.11.3 → 4.12.0 업그레이드 완료
NodeBB community.goodtek.xyz GA4 등록 완료
NodeBB 사용자 정의 헤더 활성화 완료
 
Quartz notes.goodtek.xyz favicon 변경 완료
Quartz notes.goodtek.xyz OG 이미지 변경 완료
Quartz notes.goodtek.xyz GA4 등록 완료
Cloudflare Pages 배포 완료
운영 JS에서 GA 코드 확인 완료
 
.gitignore 정리 완료
.quartz/plugins 내부 .git 제거 완료
.quartz/plugins 일반 파일로 Git 관리 확인 완료

14. 다음에 기억할 것

NodeBB

NodeBB에서 GA 코드는 다음 위치에 넣습니다.

관리자
→ 외형
→ 사용자 정의
→ 사용자 정의 헤더

그리고 반드시 활성화해야 합니다.

사용자 정의 헤더 활성화 ON

확인 명령:

curl -sL -H "Cache-Control: no-cache" "https://community.goodtek.xyz/?ga_test=$(date +%s)" \
| grep -i "G-X467H\|googletagmanager\|gtag"

Quartz

Quartz에서는 GA 스크립트 전체를 붙여넣지 않습니다.

quartz.config.yaml에 측정 ID만 넣습니다.

analytics:
  provider: google
  tagId: G-SRGFZ****

Quartz 운영 확인은 HTML이 아니라 동적으로 import되는 JS까지 확인해야 합니다.

curl -sL "https://notes.goodtek.xyz/static/scripts/script-12-341c4442.js?cache=$(date +%s)" \
| grep -i "G-SRGFZ\|googletagmanager\|gtag"

Git

Quartz + Cloudflare Pages 빌드를 위해 .quartz/plugins는 Git에 포함해야 합니다.

하지만 아래는 제외합니다.

node_modules/
**/node_modules/
public/
.quartz-cache/
quartz/.quartz-cache/
.DS_Store
**/.DS_Store

.quartz/plugins 내부에 .git 폴더가 남아 있으면 제거합니다.

find .quartz/plugins -name .git -type d -prune -exec rm -rf {} +

서브모듈로 잡히는지 확인합니다.

git ls-files --stage .quartz/plugins | head -30

정상은 100644, 문제는 160000입니다.


마무리

오늘 작업은 단순히 favicon과 GA를 붙이는 작업이 아니었습니다.

NodeBB는 관리자 화면에서 사용자 정의 헤더를 활성화해야 했고, Quartz는 GA 스크립트가 HTML에 직접 노출되지 않고 JS에서 동적으로 로드되는 구조였습니다.

또 Cloudflare Pages에서 Quartz를 빌드하려면 .quartz/plugins를 Git에 포함해야 했고, 반대로 node_modules, public, 캐시 파일은 제외해야 했습니다.

결국 운영에서 중요한 건 “설정을 넣었다”가 아니라, 실제 배포된 산출물에서 어떻게 로드되는지 끝까지 확인하는 것이었습니다.

작은 설정 하나를 바꾸는 데도 여기까지 확인해야 한다는 게 조금 번거롭긴 하지만, 이런 기록이 쌓이면 다음 작업은 훨씬 빨라집니다.