🏢 Canva 기술 스택 분석

32 min read
canvajavatypescriptreactawskubernetesgraphqlpostgresqlredisdesign-platformmicroservicesserverless

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

🏢 Canva 기술 스택 분석

전문 디자이너가 아닌 사람도 쉽게 사용할 수 있는 디자인 플랫폼 Canva의 기술 스택을 심층 분석합니다.
"Empowering the world to design" - 디자인의 민주화를 실현하는 기술


📊 회사 개요

서비스 규모

  • 월간 활성 사용자: 1.35억+ 명
  • 생성된 디자인: 150억+ 개
  • 서비스 국가: 190+ 개국
  • 템플릿: 80만+ 개
  • 미디어 업로드: 70억+ 개

엔지니어링 조직

  • 엔지니어: 600명+ (전체 직원 3,500+)
  • 문화: "Be a good human"
  • 오피스: 시드니, 마닐라, 베이징, 오스틴
  • 기술 철학: 단순함과 확장성의 균형

기술적 도전과제

  1. 대규모 미디어 처리: 수십억 개의 이미지/비디오
  2. 실시간 렌더링: 브라우저에서 복잡한 디자인
  3. 글로벌 성능: 전 세계 균일한 경험
  4. AI 통합: 스마트 디자인 제안

🏗️ 아키텍처 Overview

시스템 다이어그램

┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Frontend │────►│ API │────►│ Services │ │ (React) │ │ Gateway │ │(Microservices) └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ ┌─────▼─────┐ ┌─────▼─────┐ │ │ GraphQL │ │ Media │ │ │ Server │ │ Pipeline │ │ └───────────┘ └───────────┘ │ │ │ └────────────────────┤ │ ┌─────▼─────┐ ┌─────▼─────┐ │ CDN │ │ Database │ │(CloudFront)│ │(Aurora) │ └───────────┘ └───────────┘ 

핵심 설계 원칙

  1. User-Centric: 사용자 경험 최우선
  2. Scalable by Design: 처음부터 확장 고려
  3. Data-Driven: 모든 결정은 데이터 기반
  4. Progressive Enhancement: 점진적 기능 향상
  5. Global First: 전 세계 사용자 고려

기술 진화

  • 2013-2014: PHP 모놀리스로 시작
  • 2015-2016: 마이크로서비스 전환
  • 2017-2018: React 도입, SPA 전환
  • 2019-2020: ML/AI 플랫폼 구축
  • 2021-2022: 비디오 에디터 출시
  • 2023-현재: AI 기능 대폭 확대

🧩 기술 스택 상세

Backend 기술

마이크로서비스 아키텍처

// Canva의 Java 기반 마이크로서비스 @SpringBootApplication @EnableEurekaClient public class DesignService { @RestController @RequestMapping("/api/v1/designs") public class DesignController { private final DesignRepository designRepository; private final MediaService mediaService; private final RenderingService renderingService; private final CacheService cacheService; @PostMapping @Transactional public ResponseEntity<Design> createDesign( @RequestBody CreateDesignRequest request, @AuthenticationPrincipal User user) { // 디자인 생성 Design design = Design.builder() .userId(user.getId()) .title(request.getTitle()) .dimensions(request.getDimensions()) .template(request.getTemplateId()) .build(); // 페이지 초기화 for (int i = 0; i < request.getPageCount(); i++) { Page page = createDefaultPage(design.getDimensions()); design.addPage(page); } // 저장 design = designRepository.save(design); // 캐시 예열 cacheService.warmup(design.getId()); // 이벤트 발행 eventPublisher.publishEvent(new DesignCreatedEvent(design)); return ResponseEntity.created( URI.create("/api/v1/designs/" + design.getId()) ).body(design); } @GetMapping("/{id}/render") public ResponseEntity<RenderResult> renderDesign( @PathVariable String id, @RequestParam(defaultValue = "PNG") ExportFormat format, @RequestParam(defaultValue = "1.0") Double scale) { // 캐시 확인 String cacheKey = String.format("render:%s:%s:%.1f", id, format, scale); RenderResult cached = cacheService.get(cacheKey, RenderResult.class); if (cached != null) { return ResponseEntity.ok() .cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS)) .body(cached); } // 디자인 로드 Design design = designRepository.findById(id) .orElseThrow(() -> new NotFoundException("Design not found")); // 렌더링 요청 RenderRequest renderRequest = RenderRequest.builder() .design(design) .format(format) .scale(scale) .quality(format == ExportFormat.PDF ? Quality.PRINT : Quality.WEB) .build(); // 비동기 렌더링 CompletableFuture<RenderResult> future = renderingService.renderAsync(renderRequest); // 타임아웃 처리 try { RenderResult result = future.get(30, TimeUnit.SECONDS); // 캐시 저장 cacheService.put(cacheKey, result, Duration.ofHours(1)); return ResponseEntity.ok() .cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS)) .body(result); } catch (TimeoutException e) { // 폴백: 저품질 빠른 렌더링 return renderLowQuality(design, format); } } } } // 미디어 처리 서비스 @Service public class MediaProcessingService { private final S3Client s3Client; private final ImageProcessor imageProcessor; private final VideoProcessor videoProcessor; @Async public CompletableFuture<ProcessedMedia> processUpload( MultipartFile file, String userId) { // 1. 파일 검증 validateFile(file); // 2. S3에 원본 업로드 String key = generateS3Key(userId, file.getOriginalFilename()); String originalUrl = uploadToS3(file, key); // 3. 미디어 타입별 처리 MediaType type = detectMediaType(file); ProcessedMedia processed; switch (type) { case IMAGE: processed = processImage(file, originalUrl); break; case VIDEO: processed = processVideo(file, originalUrl); break; case SVG: processed = processSvg(file, originalUrl); break; default: throw new UnsupportedMediaException(type); } // 4. 메타데이터 추출 processed.setMetadata(extractMetadata(file)); // 5. AI 분석 (배경 제거, 객체 인식 등) if (type == MediaType.IMAGE) { processed.setAiFeatures(analyzeImage(processed)); } return CompletableFuture.completedFuture(processed); } private ProcessedMedia processImage(MultipartFile file, String originalUrl) { BufferedImage image = ImageIO.read(file.getInputStream()); // 다양한 크기 생성 Map<String, String> variants = new HashMap<>(); // 썸네일 BufferedImage thumbnail = imageProcessor.resize(image, 200, 200); variants.put("thumbnail", uploadToS3(thumbnail, key + "_thumb")); // 웹 최적화 BufferedImage webOptimized = imageProcessor.optimize(image, 1920, 1080); variants.put("web", uploadToS3(webOptimized, key + "_web")); // WebP 변환 byte[] webp = imageProcessor.convertToWebP(image); variants.put("webp", uploadToS3(webp, key + ".webp")); return ProcessedMedia.builder() .originalUrl(originalUrl) .variants(variants) .width(image.getWidth()) .height(image.getHeight()) .format(detectFormat(file)) .build(); } } 

GraphQL API

// Canva의 GraphQL 스키마 import { gql } from "apollo-server"; export const typeDefs = gql` type Query { design(id: ID!): Design designs( userId: ID! limit: Int = 20 cursor: String filter: DesignFilter ): DesignConnection! searchTemplates( query: String! category: TemplateCategory limit: Int = 50 ): [Template!]! brandKit(brandId: ID!): BrandKit } type Mutation { createDesign(input: CreateDesignInput!): Design! updateDesign(id: ID!, input: UpdateDesignInput!): Design! addElement(designId: ID!, pageIndex: Int!, element: ElementInput!): Element! uploadMedia(file: Upload!): Media! applyMagicResize(designId: ID!, targetDimensions: DimensionsInput!): Design! } type Subscription { designUpdated(designId: ID!): DesignUpdate! collaboratorJoined(designId: ID!): Collaborator! } type Design { id: ID! title: String! pages: [Page!]! owner: User! collaborators: [Collaborator!]! brandKit: BrandKit createdAt: DateTime! updatedAt: DateTime! # 계산된 필드 thumbnail(size: ThumbnailSize = MEDIUM): String! exportUrl(format: ExportFormat!): String! analytics: DesignAnalytics! } type Page { id: ID! elements: [Element!]! background: Background! dimensions: Dimensions! animations: [Animation!]! } interface Element { id: ID! type: ElementType! position: Position! size: Size! rotation: Float! opacity: Float! locked: Boolean! } type TextElement implements Element { # Element 인터페이스 필드들... content: String! font: Font! fontSize: Float! color: Color! alignment: TextAlignment! effects: [TextEffect!]! } type ImageElement implements Element { # Element 인터페이스 필드들... url: String! crop: Crop filters: [ImageFilter!]! maskUrl: String } `; // GraphQL 리졸버 export const resolvers = { Query: { design: async (_, { id }, { dataSources, user }) => { // 권한 확인 const design = await dataSources.designAPI.getDesign(id); if (!canAccessDesign(user, design)) { throw new ForbiddenError("Access denied"); } return design; }, searchTemplates: async (_, { query, category, limit }, { dataSources }) => { // Elasticsearch 검색 return dataSources.templateSearch.search({ query, filters: { category }, size: limit, // ML 기반 관련성 점수 boost: { popularity: 1.5, recent: 1.2, quality: 2.0, }, }); }, }, Mutation: { applyMagicResize: async ( _, { designId, targetDimensions }, { dataSources }, ) => { // AI 기반 리사이징 const design = await dataSources.designAPI.getDesign(designId); // 각 페이지의 요소를 새 차원에 맞게 재배치 const resizedPages = await Promise.all( design.pages.map((page) => dataSources.aiService.intelligentResize(page, targetDimensions), ), ); // 업데이트된 디자인 저장 return dataSources.designAPI.updatePages(designId, resizedPages); }, }, Design: { thumbnail: async (design, { size }, { dataSources }) => { // 동적 썸네일 생성 const cacheKey = `thumb:${design.id}:${size}`; return dataSources.thumbnailService.getOrGenerate(design, size, cacheKey); }, analytics: async (design, _, { dataSources }) => { // 실시간 분석 데이터 return dataSources.analyticsAPI.getDesignMetrics(design.id); }, }, }; 

Frontend 기술

React 기반 에디터

// Canva 스타일 에디터 컴포넌트 import React, { useRef, useEffect, useState } from "react"; import { fabric } from "fabric"; import { useDesign } from "./hooks/useDesign"; import { useCollaboration } from "./hooks/useCollaboration"; interface CanvasEditorProps { designId: string; pageIndex: number; } export const CanvasEditor: React.FC<CanvasEditorProps> = ({ designId, pageIndex, }) => { const canvasRef = useRef<HTMLCanvasElement>(null); const fabricRef = useRef<fabric.Canvas>(); const { design, updateElement, addElement } = useDesign(designId); const { collaborators, broadcastChange } = useCollaboration(designId); useEffect(() => { // Fabric.js 캔버스 초기화 fabricRef.current = new fabric.Canvas(canvasRef.current, { width: design.dimensions.width, height: design.dimensions.height, backgroundColor: "#ffffff", preserveObjectStacking: true, }); // 페이지 요소 로드 loadPageElements(design.pages[pageIndex]); // 이벤트 핸들러 설정 setupEventHandlers(); return () => { fabricRef.current?.dispose(); }; }, [design, pageIndex]); const loadPageElements = (page: Page) => { const canvas = fabricRef.current!; canvas.clear(); page.elements.forEach((element) => { const fabricObject = createFabricObject(element); canvas.add(fabricObject); }); canvas.renderAll(); }; const createFabricObject = (element: Element): fabric.Object => { switch (element.type) { case "text": return new fabric.Textbox(element.content, { left: element.position.x, top: element.position.y, fontSize: element.fontSize, fontFamily: element.font.family, fill: element.color, width: element.size.width, // 커스텀 속성 elementId: element.id, editable: true, }); case "image": return fabric.Image.fromURL(element.url, (img) => { img.set({ left: element.position.x, top: element.position.y, scaleX: element.size.width / img.width!, scaleY: element.size.height / img.height!, elementId: element.id, }); // 필터 적용 element.filters?.forEach((filter) => { applyImageFilter(img, filter); }); }); case "shape": return createShape(element); default: throw new Error(`Unknown element type: ${element.type}`); } }; const setupEventHandlers = () => { const canvas = fabricRef.current!; // 객체 수정 이벤트 canvas.on("object:modified", (e) => { const obj = e.target; const elementId = obj.get("elementId"); const updates = { position: { x: obj.left, y: obj.top }, size: { width: obj.width * obj.scaleX, height: obj.height * obj.scaleY, }, rotation: obj.angle, }; // 로컬 상태 업데이트 updateElement(elementId, updates); // 협업자에게 브로드캐스트 broadcastChange({ type: "element:updated", elementId, updates, userId: currentUser.id, }); }); // 드래그 앤 드롭 canvas.on("drop", (e) => { const dropData = e.dataTransfer.getData("application/json"); const asset = JSON.parse(dropData); addElementToCanvas(asset, { x: e.clientX - canvas.getElement().offsetLeft, y: e.clientY - canvas.getElement().offsetTop, }); }); }; // 스마트 가이드라인 const showSmartGuides = (movingObject: fabric.Object) => { const canvas = fabricRef.current!; const guidelines: fabric.Line[] = []; canvas.getObjects().forEach((obj) => { if (obj === movingObject) return; // 정렬 감지 const alignments = detectAlignments(movingObject, obj); alignments.forEach((alignment) => { const line = new fabric.Line(alignment.coords, { stroke: "#FF00FF", strokeWidth: 1, evented: false, selectable: false, }); guidelines.push(line); canvas.add(line); }); }); // 일시적으로 표시 setTimeout(() => { guidelines.forEach((line) => canvas.remove(line)); canvas.renderAll(); }, 1000); }; return ( <div className="editor-container"> <Toolbar onAddText={() => addTextElement()} onAddShape={(type) => addShapeElement(type)} onUndo={() => history.undo()} onRedo={() => history.redo()} /> <div className="canvas-wrapper"> <canvas ref={canvasRef} /> {/* 협업자 커서 */} {collaborators.map((collaborator) => ( <CollaboratorCursor key={collaborator.id} user={collaborator} position={collaborator.cursor} /> ))} </div> <Sidebar> <ElementPanel selectedElement={selectedElement} onUpdate={updateSelectedElement} /> <BrandKitPanel brandKit={design.brandKit} /> </Sidebar> </div> ); }; // 반응형 디자인 시스템 export const ResponsiveCanvas: React.FC<{ design: Design }> = ({ design }) => { const [viewport, setViewport] = useState(getViewportSize()); const [scale, setScale] = useState(1); useEffect(() => { // 뷰포트에 맞게 스케일 조정 const optimalScale = calculateOptimalScale(design.dimensions, viewport); setScale(optimalScale); }, [viewport, design.dimensions]); return ( <div className="responsive-canvas" style={{ transform: `scale(${scale})`, transformOrigin: "center", }} > <CanvasEditor {...props} /> </div> ); }; 

AI/ML 기능

스마트 디자인 제안

# Canva의 AI 기반 디자인 추천 시스템 import tensorflow as tf import numpy as np from typing import List, Dict, Tuple class DesignRecommendationEngine: def __init__(self): self.layout_model = self.load_layout_model() self.color_model = self.load_color_model() self.style_model = self.load_style_model() def suggest_layouts( self, content_type: str, elements: List[Dict], brand_guidelines: Dict ) -> List[Layout]: # 콘텐츠 특성 추출 features = self.extract_content_features(content_type, elements) # 브랜드 가이드라인 인코딩 brand_encoding = self.encode_brand_guidelines(brand_guidelines) # 레이아웃 예측 layout_predictions = self.layout_model.predict([features, brand_encoding]) # 상위 K개 레이아웃 생성 layouts = [] for pred in layout_predictions[:5]: layout = self.decode_layout(pred) # 요소 배치 최적화 optimized = self.optimize_element_placement(layout, elements) # 점수 계산 score = self.calculate_layout_score(optimized, brand_guidelines) layouts.append({ 'layout': optimized, 'score': score, 'reasoning': self.explain_layout_choice(optimized) }) return sorted(layouts, key=lambda x: x['score'], reverse=True) def auto_resize( self, original_design: Design, target_dimensions: Tuple[int, int] ) -> Design: # 중요도 맵 생성 importance_map = self.generate_importance_map(original_design) # 콘텐츠 인식 크롭 if self.needs_cropping(original_design.dimensions, target_dimensions): crop_region = self.find_optimal_crop( original_design, target_dimensions, importance_map ) cropped_design = self.apply_crop(original_design, crop_region) else: cropped_design = original_design # 요소 재배치 resized_design = self.reflow_elements( cropped_design, target_dimensions, importance_map ) # 텍스트 크기 조정 self.adjust_text_sizes(resized_design, target_dimensions) return resized_design def generate_color_palette( self, base_colors: List[str], mood: str, industry: str ) -> ColorPalette: # 색상 임베딩 color_embeddings = [self.embed_color(color) for color in base_colors] # 컨텍스트 인코딩 context = self.encode_context(mood, industry) # 팔레트 생성 palette_embedding = self.color_model.predict([ np.array(color_embeddings), context ]) # 색상 디코딩 colors = self.decode_colors(palette_embedding) # 접근성 검증 accessible_colors = self.ensure_accessibility(colors) return ColorPalette( primary=accessible_colors[0], secondary=accessible_colors[1:3], accent=accessible_colors[3:5], neutrals=self.generate_neutrals(accessible_colors[0]) ) 

인프라 및 성능 최적화

글로벌 CDN 및 엣지 컴퓨팅

// CloudFlare Workers를 사용한 엣지 최적화 export default { async fetch(request: Request, env: Env): Promise<Response> { const url = new URL(request.url); // 이미지 최적화 요청 if (url.pathname.startsWith("/media/")) { return handleMediaRequest(request, env); } // API 캐싱 if (url.pathname.startsWith("/api/")) { return handleAPIRequest(request, env); } // 정적 자산 return handleStaticAsset(request, env); }, }; async function handleMediaRequest( request: Request, env: Env, ): Promise<Response> { const url = new URL(request.url); const imageId = url.pathname.split("/")[2]; // 변환 파라미터 파싱 const options = { width: parseInt(url.searchParams.get("w") || "0"), height: parseInt(url.searchParams.get("h") || "0"), quality: parseInt(url.searchParams.get("q") || "85"), format: url.searchParams.get("fm") || "auto", }; // 캐시 키 생성 const cacheKey = `img:${imageId}:${JSON.stringify(options)}`; // 캐시 확인 const cached = await env.CACHE.get(cacheKey, "stream"); if (cached) { return new Response(cached, { headers: { "Content-Type": `image/${options.format}`, "Cache-Control": "public, max-age=31536000", "CDN-Cache-Status": "HIT", }, }); } // 원본 이미지 가져오기 const original = await fetch(`${env.ORIGIN_URL}/media/${imageId}`); if (!original.ok) { return original; } // 이미지 변환 const transformed = await transformImage( await original.arrayBuffer(), options, ); // 캐시 저장 await env.CACHE.put(cacheKey, transformed, { expirationTtl: 86400 * 30, // 30일 }); return new Response(transformed, { headers: { "Content-Type": `image/${options.format}`, "Cache-Control": "public, max-age=31536000", "CDN-Cache-Status": "MISS", }, }); } // WebAssembly 기반 이미지 변환 async function transformImage( buffer: ArrayBuffer, options: ImageOptions, ): Promise<ArrayBuffer> { // WASM 모듈 로드 (한 번만) if (!imageProcessor) { const wasmModule = await import("./image-processor.wasm"); imageProcessor = await wasmModule.default(); } // WASM 메모리에 이미지 복사 const ptr = imageProcessor.allocate(buffer.byteLength); const memory = new Uint8Array( imageProcessor.memory.buffer, ptr, buffer.byteLength, ); memory.set(new Uint8Array(buffer)); // 변환 실행 const resultPtr = imageProcessor.transform( ptr, buffer.byteLength, options.width, options.height, options.quality, options.format === "webp" ? 1 : 0, ); // 결과 복사 const resultSize = imageProcessor.getResultSize(); const result = new Uint8Array( imageProcessor.memory.buffer, resultPtr, resultSize, ).slice(); // 메모리 해제 imageProcessor.free(ptr); imageProcessor.free(resultPtr); return result.buffer; } 

기술 스택 요약:

  • Backend: Java (Spring Boot), Python (ML)
  • Frontend: React, TypeScript, Fabric.js
  • API: GraphQL, REST
  • 데이터베이스: PostgreSQL (Aurora), DynamoDB
  • 캐시: Redis, CloudFlare KV
  • 인프라: AWS, Kubernetes, CloudFlare
  • ML: TensorFlow, PyTorch
  • 실시간: WebSocket, Socket.io

💡 핵심 기술 인사이트

1. 사용자 중심 설계

  • 직관적인 드래그 앤 드롭
  • 실시간 미리보기
  • 스마트 제안 시스템

2. 확장 가능한 아키텍처

  • 마이크로서비스로 유연한 확장
  • 글로벌 CDN으로 빠른 로딩
  • 효율적인 미디어 파이프라인

3. AI 기반 자동화

  • 매직 리사이즈
  • 배경 제거
  • 스타일 제안

📈 성과 & 지표

기술적 성과

  • 페이지 로드: < 2초 (글로벌 평균)
  • 에디터 성능: 60fps 유지
  • 미디어 처리: 초당 10,000+ 이미지
  • 가용성: 99.9%+
  • API 응답: P50 < 100ms

비즈니스 영향

  • 사용자 성장: YoY 50%+
  • 기업 고객: Fortune 500 중 85%
  • 디자인 생성: 분당 5,000+ 개
  • 유료 전환율: 10%+ (업계 평균 2-3%)

🎓 Canva에서 배울 점

✅ 적용 가능한 패턴

  1. Progressive Enhancement: 점진적 기능 추가
  2. 미디어 파이프라인: 대규모 처리 최적화
  3. AI 통합: 사용자 경험 향상
  4. 글로벌 인프라: CDN 활용
  5. 템플릿 시스템: 재사용 가능한 디자인

❌ 주의사항

  1. 복잡한 렌더링: 브라우저 한계
  2. 대용량 미디어: 스토리지 비용
  3. 실시간 협업: 동기화 복잡성
  4. 글로벌 규제: 각국 법규 준수

📚 추천 리소스


🔮 미래 전망

현재 집중 분야

  1. Canva Assistant: AI 디자인 도우미
  2. 비디오 편집: 고급 비디오 기능
  3. 브랜드 관리: 엔터프라이즈 솔루션
  4. 개발자 플랫폼: API와 SDK 확대
  5. 3D 디자인: 3D 콘텐츠 지원

기술 투자 영역

  • 생성형 AI: 텍스트-이미지 생성
  • 실시간 협업: 더 나은 동시 편집
  • 엣지 컴퓨팅: 더 빠른 렌더링
  • AR/VR: 몰입형 디자인 경험
  • 블록체인: 디지털 자산 관리

"우리는 디자인을 모든 사람이 접근할 수 있도록 만들어, 세상을 더 아름답게 만들고 있습니다."

  • Canva Engineering

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

Found this helpful? Share it with others!
Tweet

🔗 Related Content

You might also be interested in these articles

🏢 company

🏢 Airbnb 기술 스택 분석

에어비앤비가 전 세계 400만 개의 숙소를 연결하는 글로벌 마켓플레이스 기술 스택 심층 분석 - Ruby on Rails에서 Service-Oriented Architecture로의 진화

24 min read
airbnb, ruby+12
Read more
🏢 company

🏢 배달의민족 기술 스택 분석

배달의민족이 한국 배달 문화를 혁신하며 동남아로 확장한 기술 스택 심층 분석 - Java, Spring, Kotlin으로 구축한 O2O 플랫폼

51 min read
baemin, woowa+10
Read more
🏢 company

🏢 당근마켓 기술 스택 분석

당근마켓이 동네 커뮤니티를 혁신하며 유니콘으로 성장한 기술 스택 심층 분석 - Ruby on Rails, Kotlin, Swift로 만든 하이퍼로컬 플랫폼

48 min read
karrot, daangn+9
Read more
🏢 company

🏢 Netflix 기술 스택 분석

넷플릭스가 전 세계 2.4억 사용자에게 스트리밍 서비스를 제공하는 기술 스택 심층 분석 - 마이크로서비스, AWS, Chaos Engineering의 교과서

9 min read
netflix, microservices+11
Read more
🏢 company

🏢 Notion 기술 스택 분석

노션이 수천만 사용자에게 All-in-One 워크스페이스를 제공하는 기술 스택 심층 분석 - 블록 기반 에디터, 실시간 협업, 그리고 확장 가능한 데이터베이스

24 min read
notion, typescript+11
Read more
🏢 company

🏢 Stripe 기술 스택 분석

스트라이프가 전 세계 수백만 비즈니스의 결제를 처리하는 기술 스택 심층 분석 - Ruby, Go, Java로 구축한 금융 인프라의 미래

33 min read
stripe, ruby+11
Read more

Found this helpful?

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