🏢 Vercel 기술 스택 분석

28 min read
vercelnextjstypescriptgorustedge-computingserverlesscdnjamstackdeploymentinfrastructure

버셀이 프론트엔드 배포의 미래를 만드는 기술 스택 심층 분석 - Next.js, Edge Functions, 그리고 글로벌 엣지 네트워크

🏢 Vercel 기술 스택 분석

프론트엔드 개발자를 위한 최고의 배포 플랫폼을 만드는 Vercel의 기술 스택을 심층 분석합니다.
"Develop. Preview. Ship." - 프론트엔드 배포를 혁신하는 기술


📊 회사 개요

서비스 규모

  • 월간 배포: 1,000만+ 건
  • 개발자: 100만+ 명
  • 월간 요청: 300억+ 건
  • 엣지 로케이션: 전 세계 100+ 개
  • 평균 빌드 시간: < 1분

엔지니어링 조직

  • 엔지니어: 200명+ (전체 직원 350+)
  • 문화: "Ship Early, Ship Often"
  • 원격 우선: 글로벌 분산 팀
  • 오픈소스: Next.js, Turborepo 등 주도

기술적 도전과제

  1. 즉각적 배포: Git push에서 프로덕션까지
  2. 글로벌 성능: 전 세계 밀리초 응답
  3. 개발자 경험: 완벽한 로컬-프로덕션 패리티
  4. 엣지 컴퓨팅: 서버리스의 다음 단계

🏗️ 아키텍처 Overview

시스템 다이어그램

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Git Push  │────►│   Builder   │────►│   Registry  │
│  (GitHub)   │     │  Platform   │     │   (OCI)     │
└─────────────┘     └─────────────┘     └─────────────┘
       │                    │                    │
       │              ┌─────▼─────┐       ┌─────▼─────┐
       │              │   Preview  │       │Edge Network│
       │              │   URLs     │       │(100+ PoPs) │
       │              └───────────┘       └───────────┘
       │                    │                    │
       └────────────────────┤                    │
                      ┌─────▼─────┐       ┌─────▼─────┐
                      │ Analytics  │       │   Edge     │
                      │  Engine    │       │ Functions  │
                      └───────────┘       └───────────┘

핵심 설계 원칙

  1. Zero Config: 설정 없이 바로 배포
  2. Edge First: 사용자 가까이에서 실행
  3. Instant Rollbacks: 즉시 이전 버전으로
  4. Preview Deployments: 모든 PR에 미리보기
  5. Framework Agnostic: 모든 프레임워크 지원

기술 진화

  • 2015: ZEIT Now로 시작
  • 2016-2017: Now.sh 플랫폼 구축
  • 2018-2019: Serverless 전환
  • 2020: Vercel로 리브랜딩, Next.js 통합
  • 2021-2022: Edge Functions 출시
  • 2023-현재: AI SDK, v0 출시

🧩 기술 스택 상세

Build Platform

빌드 시스템 (Go)

// Vercel의 빌드 파이프라인
package builder

import (
    "context"
    "fmt"
    "time"

    "github.com/vercel/builder/pkg/analyzer"
    "github.com/vercel/builder/pkg/cache"
    "github.com/vercel/builder/pkg/runtime"
)

type BuildPipeline struct {
    analyzer    *analyzer.FrameworkAnalyzer
    cache       *cache.BuildCache
    registry    *Registry
    telemetry   *Telemetry
}

type BuildRequest struct {
    RepoURL     string
    Branch      string
    CommitSHA   string
    ProjectID   string
    Environment map[string]string
}

func (bp *BuildPipeline) Build(ctx context.Context, req BuildRequest) (*BuildResult, error) {
    span := trace.StartSpan("build.pipeline")
    defer span.End()

    // 1. 소스 코드 클론
    sourceDir, err := bp.cloneRepository(ctx, req)
    if err != nil {
        return nil, fmt.Errorf("clone failed: %w", err)
    }
    defer cleanup(sourceDir)

    // 2. 프레임워크 감지
    framework, err := bp.analyzer.Detect(sourceDir)
    if err != nil {
        return nil, fmt.Errorf("framework detection failed: %w", err)
    }

    span.SetAttributes(
        attribute.String("framework", framework.Name),
        attribute.String("version", framework.Version),
    )

    // 3. 캐시 복원
    cacheKey := bp.generateCacheKey(req, framework)
    if cached := bp.cache.Restore(cacheKey); cached != nil {
        span.AddEvent("cache_hit")
        sourceDir = bp.applyCachedDependencies(sourceDir, cached)
    }

    // 4. 빌드 실행
    buildResult, err := bp.executeBuild(ctx, sourceDir, framework, req.Environment)
    if err != nil {
        return nil, fmt.Errorf("build execution failed: %w", err)
    }

    // 5. 최적화
    optimized, err := bp.optimize(buildResult, framework)
    if err != nil {
        span.RecordError(err)
        // 최적화 실패는 무시하고 진행
        optimized = buildResult
    }

    // 6. 아티팩트 업로드
    artifacts, err := bp.uploadArtifacts(ctx, optimized)
    if err != nil {
        return nil, fmt.Errorf("artifact upload failed: %w", err)
    }

    // 7. 캐시 저장
    bp.cache.Save(cacheKey, bp.extractCacheableAssets(optimized))

    // 8. 메타데이터 생성
    metadata := bp.generateMetadata(framework, buildResult, artifacts)

    return &BuildResult{
        Artifacts: artifacts,
        Metadata:  metadata,
        Duration:  time.Since(startTime),
        CacheHit:  cached != nil,
    }, nil
}

// 프레임워크별 빌드 실행
func (bp *BuildPipeline) executeBuild(
    ctx context.Context,
    sourceDir string,
    framework Framework,
    env map[string]string,
) (*BuildOutput, error) {
    // 런타임 선택
    rt := runtime.GetRuntime(framework)

    // 병렬 처리를 위한 워커 풀
    workers := runtime.NewWorkerPool(runtime.MaxWorkers())
    defer workers.Close()

    // 빌드 태스크 생성
    tasks := rt.CreateBuildTasks(sourceDir, framework)

    results := make([]*TaskResult, len(tasks))
    errChan := make(chan error, len(tasks))

    // 병렬 실행
    for i, task := range tasks {
        workers.Submit(func(idx int, t BuildTask) func() {
            return func() {
                result, err := t.Execute(ctx, env)
                if err != nil {
                    errChan <- fmt.Errorf("task %s failed: %w", t.Name, err)
                    return
                }
                results[idx] = result
            }
        }(i, task))
    }

    // 결과 수집
    workers.Wait()
    close(errChan)

    // 에러 확인
    for err := range errChan {
        if err != nil {
            return nil, err
        }
    }

    // 결과 병합
    return rt.MergeResults(results), nil
}

// 빌드 최적화
func (bp *BuildPipeline) optimize(output *BuildOutput, framework Framework) (*BuildOutput, error) {
    optimized := &BuildOutput{
        Files: make([]File, 0, len(output.Files)),
    }

    for _, file := range output.Files {
        switch {
        case isJavaScript(file):
            // Terser로 minification
            minified, err := bp.minifyJS(file)
            if err == nil {
                file = minified
            }

        case isCSS(file):
            // CSS 최적화
            optimized, err := bp.optimizeCSS(file)
            if err == nil {
                file = optimized
            }

        case isImage(file):
            // 이미지 최적화 (WebP 변환 등)
            converted, err := bp.optimizeImage(file)
            if err == nil {
                file = converted
            }
        }

        optimized.Files = append(optimized.Files, file)
    }

    // Tree shaking
    if framework.SupportsTreeShaking {
        optimized = bp.performTreeShaking(optimized)
    }

    // 번들 분석
    optimized.BundleAnalysis = bp.analyzeBundles(optimized)

    return optimized, nil
}

Edge Runtime (Rust)

// Vercel Edge Runtime 구현
use std::time::Duration;
use tokio::time::timeout;
use wasmtime::{Engine, Module, Store};

pub struct EdgeRuntime {
    engine: Engine,
    isolate_pool: IsolatePool,
    request_router: RequestRouter,
    cache: EdgeCache,
}

#[derive(Debug)]
pub struct EdgeFunction {
    id: String,
    wasm_module: Module,
    config: FunctionConfig,
    cold_start_snapshot: Option<Snapshot>,
}

impl EdgeRuntime {
    pub async fn handle_request(
        &self,
        request: Request<Body>,
        function_id: &str,
    ) -> Result<Response<Body>, Error> {
        // 1. 함수 조회
        let function = self.get_function(function_id)
            .ok_or(Error::FunctionNotFound)?;

        // 2. 캐시 확인
        if let Some(cached) = self.check_cache(&request, &function.config).await? {
            return Ok(cached);
        }

        // 3. Isolate 가져오기 (풀에서)
        let mut isolate = self.isolate_pool.acquire().await?;

        // 4. Cold start 최적화
        if let Some(snapshot) = &function.cold_start_snapshot {
            isolate.restore_from_snapshot(snapshot)?;
        } else {
            // 첫 실행 - 모듈 인스턴스화
            let instance = isolate.instantiate(&function.wasm_module)?;

            // 스냅샷 생성 (다음 cold start용)
            let snapshot = isolate.create_snapshot()?;
            self.cache_snapshot(function_id, snapshot).await;
        }

        // 5. 요청 실행 (타임아웃 포함)
        let response = timeout(
            Duration::from_millis(function.config.timeout_ms),
            self.execute_function(&mut isolate, request),
        ).await??;

        // 6. 응답 캐싱
        if should_cache(&response, &function.config) {
            self.cache_response(&request, &response, &function.config).await?;
        }

        // 7. Isolate 반환
        self.isolate_pool.release(isolate);

        Ok(response)
    }

    async fn execute_function(
        &self,
        isolate: &mut Isolate,
        request: Request<Body>,
    ) -> Result<Response<Body>, Error> {
        // Request 객체를 WASM 메모리로 전달
        let request_ptr = isolate.serialize_request(request)?;

        // 함수 호출
        let func = isolate.get_export::<Func>("handler")?;
        let response_ptr = func.call(&mut isolate.store, &[request_ptr.into()])?;

        // Response 역직렬화
        let response = isolate.deserialize_response(response_ptr)?;

        Ok(response)
    }
}

// Isolate Pool 관리
pub struct IsolatePool {
    available: Arc<Mutex<Vec<Isolate>>>,
    max_size: usize,
    memory_limit: usize,
}

impl IsolatePool {
    pub async fn acquire(&self) -> Result<Isolate, Error> {
        // 사용 가능한 isolate 확인
        if let Some(isolate) = self.available.lock().await.pop() {
            return Ok(isolate);
        }

        // 새 isolate 생성
        self.create_isolate().await
    }

    async fn create_isolate(&self) -> Result<Isolate, Error> {
        let mut config = Config::new();
        config.wasm_simd(true);
        config.wasm_bulk_memory(true);
        config.wasm_multi_value(true);

        // 메모리 제한
        config.max_wasm_stack(1024 * 1024); // 1MB stack
        config.memory_limit(self.memory_limit);

        // V8 스타일 isolate 생성
        let engine = Engine::new(&config)?;
        let store = Store::new(&engine, ());

        Ok(Isolate::new(engine, store))
    }
}

// 지능형 라우팅
pub struct RequestRouter {
    rules: Vec<RoutingRule>,
    geo_database: GeoIPDatabase,
}

impl RequestRouter {
    pub fn route(&self, request: &Request<Body>) -> RouterDecision {
        let client_ip = extract_client_ip(request);
        let geo_info = self.geo_database.lookup(client_ip);

        // 가장 가까운 엣지 로케이션 선택
        let nearest_edge = self.find_nearest_edge(&geo_info);

        // 커스텀 라우팅 규칙 적용
        for rule in &self.rules {
            if rule.matches(request, &geo_info) {
                return rule.decision.clone();
            }
        }

        RouterDecision::Edge(nearest_edge)
    }
}

Frontend (Next.js Integration)

프레임워크 최적화

// Vercel의 Next.js 최적화
import { NextConfig } from "next";
import { VercelConfig } from "@vercel/static-config";

export class NextJSOptimizer {
  private config: NextConfig;
  private vercelConfig: VercelConfig;

  async optimize(projectPath: string): Promise<OptimizedBuild> {
    // 1. 설정 로드
    this.config = await this.loadNextConfig(projectPath);
    this.vercelConfig = await this.loadVercelConfig(projectPath);

    // 2. 자동 최적화 적용
    const optimizations = [];

    // 이미지 최적화
    if (this.config.images) {
      optimizations.push(this.optimizeImages());
    }

    // 폰트 최적화
    optimizations.push(this.optimizeFonts());

    // 번들 분석 및 최적화
    optimizations.push(this.optimizeBundles());

    // ISR 설정
    if (this.hasISRPages()) {
      optimizations.push(this.configureISR());
    }

    // Edge Functions 변환
    if (this.hasEdgeAPI()) {
      optimizations.push(this.convertToEdge());
    }

    // 병렬 실행
    const results = await Promise.all(optimizations);

    return this.mergeOptimizations(results);
  }

  private async optimizeImages(): Promise<ImageOptimization> {
    const images = await this.findAllImages();

    return Promise.all(
      images.map(async (image) => {
        // Next/Image 컴포넌트로 자동 변환
        if (this.isPlainImg(image)) {
          await this.convertToNextImage(image);
        }

        // 원본 이미지 최적화
        const optimized = await this.processImage(image, {
          formats: ["webp", "avif"],
          sizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
          quality: this.config.images?.quality || 75,
        });

        return optimized;
      }),
    );
  }

  private async optimizeBundles(): Promise<BundleOptimization> {
    // Webpack 설정 분석
    const webpackConfig = this.extractWebpackConfig();

    // 자동 코드 스플리팅
    const splitChunks = {
      chunks: "all",
      cacheGroups: {
        default: false,
        vendors: false,
        framework: {
          name: "framework",
          chunks: "all",
          test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
          priority: 40,
          enforce: true,
        },
        commons: {
          name: "commons",
          chunks: "all",
          minChunks: 2,
          priority: 20,
        },
        shared: {
          name(module, chunks) {
            return crypto
              .createHash("sha1")
              .update(chunks.reduce((acc, chunk) => acc + chunk.name, ""))
              .digest("hex");
          },
          priority: 10,
          minChunks: 2,
          reuseExistingChunk: true,
        },
      },
    };

    // Tree shaking 강화
    const optimization = {
      usedExports: true,
      sideEffects: false,
      concatenateModules: true,
      minimize: true,
      minimizer: [
        new TerserPlugin({
          terserOptions: {
            parse: { ecma: 8 },
            compress: {
              ecma: 5,
              warnings: false,
              comparisons: false,
              inline: 2,
              drop_console: true,
              drop_debugger: true,
            },
            mangle: { safari10: true },
            output: {
              ecma: 5,
              comments: false,
              ascii_only: true,
            },
          },
        }),
      ],
    };

    return { splitChunks, optimization };
  }

  private async configureISR(): Promise<ISRConfig> {
    const pages = await this.findAllPages();
    const isrPages = [];

    for (const page of pages) {
      const { revalidate } = await this.extractPageConfig(page);

      if (revalidate) {
        isrPages.push({
          path: page.path,
          revalidate,
          // 스마트 무효화 설정
          dependencies: await this.analyzeDependencies(page),
          fallback: this.determineFallback(page),
        });
      }
    }

    return {
      pages: isrPages,
      // 글로벌 캐시 설정
      cache: {
        strategy: "stale-while-revalidate",
        maxAge: 31536000, // 1년
        staleWhileRevalidate: 86400, // 1일
      },
    };
  }
}

// Analytics 수집
export class VercelAnalytics {
  private queue: AnalyticsEvent[] = [];
  private flushInterval: number = 5000;

  track(event: AnalyticsEvent): void {
    this.queue.push({
      ...event,
      timestamp: Date.now(),
      sessionId: this.getSessionId(),
      visitorId: this.getVisitorId(),
    });

    if (this.queue.length >= 10) {
      this.flush();
    }
  }

  private async flush(): Promise<void> {
    if (this.queue.length === 0) return;

    const events = [...this.queue];
    this.queue = [];

    try {
      await fetch("/_vercel/insights/event", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ events }),
      });
    } catch (error) {
      // 실패한 이벤트는 다시 큐에 추가
      this.queue.unshift(...events);
    }
  }

  measureWebVitals(): void {
    // Core Web Vitals 측정
    if ("PerformanceObserver" in window) {
      // LCP (Largest Contentful Paint)
      new PerformanceObserver((list) => {
        const entries = list.getEntries();
        const lastEntry = entries[entries.length - 1];
        this.track({
          type: "web-vital",
          name: "LCP",
          value: lastEntry.renderTime || lastEntry.loadTime,
        });
      }).observe({ entryTypes: ["largest-contentful-paint"] });

      // FID (First Input Delay)
      new PerformanceObserver((list) => {
        const entries = list.getEntries();
        entries.forEach((entry) => {
          this.track({
            type: "web-vital",
            name: "FID",
            value: entry.processingStart - entry.startTime,
          });
        });
      }).observe({ entryTypes: ["first-input"] });

      // CLS (Cumulative Layout Shift)
      let clsValue = 0;
      new PerformanceObserver((list) => {
        list.getEntries().forEach((entry) => {
          if (!entry.hadRecentInput) {
            clsValue += entry.value;
          }
        });
        this.track({
          type: "web-vital",
          name: "CLS",
          value: clsValue,
        });
      }).observe({ entryTypes: ["layout-shift"] });
    }
  }
}

인프라 및 네트워크

글로벌 엣지 네트워크

// Edge Network 관리
export class EdgeNetwork {
  private nodes: Map<string, EdgeNode> = new Map();
  private healthChecker: HealthChecker;
  private loadBalancer: LoadBalancer;

  async routeRequest(request: Request): Promise<EdgeNode> {
    // 1. 클라이언트 위치 파악
    const clientLocation = await this.geolocate(request.ip);

    // 2. 가용한 노드 찾기
    const availableNodes = await this.getHealthyNodes();

    // 3. 최적 노드 선택
    const selectedNode = this.selectOptimalNode(
      clientLocation,
      availableNodes,
      request,
    );

    // 4. 요청 라우팅
    return selectedNode;
  }

  private selectOptimalNode(
    clientLocation: GeoLocation,
    nodes: EdgeNode[],
    request: Request,
  ): EdgeNode {
    // 점수 계산
    const scores = nodes.map((node) => ({
      node,
      score: this.calculateNodeScore(node, clientLocation, request),
    }));

    // 가장 높은 점수의 노드 선택
    scores.sort((a, b) => b.score - a.score);

    return scores[0].node;
  }

  private calculateNodeScore(
    node: EdgeNode,
    clientLocation: GeoLocation,
    request: Request,
  ): number {
    let score = 100;

    // 거리 점수 (가장 중요)
    const distance = this.calculateDistance(node.location, clientLocation);
    score -= distance / 100; // 100km당 1점 감소

    // 부하 점수
    const load = node.getCurrentLoad();
    score -= load * 0.5; // 부하율당 0.5점 감소

    // 특수 라우팅 규칙
    if (request.headers["x-vercel-ip-country"] === node.country) {
      score += 10; // 같은 국가 보너스
    }

    // 캐시 히트 가능성
    if (node.hasInCache(request.url)) {
      score += 20; // 캐시 보너스
    }

    return score;
  }
}

// DDoS 방어
export class DDoSProtection {
  private rateLimiter: RateLimiter;
  private blacklist: Set<string> = new Set();
  private suspicious: Map<string, number> = new Map();

  async checkRequest(request: Request): Promise<boolean> {
    const ip = request.ip;

    // 블랙리스트 체크
    if (this.blacklist.has(ip)) {
      return false;
    }

    // Rate limiting
    const allowed = await this.rateLimiter.check(ip, {
      windowMs: 60 * 1000, // 1분
      max: 100, // 최대 100 요청
    });

    if (!allowed) {
      this.incrementSuspicion(ip);
      return false;
    }

    // 패턴 분석
    if (this.detectMaliciousPattern(request)) {
      this.incrementSuspicion(ip);
      return false;
    }

    return true;
  }

  private detectMaliciousPattern(request: Request): boolean {
    // User-Agent 검사
    const ua = request.headers["user-agent"];
    if (!ua || this.isBotUserAgent(ua)) {
      return true;
    }

    // 요청 패턴 검사
    if (this.isScrapingPattern(request)) {
      return true;
    }

    // Payload 크기 검사
    if (request.contentLength > 10 * 1024 * 1024) {
      // 10MB
      return true;
    }

    return false;
  }

  private incrementSuspicion(ip: string): void {
    const current = this.suspicious.get(ip) || 0;
    this.suspicious.set(ip, current + 1);

    // 임계값 초과 시 블랙리스트
    if (current + 1 >= 10) {
      this.blacklist.add(ip);
      this.suspicious.delete(ip);

      // 일정 시간 후 해제
      setTimeout(() => {
        this.blacklist.delete(ip);
      }, 3600 * 1000); // 1시간
    }
  }
}

기술 스택 요약:

  • Backend: Go, Rust, Node.js
  • Frontend: Next.js, React, TypeScript
  • Edge Runtime: WebAssembly, V8 Isolates
  • 인프라: AWS, Cloudflare, 자체 CDN
  • 데이터베이스: PostgreSQL, DynamoDB, Redis
  • 모니터링: 자체 분석 플랫폼
  • 빌드 도구: Turborepo, Webpack, SWC

💡 핵심 기술 인사이트

1. 엣지 컴퓨팅의 미래

  • 사용자 가까이에서 코드 실행
  • 콜드 스타트 최소화
  • 글로벌 일관된 성능

2. 개발자 경험 혁신

  • Git push만으로 배포
  • 즉각적인 미리보기
  • 프레임워크 자동 감지

3. 성능 최적화 자동화

  • 이미지 자동 최적화
  • 코드 스플리팅
  • 캐싱 전략 자동화

📈 성과 & 지표

기술적 성과

  • 배포 시간: Git push → 프로덕션 < 60초
  • 빌드 성공률: 99.5%+
  • 엣지 응답시간: P50 < 50ms
  • 가용성: 99.99%
  • 글로벌 커버리지: 100+ PoP

비즈니스 영향

  • 개발자 생산성: 3배 향상
  • 페이지 로드 속도: 평균 40% 개선
  • SEO 점수: 평균 20점 상승
  • 개발 비용: 60% 절감

🎓 Vercel에서 배울 점

✅ 적용 가능한 패턴

  1. Preview Deployments: PR별 미리보기
  2. Edge Functions: 엣지에서 실행
  3. 자동 최적화: 프레임워크별 최적화
  4. Analytics: 실시간 성능 모니터링
  5. Rollback: 즉시 이전 버전으로

❌ 주의사항

  1. 벤더 종속: Vercel 전용 기능
  2. 비용: 트래픽 증가 시 비용 상승
  3. 제한사항: 함수 실행 시간 제한
  4. 커스터마이징: 제한된 설정

📚 추천 리소스


🔮 미래 전망

현재 집중 분야

  1. AI Integration: v0, AI SDK
  2. Edge Databases: 분산 데이터베이스
  3. Conformance: 프레임워크 최적화
  4. Storage: Edge 스토리지 솔루션
  5. Monitoring: 더 정교한 분석

기술 투자 영역

  • WebAssembly: 더 빠른 Edge Runtime
  • Machine Learning: 자동 최적화
  • 5G Edge: 초저지연 컴퓨팅
  • Blockchain: 분산 배포
  • IoT: 엣지 디바이스 지원

"우리는 프론트엔드 클라우드를 구축하여, 개발자가 최고의 사용자 경험을 만드는 데 집중할 수 있도록 돕고 있습니다."

  • Vercel Engineering

마지막 업데이트: 2025-01-28

Found this helpful? Share it with others!
Tweet

🔗 Related Content

You might also be interested in these articles

🏗️ stack

🚀 Modern SaaS MVP Stack

4주 안에 SaaS를 출시하기 위한 검증된 기술 스택 - Next.js, Supabase, Stripe로 빠르게 시작하기

8 min read
nextjs, supabase +13
Read more
🔁 alternative

🔁 Vercel Alternatives

Vercel을 대체할 수 있는 프론트엔드 및 풀스택 호스팅 플랫폼 비교

9 min read
alternative, vercel +4
Read more
🏢 company

🏢 Canva 기술 스택 분석

캔바가 1억 명 이상의 사용자에게 쉽고 강력한 디자인 도구를 제공하는 기술 스택 심층 분석 - Java, TypeScript, AWS로 구축한 대규모 디자인 플랫폼

32 min read
canva, java +10
Read more
🏢 company

🏢 Figma 기술 스택 분석

피그마가 브라우저에서 실시간 협업 디자인 툴을 구현한 기술 스택 심층 분석 - C++, WebAssembly, WebGL로 만든 차세대 디자인 플랫폼

22 min read
figma, c++ +11
Read more

Found this helpful?

Help us improve this content by contributing on GitHub or sharing your feedback with the community.