Auto Resume
2025/03/01
n°64
category : Recap
☼
2025/03/01
n°64
category : Recap

배포된 사이트: 링크
깃헙 레포 : 링크
작업 소요 시간: 1.5일 (MVP 버전)
Auto Resume는 Figma 프로젝트를 원본 데이터(Source of Truth)로 삼는 정적 HTML 이력서 웹사이트입니다.
이번 프로젝트에는 다음과 같은 목적이 있었습니다.
Figma = Source of Truth 피그마 문서와 동기화된 웹사이트
자동 배포: Figma 변경 → JSON 변환 → HTML 생성 → 배포까지 수동 개입 없이 처리.
Figma 변경점이 CI/CD의 기준점이 됨
최소한의 UI 코드와 최대한의 동기화
최근 블로그에 Tiptap 에디터를 적용하면서 동적 마크업, 특히 직렬화된 HTML 파싱에 대해 고민 중이었습니다. 서버/클라이언트 컴포넌트와 같은 방식 말고 다른 방법은 없을까 궁금했습니다.
마침, 하드 코딩하는 것이 번거로워 업데이트를 안하게 된 제 이력서 웹사이트가 눈에 들어왔습니다. 실제로 지원할 때 사용하는 이력서와 꽤 큰 차이가 있었습니다. 바로 해봐야겠다 싶었습니다.
노트에 설계와 과정, 필요한 부분을 정리했습니다.
Figma에서 문서가 바뀌었다는 신호만 서버에 전달할 수 있다면, 나머지는 가능하지 않을까 싶었습니다.
stateDiagram
state "Figma > Source Truth" as Truth {
이력서작성: 새로운 이력서를 작성한다
버전변경: 이력서 파일을 피그마에서 "Publish"한다
웹훅: 피그마에서 웹훅이 실행된다.
이력서작성 --> 버전변경
버전변경 --> 웹훅
}
Truth --> Server: 버전 변경을 서버에 알림
state "Workers(서버리스)" as Server {
Listen: 📡 웹훅으로부터 버전 변경을 감지한다.
FigmaAPI: 새로운 버전의 데이터를 피그마 API로 요청한다.
R2: 응답 받은 피그마 JSON을 버켓에 저장한다.
FIN: Cloudflare Pages의 deploy-hook을 트리거하고 Workers 종료.
Listen --> FigmaAPI
FigmaAPI --> R2
R2 --> FIN
}
Server --> Build: Pages Deploy Hook으로 빌드 트리거
state "빌드 Build" as Build {
INIT: Deploy Hook이 실행되면 Github 레포에 저장된 빌드 스크립트가 실행된다.
GET: R2에 저장된 JSON을 가져온다.
BACKUP: 메타 데이터를 추출해 저장한다.
PARSE: JSON을 파싱하여 HTML을 생성한다.
배포트리거: 생성이 완료된 HTML을 Pages가 배포하도록 한다.
INIT --> GET
GET --> BACKUP
GET --> PARSE
BACKUP --> 배포트리거
PARSE --> 배포트리거
}
Build --> StaticSite: Pages를 통한 배포
state "배포: 정적 사이트 호스팅" as StaticSite {
Pages: 빌드 완료 이후 새로 생성된 HTML을 감지한다.
Deploy: Pages는 새로운 HTML을 배포한다.
Pages --> Deploy
}계획을 작성할 당시엔 다음과 같은 이유에서 스택을 결정했습니다.
Figma Webhook | Figma 프로젝트 Publish 시 웹훅 트리거 (FILE_UPDATE 이벤트 사용) |
Cloudflare Worker | 웹훅 이벤트 처리, Figma API 호출, JSON을 R2에 저장, Deploy Hook 실행 |
Cloudflare R2 | 최신 Figma JSON을 저장하는 클라우드 스토리지. 무료 플랜에서 Workers 실행은 30s 한도가 있다. R2에 JSON을 저장하고, 배포 스크립트가 실행될 때 JSON을 가져온다. |
GitHub Repository | 빌드 및 배포 스크립트 저장 |
Node.js + Handlebars | 빌드 스크립트에서 사용된다. JSON을 기반으로 HTML 생성한다. |
Cloudflare Pages | 정적 HTML을 자동 배포하기 위해 CDN을 사용한다. |
위 내용을 바탕으로 각 단계를 구체화했습니다.
Figma 프로젝트가 Publish되면, 웹훅을 통해 Cloudflare Worker를 호출.
Worker는 Figma API를 사용하여 최신 JSON을 가져옴.
가져온 JSON을 Cloudflare R2에 저장한 후, Pages Deploy Hook을 트리거.
Deploy Hook이 호출되면 빌드 스크립트를 실행.
빌드가 실행되는 Node.js 환경에서:
R2에서 Figma JSON을 가져옴.
Handlebars를 사용해 JSON 기반 마크업 생성.
생성된 HTML을 dist/ 폴더에 저장.
Cloudflare Pages의 dist/ 경로로 정적 사이트를 자동 배포.
알고보니 Figma Webhook은 Figma Pro만 가능했습니다. 고민 끝에 Figma Pro를 사용하지 않기로 결정했습니다. 이 결정에는 다음과 같은 이유가 있었습니다.
프로젝트 목적 자체가 Figma Design Mode만 사용하는 것이었다.
Figma Pro (Dev Mode)가 제공하는 마크업과 CSS를 사용해야 한다.
Figma JSON의 Raw Data가 아니라 Figma 내부의 마크업 생성 로직이 의존성 레이어로 추가된다.
Figma API JSON이 변동사항이 적고, 대응도 쉽다.
또한 경험 상, Figma가 생성한 마크업과 CSS의 신뢰성이 높지 않았다.
Figma Webhook의 대안으로 Figma 커스텀 플러그인을 만들면, 본래 목적을 이룰 수 있다.
Figma에서 디자인 작업을 한 뒤, 작업 변동 여부만 서버에 전달하는 기능만 필요한 것이라 충분히 가능해 보였습니다.
웹훅을 대체 할 커스텀 플러그인을 만들었습니다. 커스텀 플러그인 내부에서 Figma API를 미리 요청하고 Worker로 넘기면, Worker의 런타임 시간도 줄어들어 더욱 적절해 보였습니다.
피그마 커스텀 플러그인은 Figma Desktop App에서 열 수 있습니다. manifest.json으로 플러그인을 정의하고, ui.html, plugin.js로 간단한 플러그인을 개발했습니다. 여기서 가장 중요한 plugin.js만 살펴보자면:
// figma는 플러그인이 실행될 때 전역 변수로 제공된다.
// 플러그인의 ui를 html로 연다. ui.html
figma.showUI(__html__, { width: 300, height: 150 });
// 플러그인에서 Figma API로 JSON을 요청한다.
async function fetchFigmaJSON() {
try {
console.log("Fetching Figma JSON...");
const response = await fetch(`https://api.figma.com/v1/files/${FIGMA_FILE_KEY}`, {
headers: { "X-Figma-Token": FIGMA_API_KEY }
});
if (!response.ok) throw new Error(`Failed to fetch Figma JSON: ${response.statusText}`);
const json = await response.json();
console.log("Fetched JSON successfully:", json);
return json;
} catch (error) {
console.error("Figma API Error:", error.message);
figma.notify("❌ Figma API Error: " + error.message);
return null;
}
}
// figma.ui에 onmessage 이벤트를 등록한다.
figma.ui.onmessage = async (msg) => {
if (msg.type === "send-update") {
// "send-update"라는 이벤트로 요청을 시작한다.
console.log("Send update triggered");
try {
// 1. Figma JSON 요청
const jsonData = await fetchFigmaJSON();
if (!jsonData) {
figma.notify("❌[FIGMA_API]:Failed to fetch Figma data");
}
console.log("✅[FIGMA_API]:", JSON.stringify(jsonData));
// 2. R2로 Figma JSON을 저장한다.
const response = await fetch("https://auto-resume-worker.leetekwoo.com", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(jsonData),
});
console.log("Worker response status:", response.status);
if (!response.ok) {
throw new Error(`❌[WORKER] Error: ${response.statusText}`);
}
const res = await response.json();
figma.notify("✅[DONE]: Resume update sent successfully!");
} catch (error) {
console.error("❌ Fetch Error:", error.message);
figma.notify("❌ Network error: " + error.message);
} finally {
// 정확히 figma.notify가 실행된 이후 닫히도록 하기 위해 추가
await new Promise(r => setTimeout(r, 500));
figma.closePlugin();
}
}
};플러그인이 실행되면, ui.html의 버튼 이벤트로 실행합니다. workers와의 통신에서 CORS 때문에 꽤 오랫동안 트러블 슛팅을 했는데, 이후 살펴보도록 하겠습니다.
JSON으로 마크업을 구성하기 위해 피그마 JSON을 분석했습니다. Figma API의 GET 요청은 각 그래픽 요소를 노드로 나타냅니다. 그래픽 정보는 키-값으로 표현되고, children의 배열로 계층이 표현됩니다.
마크업을 표현하는 일반적인 자료 구조(ex:DOM api, ReactElement)와 유사하지만, Figma 고유의 필드명과 프로퍼티가 다르기 때문에 별도의 맵핑과 파싱이 필요합니다.
이번 프로젝트에서 중요하게 사용한 필드들은 다음과 같습니다:
{
// 부모:n번째 자식 - 으로 노드 연결관계를 표현한다
"id": "0:1",
// 피그마 레이어의 이름이 설정된다.
// NOTE: 파싱에서 로직을 단순화하기 위한 인덱스로 사용했다.
"name": "truth-resume",
// NOTE: 파싱에서 사용. 타입 구분이 중요하다.
"type": "DOCUMENT | CANVAS | FRAME | LAYOUT | TEXT | IMAGE | ...",
// NOTE: HTTP 캐시 헤더처럼 활용할 수 있다
"lastModified": "2025-03-01T03:10:10Z",
// NOTE: 재귀적으로 계층 구조를 나...