Craft: 네이티브 우선 문서 탁월함
Craft가 Apple Design Award 2021을 수상한 이유: 네이티브 성능, 중첩 페이지, 블록 아키텍처, 원클릭 웹 퍼블리싱. Swift 구현 패턴 포함.
Craft: 네이티브 우선 문서 작성의 정수
"우리는 사고를 위한 도구가 생각의 장애물이 아닌 확장이 되어야 한다고 믿습니다."
Craft는 네이티브 앱이 웹 앱으로는 도달할 수 없는 경험을 제공할 수 있음을 증명합니다. 깊은 플랫폼 통합으로 구축되어, 디지털 노트 작성이 종이처럼 자연스럽게 느껴지는 반응성과 완성도를 달성했습니다.
핵심 요점
- 네이티브가 Electron을 이긴다 - 50ms 미만의 응답 시간은 웹 래퍼가 아닌 플랫폼별 UI를 요구합니다
- 페이지 안의 페이지 - 모든 블록이 페이지가 될 수 있어, 콘텐츠에서 구조가 자연스럽게 형성됩니다
- 다양한 레이아웃, 동일한 데이터 - 리스트, 카드, 갤러리, 보드가 각 맥락에 맞는 뷰를 제공합니다
- 원클릭 웹 퍼블리싱 - 공유 페이지는 수신자의 계정이나 앱 다운로드 없이도 작동합니다
- 동일하지 않고 플랫폼에 적합하게 - Mac용 Craft는 Mac답게, iOS용 Craft는 iOS답게 느껴집니다
Craft가 중요한 이유
Craft는 크로스 플랫폼 편의성을 위해 네이티브 성능을 타협하지 않음으로써 Apple Design Award 2021을 수상했고 지속적으로 App Store에서 인정받고 있습니다.
주요 성과: - iOS, macOS, Windows에서 진정한 네이티브 앱 (Electron 아님) - 모든 인터랙션에서 50ms 미만의 응답 시간 - 오프라인 우선 설계와 원활한 동기화 - 웹 앱의 버벅거림 없는 블록 기반 아키텍처 - 계정 없이도 작동하는 아름다운 공유 페이지
핵심 디자인 철학
네이티브 우선, 웹 래핑 아님
Craft의 결정적 선택: 공유 비즈니스 로직을 사용하되 플랫폼별 UI로 각 플랫폼용 네이티브 앱을 구축합니다.
ELECTRON/WEB APPROACH CRAFT'S NATIVE APPROACH
───────────────────────────────────────────────────────────────────
Single codebase (JavaScript) Platform-specific UI (Swift, etc.)
Web rendering engine Native rendering
Cross-platform "consistency" Platform-appropriate behavior
~200ms input latency ~16ms input latency
Generic keyboard shortcuts Platform-native shortcuts
Web-standard text selection Native text engine
핵심 통찰: 사용자는 모든 곳에서 똑같이 보이는 앱을 원하지 않습니다—자신의 플랫폼에서 자연스럽게 느껴지는 앱을 원합니다.
macOS via Catalyst (제대로 구현)
Craft는 Mac Catalyst를 사용하지만 진정한 Mac 네이티브처럼 느껴지도록 대폭 커스터마이징합니다:
// Craft's Catalyst customization approach
#if targetEnvironment(macCatalyst)
extension SceneDelegate {
func scene(_ scene: UIScene, willConnectTo session: UISceneSession) {
guard let windowScene = scene as? UIWindowScene else { return }
// Enable native Mac toolbar
if let titlebar = windowScene.titlebar {
titlebar.titleVisibility = .hidden
titlebar.toolbar = createNativeToolbar()
}
// Mac-specific window sizing
windowScene.sizeRestrictions?.minimumSize = CGSize(width: 800, height: 600)
// Enable window tabs
UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.forEach { $0.titlebar?.toolbar?.showsBaselineSeparator = false }
}
private func createNativeToolbar() -> NSToolbar {
let toolbar = NSToolbar(identifier: "CraftToolbar")
toolbar.displayMode = .iconOnly
toolbar.delegate = self
return toolbar
}
}
#endif
패턴 라이브러리
1. 블록 기반 콘텐츠 아키텍처
Craft의 모든 요소는 블록입니다. 콘텐츠가 있는 모든 블록은 잠재적으로 페이지가 될 수 있어, 인지적 부담 없이 무한 중첩이 가능합니다.
블록 유형:
TEXT BLOCKS
├── Paragraph (default)
├── Heading 1, 2, 3
├── Quote
├── Callout (info, warning, success)
└── Code (with syntax highlighting)
MEDIA BLOCKS
├── Image (with caption)
├── Video
├── File attachment
├── Drawing (Apple Pencil)
└── Audio recording
STRUCTURAL BLOCKS
├── Toggle (collapsible)
├── Page (nested document)
├── Divider
└── Table
구현 개념:
protocol CraftBlock: Identifiable {
var id: UUID { get }
var content: BlockContent { get set }
var style: BlockStyle { get set }
var children: [any CraftBlock] { get set }
var metadata: BlockMetadata { get }
}
enum BlockContent {
case text(AttributedString)
case image(ImageData)
case page(PageReference)
case toggle(isExpanded: Bool)
case code(language: String, content: String)
case table(TableData)
}
struct BlockMetadata {
let created: Date
let modified: Date
let createdBy: UserReference?
}
// Every block can contain other blocks
class PageBlock: CraftBlock {
var id = UUID()
var content: BlockContent
var style: BlockStyle
var children: [any CraftBlock] = []
var metadata: BlockMetadata
// A page is just a block that can be opened full-screen
var canOpenAsPage: Bool { true }
}
핵심 통찰: 모든 블록이 페이지가 될 수 있을 때, 정보 아키텍처는 미리 정해진 구조가 아닌 콘텐츠에서 자연스럽게 나타납니다.
2. 중첩 페이지 (페이지 안의 페이지)
Craft의 대표 기능: 모든 블록이 페이지가 될 수 있고, 페이지는 무한히 중첩됩니다.
DOCUMENT STRUCTURE
───────────────────────────────────────────────────────────────────
📄 Project Alpha
├── 📝 Introduction paragraph
├── 📄 Research Notes ← This is a page (nested document)
│ ├── 📝 User interviews
│ ├── 📄 Interview: Sarah ← Another nested page
│ │ └── 📝 Key insights
│ └── 📝 Competitive analysis
├── 📝 Timeline overview
└── 📄 Meeting Notes ← Another page
└── 📝 Action items
Navigation: Breadcrumb trail shows path
Project Alpha > Research Notes > Interview: Sarah
SwiftUI 구현 패턴:
struct PageView: View {
@Bindable var page: PageDocument
@State private var selectedBlock: CraftBlock?
var body: some View {
ScrollView {
LazyVStack(alignment: .leading, spacing: 0) {
ForEach(page.blocks) { block in
BlockView(block: block, onTap: { tapped in
if tapped.canOpenAsPage {
navigateToPage(tapped)
} else {
selectedBlock = tapped
}
})
}
}
}
.navigationTitle(page.title)
.toolbar {
// Breadcrumb navigation
ToolbarItem(placement: .principal) {
BreadcrumbView(path: page.ancestorPath)
}
}
}
}
struct BreadcrumbView: View {
let path: [PageReference]
var body: some View {
HStack(spacing: 4) {
ForEach(Array(path.enumerated()), id: \.element.id) { index, page in
if index > 0 {
Image(systemName: "chevron.right")
.font(.caption)
.foregroundStyle(.secondary)
}
Button(page.title) {
navigateTo(page)
}
.buttonStyle(.plain)
.foregroundStyle(index == path.count - 1 ? .primary : .secondary)
}
}
}
}
3. 카드 레이아웃 시스템
Craft는 페이지에 5가지 시각적 스타일을 제공하여 문서를 한눈에 파악할 수 있게 합니다.
카드 스타일:
STYLE 1: LIST (기본)
┌────────────────────────────────────────┐
│ 📄 Page Title │
│ Preview text appears here... │
└────────────────────────────────────────┘
STYLE 2: CARD (중간 크기)
┌──────────────────┐
│ ┌──────────────┐ │
│ │ [Image] │ │
│ └──────────────┘ │
│ Page Title │
│ Preview text... │
└──────────────────┘
STYLE 3: CARD (큰 크기)
┌────────────────────────────────────────┐
│ ┌────────────────────────────────────┐ │
│ │ │ │
│ │ [Cover Image] │ │
│ │ │ │
│ └────────────────────────────────────┘ │
│ Page Title │
│ Longer preview text with more room... │
└────────────────────────────────────────┘
STYLE 4: GALLERY (그리드)
┌────────┐ ┌────────┐ ┌────────┐
│ [Img] │ │ [Img] │ │ [Img] │
│ Title │ │ Title │ │ Title │
└────────┘ └────────┘ └────────┘
STYLE 5: BOARD (칸반 스타일)
│ To Do │ Doing │ Done │
├──────────┼──────────┼──────────┤
│ Task 1 │ Task 3 │ Task 5 │
│ Task 2 │ Task 4 │ │
구현:
enum PageLayoutStyle: String, CaseIterable {
case list = "list"
case cardMedium = "card_medium"
case cardLarge = "card_large"
case gallery = "gallery"
case board = "board"
}
struct PageGridView: View {
let pages: [PageReference]
let style: PageLayoutStyle
var body: some View {
switch style {
case .list:
LazyVStack(spacing: 8) {
ForEach(pages) { page in
ListRowView(page: page)
}
}
case .cardMedium, .cardLarge:
let columns = style == .cardMedium ? 3 : 2
LazyVGrid(columns: Array(repeating: .init(.flexible()), count: columns)) {
ForEach(pages) { page in
CardView(page: page, size: style == .cardLarge ? .large : .medium)
}
}
case .gallery:
LazyVGrid(columns: Array(repeating: .init(.flexible()), count: 4)) {
ForEach(pages) { page in
GalleryThumbnail(page: page)
}
}
case .board:
BoardView(pages: pages)
}
}
}
4. 페이지 배경과 테마
각 페이지는 배경과 강조 색상을 통해 고유한 시각적 아이덴티티를 가질 수 있습니다.
struct PageAppearance {
// Background options
enum Background {
case solid(Color)
case gradient(Gradient)
case image(ImageReference)
case paper(PaperTexture)
}
// Paper textures for writing feel
enum PaperTexture: String {
case none
case lined
case grid
case dotted
}
var background: Background = .solid(.white)
var accentColor: Color = .blue
var iconEmoji: String?
var coverImage: ImageReference?
}
// Applied to page
struct PageContainer: View {
let page: PageDocument
var body: some View {
ZStack {
// Background layer
backgroundView(for: page.appearance.background)
// Content layer
PageContentView(page: page)
}
}
@ViewBuilder
private func backgroundView(for background: PageAppearance.Background) -> some View {
switch background {
case .solid(let color):
color.ignoresSafeArea()
case .gradient(let gradient):
LinearGradient(gradient: gradient, startPoint: .top, endPoint: .bottom)
.ignoresSafeArea()
case .image(let ref):
AsyncImage(url: ref.url) { image in
image.resizable().scaledToFill()
} placeholder: {
Color.gray.opacity(0.1)
}
.ignoresSafeArea()
.overlay(.ultraThinMaterial)
case .paper(let texture):
PaperTextureView(texture: texture)
}
}
}
5. 페이지 공유 (웹 퍼블리싱)
Craft는 문서로부터 아름답고 반응형인 웹 페이지를 생성합니다. 보는 데 계정이 필요하지 않습니다.
DOCUMENT IN CRAFT SHARE PAGE ON WEB
───────────────────────────────────────────────────────────────────
📄 Project Proposal https://www.craft.do/s/abc123
├── 📝 Executive Summary →
├── 📄 Budget Details Clean, responsive layout
├── 📝 Timeline Typography preserved
└── 📄 Team Bios Images optimized
Dark mode supported
No Craft account needed
주요 기능: - 원클릭 퍼블리싱 - 커스텀 도메인 사용 가능 - 비밀번호 보호 옵션 - 조회수 분석 - SEO 친화적 렌더링 - 모든 기기에서 반응형
비주얼 디자인 시스템
색상 팔레트
extension Color {
// Craft's signature palette
static let craftPurple = Color(hex: "#6366F1") // Primary accent
static let craftBackground = Color(hex: "#FAFAFA") // Light mode
static let craftSurface = Color(hex: "#FFFFFF")
// Semantic colors
static let craftSuccess = Color(hex: "#10B981")
static let craftWarning = Color(hex: "#F59E0B")
static let craftError = Color(hex: "#EF4444")
static let craftInfo = Color(hex: "#3B82F6")
// Text hierarchy
static let craftTextPrimary = Color(hex: "#111827")
static let craftTextSecondary = Color(hex: "#6B7280")
static let craftTextMuted = Color(hex: "#9CA3AF")
}
타이포그래피
struct CraftTypography {
// 문서 타이포그래피
static let title = Font.system(size: 32, weight: .bold, design: .default)
static let heading1 = Font.system(size: 28, weight: .semibold)
static let heading2 = Font.system(size: 22, weight: .semibold)
static let heading3 = Font.system(size: 18, weight: .medium)
static let body = Font.system(size: 16, weight: .regular)
static let caption = Font.system(size: 14, weight: .regular)
// 코드 블록
static let code = Font.system(size: 14, weight: .regular, design: .monospaced)
// 줄 높이 (가독성을 위해 넉넉하게)
static let bodyLineSpacing: CGFloat = 8
static let headingLineSpacing: CGFloat = 4
}
간격 시스템
struct CraftSpacing {
// 블록 간격
static let blockGap: CGFloat = 4 // 블록 사이
static let sectionGap: CGFloat = 24 // 섹션 사이
static let pageMargin: CGFloat = 40 // 페이지 가장자리 (데스크톱)
static let mobileMargin: CGFloat = 16 // 페이지 가장자리 (모바일)
// 콘텐츠 너비
static let maxContentWidth: CGFloat = 720 // 최적의 가독성
static let wideContentWidth: CGFloat = 960 // 테이블, 갤러리용
}
애니메이션 철학
부드러운 페이지 전환
// 페이지 간 내비게이션은 matchedGeometry를 사용
struct NavigationTransition: View {
@Namespace private var namespace
@State private var selectedPage: PageReference?
var body: some View {
ZStack {
// 페이지 목록
PageListView(
onSelect: { page in
withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) {
selectedPage = page
}
},
namespace: namespace
)
// 선택된 페이지가 카드 위치에서 확장됨
if let page = selectedPage {
PageDetailView(page: page)
.matchedGeometryEffect(id: page.id, in: namespace)
.transition(.scale.combined(with: .opacity))
}
}
}
}
블록 애니메이션
// 블록은 순서 변경 시 애니메이션 적용
struct AnimatedBlockList: View {
@Binding var blocks: [CraftBlock]
var body: some View {
LazyVStack(spacing: CraftSpacing.blockGap) {
ForEach(blocks) { block in
BlockView(block: block)
.transition(.asymmetric(
insertion: .move(edge: .top).combined(with: .opacity),
removal: .move(edge: .bottom).combined(with: .opacity)
))
}
}
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: blocks)
}
}
우리 작업을 위한 교훈
1. 네이티브 성능은 투자할 가치가 있다
사용자는 16ms와 200ms 지연 시간의 차이를 체감한다. 네이티브 앱이 사용감에서 승리한다.
2. 페이지 안의 페이지가 유기적 구성을 가능하게 한다
구조가 콘텐츠에서 자연스럽게 생겨나도록 하라. 사용자에게 처음부터 계층 구조를 결정하도록 강요하지 마라.
3. 동일한 콘텐츠에 대한 다양한 시각적 레이아웃
카드, 리스트, 갤러리—같은 데이터지만 상황에 따라 다른 뷰를 제공한다.
4. 마찰 없는 공유
원클릭 웹 퍼블리싱은 "이걸 어떻게 공유하지?"라는 문제를 완전히 제거한다.
5. 플랫폼에 동일하게가 아닌, 플랫폼에 적합하게
Mac에서의 Craft는 Mac 앱처럼 느껴진다. iOS에서의 Craft는 iOS 앱처럼 느껴진다. 그것이 목표다.
자주 묻는 질문
Craft는 Notion과 어떻게 다른가요?
Craft는 네이티브(Apple 플랫폼 전용으로 구축)인 반면, Notion은 웹 기반이다. 이는 Craft가 50ms 미만의 응답 시간을 달성하고, 완전한 오프라인 작동이 가능하며, Apple Pencil, Shortcuts, 시스템 전체 검색과 같은 iOS/macOS 기능과 깊이 통합됨을 의미한다. Notion은 더 많은 데이터베이스 기능을 제공하고, Craft는 글쓰기 경험을 우선시한다.
Craft를 오프라인에서 사용할 수 있나요?
가능하다. Craft는 모든 문서를 로컬에 저장하고 연결 시 iCloud를 통해 동기화한다. 인터넷 없이도 문서를 생성, 편집, 정리할 수 있다. 재연결하면 변경 사항이 자동으로 동기화된다.
Craft 페이지를 공유하면 어떻게 되나요?
Craft는 craft.do URL에 반응형 웹 페이지를 생성한다. 수신자는 계정을 만들거나 앱을 설치하지 않고도 모든 브라우저에서 볼 수 있다. 페이지는 문서의 타이포그래피, 이미지, 중첩 페이지 구조를 그대로 유지한다.
Craft는 Markdown을 지원하나요?
Craft는 자체 블록 형식을 사용하지만 Markdown으로 내보내기가 가능하다. 콘텐츠를 Markdown으로 복사하거나 전체 문서를 내보낼 수 있다. 편집 중 일부 Markdown 단축키(예: 제목용 #)가 작동하지만, Craft는 일반 텍스트 마크업보다 시각적 편집을 강조한다.
Craft에서 중첩 페이지는 어떻게 작동하나요?
페이지 아이콘을 누르거나 /page를 입력하면 모든 텍스트 블록이 페이지가 될 수 있다. 중첩 페이지는 상위 문서 내에 인라인으로 표시되며 전체 화면으로 열 수 있다. 브레드크럼 내비게이션은 계층 구조에서 현재 위치를 보여준다. 이를 통해 처음부터 폴더 구조를 강제하지 않고도 자연스러운 구성이 가능하다.