옵시디언 최고의 장점이자 진입장벽, 커스터마이징을 Vive Coding으로 해결하기
최근 다시 Obsidian을 제대로 활용해보고자 이것저것 시도하는 중인데,
이번 포스팅에서는 독서 노트 관리 & 시각화를 어떻게 하면 보기 좋게 만들 수 있을지 정리해봤다.
최소한의 Obsidian 경험은 있다는 가정 하에 작성..
👉 참고: Plugin Github Repo
국내 도서 정보를 크롤링해서 독서 노트의 frontmatter
에 넣어주는 플러그인이다.
필자는 크레마클럽
을 사용중이라 이걸 사용하지만, 교보문고
기반 플러그인도 있는 것으로 알고있다.
Note title에 책 제목을 입력하고 해당 플러그인을 실행하면
메타데이터들이 자동으로 frontmatter
에 들어가기 때문에 이후 Dataview
로 다루기가 편해진다.
해당 플러그인에 대한 상세 설명은 Obsidian 내의 Plugin Store
또는 플러그인 Github Repo 를 참고하면 된다.
이건 Obsidian 내에서는 너무 유명한 플러그인이라 따로 자세한 설명은 생략한다.
이번 글에서는 dataviewjs
까지 활용하므로
Options
→ Dataview
→ Enable JavaScript Queries
를 켜주자.
가장 단순하게, 작성되어있는 독서 노트들을 아래와 같이 테이블로 출력할 수 있다.
// dataview
TABLE WITHOUT ID
"" as "표지",
file.link as "제목",
author as "저자",
my_rate as "별점",
book_note as "한줄평"
FROM #📚독서
SORT my_rate DESC
역시 순정은 깔끔하긴 하지만, 뭔가 밋밋하다.
Obsidian에서는 Options
→ Appearance
→ CSS Snippets
에서 CSS 파일을 등록할 수 있다.
그리고 Note의frontmatter
에 cssclasses
를 입력하면 해당 note에 스타일이 적용된다.
이걸로 note를 내맘대로 커스텀 할 수 있긴한데..
obsidian 처음 쓸 때(2023년)만해도 필자는 CSS의 ㅅ자도 모르는 늅늅이었기에 당시에는 거의 신경도 못썼었다.
- 노트 본문 폭을 전체 화면으로 늘려주는 CSS 스니펫
👉 참고: Obsidian Full-Width CSS
지금은 모르겠는데 2년전만 해도
Note 너비를 변경하는 옵션
이 global
로만 있고 Note 별로 각각 설정은 못했었다.
특정 Note만 full-width
설정하기위해 구글링해서 찾은 CSS 파일을 사용했었다.
- Dataview 테이블을 카드 레이아웃으로 바꿔주는 CSS
👉 참고: CSS Snnipet - Cards.css
DataView
로 만들어진 테이블을 Card 형식으로 꾸며주는 css 파일을 써봤는데,
기본적으로 깔끔하긴하지만 뭔가 나만의 taste를 추가하기엔 어려움이 있었다.
필자는 여전히 CSS 전문가는 아니지만 이제 Vive Coding(AI 딸깍)이 가능해졌다.
위의 Cards.css
를 바탕으로, 독서노트 전용으로 사용하기 위한 CSS Snnipet을 llm과 함께 만들었다.
👉 참고: BookCard.css - Github
독서 노트 작성 Flow
1. 독서 노트 생성 및 Title로 책 제목 입력
2.Korean Book Info
플러그인을 통해frontmatter
에 책 정보 등록
3. 읽으면서 노트 내용 채우기
4. 완독 후frontmatter
에my_rate (별점)
과book_note (한줄평)
입력
dataviewjs, CSS와 함께라면 옵시디언 꾸미기, 옵꾸를 마음껏 누릴 수 있다.
100% Vibe Coding이 목표였지만,
LLM이dataviewjs
데이터 훈련은 많이 못했는지 5% 아쉬워서 그정도는 직접 수정
/* dataviewjs */
// 1️⃣ Book Notes 페이지 가져오기 (정렬: 별점 -> 생성일)
const currentFolder = dv.current().file.folder;
const pages = dv.pages(`"${currentFolder}"`)
.where(p => p.file.tags.includes("#📚독서"))
.sort(p => [p.my_rate ?? 0, p.file.cday], 'desc');
// 2️⃣ 카드 컨테이너 생성
const container = document.createElement("div");
container.className = "book-cards";
// 3️⃣ 카드 생성
pages.forEach(p => {
const rate = p.my_rate ?? 0;
// 카드
const card = document.createElement("div");
card.className = "book-card";
// 이미지 (클릭 시 이동)
const img = document.createElement("img");
img.src = p.cover_url ?? "";
img.alt = "표지";
img.addEventListener("click", () => {
app.workspace.openLinkText(p.file.path, "/", true);
});
card.appendChild(img);
// 콘텐츠 div
const content = document.createElement("div");
content.className = "content";
card.appendChild(content);
// 제목 (링크 포함)
const titleDiv = document.createElement("div");
titleDiv.className = "title";
const titleLink = document.createElement("a");
titleLink.textContent = p.file.name;
titleLink.href = "#";
titleLink.style.textDecoration = "none";
titleLink.style.color = "inherit";
titleLink.addEventListener("click", (e) => {
e.preventDefault();
app.workspace.openLinkText(p.file.path, "/", true);
});
titleDiv.appendChild(titleLink);
content.appendChild(titleDiv);
// 별점 + 한줄평 div
const ratingNoteWrapper = document.createElement("div");
ratingNoteWrapper.className = "rating-note-wrapper";
ratingNoteWrapper.style.display = "flex";
ratingNoteWrapper.style.flexDirection = "column";
ratingNoteWrapper.style.gap = "0.25rem";
// 별점
const ratingDiv = document.createElement("div");
ratingDiv.className = "rating";
if (rate) {
for (let i = 1; i <= 5; i++) {
const fillPercent = Math.max(Math.min(rate - (i - 1), 1), 0) * 100;
const starSpan = document.createElement("span");
starSpan.className = "star";
starSpan.textContent = "★";
const filled = document.createElement("span");
filled.className = "filled";
filled.textContent = "★";
filled.style.width = `${fillPercent}%`;
starSpan.appendChild(filled);
ratingDiv.appendChild(starSpan);
}
const numText = document.createElement("span");
numText.textContent = ` ${rate}`;
ratingDiv.appendChild(numText);
} else {
ratingDiv.textContent = "별점 없음";
}
ratingNoteWrapper.appendChild(ratingDiv);
// 한줄평
const noteDiv = document.createElement("div");
noteDiv.className = "note";
noteDiv.textContent = p.book_note ?? "";
ratingNoteWrapper.appendChild(noteDiv);
content.appendChild(ratingNoteWrapper);
// 메타데이터
const metaDiv = document.createElement("div");
metaDiv.className = "meta";
if (p.author) {
const authorDiv = document.createElement("div");
authorDiv.className = "author";
authorDiv.textContent = p.author;
metaDiv.appendChild(authorDiv);
}
if (p.publisher || p.publish_date) {
const pubDiv = document.createElement("div");
pubDiv.className = "publisher"
pubDiv.textContent = `${p.publisher ?? ""} / ${p.publish_date ? dv.date(p.publish_date).toFormat("yyyy-MM-dd") : ""}`;
metaDiv.appendChild(pubDiv);
}
if (p.status || p.start_read_date || p.finish_read_date) {
const statusDiv = document.createElement("div");
statusDiv.className = "status";
statusDiv.textContent = `${p.status ?? ""} / ${p.start_read_date ? dv.date(p.start_read_date).toFormat("yyyy-MM-dd") : ""} ~ ${p.finish_read_date ? dv.date(p.finish_read_date).toFormat("yyyy-MM-dd") : ""}`;
metaDiv.appendChild(statusDiv);
}
content.appendChild(metaDiv);
// 카드 컨테이너에 추가
container.appendChild(card);
});
// 4️⃣ DataviewJS에 container 삽입
dv.container.appendChild(container);
일단 바이브만 느끼고 디테일엔 신경쓰지 말자..?
dataviewjs와 css가 제대로 먹혔다면 다음과 같이 Cards 형태로 보기좋게 정리된다.
여기서 끝내면 아쉬워서 별점 구간을 티어(S, A, B, …)로 매핑해서 한눈에 보는 표도 만들어봤다. 물론 vive로..
// dataview
TABLE WITHOUT ID
Tier,
join(rows.file.link, ", ") as "책 목록"
FROM #📚독서
SORT my_rate DESC
GROUP BY choice(floor(my_rate) = 5, "S",
choice(floor(my_rate) = 4, "A",
choice(floor(my_rate) = 3, "B",
choice(floor(my_rate) = 2, "C",
choice(floor(my_rate) = 1, "D", "미분류"))))) as "Tier"
SORT rows.my_rate DESC