🏢 Notion 기술 스택 분석
노션이 수천만 사용자에게 All-in-One 워크스페이스를 제공하는 기술 스택 심층 분석 - 블록 기반 에디터, 실시간 협업, 그리고 확장 가능한 데이터베이스
🏢 Notion 기술 스택 분석
문서, 데이터베이스, 칸반 보드를 하나로 통합한 Notion의 기술 스택을 심층 분석합니다.
"A new tool that blends your everyday work apps into one" - 모든 것을 블록으로 만드는 기술
📊 회사 개요
서비스 규모
- 사용자: 3,000만+ (개인 및 팀)
- 기업 고객: 400만+ 팀
- 월간 블록 생성: 10억+ 개
- 데이터베이스 항목: 5억+ 개
- 일일 API 요청: 1억+ 건
엔지니어링 조직
- 엔지니어: 200명+ (전체 직원 500+)
- 문화: "Augmenting Human Intellect"
- 원격 우선: 글로벌 분산 팀
- 기술 철학: 단순함과 강력함의 균형
기술적 도전과제
- 블록 기반 에디터: 무한히 중첩 가능한 구조
- 실시간 협업: 동시 편집과 충돌 해결
- 유연한 데이터베이스: 스프레드시트 같은 강력함
- 오프라인 지원: 로컬 우선 아키텍처
🏗️ 아키텍처 Overview
시스템 다이어그램
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Clients │────►│ API │────►│ Database │
│(Web/Desktop)│ │ Gateway │ │ Service │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
│ ┌─────▼─────┐ ┌─────▼─────┐
│ │ Real-time │ │PostgreSQL │
│ │ Service │ │ Cluster │
│ └───────────┘ └───────────┘
│ │ │
└────────────────────┤ │
┌─────▼─────┐ ┌─────▼─────┐
│ Search │ │ Block │
│ Service │ │ Storage │
└───────────┘ └───────────┘
핵심 설계 원칙
- Everything is a Block: 모든 콘텐츠는 블록
- Local-First: 오프라인 우선 설계
- CRDT: 충돌 없는 동시 편집
- Flexible Schema: 동적 데이터베이스 구조
- Progressive Enhancement: 점진적 기능 향상
기술 진화
- 2013-2015: 초기 프로토타입 (웹 기반)
- 2016-2017: 데스크톱 앱 출시
- 2018-2019: 데이터베이스 기능 추가
- 2020-2021: API 플랫폼 공개
- 2022-2023: AI 통합 (Notion AI)
- 2024-현재: 엔터프라이즈 확장
🧩 기술 스택 상세
Backend 기술
블록 저장 시스템
// Notion의 블록 모델
interface Block {
id: string;
type: BlockType;
properties: Record<string, any>;
content?: string[];
children?: string[]; // 자식 블록 ID
parent: string;
created_time: number;
last_edited_time: number;
created_by: string;
last_edited_by: string;
version: number;
}
type BlockType =
| "text"
| "heading_1"
| "heading_2"
| "heading_3"
| "bulleted_list_item"
| "numbered_list_item"
| "toggle"
| "to_do"
| "quote"
| "divider"
| "callout"
| "image"
| "video"
| "file"
| "pdf"
| "bookmark"
| "code"
| "equation"
| "database"
| "table_row"
| "column_list"
| "synced_block"
| "template";
// 블록 서비스
export class BlockService {
private db: PostgresClient;
private cache: RedisClient;
private search: ElasticsearchClient;
async getBlock(blockId: string, depth: number = 0): Promise<Block> {
// 캐시 확인
const cached = await this.cache.get(`block:${blockId}`);
if (cached) return JSON.parse(cached);
// DB에서 조회
const block = await this.db.query("SELECT * FROM blocks WHERE id = $1", [
blockId,
]);
// 자식 블록 재귀적 로드
if (depth > 0 && block.children?.length > 0) {
block.childBlocks = await Promise.all(
block.children.map((childId) => this.getBlock(childId, depth - 1)),
);
}
// 캐시 저장
await this.cache.setex(`block:${blockId}`, 300, JSON.stringify(block));
return block;
}
async updateBlock(
blockId: string,
updates: Partial<Block>,
userId: string,
): Promise<Block> {
// 낙관적 잠금
const current = await this.getBlock(blockId);
if (updates.version !== current.version) {
throw new ConflictError("Version mismatch");
}
// 업데이트 적용
const updated = {
...current,
...updates,
version: current.version + 1,
last_edited_time: Date.now(),
last_edited_by: userId,
};
// 트랜잭션으로 저장
await this.db.transaction(async (trx) => {
// 블록 업데이트
await trx.query(
`UPDATE blocks
SET properties = $1, content = $2, version = $3,
last_edited_time = $4, last_edited_by = $5
WHERE id = $6`,
[
updated.properties,
updated.content,
updated.version,
updated.last_edited_time,
updated.last_edited_by,
blockId,
],
);
// 히스토리 저장
await trx.query(
`INSERT INTO block_history
(block_id, version, properties, content, edited_by, edited_at)
VALUES ($1, $2, $3, $4, $5, $6)`,
[
blockId,
current.version,
current.properties,
current.content,
userId,
Date.now(),
],
);
});
// 캐시 무효화
await this.cache.del(`block:${blockId}`);
// 실시간 브로드캐스트
await this.broadcastUpdate(blockId, updated);
// 검색 인덱스 업데이트
await this.search.update({
index: "blocks",
id: blockId,
body: updated,
});
return updated;
}
}
데이터베이스 시스템
// Notion의 유연한 데이터베이스
interface Database {
id: string;
title: string;
properties: Record<string, Property>;
views: View[];
filters: Filter[];
sorts: Sort[];
}
interface Property {
id: string;
name: string;
type: PropertyType;
options?: any;
}
type PropertyType =
| "title"
| "text"
| "number"
| "select"
| "multi_select"
| "date"
| "person"
| "files"
| "checkbox"
| "url"
| "email"
| "phone"
| "formula"
| "relation"
| "rollup";
// 데이터베이스 쿼리 엔진
export class DatabaseQueryEngine {
async executeQuery(
databaseId: string,
query: DatabaseQuery,
): Promise<QueryResult> {
// 1. 스키마 로드
const schema = await this.getSchema(databaseId);
// 2. 쿼리 최적화
const optimizedQuery = this.optimizeQuery(query, schema);
// 3. 실행 계획 생성
const executionPlan = this.createExecutionPlan(optimizedQuery);
// 4. 쿼리 실행
let results = await this.executeplan(executionPlan);
// 5. 필터 적용
if (query.filter) {
results = this.applyFilters(results, query.filter);
}
// 6. 정렬 적용
if (query.sorts) {
results = this.applySorts(results, query.sorts);
}
// 7. Formula/Rollup 계산
results = await this.computeDerivedProperties(results, schema);
// 8. 페이지네이션
const paginated = this.paginate(results, query.page_size, query.cursor);
return {
results: paginated.items,
has_more: paginated.has_more,
next_cursor: paginated.next_cursor,
};
}
// Relation 처리
async resolveRelations(
rows: DatabaseRow[],
property: RelationProperty,
): Promise<void> {
const relatedIds = rows.flatMap(
(row) => row.properties[property.id]?.relation || [],
);
const relatedRows = await this.batchGetRows(
property.relation_database_id,
relatedIds,
);
// 관계 데이터 매핑
const relatedMap = new Map(relatedRows.map((row) => [row.id, row]));
rows.forEach((row) => {
const relationIds = row.properties[property.id]?.relation || [];
row.properties[property.id].resolved = relationIds.map((id) =>
relatedMap.get(id),
);
});
}
}
Frontend 기술
블록 에디터
// Notion의 블록 에디터 컴포넌트
import { useEffect, useRef, useState } from "react";
import { Editor, Transforms, createEditor } from "slate";
import { useCollaboration } from "./collaboration";
interface BlockEditorProps {
blockId: string;
initialContent: any;
onSave: (content: any) => void;
}
export function BlockEditor({
blockId,
initialContent,
onSave,
}: BlockEditorProps) {
const editor = useMemo(() => withNotion(createEditor()), []);
const [value, setValue] = useState(initialContent);
const collaboration = useCollaboration(blockId);
// 협업 동기화
useEffect(() => {
const unsubscribe = collaboration.subscribe((operation) => {
if (operation.source !== "local") {
// 원격 변경사항 적용
Editor.withoutNormalizing(editor, () => {
operation.ops.forEach((op) => {
editor.apply(op);
});
});
}
});
return unsubscribe;
}, [collaboration]);
// 로컬 변경사항 처리
const handleChange = (newValue: any) => {
setValue(newValue);
// 변경사항 감지
const operations = editor.operations;
if (operations.length > 0) {
// 협업 서버로 전송
collaboration.sendOperations(operations);
// 디바운스된 저장
debouncedSave(newValue);
}
};
// 블록 타입별 렌더링
const renderElement = useCallback((props: any) => {
const { element, children, attributes } = props;
switch (element.type) {
case "heading_1":
return <h1 {...attributes}>{children}</h1>;
case "code":
return (
<CodeBlock
{...attributes}
language={element.language}
onLanguageChange={(lang) =>
updateBlockProperty(element, "language", lang)
}
>
{children}
</CodeBlock>
);
case "database":
return (
<DatabaseView
{...attributes}
databaseId={element.databaseId}
viewId={element.viewId}
/>
);
case "toggle":
return (
<ToggleBlock
{...attributes}
isOpen={element.isOpen}
onToggle={() =>
updateBlockProperty(element, "isOpen", !element.isOpen)
}
>
{children}
</ToggleBlock>
);
default:
return <p {...attributes}>{children}</p>;
}
}, []);
// 슬래시 명령어
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "/") {
showBlockMenu();
}
// 블록 이동 단축키
if (event.metaKey || event.ctrlKey) {
switch (event.key) {
case "ArrowUp":
moveBlockUp();
break;
case "ArrowDown":
moveBlockDown();
break;
case "d":
duplicateBlock();
break;
}
}
};
return (
<Slate editor={editor} value={value} onChange={handleChange}>
<Editable
renderElement={renderElement}
onKeyDown={handleKeyDown}
placeholder="Type '/' for commands..."
spellCheck
autoFocus
/>
</Slate>
);
}
// Notion 특화 플러그인
function withNotion(editor: Editor) {
const { insertData, insertText, deleteBackward } = editor;
// 드래그 앤 드롭 지원
editor.insertData = (data: DataTransfer) => {
if (data.files.length > 0) {
// 파일 업로드 처리
Array.from(data.files).forEach((file) => {
uploadFile(file).then((url) => {
insertFileBlock(editor, url, file.type);
});
});
return;
}
insertData(data);
};
// 자동 포맷팅
editor.insertText = (text: string) => {
// 마크다운 단축키
if (text === " ") {
const [match] = Editor.nodes(editor, {
match: (n) => n.type === "paragraph",
});
if (match) {
const [node, path] = match;
const text = Node.string(node);
// 헤딩 변환
if (text.match(/^#{1,3}/)) {
const level = text.match(/^#+/)[0].length;
Transforms.setNodes(
editor,
{ type: `heading_${level}` },
{ at: path }
);
Transforms.delete(editor, {
at: { path, offset: 0 },
distance: level + 1,
});
return;
}
// 리스트 변환
if (text.match(/^[-*]/)) {
Transforms.setNodes(
editor,
{ type: "bulleted_list_item" },
{ at: path }
);
Transforms.delete(editor, {
at: { path, offset: 0 },
distance: 2,
});
return;
}
}
}
insertText(text);
};
return editor;
}
실시간 협업
CRDT 기반 동기화
// Notion의 CRDT 구현
import { Doc, Text } from "yjs";
import { WebsocketProvider } from "y-websocket";
import { IndexeddbPersistence } from "y-indexeddb";
export class CollaborationManager {
private doc: Doc;
private provider: WebsocketProvider;
private persistence: IndexeddbPersistence;
private awareness: Awareness;
constructor(pageId: string, userId: string) {
this.doc = new Doc();
// 로컬 영속성 (오프라인 지원)
this.persistence = new IndexeddbPersistence(pageId, this.doc);
// WebSocket 연결
this.provider = new WebsocketProvider(
"wss://notion-sync.com",
pageId,
this.doc,
{
params: { userId },
WebSocketPolyfill: ReconnectingWebSocket,
},
);
// Awareness (커서, 선택 영역)
this.awareness = this.provider.awareness;
this.awareness.setLocalState({
user: { id: userId, color: generateUserColor(userId) },
cursor: null,
});
}
// 텍스트 동기화
syncText(blockId: string, callback: (text: string) => void) {
const ytext = this.doc.getText(blockId);
ytext.observe((event) => {
callback(ytext.toString());
});
return {
insert: (index: number, text: string) => {
ytext.insert(index, text);
},
delete: (index: number, length: number) => {
ytext.delete(index, length);
},
format: (index: number, length: number, attributes: any) => {
ytext.format(index, length, attributes);
},
};
}
// 블록 트리 동기화
syncBlockTree(callback: (tree: BlockTree) => void) {
const ytree = this.doc.getMap("blocks");
ytree.observe((event) => {
const tree = this.buildBlockTree(ytree);
callback(tree);
});
return {
addBlock: (parentId: string, block: Block) => {
this.doc.transact(() => {
ytree.set(block.id, block);
const parent = ytree.get(parentId);
if (parent) {
parent.children = [...(parent.children || []), block.id];
ytree.set(parentId, parent);
}
});
},
moveBlock: (blockId: string, newParentId: string, index: number) => {
this.doc.transact(() => {
// 트랜잭션으로 원자적 이동
const block = ytree.get(blockId);
const oldParent = this.findParent(blockId, ytree);
const newParent = ytree.get(newParentId);
if (oldParent) {
oldParent.children = oldParent.children.filter(
(id) => id !== blockId,
);
ytree.set(oldParent.id, oldParent);
}
if (newParent) {
newParent.children.splice(index, 0, blockId);
ytree.set(newParentId, newParent);
}
block.parent = newParentId;
ytree.set(blockId, block);
});
},
};
}
}
오프라인 지원
Local-First 아키텍처
// 오프라인 우선 데이터 동기화
export class OfflineSync {
private localDB: IDBDatabase;
private syncQueue: SyncOperation[] = [];
private isOnline: boolean = navigator.onLine;
constructor() {
// 온라인 상태 모니터링
window.addEventListener("online", () => this.handleOnline());
window.addEventListener("offline", () => this.handleOffline());
// Service Worker 등록
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js");
}
}
async saveLocal(operation: Operation) {
const tx = this.localDB.transaction(["operations"], "readwrite");
const store = tx.objectStore("operations");
// 로컬 저장
await store.add({
...operation,
timestamp: Date.now(),
synced: false,
});
// 온라인이면 즉시 동기화
if (this.isOnline) {
this.syncOperation(operation);
} else {
// 오프라인이면 큐에 추가
this.syncQueue.push(operation);
}
}
private async handleOnline() {
this.isOnline = true;
// 대기 중인 작업 동기화
const pendingOps = await this.getPendingOperations();
for (const op of pendingOps) {
try {
await this.syncOperation(op);
await this.markSynced(op.id);
} catch (error) {
if (error.code === "CONFLICT") {
// 충돌 해결
await this.resolveConflict(op, error.serverVersion);
}
}
}
}
private async resolveConflict(localOp: Operation, serverVersion: any) {
// 3-way merge
const baseVersion = await this.getBaseVersion(localOp.blockId);
const merged = this.threeWayMerge(baseVersion, localOp, serverVersion);
// 병합된 버전 저장
await this.saveLocal(merged);
// UI에 충돌 알림
this.notifyConflictResolved(localOp.blockId, merged);
}
}
// Service Worker
self.addEventListener("fetch", (event) => {
if (event.request.url.includes("/api/")) {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
const fetchPromise = fetch(event.request).then((networkResponse) => {
// 캐시 업데이트
if (networkResponse.status === 200) {
const responseToCache = networkResponse.clone();
caches.open("notion-api-v1").then((cache) => {
cache.put(event.request, responseToCache);
});
}
return networkResponse;
});
// 캐시 우선, 네트워크 폴백
return cachedResponse || fetchPromise;
}),
);
}
});
기술 스택 요약:
- Frontend: React, TypeScript, Slate.js, Electron
- Backend: Node.js, PostgreSQL, Redis
- 실시간: WebSocket, Yjs (CRDT)
- 검색: Elasticsearch
- 인프라: AWS, Cloudflare
- 모바일: React Native + Native
💡 핵심 기술 인사이트
1. 블록 기반 아키텍처의 힘
- 무한한 확장성과 조합 가능성
- 일관된 데이터 모델
- 재사용 가능한 컴포넌트
2. 유연한 데이터베이스 시스템
- 스키마리스 설계
- 다양한 뷰 (테이블, 보드, 갤러리 등)
- 강력한 필터링과 정렬
3. 로컬 우선 설계
- 오프라인에서도 완전한 기능
- 빠른 반응성
- 데이터 소유권 보장
📈 성과 & 지표
기술적 성과
- 페이지 로드: < 1초 (캐시된 경우)
- 실시간 동기화: < 100ms 지연
- 오프라인 지원: 100% 기능 가능
- API 응답시간: P50 < 50ms
- 검색 성능: < 200ms (수백만 블록)
비즈니스 영향
- 사용자 증가: YoY 100%+
- 기업 고객: Fortune 500 중 50%+
- 개발자 생산성: 40% 향상 (자체 측정)
- API 파트너: 1,000+ 통합
🎓 Notion에서 배울 점
✅ 적용 가능한 패턴
- 블록 기반 설계: 확장 가능한 콘텐츠 모델
- CRDT 협업: 충돌 없는 동시 편집
- 로컬 우선: 오프라인 지원 아키텍처
- 유연한 스키마: 동적 데이터베이스
- 점진적 향상: 기능별 단계적 로딩
❌ 주의사항
- 복잡한 데이터 모델: 블록 트리 관리
- 성능 최적화: 대규모 페이지 처리
- 검색 색인: 실시간 업데이트 부하
- 동기화 복잡성: 충돌 해결 로직
📚 추천 리소스
🔮 미래 전망
현재 집중 분야
- Notion AI: GPT 통합 작성 도우미
- Notion Calendar: 일정 관리 통합
- Notion Mail: 이메일 클라이언트
- 고급 자동화: Zapier 같은 워크플로우
- 엔터프라이즈 보안: SSO, 감사 로그
기술 투자 영역
- WASM: 브라우저 성능 향상
- 분산 시스템: 글로벌 확장
- AI/ML: 스마트 추천, 자동 정리
- 모바일 최적화: 네이티브 수준 경험
- 개발자 플랫폼: 더 많은 API, SDK
"우리는 도구를 만드는 것이 아니라, 사람들이 더 나은 방식으로 생각하고 일할 수 있는 환경을 만들고 있습니다."
- Notion Engineering
마지막 업데이트: 2025-01-28