Satori를 통해 HTML과 CSS를 SVG로 변환할 수 있다는 것을 발견하고, 이를 활용해 JSX 컴포넌트로 깃허브 리드미를 꾸미는 방법을 소개하려고 한다.
먼저 완성본이다.
로딩 중...
로딩 중...
먼저 리드미 레포지토리를 로컬 환경에 클론한다.
리드미 레포지토리는 본인 닉네임과 동일한 이름으로 생성되어 있으며, 없다면 생성하면 된다.
git clone https://github.com/<your-nickname>/<your-nickname>.git
처음 레포지토리를 만들었거나 따로 리드미 레포지토리를 건드린 적이 없다면 파일 구조는 다음과 같을 것이다.
그 후, 레포지토리 루트에서 다음 명령어를 실행하여 프로젝트를 초기화한다.
npm init
이제 리드미 파일을 꾸미기 위해 필요한 패키지를 설치한다.
npm install satori react @babel/cli @babel/core @babel/preset-react babel-plugin-add-import-extension
설치가 완료되었다면, 기본 설정을 위해 .babelrc
파일을 생성한다.
// .babelrc
{
"presets": ["@babel/preset-react"],
"plugins": [
["babel-plugin-add-import-extension", { "extension": "js" }]
]
}
package.json
파일 스크립트 및 모듈 설정도 추가한다.
// package.json
...
"scripts": {
"prebuild": "babel src --out-dir dist", // 빌드 전에 바벨로 트랜스파일
"build": "node dist/main.js" // 트랜스파일된 파일을 실행
},
"type": "module",
...
마지막으로 src
디렉토리와 resources
디렉토리를 생성하고, src
디렉토리 안에 render.jsx
파일을 생성한다.
지금까지의 파일 구조는 다음과 같다.
이제 작성한 JSX 컴포넌트를 SVG로 렌더링하는 함수를 작성한다. render.jsx
파일을 열고 다음 코드를 작성한다.
satori를 사용하여 SVG로 렌더링하고 이를 따로 저장한다.
폰트의 경우 TTF, OTF, WOFF 형식만 사용할 수 있다. 미리 폰트를 다운받아 레포지토리 루트에 위치시킨다.
resources
디렉토리에 저장한다.import React from 'react';
import satori from 'satori';
import fs from 'fs';
export async function generateSvg(element, outputName, options = {}) {
const font = fs.readFileSync('./SeoulAlrimTTF-Heavy.ttf')
const svg = await satori(
element,
{
width: options.width,
height: options.height,
fonts: [
{
name: "SeoulAlrimTTF-Heavy",
data: font,
weight: 900,
style: "normal",
},
],
loadAdditionalAsset: async (code, segment) => {
if (code === "emoji") {
// 유니코드 이모지 코드 포인트 16진수 변환
const codePoint = segment.codePointAt(0).toString(16);
try {
// Twemoji SVG API를 사용하여 이모지 가져오기
const response = await fetch(`https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/${codePoint}.svg`);
if (response.ok) {
const svgText = await response.text();
// SVG 텍스트를 base64로 인코딩하여 반환
return `data:image/svg+xml;base64,${Buffer.from(svgText).toString('base64')}`;
}
} catch (error) {
console.warn(`Failed to load emoji ${segment}:`, error);
}
// 이모지 로드 실패 fallback
return `data:image/svg+xml;base64,${Buffer.from('<svg xmlns="http://www.w3.org/2000/svg" width="72" height="72" viewBox="0 0 72 72"><rect width="72" height="72" fill="#ccc"/><text x="36" y="45" text-anchor="middle" font-size="20">?</text></svg>').toString('base64')}`;
}
return undefined;
}
}
);
fs.writeFileSync(`./resources/${outputName}.svg`, svg);
return svg;
}
이제 작성한 함수를 테스트해보자. src
디렉토리 안에 main.jsx
파일을 생성하고, 다음 코드를 작성한다.
import React from "react";
import { generateSvg } from "./render";
async function main() {
await generateSvg(<div>✨ 테스트입니다</div>, 'test');
}
main();
코드 작성 후 빌드를 실행한다.
npm run build
빌드 후에 resources
디렉토리에 테스트 SVG 파일이 생성된 것을 확인할 수 있다. SVG 프리뷰로 열어보면 다음과 같다.
로딩 중...
리드미에 추가하여 확인해보면 다음과 같다.
로딩 중...
picture 요소를 사용하여 사용자의 시스템에 따라 이미지가 자동으로 변경되도록 설정한다.
<picture>
<source media="(prefers-color-scheme: dark)" srcset="다크모드경로">
<img src="라이트모드경로" />
</picture>
이제 본인이 원하는대로 컴포넌트를 작성하면 된다. 코드 가독성을 위해 컴포넌트를 여러 파일로 분리할 수 있다.
import React from "react";
import { generateSvg } from "./render";
import Kamilereon from "./kamilereon";
import Takeoff from "./takeoff";
async function main() {
await generateSvg(<Title darkMode>✨ Toy Project</Title>, "darkToyProject");
await generateSvg(<Title darkMode>⚡ Preferred Tech</Title>, "darkFavoriteTech");
await generateSvg(<Title darkMode>💻 Coding Activity</Title>, "darkCodingActivity");
await generateSvg(<Title darkMode>💡 Algorithm Problem Solving</Title>, "darkAlgorithmProblemSolving");
await generateSvg(<Title>✨ Toy Project</Title>, "toyProject");
await generateSvg(<Title>⚡ Preferred Tech</Title>, "favoriteTech");
await generateSvg(<Title>💻 Coding Activity</Title>, "codingActivity");
await generateSvg(
<Title>💡 Algorithm Problem Solving</Title>,
"algorithmProblemSolving"
);
await generateSvg(<Kamilereon />, "kamilereon",
{ width: 600, height: 60 });
await generateSvg(<Takeoff />, "takeoff",
{ width: 600, height: 60 });
}
function Title({ children, darkMode = false }) {
return (
<div
style={{
position: "relative",
color: darkMode ? "white" : "black",
display: "flex",
fontSize: "24px",
fontFamily: "SeoulAlrimTTF-Heavy",
padding: "5px 0px",
}}
>
{children}
</div>
);
}
main();
전체코드는 여기에서 확인할 수 있다.