🏢 Figma 기술 스택 분석
피그마가 브라우저에서 실시간 협업 디자인 툴을 구현한 기술 스택 심층 분석 - C++, WebAssembly, WebGL로 만든 차세대 디자인 플랫폼
🏢 Figma 기술 스택 분석
브라우저에서 네이티브 앱 수준의 디자인 툴을 구현한 Figma의 기술 스택을 심층 분석합니다.
"Design, prototype, and collaborate in real time" - 웹 기술의 한계를 넘어서는 혁신
📊 회사 개요
서비스 규모
- 월간 활성 사용자: 400만+ 명
- 생성된 디자인 객체: 30억+ 개
- 팀: 100만+ 개
- 동시 편집자: 파일당 200+ 명
- 렌더링 성능: 60fps 유지
엔지니어링 조직
- 엔지니어: 300명+ (Adobe 인수 후 확대)
- 문화: "Make design accessible to everyone"
- 기술 철학: 웹 기술의 한계 돌파
- 오픈소스: 제한적 (핵심 기술은 비공개)
기술적 도전과제
- 브라우저 성능: 네이티브 앱 수준 속도
- 실시간 협업: 수백 명 동시 편집
- 벡터 렌더링: 복잡한 그래픽 실시간 처리
- 대규모 파일: GB 단위 디자인 파일
🏗️ 아키텍처 Overview
시스템 다이어그램
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Browser │────►│ WebSocket │────►│Multiplayer │
│ (Client) │ │ Server │ │ Engine │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
│ ┌─────▼─────┐ ┌─────▼─────┐
│ │ Render │ │ Document │
│ │ Server │ │ Store │
│ └───────────┘ └───────────┘
│ │ │
└────────────────────┼────────────────────┘
┌─────▼─────┐
│WebAssembly│
│ Engine │
└───────────┘
핵심 설계 원칙
- Performance First: 60fps 렌더링 목표
- Web-Native: 플러그인 없는 순수 웹
- Real-time Sync: 밀리초 단위 동기화
- Infinite Canvas: 무한 확장 가능
- Version Control: Git 같은 디자인 버전 관리
기술 진화
- 2012-2013: WebGL 프로토타입
- 2014-2015: C++ 엔진 개발
- 2016-2017: WebAssembly 이전
- 2018-2019: 멀티플레이어 엔진
- 2020-2021: 대규모 최적화
- 2022-현재: AI 기능 통합
🧩 기술 스택 상세
렌더링 엔진 (C++ → WebAssembly)
벡터 그래픽 엔진
// Figma의 렌더링 엔진 핵심 (C++)
class FigmaRenderer {
private:
std::unique_ptr<Canvas> canvas;
std::unique_ptr<Scene> scene;
RenderCache cache;
GPUContext* gpu_context;
public:
void render(const Frame& frame) {
// 1. 더티 영역 계산
auto dirty_regions = calculate_dirty_regions(frame);
// 2. 렌더 트리 구축
auto render_tree = build_render_tree(frame.root);
// 3. 레이어 최적화
optimize_layers(render_tree);
// 4. GPU 명령 생성
auto commands = generate_gpu_commands(render_tree, dirty_regions);
// 5. WebGL로 렌더링
gpu_context->execute(commands);
// 6. 캐시 업데이트
cache.update(dirty_regions);
}
// 효율적인 히트 테스트
Node* hit_test(const Point& point) {
// Spatial indexing으로 빠른 검색
auto candidates = spatial_index.query(point);
for (auto node : candidates) {
if (node->contains_point(point)) {
// 정확한 경로 테스트
if (precise_hit_test(node, point)) {
return node;
}
}
}
return nullptr;
}
// 벡터 경로 래스터화
void rasterize_path(const Path& path, const Transform& transform) {
// Bezier 곡선을 삼각형으로 분해
auto triangles = tessellate_path(path);
// 안티엘리어싱 적용
apply_msaa(triangles, 4); // 4x MSAA
// GPU 버퍼에 업로드
gpu_context->upload_triangles(triangles);
}
};
// 효율적인 텍스트 렌더링
class TextRenderer {
private:
struct GlyphCache {
std::unordered_map<GlyphKey, GlyphData> glyphs;
TextureAtlas atlas;
};
GlyphCache cache;
public:
void render_text(const TextNode& text) {
// 글리프 준비
prepare_glyphs(text.content, text.font);
// 레이아웃 계산
auto layout = calculate_layout(text);
// SDF (Signed Distance Field) 렌더링
for (const auto& glyph_run : layout.runs) {
render_glyph_run(glyph_run);
}
}
void render_glyph_run(const GlyphRun& run) {
// 배치 렌더링으로 드로우콜 최소화
auto batch = create_glyph_batch(run);
// 서브픽셀 안티엘리어싱
apply_subpixel_aa(batch);
gpu_context->draw_instanced(batch);
}
};
WebAssembly 브릿지
// TypeScript에서 WebAssembly 모듈 사용
export class FigmaEngine {
private wasmModule: WebAssembly.Module;
private wasmInstance: WebAssembly.Instance;
private memory: WebAssembly.Memory;
private renderFunc: (framePtr: number) => void;
async initialize() {
// WebAssembly 모듈 로드
const response = await fetch("/figma-engine.wasm");
const bytes = await response.arrayBuffer();
// 컴파일 및 인스턴스화
this.wasmModule = await WebAssembly.compile(bytes);
// 메모리 공유
this.memory = new WebAssembly.Memory({
initial: 256, // 16MB
maximum: 16384, // 1GB
shared: true, // SharedArrayBuffer for threading
});
// Import 객체 설정
const imports = {
env: {
memory: this.memory,
// WebGL 바인딩
glCreateShader: (type: number) => gl.createShader(type),
glShaderSource: (shader: number, source: number) => {
const sourceStr = this.readString(source);
gl.shaderSource(shaders.get(shader), sourceStr);
},
glCompileShader: (shader: number) =>
gl.compileShader(shaders.get(shader)),
// ... 더 많은 WebGL 함수들
},
// JavaScript 콜백
js: {
onRenderComplete: () => this.handleRenderComplete(),
requestAnimationFrame: (callback: number) => {
requestAnimationFrame(() => {
this.wasmInstance.exports.invokeCallback(callback);
});
},
},
};
this.wasmInstance = await WebAssembly.instantiate(this.wasmModule, imports);
this.renderFunc = this.wasmInstance.exports.render as any;
}
render(sceneData: SceneData) {
// Scene 데이터를 WASM 메모리에 복사
const scenePtr = this.allocateScene(sceneData);
// C++ 렌더링 함수 호출
this.renderFunc(scenePtr);
// 메모리 정리
this.freeScene(scenePtr);
}
// 효율적인 메모리 관리
private allocateScene(scene: SceneData): number {
const size = this.calculateSceneSize(scene);
const ptr = this.wasmInstance.exports.malloc(size);
// 구조체 데이터 직렬화
const view = new DataView(this.memory.buffer, ptr, size);
this.serializeScene(scene, view);
return ptr;
}
}
실시간 협업 시스템
멀티플레이어 엔진
// Rust로 구현된 고성능 동기화 서버
use tokio::net::{TcpListener, TcpStream};
use dashmap::DashMap;
use bytes::{Bytes, BytesMut};
#[derive(Clone)]
struct FigmaDocument {
id: String,
nodes: Arc<RwLock<HashMap<NodeId, Node>>>,
operations: Arc<Mutex<Vec<Operation>>>,
clients: Arc<DashMap<ClientId, Client>>,
}
#[derive(Debug)]
enum Operation {
Create { node_id: NodeId, data: NodeData },
Update { node_id: NodeId, changes: Vec<Change> },
Delete { node_id: NodeId },
Move { node_id: NodeId, parent_id: NodeId, index: usize },
}
struct MultiplayerServer {
documents: Arc<DashMap<String, FigmaDocument>>,
connections: Arc<DashMap<ClientId, Connection>>,
}
impl MultiplayerServer {
async fn handle_client(self: Arc<Self>, stream: TcpStream, client_id: ClientId) {
let (reader, writer) = stream.split();
loop {
match read_message(&mut reader).await {
Ok(message) => {
self.process_message(client_id, message).await;
}
Err(_) => {
self.disconnect_client(client_id).await;
break;
}
}
}
}
async fn process_message(&self, client_id: ClientId, message: Message) {
match message {
Message::Operation { doc_id, op } => {
self.handle_operation(doc_id, client_id, op).await;
}
Message::Subscribe { doc_id } => {
self.subscribe_to_document(client_id, doc_id).await;
}
Message::Cursor { position } => {
self.broadcast_cursor(client_id, position).await;
}
}
}
async fn handle_operation(
&self,
doc_id: String,
client_id: ClientId,
operation: Operation
) {
let doc = self.documents.get(&doc_id).unwrap();
// 1. 변환 (Operational Transform)
let transformed = self.transform_operation(operation, &doc).await;
// 2. 적용
self.apply_operation(&doc, &transformed).await;
// 3. 브로드캐스트
self.broadcast_operation(&doc, client_id, &transformed).await;
// 4. 영속성
self.persist_operation(&doc_id, &transformed).await;
}
// OT (Operational Transformation) 구현
async fn transform_operation(
&self,
op: Operation,
doc: &FigmaDocument
) -> Operation {
let concurrent_ops = doc.operations.lock().await;
let mut transformed = op;
for concurrent_op in concurrent_ops.iter() {
transformed = self.transform_pair(transformed, concurrent_op);
}
transformed
}
fn transform_pair(&self, op1: Operation, op2: &Operation) -> Operation {
match (op1, op2) {
(Operation::Move { node_id: id1, parent_id: p1, index: i1 },
Operation::Move { node_id: id2, parent_id: p2, index: i2 }) => {
if id1 == *id2 {
// 같은 노드 이동 - 나중 것 우선
op1
} else if p1 == *p2 {
// 같은 부모로 이동
if i1 > *i2 {
Operation::Move {
node_id: id1,
parent_id: p1,
index: i1 - 1
}
} else {
op1
}
} else {
op1
}
}
// ... 더 많은 변환 규칙
_ => op1
}
}
}
// 효율적인 diff 알고리즘
fn calculate_diff(before: &Node, after: &Node) -> Vec<Change> {
let mut changes = Vec::new();
// 속성 비교
if before.properties != after.properties {
changes.push(Change::Properties {
old: before.properties.clone(),
new: after.properties.clone(),
});
}
// 자식 노드 비교 (Myers diff algorithm)
let child_diff = myers_diff(&before.children, &after.children);
changes.extend(child_diff);
changes
}
Frontend 기술
React 기반 UI
// Figma의 React 컴포넌트 시스템
import { useEffect, useRef, memo } from "react";
import { FigmaEngine } from "./wasm/engine";
interface CanvasProps {
document: FigmaDocument;
onSelectionChange: (selection: NodeId[]) => void;
}
export const Canvas = memo(({ document, onSelectionChange }: CanvasProps) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const engineRef = useRef<FigmaEngine>();
const [selection, setSelection] = useState<NodeId[]>([]);
useEffect(() => {
// WebAssembly 엔진 초기화
const initEngine = async () => {
const engine = new FigmaEngine();
await engine.initialize();
engineRef.current = engine;
// WebGL 컨텍스트 설정
const gl = canvasRef.current!.getContext("webgl2", {
alpha: false,
antialias: false, // MSAA는 직접 구현
preserveDrawingBuffer: false,
powerPreference: "high-performance",
});
engine.setGLContext(gl);
};
initEngine();
}, []);
// 렌더링 루프
useEffect(() => {
let frameId: number;
const renderLoop = () => {
if (engineRef.current && document) {
// 프레임 준비
const frameData = prepareFrameData(document, selection);
// WASM 엔진으로 렌더링
engineRef.current.render(frameData);
// 성능 모니터링
measurePerformance();
}
frameId = requestAnimationFrame(renderLoop);
};
renderLoop();
return () => cancelAnimationFrame(frameId);
}, [document, selection]);
// 입력 처리
const handleMouseDown = useCallback(
(e: MouseEvent) => {
const point = { x: e.clientX, y: e.clientY };
const hitNode = engineRef.current?.hitTest(point);
if (hitNode) {
if (e.shiftKey) {
// 다중 선택
setSelection((prev) => [...prev, hitNode.id]);
} else {
setSelection([hitNode.id]);
}
onSelectionChange(selection);
}
},
[onSelectionChange]
);
return (
<div className="canvas-container">
<canvas
ref={canvasRef}
className="figma-canvas"
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onWheel={handleWheel}
/>
<SelectionOverlay selection={selection} />
<Rulers zoom={zoom} offset={offset} />
</div>
);
});
// 선택 영역 오버레이
const SelectionOverlay = ({ selection }: { selection: NodeId[] }) => {
const overlayRef = useRef<SVGSVGElement>(null);
useEffect(() => {
// 선택된 노드들의 경계 상자 계산
const bounds = selection.map((nodeId) => {
const node = getNode(nodeId);
return calculateBounds(node);
});
// SVG로 선택 영역 렌더링
renderSelectionBounds(overlayRef.current!, bounds);
}, [selection]);
return (
<svg
ref={overlayRef}
className="selection-overlay"
style={{ position: "absolute", inset: 0, pointerEvents: "none" }}
/>
);
};
성능 최적화
가상화 및 LOD (Level of Detail)
// 대규모 디자인 파일 최적화
class ViewportManager {
private visibleNodes: Set<NodeId> = new Set();
private lodLevels: Map<NodeId, LODLevel> = new Map();
updateViewport(viewport: Viewport, sceneRoot: Node) {
// Quadtree로 빠른 공간 검색
const candidates = this.quadtree.query(viewport.bounds);
// 뷰포트에 보이는 노드만 렌더링
const newVisible = new Set<NodeId>();
for (const node of candidates) {
const distance = this.calculateDistance(node, viewport);
if (distance < viewport.radius) {
newVisible.add(node.id);
// LOD 레벨 결정
const lod = this.calculateLOD(node, viewport.zoom);
this.lodLevels.set(node.id, lod);
}
}
// 차이 계산
const added = difference(newVisible, this.visibleNodes);
const removed = difference(this.visibleNodes, newVisible);
// 증분 업데이트
this.handleNodesAdded(added);
this.handleNodesRemoved(removed);
this.visibleNodes = newVisible;
}
calculateLOD(node: Node, zoom: number): LODLevel {
const pixelSize = node.bounds.width * zoom;
if (pixelSize < 10) {
return LODLevel.INVISIBLE;
} else if (pixelSize < 50) {
return LODLevel.SIMPLIFIED;
} else if (pixelSize < 200) {
return LODLevel.NORMAL;
} else {
return LODLevel.DETAILED;
}
}
}
// 텍스처 아틀라스 관리
class TextureAtlasManager {
private atlases: Map<string, TextureAtlas> = new Map();
private lruCache: LRUCache<string, AtlasRegion> = new LRUCache(1000);
async getTexture(imageId: string): Promise<AtlasRegion> {
// 캐시 확인
const cached = this.lruCache.get(imageId);
if (cached) return cached;
// 이미지 로드
const image = await this.loadImage(imageId);
// 적절한 아틀라스 찾기
const atlas = this.findSuitableAtlas(image.width, image.height);
// 아틀라스에 패킹
const region = atlas.pack(image);
// 캐시 저장
this.lruCache.set(imageId, region);
return region;
}
}
기술 스택 요약:
- 렌더링: C++ → WebAssembly, WebGL 2.0
- Frontend: React, TypeScript, MobX
- Backend: Rust, PostgreSQL, Redis
- 실시간: WebSocket, Operational Transform
- 인프라: AWS, Cloudflare Workers
- 빌드: Bazel, custom toolchain
💡 핵심 기술 인사이트
1. WebAssembly의 혁신적 활용
- 네이티브 성능을 웹에서 구현
- C++ 코드 재사용
- 메모리 효율적 관리
2. 커스텀 렌더링 파이프라인
- GPU 가속 벡터 그래픽
- 효율적인 캐싱 전략
- 적응형 품질 조절
3. 실시간 협업의 정교함
- Operational Transform 알고리즘
- 충돌 없는 동시 편집
- 최소 지연 동기화
📈 성과 & 지표
기술적 성과
- 렌더링 성능: 60fps (복잡한 디자인에서도)
- 로딩 시간: < 3초 (대규모 파일)
- 동시 편집: 200+ 명 지원
- 지연 시간: < 50ms (같은 지역)
- 메모리 효율: 네이티브 앱 대비 70%
비즈니스 영향
- 시장 점유율: 디자인 툴 시장 1위
- Adobe 인수: $20B (2022년)
- 사용자 만족도: NPS 70+
- 개발 속도: 기존 툴 대비 3배
🎓 Figma에서 배울 점
✅ 적용 가능한 패턴
- WebAssembly 활용: 고성능 웹 앱
- Custom 렌더링: WebGL 직접 제어
- OT 알고리즘: 실시간 협업
- LOD 시스템: 대규모 콘텐츠 처리
- Spatial Indexing: 효율적 히트 테스트
❌ 주의사항
- 높은 기술 장벽: C++/WASM 전문성
- 브라우저 제약: 메모리, API 한계
- 복잡한 상태 관리: 동기화 로직
- 성능 최적화: 지속적 튜닝 필요
📚 추천 리소스
- Figma Engineering Blog
- WebAssembly for Web Developers
- Operational Transformation
- Building a Collaborative Editor
🔮 미래 전망
현재 집중 분야
- AI 디자인 도구: 자동 레이아웃, 생성
- Dev Mode: 개발자 핸드오프 개선
- 3D 지원: 3D 디자인 도구
- 더 큰 파일: TB급 파일 지원
- 플러그인 생태계: 더 강력한 API
기술 투자 영역
- WebGPU: 차세대 GPU API
- WASM SIMD: 벡터 연산 가속
- SharedArrayBuffer: 멀티스레딩
- AI/ML: 디자인 인텔리전스
- 분산 시스템: 글로벌 확장
"우리는 디자인 도구를 만드는 것이 아니라, 창의성을 위한 플랫폼을 만들고 있습니다."
- Figma Engineering
마지막 업데이트: 2025-01-28