Things 3: 집중된 단순함의 예술
Things 3가 제약을 통해 단순함을 달성한 방법: 자연스러운 계층, When과 Deadline 분리, 키보드 우선 디자인, 의미 있는 색상 절제. SwiftUI 구현 패턴 포함.
Things 3: 집중된 단순함의 예술
"우리는 단순함이 어렵다고 믿습니다." — Cultured Code
Things 3는 종종 지금까지 만들어진 가장 아름다운 할 일 관리 앱이라고 불립니다. 그 아름다움은 중요한 것에 대한 철저한 집중과 그 외 모든 것의 제거에서 비롯됩니다.
Things 3가 중요한 이유
Things 3는 제약이 명확성을 만든다는 것을 보여줍니다. 경쟁 앱들이 기능을 쌓아올리는 동안(47가지 옵션이 있는 반복 작업, 의존성과 간트 차트가 있는 프로젝트), Things는 묻습니다: 사람이 실제로 일을 처리하는 데 무엇이 필요한가?
핵심 성과: - 깊이를 희생하지 않으면서 시각적 단순함을 달성 - 날짜를 의미 있게 만듦 (오늘, 저녁, 언젠가) - 인간이 업무에 대해 생각하는 방식을 반영하는 계층 구조 생성 - 키보드 우선도 아름다울 수 있음을 증명 - macOS/iOS 네이티브 앱 디자인의 표준을 정립
핵심 요점
- "언제 할 것인가?"와 "언제까지인가?"를 분리하라 - Things의 이중 날짜 모델(When + Deadline)은 대부분의 할 일 앱을 괴롭히는 모호함을 제거합니다; 예정된 의도와 엄격한 마감일은 서로 다른 목적을 갖습니다
- 언젠가는 기능이지, 실패 상태가 아니다 - 아이디어에 압박 없는 공간을 주면 받은 편지함 불안을 방지합니다; 언젠가에 있는 작업은 오늘을 어지럽히거나 죄책감을 유발하지 않습니다
- 영역은 절대 완료되지 않고, 프로젝트는 완료된다 - 이 구분은 인간의 인지를 반영합니다: "업무"는 지속적인 삶의 영역이고, "v2.0 출시"는 결승선이 있습니다; 이를 혼합하면 혼란이 생깁니다
- 색상은 의미를 위해 남겨두라 - Things의 인터페이스는 95%가 중립색(검정, 흰색, 회색)입니다; 색상은 의미 정보에만 나타납니다(노란색=오늘, 빨간색=마감일, 인디고=저녁)
- 완료는 보람 있어야 한다 - 체크박스 애니메이션(채우기 → 체크 표시 → 소리)은 500ms가 걸리지만 도파민을 전달합니다; 작업 완료가 내재적으로 동기부여가 됩니다
핵심 디자인 원칙
1. 자연스러운 계층 구조
Things는 인간이 생각하는 방식대로 작업을 구성합니다: 넓은 것(삶의 영역)에서 구체적인 것(개별 작업)으로.
THINGS HIERARCHY:
Area (Work, Personal, Health) ← Broad life categories
│
└── Project (Launch App) ← Finite goal with tasks
│
└── Heading (Design) ← Optional grouping
│
└── To-Do ← Single actionable item
│
└── Checklist ← Sub-steps within task
Example:
┌─────────────────────────────────────────────────────────────┐
│ [A] WORK (Area) │
│ ├── [P] Ship Version 2.0 (Project) │
│ │ ├── [H] Design │
│ │ │ ├── [ ] Create new icons │
│ │ │ └── [ ] Update color palette │
│ │ └── [H] Development │
│ │ ├── [ ] Implement auth flow │
│ │ └── [ ] Add analytics │
│ └── [P] Website Redesign (Project) │
│ └── [ ] Write copy for landing page │
└─────────────────────────────────────────────────────────────┘
핵심 통찰: 영역은 절대 완료되지 않습니다(삶의 카테고리이므로). 프로젝트는 완료됩니다(금요일까지 출시). 이 구분이 계획 마비를 제거합니다.
데이터 모델:
// Things' hierarchy expressed in code
struct Area: Identifiable {
let id: UUID
var title: String
var projects: [Project]
var tasks: [Task] // Loose tasks not in projects
}
struct Project: Identifiable {
let id: UUID
var title: String
var notes: String?
var deadline: Date?
var headings: [Heading]
var tasks: [Task]
var isComplete: Bool
}
struct Heading: Identifiable {
let id: UUID
var title: String
var tasks: [Task]
}
struct Task: Identifiable {
let id: UUID
var title: String
var notes: String?
var when: TaskSchedule?
var deadline: Date?
var tags: [Tag]
var checklist: [ChecklistItem]
var isComplete: Bool
}
enum TaskSchedule {
case today
case evening
case specificDate(Date)
case someday
}
2. 마감일이 아닌 '언제'
Things는 "언제 이것을 할 것인가?"(오늘, 저녁, 언젠가)와 "언제까지 완료해야 하는가?"(마감일)를 분리합니다. 대부분의 앱은 이를 혼동합니다.
TRADITIONAL DATE MODEL:
┌──────────────────────────────────────────────────────────────┐
│ Task: Write report │
│ Due: March 15th │
│ │
│ Problem: Is March 15th when I'll do it, or when it's due? │
│ If it's due, when should I start? │
└──────────────────────────────────────────────────────────────┘
THINGS DATE MODEL:
┌──────────────────────────────────────────────────────────────┐
│ Task: Write report │
│ When: Today (I'll work on it today) │
│ Deadline: March 15th (must be complete by then) │
│ │
│ Clarity: Two separate questions, two separate answers │
└──────────────────────────────────────────────────────────────┘
SMART LISTS (automatic filtering):
┌───────────────────┬─────────────────────────────────────────┐
│ [I] INBOX │ Uncategorized captures │
├───────────────────┼─────────────────────────────────────────┤
│ [*] TODAY │ when == .today OR deadline == today │
├───────────────────┼─────────────────────────────────────────┤
│ [E] THIS EVENING │ when == .evening │
├───────────────────┼─────────────────────────────────────────┤
│ [D] UPCOMING │ when == .specificDate (future) │
├───────────────────┼─────────────────────────────────────────┤
│ [A] ANYTIME │ when == nil AND deadline == nil │
├───────────────────┼─────────────────────────────────────────┤
│ [S] SOMEDAY │ when == .someday │
├───────────────────┼─────────────────────────────────────────┤
│ [L] LOGBOOK │ isComplete == true │
└───────────────────┴─────────────────────────────────────────┘
핵심 통찰: "언젠가"는 실패 상태가 아닙니다—압력 해소입니다. 아이디어는 오늘을 어지럽히지 않고 자리를 가집니다.
SwiftUI 구현:
struct WhenPicker: View {
@Binding var schedule: TaskSchedule?
@Binding var deadline: Date?
@State private var showDatePicker = false
var body: some View {
VStack(spacing: 12) {
// Quick options
HStack(spacing: 8) {
WhenButton(
icon: "star.fill",
label: "Today",
color: .yellow,
isSelected: schedule == .today
) {
schedule = .today
}
WhenButton(
icon: "moon.fill",
label: "Evening",
color: .indigo,
isSelected: schedule == .evening
) {
schedule = .evening
}
WhenButton(
icon: "calendar",
label: "Pick Date",
color: .red,
isSelected: showDatePicker
) {
showDatePicker = true
}
WhenButton(
icon: "archivebox.fill",
label: "Someday",
color: .brown,
isSelected: schedule == .someday
) {
schedule = .someday
}
}
// Deadline ("when"과 별도)
if let deadline = deadline {
HStack {
Image(systemName: "flag.fill")
.foregroundStyle(.red)
Text("Deadline: \(deadline, style: .date)")
Spacer()
Button("Clear") { self.deadline = nil }
}
.font(.caption)
}
}
}
}
struct WhenButton: View {
let icon: String
let label: String
let color: Color
let isSelected: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
VStack(spacing: 4) {
Image(systemName: icon)
.font(.title2)
Text(label)
.font(.caption2)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 8)
.background(isSelected ? color.opacity(0.2) : .clear)
.cornerRadius(8)
}
.buttonStyle(.plain)
.foregroundStyle(isSelected ? color : .secondary)
}
}
3. 키보드 우선, 마우스 친화적
모든 조작을 키보드만으로 근육 기억 수준의 효율성으로 수행할 수 있으면서도, 마우스 사용자에게도 직관적으로 다가갑니다.
키보드 단축키 (기억하기 쉬운 패턴):
탐색:
Cmd+1-6 스마트 목록으로 이동 (Inbox, Today 등)
Cmd+Up/Down 섹션 간 이동
Space 퀵 룩 (작업 세부정보 미리보기)
작업 조작:
Cmd+K 작업 완료
Cmd+S 일정 지정 (When 선택기)
Cmd+Shift+D 마감일 설정
Cmd+Shift+M 프로젝트로 이동
Cmd+Shift+T 태그 추가
생성:
Space/Return 새 작업 생성 (컨텍스트 인식)
Cmd+N Inbox에 새 작업
Cmd+Opt+N 새 프로젝트
빠른 입력 (전역 단축키):
Ctrl+Space 어디서든 플로팅 빠른 입력 창 열기
┌─────────────────────────────────────────────────┐
│ [ ] | │
│ 현재 위치에 새 작업이 나타납니다 │
└─────────────────────────────────────────────────┘
핵심 인사이트: 단축키는 발견 가능하되 필수가 아니어야 합니다. 인터페이스가 키를 암시하되 강요하지 않습니다.
CSS 패턴 (단축키 힌트):
.action-button {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
}
.shortcut-hint {
font-size: 11px;
font-family: var(--font-mono);
padding: 2px 6px;
background: var(--surface-subtle);
border-radius: 4px;
color: var(--text-tertiary);
opacity: 0;
transition: opacity 0.15s ease;
}
/* 호버 시 표시 */
.action-button:hover .shortcut-hint {
opacity: 1;
}
/* 키보드로 포커스될 때 항상 표시 */
.action-button:focus-visible .shortcut-hint {
opacity: 1;
}
SwiftUI 빠른 입력:
struct QuickEntryPanel: View {
@Binding var isPresented: Bool
@State private var taskTitle = ""
@FocusState private var isFocused: Bool
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 12) {
// 체크박스 (시각적 용도)
Circle()
.strokeBorder(.tertiary, lineWidth: 1.5)
.frame(width: 20, height: 20)
// Task input
TextField("New To-Do", text: $taskTitle)
.textFieldStyle(.plain)
.font(.body)
.focused($isFocused)
.onSubmit {
createTask()
}
}
.padding()
.background(.ultraThinMaterial)
.cornerRadius(12)
.shadow(color: .black.opacity(0.2), radius: 20, y: 10)
}
.frame(width: 500)
.onAppear {
isFocused = true
}
.onExitCommand {
isPresented = false
}
}
private func createTask() {
guard !taskTitle.isEmpty else { return }
// Create task in Inbox
TaskStore.shared.createTask(title: taskTitle)
taskTitle = ""
}
}
4. 시각적 절제
Things는 색상을 절제하며, 오직 의미를 전달할 때만 사용합니다. 인터페이스 대부분은 중립적인 톤으로 구성되어 색상이 적용된 요소가 돋보이도록 합니다.
THINGS의 색상 사용 원칙:
중립색 (인터페이스의 95%):
├── 배경: 순백색 / 짙은 회색
├── 텍스트: 검정 / 흰색
├── 테두리: 매우 은은한 회색
└── 아이콘: 단색
의미를 담은 색상 (5%):
├── 노랑 [*] = 오늘
├── 빨강 [!] = 마감일 / 기한 초과
├── 파랑 [#] = 태그 (사용자 지정)
├── 초록 [x] = 완료 애니메이션
└── 남색 [E] = 저녁
시각적 예시:
┌─────────────────────────────────────────────────────────────┐
│ 오늘 [*] (노랑) │
├─────────────────────────────────────────────────────────────┤
│ ( ) 문서 작성하기 │
│ 프로젝트 이름 - [!] 내일 마감 (빨강) │
│ │
│ ( ) 풀 리퀘스트 검토하기 │
│ [#] work (파랑 태그) │
│ │
│ ( ) 치과에 전화하기 │
│ [E] 오늘 저녁 (남색 배지) │
└─────────────────────────────────────────────────────────────┘
주목: 대부분의 텍스트는 검정색입니다. 색상은 의미가 있는 곳에만 나타납니다.
핵심 인사이트: 모든 것이 화려하면 아무것도 돋보이지 않습니다. 색상은 정보 전달을 위해 아껴 사용하세요.
CSS 패턴:
:root {
/* 중립 팔레트 (UI 대부분) */
--surface-primary: #ffffff;
--surface-secondary: #f5f5f7;
--text-primary: #1d1d1f;
--text-secondary: #86868b;
--border: rgba(0, 0, 0, 0.08);
/* 시맨틱 색상 (절제하여 사용) */
--color-today: #f5c518;
--color-deadline: #ff3b30;
--color-evening: #5856d6;
--color-complete: #34c759;
--color-tag: #007aff;
}
/* 태스크 행 - 대부분 중립색 */
.task-row {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 16px;
border-bottom: 1px solid var(--border);
background: var(--surface-primary);
color: var(--text-primary);
}
/* 의미 전달에만 색상 사용 */
.deadline-badge {
font-size: 12px;
color: var(--color-deadline);
}
.deadline-badge.overdue {
font-weight: 600;
color: var(--color-deadline);
}
.tag {
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
background: var(--color-tag);
color: white;
}
.evening-badge {
color: var(--color-evening);
}
5. 기분 좋은 완료 경험
완료 애니메이션은 단순한 피드백이 아닌 보상의 느낌을 전달합니다. 만족스러운 "플링" 효과음과 부드러운 체크 애니메이션이 태스크 완료를 기분 좋은 경험으로 만들어 줍니다.
완료 시퀀스:
프레임 1: ○ 태스크 제목
프레임 2-4: ◔ (원이 시계 방향으로 채워짐)
프레임 5-7: ● (완전히 채워지고 잠시 멈춤)
프레임 8-10: ✓ (체크 표시가 나타나며 살짝 확대됨)
프레임 11+: 행이 슬라이드되어 사라지고 리스트가 빈 공간을 메움
오디오: 프레임 7-8에서 부드러운 "플링" 소리 (완료 순간)
햅틱: iOS에서 완료 시 가벼운 탭 진동
SwiftUI 구현:
struct CompletableCheckbox: View {
@Binding var isComplete: Bool
@State private var animationPhase: AnimationPhase = .unchecked
enum AnimationPhase {
case unchecked, filling, checked, done
}
var body: some View {
Button {
completeWithAnimation()
} label: {
ZStack {
// Background circle
Circle()
.strokeBorder(.tertiary, lineWidth: 1.5)
.frame(width: 22, height: 22)
// Fill animation
Circle()
.trim(from: 0, to: fillAmount)
.stroke(.green, lineWidth: 1.5)
.frame(width: 22, height: 22)
.rotationEffect(.degrees(-90))
// Checkmark
Image(systemName: "checkmark")
.font(.system(size: 12, weight: .bold))
.foregroundStyle(.green)
.scaleEffect(checkScale)
.opacity(checkOpacity)
}
}
.buttonStyle(.plain)
.sensoryFeedback(.success, trigger: animationPhase == .checked)
}
private var fillAmount: CGFloat {
switch animationPhase {
case .unchecked: return 0
case .filling: return 1
case .checked, .done: return 1
}
}
private var checkScale: CGFloat {
animationPhase == .checked ? 1.2 : (animationPhase == .done ? 1.0 : 0)
}
private var checkOpacity: CGFloat {
animationPhase == .unchecked || animationPhase == .filling ? 0 : 1
}
private func completeWithAnimation() {
// Phase 1: Fill the circle
withAnimation(.easeInOut(duration: 0.2)) {
animationPhase = .filling
}
// Phase 2: Show checkmark with bounce
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
withAnimation(.spring(response: 0.3, dampingFraction: 0.5)) {
animationPhase = .checked
}
}
// Phase 3: Settle and mark complete
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
withAnimation(.easeOut(duration: 0.15)) {
animationPhase = .done
}
isComplete = true
}
}
}
---
## 활용 가능한 패턴
### 패턴 1: 점진적 작업 상세 정보
작업 상세 정보는 별도의 화면으로 이동하지 않고 인라인으로 확장됩니다.
struct ExpandableTask: View {
let task: Task
@State private var isExpanded = false
var body: some View {
VStack(alignment: .leading, spacing: 0) {
// Always visible row
TaskRow(task: task, isExpanded: $isExpanded)
// Expandable details
if isExpanded {
TaskDetails(task: task)
.padding(.leading, 44) // Align with title
.transition(.asymmetric(
insertion: .push(from: .top).combined(with: .opacity),
removal: .push(from: .bottom).combined(with: .opacity)
))
}
}
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: isExpanded)
}
}
### 패턴 2: 자연어 입력
Things는 자연어를 파싱하여 날짜를 설정합니다: "tomorrow", "next week", "June 15".
function parseNaturalDate(input: string): TaskSchedule | null {
const lower = input.toLowerCase();
```html
// Today shortcuts
if (['today', 'tod', 'now'].includes(lower)) {
return { type: 'today' };
}
// Evening
if (['tonight', 'evening', 'eve'].includes(lower)) {
return { type: 'evening' };
}
// Tomorrow
if (['tomorrow', 'tom', 'tmrw'].includes(lower)) {
const date = new Date();
date.setDate(date.getDate() + 1);
return { type: 'date', date };
}
// Someday
if (['someday', 'later', 'eventually'].includes(lower)) {
return { type: 'someday' };
}
// Relative days
const inMatch = lower.match(/in (\d+) days?/);
if (inMatch) {
const date = new Date();
date.setDate(date.getDate() + parseInt(inMatch[1]));
return { type: 'date', date };
}
// Day names
const days = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
const dayIndex = days.findIndex(d => lower.startsWith(d.slice(0, 3)));
if (dayIndex !== -1) {
const date = getNextDayOfWeek(dayIndex);
return { type: 'date', date };
}
return null;
}
패턴 3: 리스트 기반 드래그 앤 드롭
재정렬은 명확한 시각적 피드백을 제공하는 직접 조작 방식입니다.
.task-row {
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.task-row.dragging {
transform: scale(1.02);
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.15),
0 0 0 2px var(--color-focus);
z-index: 100;
}
.task-row.drop-above::before {
content: '';
position: absolute;
top: -2px;
left: 44px;
right: 16px;
height: 4px;
background: var(--color-focus);
border-radius: 2px;
}
.task-row.drop-below::after {
content: '';
position: absolute;
bottom: -2px;
left: 44px;
right: 16px;
height: 4px;
background: var(--color-focus);
border-radius: 2px;
}
디자인 교훈
- 관심사 분리: "언제 이 작업을 할까?"와 "마감일이 언제지?"는 다른 질문이다
- 색상 절제: 색상은 의미를 위해 아껴두고, 중립색이 기본이다
- 완료는 보상: 작업 완료가 기분 좋게 느껴지도록 만든다
- 키보드는 가속, 마우스는 환영: 어느 쪽도 타협하지 않고 양쪽 모두를 위해 설계한다
- 자연스러운 계층 구조: 인간이 이미 업무에 대해 생각하는 방식을 반영한다 (영역 → 프로젝트 → 작업)
- 언젠가는 기능이다: 아이디어에 부담을 주지 않는 안식처를 제공한다
자주 묻는 질문
Things 3에서 "When"과 "Deadline"의 차이점은 무엇인가요?
"When"은 "언제 이 작업을 할 것인가?"에 대한 답이다 (오늘, 오늘 저녁, 언젠가, 또는 특정 날짜). "Deadline"은 "언제까지 완료해야 하는가?"에 대한 답이다. 대부분의 작업 관리 앱은 이 두 가지를 혼동하지만, 실제로는 서로 다른 목적을 가진다. 어떤 작업이 금요일이 마감(Deadline)이지만 수요일에 작업할 계획(When)일 수 있다. 이러한 분리를 통해 엄격한 마감일을 추적하면서도 의도에 따라 하루를 계획할 수 있다.
Things 3의 계층 구조(영역, 프로젝트, 작업)는 어떻게 작동하나요?
영역(Areas)은 절대 완료되지 않는 광범위한 삶의 카테고리다 (업무, 개인, 건강). 프로젝트(Projects)는 명확한 종료 상태가 있는 유한한 목표다 (앱 출시, 휴가 계획). 작업(Tasks)은 단일 실행 가능한 항목이다. 제목(Headings)은 선택적으로 프로젝트 내 작업을 그룹화한다. 이는 인간이 자연스럽게 업무에 대해 생각하는 방식을 반영한다: 지속적인 책임 안에 시간 제한이 있는 목표가 있고, 그 안에 개별 행동이 있다.
Things 3는 왜 색상을 거의 사용하지 않나요?
Things는 의미론적 의미를 위해서만 색상을 사용한다. 인터페이스의 95%가 중립색(검정, 흰색, 회색)이므로 색상이 나타날 때 정보를 전달한다: 노란색은 오늘, 빨간색은 마감/지연, 남색은 저녁, 파란색은 태그를 나타낸다. 모든 것이 화려하다면 아무것도 눈에 띄지 않을 것이다. 이러한 절제가 의미 있는 색상을 놓칠 수 없게 만든다.
"언젠가(Someday)"란 무엇이며 왜 가치 있나요?
언젠가(Someday)는 기억하고 싶지만 활성 목록을 어지럽히고 싶지 않은 작업을 위한 전용 공간이다. 실패 상태가 아니라 압력 해소 밸브다. "언젠가" 할 아이디어들(스페인어 배우기, 그 책 읽기)이 매일의 죄책감 없이 안식처를 가진다. 매주 Someday를 검토하고 준비가 되면 작업을 오늘(Today)로 승격시킬 수 있다.
Things 3의 완료 애니메이션은 어떻게 작동하나요?
체크박스를 탭하면 원이 시계 방향으로 채워지고(200ms), 약간의 바운스와 함께 체크마크가 나타나며(스프링 애니메이션), 부드러운 "핑" 소리와 iOS에서는 햅틱 피드백이 동반된다. 그런 다음 행이 슬라이드하며 사라진다(150ms). 전체 시퀀스는 약 500ms가 걸리지만, 이 의도적인 타이밍이 작업 완료를 보람 있게 느끼게 하는 작은 도파민 히트를 만들어낸다.
리소스
- 웹사이트: culturedcode.com/things
- 디자인 철학: Cultured Code 블로그의 단순함에 관한 글
- 키보드 단축키: 내장 도움말 메뉴 (Cmd+/)
- GTD 통합: Things가 Getting Things Done 방법론에 어떻게 매핑되는지