🏢 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