🏢 Canva 기술 스택 분석
캔바가 1억 명 이상의 사용자에게 쉽고 강력한 디자인 도구를 제공하는 기술 스택 심층 분석 - Java, TypeScript, AWS로 구축한 대규모 디자인 플랫폼
🏢 Canva 기술 스택 분석
전문 디자이너가 아닌 사람도 쉽게 사용할 수 있는 디자인 플랫폼 Canva의 기술 스택을 심층 분석합니다.
"Empowering the world to design" - 디자인의 민주화를 실현하는 기술
📊 회사 개요
서비스 규모
- 월간 활성 사용자: 1.35억+ 명
- 생성된 디자인: 150억+ 개
- 서비스 국가: 190+ 개국
- 템플릿: 80만+ 개
- 미디어 업로드: 70억+ 개
엔지니어링 조직
- 엔지니어: 600명+ (전체 직원 3,500+)
- 문화: "Be a good human"
- 오피스: 시드니, 마닐라, 베이징, 오스틴
- 기술 철학: 단순함과 확장성의 균형
기술적 도전과제
- 대규모 미디어 처리: 수십억 개의 이미지/비디오
- 실시간 렌더링: 브라우저에서 복잡한 디자인
- 글로벌 성능: 전 세계 균일한 경험
- AI 통합: 스마트 디자인 제안
🏗️ 아키텍처 Overview
시스템 다이어그램
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Frontend │────►│ API │────►│ Services │
│ (React) │ │ Gateway │ │(Microservices)
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
│ ┌─────▼─────┐ ┌─────▼─────┐
│ │ GraphQL │ │ Media │
│ │ Server │ │ Pipeline │
│ └───────────┘ └───────────┘
│ │ │
└────────────────────┤ │
┌─────▼─────┐ ┌─────▼─────┐
│ CDN │ │ Database │
│(CloudFront)│ │(Aurora) │
└───────────┘ └───────────┘
핵심 설계 원칙
- User-Centric: 사용자 경험 최우선
- Scalable by Design: 처음부터 확장 고려
- Data-Driven: 모든 결정은 데이터 기반
- Progressive Enhancement: 점진적 기능 향상
- 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에서 배울 점
✅ 적용 가능한 패턴
- Progressive Enhancement: 점진적 기능 추가
- 미디어 파이프라인: 대규모 처리 최적화
- AI 통합: 사용자 경험 향상
- 글로벌 인프라: CDN 활용
- 템플릿 시스템: 재사용 가능한 디자인
❌ 주의사항
- 복잡한 렌더링: 브라우저 한계
- 대용량 미디어: 스토리지 비용
- 실시간 협업: 동기화 복잡성
- 글로벌 규제: 각국 법규 준수
📚 추천 리소스
🔮 미래 전망
현재 집중 분야
- Canva Assistant: AI 디자인 도우미
- 비디오 편집: 고급 비디오 기능
- 브랜드 관리: 엔터프라이즈 솔루션
- 개발자 플랫폼: API와 SDK 확대
- 3D 디자인: 3D 콘텐츠 지원
기술 투자 영역
- 생성형 AI: 텍스트-이미지 생성
- 실시간 협업: 더 나은 동시 편집
- 엣지 컴퓨팅: 더 빠른 렌더링
- AR/VR: 몰입형 디자인 경험
- 블록체인: 디지털 자산 관리
"우리는 디자인을 모든 사람이 접근할 수 있도록 만들어, 세상을 더 아름답게 만들고 있습니다."
- Canva Engineering
마지막 업데이트: 2025-01-28