D2 다이어그램 렌더링
Hugo 에서는 아직 D2을 정식으로 지원하지 않기 때문에 D2 코드를 랜더링하기 위해서는 별도의 작업이 필요합니다. Script 방식과 Render Hook 방식, Shortcode 방식등을 사용할 수 있는데, 각 방식은 특정 상황에 따라 장점이 있으며, 특히 Obsidian과 같은 도구에서 D2를 이미 사용하고 있다면 Script 방식이나 Render Hook 방식이 더 유리할 수 있습니다.
- Script 방식은 D2 코드를 SVG 파일로 변환하여 Hugo 사이트에 정적 파일로 포함하는 방식입니다. 다이어그램을 미리 렌더링하므로 페이지 로드 시간이 빠르며, Cloudflare 등 클라우드 빌드 환경에서도 문제가 발생하지 않습니다.
- Render Hook 방식은 서버 기반 처리로 페이지 로드 성능이 뛰어나지만, 추가적인 서버 설정과 관리가 필요합니다. 특히, SEO와 성능이 중요한 경우에는 Render Hook 방식이 더 유리합니다. 다만, CloudFlare 등의 배포 사이트를 이용하는 경우, 사용하기 어렵습니다.
- Shortcode 방식은 설치와 사용이 간단하고 서버를 요구하지 않기 때문에 설정이 더 쉽지만, 클라이언트 성능에 의존하는 점이 단점입니다. 다이어그램이 많거나 페이지 성능이 중요한 경우에는 적합하지 않을 수 있습니다. 따라서 배포 사이트를 이용하는 경우에는 Script 방식이, 성능과 SEO를 중시하는 대규모 사이트라면 Render Hook 방식이 적합하고, 설치와 유지 관리가 간편한 소규모 사이트나 단순한 프로젝트에는 Shortcode 방식이 더 나은 선택이 될 수 있습니다.
Script 방식
작동 원리
D2 코드를 Markdown 파일에서 추출하고, 이를 미리 SVG 파일로 변환한 후, Hugo 빌드 시 해당 SVG 파일을 Hugo 사이트에 정적 파일로 포함하는 방식입니다.
Script
다음 글에서 Script 를 확인할 수 있습니다.
Render Hook 방식
Render Hook은 Hugo에서 특정 Markdown 요소, 예를 들어 코드 블록을 맞춤형으로 렌더링할 수 있게 해주는 기능입니다. 이 방식은 코드 블록 언어를 감지하여 D2로 지정된 코드 블록만 처리합니다. 특히 Obsidian에서 D2를 이미 사용하는 경우, 이 방식으로 문법을 그대로 유지하면서 Hugo에서도 사용할 수 있습니다. Erick Navarro blog 에 소개와 설명이 잘 되어 있습니다. Render hook 방식을 위해서는 HTTP 서버가 필요합니다. 이 서버는 D2 코드를 받아 다이어그램을 SVG 형식으로 변환하고, Hugo가 이를 페이지에 포함할 수 있게 도와줍니다. Hugo는 직접적으로 D2를 처리하지 않기 때문에, 이 HTTP 서버가 중요한 역할을 합니다.
Go 서버 만들기
Go 서버 코드 작성
아래의 Go 코드를 사용하여 D2 다이어그램을 처리하는 간단한 HTTP 서버를 만듭니다. 이 서버는 POST
요청으로 D2 코드를 받아 d2
CLI를 실행하여 SVG로 변환한 후 반환합니다.
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os/exec"
)
func handleRenderRequest(w http.ResponseWriter, r *http.Request) {
requestBody, err := io.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
defer r.Body.Close()
output, err := renderText(string(requestBody))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, output)
}
func renderText(content string) (string, error) {
command := exec.Command("d2", "-")
command.Stdin = bytes.NewBuffer([]byte(content))
output, err := command.Output()
if err != nil {
return "", err
}
return string(output), nil
}
func main() {
http.HandleFunc("/render", handleRenderRequest)
fmt.Println("Starting server on :8080")
http.ListenAndServe(":8080", nil)
}
Go 서버 빌드
Go 서버 코드를 작성한 후, 이를 실행 파일로 빌드해야 합니다. Go는 간단한 명령으로 코드를 실행 파일로 변환할 수 있습니다.
go build -o d2server.exe d2server.go
- 위 명령어는
d2server.go
파일을 컴파일하여d2server.exe
라는 실행 파일을 생성합니다.
D2 렌더링
D2 다이어그램을 Hugo에서 Render Hook을 사용하여 렌더링하는 경우, 코드 블록 내에서 D2 코드를 작성한 후, Render Hook이 HTTP 서버로 코드를 보내 변환된 SVG를 받아 페이지에 포함합니다.
Render Hook 코드 작성
layouts/_default/_markup/render-codeblock-d2.html
파일을 추가하고 다음의 내용으로 저장합니다.
{{- $renderHookName := "d2" }}
{{- $inner := trim .Inner "\n\r" }}
{{- $position := .Position }}
{{- $apiEndpoint := "http://localhost:8080/render" }}
{{- $opts := dict "method" "post" "body" $inner }}
{{- with resources.GetRemote $apiEndpoint $opts }}
{{- if .Err }}
<div>Error rendering D2 diagram</div>
{{- else }}
{{ .Content | safeHTML }}
{{- end }}
{{- else }}
<div>Error: Unable to connect to server</div>
{{- end }}
Hugo 빌드
go 서버 실행
Hugo에서 D2 다이어그램을 렌더링하기 위해서는 빌드 전에 HTTP 서버가 실행되고 있어야 합니다. 이를 위해 Hugo 빌드를 시작하기 전에 Go로 만든 서버를 실행합니다.
./d2server.exe
- 이 명령어로 HTTP 서버를 실행한 후, Hugo는 D2 코드 블록을 감지하고 해당 코드를 서버로 보내 변환된 SVG를 받을 수 있습니다.
Hugo 빌드
서버가 실행된 후, 다음 명령어로 Hugo 사이트를 빌드합니다.
hugo --gc --minify
- 이 명령은 Hugo 사이트를 빌드하며, 빌드 과정에서 D2 다이어그램이 렌더링된 SVG로 변환되어 Hugo 페이지에 포함됩니다.
빌드 후 서버 종료
Hugo 빌드가 완료되면, 더 이상 HTTP 서버가 필요 없기 때문에 서버를 종료할 수 있습니다.
taskkill /IM d2server.exe /F
- 이 명령어는
d2server.exe
프로세스를 종료시킵니다.
Shortcode 방식
Hugo에서 D2 다이어그램을 렌더링하기 위한 또 다른 방법은 Shortcode를 사용하는 것입니다. Shortcode는 Hugo에서 동적 콘텐츠를 간단하게 삽입할 수 있도록 해주는 기능으로, 이를 사용하면 D2 다이어그램을 별도의 서버나 외부 프로세스를 통해 렌더링할 수 있습니다. 이 방식은 특정 템플릿과 자바스크립트를 함께 사용하여 Hugo 사이트에서 다이어그램을 쉽게 삽입하는 데 유용합니다.
Shortcode 정의
먼저 layouts/shortcodes/
디렉토리 안에 d2-diagram.html
이라는 Shortcode 파일을 만듭니다. 이 파일은 D2 다이어그램을 렌더링하는 로직을 포함하게 됩니다.
d2-diagram.html
<div class="d2-diagram">
<pre>{{ .Inner | htmlEscape }}</pre>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const diagramCode = document.querySelector('.d2-diagram pre').innerText;
renderD2FromSource(diagramCode); // 이 함수는 D2 다이어그램을 렌더링하는 자바스크립트 코드
});
</script>
.Inner
: Shortcode 내부에 정의된 콘텐츠를 가져옵니다.renderD2FromSource()
: D2 다이어그램을 렌더링하기 위한 자바스크립트 함수로, D2 코드 블록을 SVG로 변환하는 작업을 처리합니다. 이 함수는 브라우저 측에서 실행되며, D2 다이어그램을 시각적으로 표현하는 역할을 합니다.
자바스크립트 파일 설정
Shortcode가 제대로 동작하려면, D2 다이어그램을 처리하는 자바스크립트 코드가 필요합니다. 이 코드는 D2 코드를 SVG로 변환하고 웹 페이지에 삽입하는 역할을 합니다. Hugo의 정적 파일 디렉토리인 static/js/
폴더에 자바스크립트 파일을 생성합니다.
d2-diagram.js
function renderD2FromSource(source) {
const svg = d2(source); // D2 코드를 받아서 SVG로 변환
document.querySelector('.d2-diagram').innerHTML = svg;
}
d2()
함수는 D2 코드를 받아서 SVG 형식으로 변환하는 역할을 합니다. 이 스크립트 파일은 페이지가 로드될 때마다 다이어그램을 자동으로 렌더링해 줍니다.
Hugo 사이트에 자바스크립트 추가
생성한 자바스크립트 파일을 Hugo 템플릿에 포함시켜야 합니다. 이를 위해 layouts/_default/baseof.html
파일에 아래의 코드를 추가합니다.
<script src="/js/d2-diagram.js"></script>
이 코드는 모든 페이지에서 d2-diagram.js
파일을 불러오게 하여, D2 다이어그램을 자동으로 렌더링할 수 있도록 설정합니다.
Markdown에서 Shortcode 사용
이제 D2 다이어그램을 Hugo 페이지에서 렌더링할 수 있습니다. Markdown 파일에서 아래와 같이 Shortcode를 호출하여 D2 다이어그램을 삽입합니다.
Markdown 예시:
{ {< d2-diagram >}}
a -> b: "Hello"
b -> c: "World"
{ {< /d2-diagram >}}
이 코드에서는 d2-diagram
Shortcode를 호출하여 D2 코드 블록을 정의하고 있습니다. 이 코드는 최종적으로 다이어그램으로 변환되어 페이지에 삽입됩니다.
방식별 장단점
Script 방식
장점
- 클라우드 빌드 호환성: Hugo 빌드 시 D2 다이어그램을 렌더링하기 위해 로컬 서버에 의존하지 않습니다. Cloudflare 등 클라우드 빌드 환경에서도 문제가 발생하지 않으며, 로컬에서 미리 렌더링한 정적 파일(SVG)을 사용합니다.
- 더 나은 성능: 다이어그램을 미리 렌더링하므로, 빌드 시 성능이 개선되고 페이지 로드 시간이 빨라집니다. 브라우저가 클라이언트 측에서 추가적인 렌더링 작업을 수행할 필요가 없습니다.
- 관리 용이성: 모든 D2 다이어그램이 정적 파일로 관리되기 때문에, 각 다이어그램의 상태를 명확하게 확인할 수 있으며, 캐싱이나 SEO 측면에서도 이점이 있습니다.
- 복잡한 구조 지원: D2 다이어그램을 완벽하게 제어하고, 파일명 변환 및 대체와 같은 작업을 자동화할 수 있습니다.
단점
- 초기 설정 및 유지보수: Markdown 파일에서 D2 코드를 추출하고 변환하는 별도의 스크립트를 작성해야 하므로 초기 설정이 다소 복잡할 수 있습니다. 또한, 프로젝트가 커질수록 스크립트의 유지보수가 필요합니다.
- 다이어그램 실시간 업데이트 불가: 다이어그램이 정적 파일로 저장되기 때문에, 실시간으로 코드를 수정하거나 결과를 확인하려면 수동으로 다시 빌드해야 합니다. 실시간 렌더링은 불가능합니다.
Render Hook 방식
장점
- 서버 측 처리: 빌드 중에 다이어그램을 서버에서 렌더링하기 때문에 클라이언트 측의 부담이 없습니다. 이로 인해 페이지 로드 시간이 빠릅니다.
- 통일된 렌더링: 모든 다이어그램이 빌드 중에 동일한 방식으로 처리되므로, 각 브라우저나 클라이언트 환경에 따른 차이가 없습니다.
- Markdown 문법 유지: Obsidian이나 다른 툴에서 사용하는 D2 문법을 그대로 유지할 수 있습니다. Render Hook을 통해 Markdown의 코드 블록을 감지하고, 별도의 문법 변형 없이 다이어그램을 생성할 수 있습니다.
- 빌드 결과물이 정적: 최종적으로 빌드된 HTML 페이지에 렌더링된 다이어그램이 포함되므로, 다이어그램이 추가적으로 로드되지 않고 바로 표시됩니다.
- SEO에 유리: 빌드 시 모든 다이어그램이 정적 HTML로 변환되므로, 검색 엔진 크롤러가 다이어그램 내용을 쉽게 인덱싱할 수 있습니다.
단점
- HTTP 서버 필요: D2 코드를 SVG로 변환하기 위해 빌드 중에 별도의 HTTP 서버가 필요합니다. 이 서버가 작동하지 않으면 다이어그램 렌더링이 실패합니다.
- 복잡성 증가: 서버를 설정하고 실행해야 하는 과정이 추가되므로, 설정과 유지보수가 더 복잡해질 수 있습니다.
- 성능 부하: 빌드 과정에서 모든 다이어그램을 서버에서 처리하기 때문에, 다수의 다이어그램을 포함한 사이트의 경우 빌드 시간이 길어질 수 있습니다.
Shortcode 방식
Shortcode 방식은 D2 다이어그램을 Hugo 템플릿을 통해 삽입하고, 브라우저에서 자바스크립트를 사용해 클라이언트 측에서 렌더링하는 방식입니다.
장점
- 서버 불필요: Shortcode 방식은 별도의 HTTP 서버를 요구하지 않으며, 모든 렌더링 작업이 클라이언트 측에서 이루어지기 때문에, 서버를 추가로 관리할 필요가 없습니다.
- 설치와 유지 관리 용이: 설정 과정이 간단하며, HTML과 자바스크립트만으로 구현할 수 있어 복잡한 서버 설정이 필요 없습니다.
- 실시간 렌더링: 사용자가 페이지를 로드할 때마다 자바스크립트가 실행되어 D2 다이어그램을 즉시 렌더링합니다. 브라우저에서 직접 렌더링하기 때문에 실시간으로 다이어그램을 확인할 수 있습니다.
- 테마에 따라 모드를 변경할 수 있습니다. 즉, 다크테마에서는 다크모드 랜더링이 가능합니다.
단점
- 성능 저하: 클라이언트 측에서 자바스크립트를 사용해 렌더링하기 때문에, 다이어그램이 많은 경우 페이지 로드 성능에 영향을 미칠 수 있습니다. 특히, 저사양 기기나 네트워크 속도가 느린 환경에서는 로딩 속도가 느려질 수 있습니다.
- 브라우저 의존성: 다이어그램 렌더링이 자바스크립트에 의존하므로, 사용자가 자바스크립트를 비활성화하거나 브라우저의 성능에 따라 다이어그램 표시가 지연될 수 있습니다.
- SEO에 불리: 다이어그램이 클라이언트 측에서 렌더링되기 때문에, 검색 엔진 크롤러가 다이어그램을 제대로 인식하지 못할 가능성이 있습니다. 이로 인해 SEO 측면에서 불리할 수 있습니다.
성능 차이
- Script 방식: 다이어그램을 미리 렌더링하므로, 빌드 시 성능이 개선되고 페이지 로드 시간이 빨라집니다.
- Render Hook 방식: 빌드 시 서버에서 다이어그램을 처리하기 때문에, 빌드 시간이 더 오래 걸릴 수 있지만 최종적으로 생성된 페이지는 정적으로 다이어그램을 포함하여 페이지 로드 성능은 더 우수합니다.
- Shortcode 방식: 페이지 로드 시 브라우저에서 다이어그램을 렌더링하므로, 빌드 속도는 빠르지만 다이어그램이 많을 경우 클라이언트 성능에 따라 페이지 로딩 시간이 느려질 수 있습니다.
비교 요약
방법 | 장점 | 단점 |
---|---|---|
스크립트 방식 | 클라우드 호환성, 더 나은 성능, 정적 파일 관리, 복잡한 구조 지원 | 초기 설정 복잡, 실시간 업데이트 불가 |
Render Hook | 자동 렌더링, 실시간 렌더링, 유연한 템플릿 적용 | 클라우드 환경 호환성 문제, 추가 서버 필요, 성능 이슈 |
Shortcode | 간편한 설정, 실시간 렌더링, 별도 서버 필요 없음 | 브라우저 성능 의존, SEO 문제, 복잡한 다이어그램 관리 어려움 |