← 개발일지

옵시디언 Templater로 정기모임 기록 템플릿 만들기


정기모임, 기록이 쌓여야 자산이 된다

매달 한 번 모이는 스터디, 독서 모임, 발표 모임. 이런 정기모임을 몇 년간 하다 보면 "그때 누가 뭘 발표했더라?" 하는 순간이 반드시 온다. 기록이 없으면 그 시간은 그냥 지나간 것이고, 기록이 있으면 수십 회 분량의 인사이트 아카이브가 된다.

이 글에서는 옵시디언의 Templater 플러그인을 사용해서 정기모임 기록 템플릿을 처음부터 설계하는 과정을 다룬다. 단순히 "이 코드를 복사하세요"가 아니라, 설계 과정에서 마주치는 실제 문제와 그에 대한 판단 기준을 함께 정리했다.

필요한 것

옵시디언이 설치되어 있고, Community plugins에서 Templater 플러그인을 활성화한 상태를 전제한다. Templater 설정에서 Template folder location을 지정해 두어야 한다 (예: _templates).

Dataview 플러그인은 필수는 아니지만, frontmatter를 구조화해 두면 나중에 통계 쿼리를 짤 수 있으므로 설치를 권장한다.

설계 전에 결정할 것들

템플릿 코드를 작성하기 전에 먼저 정해야 하는 것들이 있다. 이 판단을 건너뛰고 코드부터 짜면 나중에 구조를 뜯어고치게 된다.

파일명의 primary key를 뭘로 잡을 것인가

정기모임 파일명에 쓸 수 있는 후보는 크게 세 가지다.

회차 번호 (모임 48회): 누적감이 있고 직관적이다. 하지만 과거 기록을 복원하려 할 때, 건너뛴 달이 정확히 언제인지 모르면 회차 번호가 추정치가 된다. 추정치가 섞인 데이터는 신뢰할 수 없어서 결국 안 쓰게 된다.

날짜 (모임 2026-04): 사실 기반이라 틀릴 여지가 없다. 파일 탐색기에서 자동 정렬되고, 검색도 쉽다. 월 1회 모임이라면 YYYY-MM만으로 유일성이 보장된다.

회차 + 날짜 병기 (모임 48회 (2026-04)): 둘 다 볼 수 있지만 파일명이 길어지고, 회차 추정 문제는 여전히 남는다.

결론적으로 YYYY-MM을 primary key로 쓰고, 회차는 frontmatter에 보조 정보로 넣는 방식을 추천한다. 회차는 "현재 폴더에 존재하는 파일 수 + 1"로 자동 계산하면, 과거 기록을 어떤 순서로 복원하든 꼬이지 않는다.

폴더 구조

파일이 100개 이하라면 flat 구조로도 충분하지만, 수년간 운영한 모임이라면 연도별 서브폴더가 탐색에 유리하다. Dataview 쿼리는 하위 폴더까지 재귀 탐색하므로 폴더를 나눠도 기능상 차이가 없다.

모임/
├── 2024/
│   ├── 모임 2024-01.md
│   └── 모임 2024-03.md    ← 2월은 건너뜀
├── 2025/
└── 2026/
    └── 모임 2026-04.md

연도 폴더는 Templater 스크립트에서 자동 생성할 수 있다. app.vault.getAbstractFileByPath()로 존재 여부를 확인하고 없으면 app.vault.createFolder()를 호출하면 된다.

frontmatter에 뭘 넣을 것인가

나중에 "누가 가장 많이 발표했는가", "어떤 주제가 많았는가" 같은 쿼리를 짜고 싶다면 frontmatter 설계가 중요하다. 핵심은 발표자와 주제를 분리된 list가 아니라 객체 list로 묶는 것이다.

# 이렇게 하면 안 된다 — 인덱스 매칭에 의존
발표자: [김철수, 이영희]
주제: [AI의 미래, 와인 입문]

# 이렇게 해야 한다 — 매핑이 깨지지 않음
발표:
  - 발표자: 김철수
    주제: "AI의 미래"
  - 발표자: 이영희
    주제: "와인 입문"

핵심 구현 패턴

신규/복원 듀얼 모드

오래 운영한 모임이라면 과거 기록을 소급 입력하는 경우가 생긴다. Templater의 tp.system.suggester()로 모드를 선택하게 하고, 복원 모드에서는 날짜를 직접 입력받으면 된다.

const mode = await tp.system.suggester(
  ["📝 이번 달 모임 (오늘 날짜)", "📂 과거 모임 복원"],
  ["new", "restore"]
);

let targetDate;
if (mode === "restore") {
  const input = await tp.system.prompt("모임 날짜 (YYYY-MM)", "2024-01");
  targetDate = moment(input + "-01");
} else {
  targetDate = moment();
}

회차 자동 계산

폴더 내 파일을 정규식으로 필터링하고 개수를 세면 된다. Math.max 방식이 아니라 length + 1을 쓰는 이유는, 파일명에 회차가 들어가지 않기 때문이다 (primary key가 YYYY-MM이므로).

const allFiles = app.vault.getMarkdownFiles()
  .filter(f => f.path.startsWith(ROOT_FOLDER + "/")
            && /^모임 \d{4}-\d{2}$/.test(f.basename));
const session = allFiles.length + 1;

이전/다음 노트 네비게이션

모든 기존 노트의 월을 추출해서 정렬한 뒤, 현재 월 기준으로 직전/직후를 찾는다. 건너뛴 달이 있어도 정확히 연결된다.

const allMonths = allFiles
  .map(f => f.basename.match(/(\d{4}-\d{2})/)?.[1])
  .filter(Boolean)
  .sort();

const prevMonth = allMonths.filter(m => m < monthKey).pop();
const nextMonth = allMonths.filter(m => m > monthKey)[0];

다음 회차 링크는 생성 시점에 dead link지만, 다음 모임에서 해당 노트를 만드는 순간 Obsidian이 자동 연결한다. 추가 작업 없이 양방향 navigation이 생기는 구조다.

중복 방지

같은 달에 실수로 두 번 실행하면 기존 노트를 덮어쓸 수 있다. app.vault.getMarkdownFiles()에서 파일명을 확인하고, 이미 있으면 Notice로 경고 후 중단한다.

const existing = app.vault.getMarkdownFiles()
  .find(f => f.basename === expectedFilename);
if (existing) {
  new Notice(`⚠️ ${expectedFilename} 노트가 이미 존재합니다.`);
  return;
}

한계와 주의점

복원 시 기존 노트의 네비게이션 링크는 갱신되지 않는다. Templater는 "현재 생성 중인 노트"만 제어할 수 있고, 다른 파일을 수정하는 기능이 없다. 과거 노트 10개를 한꺼번에 복원하면, 각 노트의 이전/다음 링크가 복원 당시 상태로 고정된다. 실용적으로는 큰 문제가 아니지만, 완벽주의자라면 Linter 플러그인이나 별도 스크립트로 일괄 갱신하는 방법을 고려해야 한다.

회차 번호는 "만든 시점 기준 순번"이다. 2024-01을 먼저 복원하고 2023-06을 나중에 복원하면, 2024-01이 1회, 2023-06이 2회가 된다. 월: 필드가 진짜 식별자이므로 회차 불일치는 기능상 무해하지만, 신경 쓰인다면 복원은 시간순으로 하는 것을 권장한다.

tR += 패턴의 주의사항. Templater의 `` 블록에서 출력을 생성할 때 tR +=을 사용하는데, 줄바꿈(\n)을 빠뜨리면 YAML이 깨진다. frontmatter 안에서 동적 리스트를 생성할 때 특히 주의해야 한다.

다음 단계

템플릿이 완성되면 Dataview로 대시보드를 만들어 보는 것을 추천한다. frontmatter를 구조화해 뒀으므로, 다음과 같은 쿼리가 가능하다.

  • 발표자별 총 발표 횟수
  • 연도별 모임 횟수 추이
  • 가장 많이 등장한 주제 키워드

기록이 10회, 20회 쌓이면 이 데이터가 모임의 역사가 된다. 템플릿은 그 역사를 만들기 위한 첫 번째 투자다.