블로그 만들기 05 - 별자리 그래프 만들기
2025-08-28
🧠 요약
- 3D 그래프 구성: react-force-graph로 그래프 그리기
- 데이터 파싱: 블로그 게시글 데이터를 그래프 데이터 형식에 맞게 파싱하기
📌 본문
🌌 react-force-graph로 나만의 별자리 블로그 만들기
블로그 게시글들을 하나의 우주처럼, 별과 행성으로 표현해본다면 어떨까?
한동안 obsidian을 많이 사용하면서, 가장 사용해보고 싶었지만 잘 사용할 수 없었던 것이 그래프 뷰였다. 아무래도 글마다 백링크를 통해 연관성을 주어야하는데, 내가 잘 활용을 하지 못하고 있는것 같았다.
보기만 해도 굉장하다..
나도 뭐.. 한것같긴한데..
어쨌든 이걸 응용해서 블로그에 그래프로 게시글마다의 연관성을 보여주고자 한다.
react-force-graph 이 라이브러리를 사용하겠다.
🎯 목표
-
블로그 게시글을 3D 그래프로 표현
-
slug 경로를 기반으로 트리 형태를 시각화
-
tag로 연결된 관계 표현
-
각 노드는 별처럼, 배경은 우주처럼 구성
-
클릭 시 실제 게시글/태그/카테고리로 이동 가능
🛠️ 사용한 기술
-
react-force-graph-3d: 3D force-directed graph 시각화 라이브러리
-
three.js: 배경 이미지 및 커스텀 3D 객체 렌더링
-
Next.js (App Router) + TypeScript
-
CanvasTexture를 활용한 3D 텍스트 라벨
-
Image 컴포넌트로 우주 배경 추가
📝 데이터 파싱
- 블로그 게시글 구조
export type Post = { id: string; slug: string; title: string; date: Date; tags: string[]; description: string; content: string; };
- slug: "프로젝트/블로그만들기/블로그 만들기 01"처럼 경로 구조
- tags: "Next.js", "Vercel" 등의 키워드
여기서 현재 그래프에서 사용하고 있는 요소는 id, slug, title, tags이다.
- 그래프 데이터 변환
import { Post } from "app/_lib/post"; interface GraphNode { id: string; group: string; } interface GraphLink { source: string; target: string; } export interface GraphData { nodes: GraphNode[]; links: GraphLink[]; } export const getGraphData = (posts: Post[]): GraphData => { const nodesMap = new Map<string, GraphNode>(); const linkSet = new Set<string>(); // 중복 방지를 위한 Set const links: GraphLink[] = []; const addNode = (id: string, group: GraphNode["group"]) => { const upperId = id.toUpperCase(); // 대소문자 통일 if (!nodesMap.has(upperId)) { nodesMap.set(upperId, { id: upperId, group }); } return upperId; }; for (const post of posts) { const titleId = addNode(post.title, "posts"); // Slug를 트리로 표현 const parts = post.slug.split("/"); for (let i = 0; i < parts.length - 1; i++) { const parent = addNode(parts[i] ?? "", "parent_groups"); const childId = parts[i + 1] ?? ""; const child = addNode( childId, i === parts.length - 2 ? "posts" : "groups", ); const key = `${parent}->${child}`; if (!linkSet.has(key)) { links.push({ source: parent, target: child }); linkSet.add(key); } } // 태그 → 게시글 연결 for (const tag of post.tags) { const tagId = addNode(tag, "tags"); const key = `${titleId}->${tagId}`; if (!linkSet.has(key)) { links.push({ source: titleId, target: tagId }); linkSet.add(key); } } } return { nodes: Array.from(nodesMap.values()), links, }; };
각 게시글마다 slug를 통해 트리 노드를 표현할 수 있다. 내 블로그에서는 계층을 2계층만 사용하고 있기 때문에 "parent_groups", "groups" 두개의 그룹 노드로 분리해주었다.
또한, tag들의 중복을 제거한 뒤, 게시글 제목과 연결해주면 된다.
🌌 그래프 그리기
그래프를 표시하기 전, 그래프를 가져올 때는 dynamic을 사용해야한다.
import dynamic from "next/dynamic"; import BackgroundImage from "public/background1.jpg"; const ForceGraph3D = dynamic(() => import("react-force-graph-3d"), { ssr: false, });
ForceGraph3D 컴포넌트를 서버사이드렌더링 하지 않고, 클라이언트 사이드에서만 렌더링하게 하기 위함인데, 그 이유는 다음과 같다.
- react-force-graph-3d는 브라우저 전용 API를 사용한다.
window, document, WebGLRenderingContext 등 여러 API를 사용하므로 SSR 사용이 불가하다.
- 페이지 로딩 최적화를 할 수 있다.
dynamic 함수는 코드 스플리팅 기능도 제공한다. 즉, 초기 페이지 로딩 시 react-force-graph-3d 와 같이 무거운 컴포넌트를 미리 가져오지 않고, 해당 컴포넌트가 실제로 필요한 시점에서만 로드함으로써 퍼포먼스를 최적화 할 수 있다.
그리고 기본적으로 backgroundColor를 조절할 수 있는데, 단색의 색상보다, 이미지를 추가하고싶어서 다음과 같이 설정했다.
<Image src={BackgroundImage} alt="Background" fill style={{ objectFit: "cover" }} priority /> <ForceGraph3D ref={fgRef} graphData={graphData} backgroundColor="#ffffff00" nodeLabel="id" nodeAutoColorBy="group" nodeRelSize={4} cooldownTicks={100} // 시뮬레이션 지속 시간 linkCurvature={0.25} linkDirectionalArrowLength={3.5} linkDirectionalArrowRelPos={1} showNavInfo={true} onNodeClick={(node) => handleNodeClick(node)} nodeThreeObject={(node) => { const group = new THREE.Group(); const colorMap = { parent_groups: "#FF69B4", // 핫핑크 (부모 그룹 느낌) groups: "#00BFFF", // 딥스카이 블루 (은하 느낌) posts: "#FFD700", // 밝은 금색 (별빛 느낌) tags: "#ADFF2F", // 형광 연두 (네온빛 태그) }; // 타입별 구체 크기 설정 const sizeMap = { parent_groups: 15, groups: 10, posts: 8, tags: 6, }; const radius = sizeMap[node.group as keyof typeof sizeMap] || 5; const sphere = new THREE.Mesh( new THREE.SphereGeometry(radius), new THREE.MeshStandardMaterial({ color: colorMap[node.group as keyof typeof colorMap] || "#cccccc", emissive: colorMap[node.group as keyof typeof colorMap] || "#cccccc", emissiveIntensity: 0.7, metalness: 0.6, roughness: 0.2, }), ); group.add(sphere); if (node.group === "groups" || node.group === "parent_groups") { const label = new THREE.Sprite( new THREE.SpriteMaterial({ map: createTextTexture(node.id?.toString() || ""), depthWrite: false, }), ); label.scale.set(60, 40, 3); // 텍스트 사이즈 label.position.set(0, radius + 5, 0); // 텍스트 위치 위로 group.add(label); } return group; }} />
이런식으로 그래프의 원 배경색상을 "#FFFFFF00"으로 투명하게 만든 뒤, 미리 사용하고 싶은 배경 이미지를 추가하면 된다.
구성하면 이런식으로 나오게 된다.
우주를 배경으로 별처럼 꾸며보고자 했으나.. 미적 감각 이슈로 천천히 개선해보기로 하였다.
🔄 회고 및 인사이트
- react-force-graph 적재적소에 잘 사용한다면 기가막힐 것 같다.
- 우주를 채운다는 기분으로 블로그를 더 열심히 할 수 있을것 같다.
- 추후에 기능을 더 추가해서 멋지게 바꿔보고 싶다.