URL 공유: 백엔드 없는 계획 협업
이 강의를 마치면 할 수 있는 것
- ✅ URL을 통해 계획과 주석을 공유 - 계정 로그인이나 서버 배포 불필요
- ✅ deflate 압축과 Base64 인코딩이 데이터를 URL hash에 어떻게 임베딩하는지 이해
- ✅ 공유 모드(읽기 전용)와 로컬 모드(편집 가능)의 차이점 구분
- ✅
PLANNOTATOR_SHARE환경 변수로 공유 기능 제어 - ✅ URL 길이 제한과 공유 실패 상황 처리
현재 겪고 있는 어려움
문제 1: 팀원에게 AI가 생성한 계획 검토를 요청하고 싶지만 협업 플랫폼이 없습니다.
문제 2: 스크린샷이나 텍스트 복사로 검토 내용을 공유하면 상대방이 주석을 직접 볼 수 없습니다.
문제 3: 온라인 협업 서버 배포 비용이 높거나 회사 보안 정책이 허용하지 않습니다.
문제 4: 간단하고 빠른 공유 방법이 필요하지만 데이터 프라이버시를 어떻게 보장할지 모릅니다.
Plannotator가 도와드립니다:
- 백엔드 서버 불필요, 모든 데이터가 URL에 압축됨
- 공유 링크에 완전한 계획과 주석 포함, 수신자가 바로 확인 가능
- 데이터가 로컬 기기를 떠나지 않아 프라이버시 안전
- 생성된 URL을 어떤 메신저에든 복사 가능
언제 이 방법을 사용할까
사용 시나리오:
- 팀원에게 AI 생성 구현 계획 검토 요청
- 동료에게 코드 리뷰 결과 공유
- 검토 내용을 노트에 저장 (Obsidian/Bear 연동과 함께)
- 계획에 대한 피드백을 빠르게 받기
적합하지 않은 시나리오:
- 실시간 협업 편집 필요 (Plannotator 공유는 읽기 전용)
- 계획 내용이 URL 길이 제한 초과 (보통 수천 줄)
- 공유 내용에 민감한 정보 포함 (URL 자체는 암호화되지 않음)
보안 주의
공유 URL에는 완전한 계획과 주석이 포함됩니다. 민감한 정보(API 키, 비밀번호 등)가 포함된 내용은 공유하지 마세요. 공유 URL은 누구나 접근할 수 있으며 자동으로 만료되지 않습니다.
핵심 개념
URL 공유란 무엇인가
URL 공유는 Plannotator가 제공하는 백엔드 없는 협업 방식으로, 계획과 주석을 URL hash에 압축하여 서버 없이 공유 기능을 구현합니다.
왜 '백엔드 없음'이라고 할까요?
기존 협업 솔루션은 계획과 주석을 저장하기 위해 백엔드 서버가 필요하고, 사용자는 ID나 토큰으로 접근합니다. Plannotator의 URL 공유는 어떤 백엔드에도 의존하지 않습니다—모든 데이터가 URL에 있어서 수신자가 링크를 열면 바로 내용을 파싱합니다. 이는 프라이버시(데이터 업로드 없음)와 간편함(서비스 배포 불필요)을 보장합니다.
작동 원리
┌─────────────────────────────────────────────────────────┐
│ 사용자 A (공유자) │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. 계획 검토, 주석 추가 │
│ ┌──────────────────────┐ │
│ │ Plan: 구현 계획 │ │
│ │ Annotations: [ │ │
│ │ {type: 'REPLACE'},│ │
│ │ {type: 'COMMENT'} │ │
│ │ ] │ │
│ └──────────────────────┘ │
│ │ │
│ ▼ │
│ 2. Export → Share 클릭 │
│ │ │
│ ▼ │
│ 3. 데이터 압축 │
│ JSON → deflate → Base64 → URL 안전 문자 │
│ ↓ │
│ https://share.plannotator.ai/#eJyrVkrLz1... │
│ │
└─────────────────────────────────────────────────────────┘
│
│ URL 복사
▼
┌─────────────────────────────────────────────────────────┐
│ 사용자 B (수신자) │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. 공유 URL 열기 │
│ https://share.plannotator.ai/#eJyrVkrLz1... │
│ │ │
│ ▼ │
│ 2. 브라우저가 hash 파싱 │
│ URL 안전 문자 → Base64 디코딩 → deflate 압축 해제 → JSON │
│ │ │
│ ▼ │
│ 3. 계획과 주석 복원 │
│ ┌──────────────────────┐ │
│ │ Plan: 구현 계획 │ ✅ 읽기 전용 모드 │
│ │ Annotations: [ │ (결정 제출 불가) │
│ │ {type: 'REPLACE'},│ │
│ │ {type: 'COMMENT'} │ │
│ │ ] │ │
│ └──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘압축 알고리즘 상세
단계 1: JSON 직렬화
{
"p": "# Plan\n\nStep 1...",
"a": [
["R", "old text", "new text", null, null],
["C", "context", "comment text", null, null]
],
"g": ["image1.png", "image2.png"]
}단계 2: Deflate-raw 압축
- 네이티브
CompressionStream('deflate-raw')API 사용 - 일반적인 압축률 60-80% (텍스트 반복도에 따라 다름, 소스 코드 정의 아님)
- 소스 코드 위치:
packages/ui/utils/sharing.ts:34
단계 3: Base64 인코딩
const base64 = btoa(String.fromCharCode(...compressed));단계 4: URL 안전 문자 치환
base64
.replace(/\+/g, '-') // + → -
.replace(/\//g, '_') // / → _
.replace(/=/g, ''); // = → ''(패딩 제거)왜 특수 문자를 치환할까요?
URL에서 일부 문자는 특별한 의미가 있습니다(+는 공백, /는 경로 구분자). Base64 인코딩 후 이런 문자가 포함될 수 있어 URL 파싱 오류가 발생합니다. -와 _로 치환하면 URL이 안전해지고 복사할 수 있습니다.
주석 형식 최적화
압축 효율을 위해 Plannotator는 간소화된 주석 형식(ShareableAnnotation)을 사용합니다:
| 원본 Annotation | 간소화 형식 | 설명 |
|---|---|---|
{type: 'DELETION', originalText: '...', text: undefined, ...} | ['D', 'old text', null, images?] | D = Deletion, null은 text 없음 |
{type: 'REPLACEMENT', originalText: '...', text: 'new...', ...} | ['R', 'old text', 'new text', null, images?] | R = Replacement |
{type: 'COMMENT', originalText: '...', text: 'comment...', ...} | ['C', 'old text', 'comment text', null, images?] | C = Comment |
{type: 'INSERTION', originalText: '...', text: 'new...', ...} | ['I', 'context', 'new text', null, images?] | I = Insertion |
{type: 'GLOBAL_COMMENT', text: '...', ...} | ['G', 'comment text', null, images?] | G = Global comment |
필드 순서가 고정되어 있고 키 이름을 생략하여 데이터 양을 크게 줄입니다. 소스 코드 위치: packages/ui/utils/sharing.ts:76
공유 URL 구조
https://share.plannotator.ai/#<compressed_data>
↑
hash 부분- 기본 도메인:
share.plannotator.ai(독립 공유 페이지) - Hash 구분자:
#(서버로 전송되지 않음, 완전히 프론트엔드에서 파싱) - 압축 데이터: Base64url 인코딩된 압축 JSON
🎒 시작하기 전 준비
사전 조건:
- ✅ 계획 검토 기초 완료, 주석 추가 방법 이해
- ✅ 계획 주석 튜토리얼 완료, 주석 유형 이해
- ✅ 브라우저가
CompressionStreamAPI 지원 (모든 최신 브라우저 지원)
공유 기능 활성화 확인:
# 기본적으로 활성화됨
echo $PLANNOTATOR_SHARE
# 공유 비활성화 필요 시 (예: 기업 보안 정책)
export PLANNOTATOR_SHARE=disabled환경 변수 설명
PLANNOTATOR_SHARE는 공유 기능의 활성화 상태를 제어합니다:
- 설정되지 않았거나 "disabled"가 아닌 경우: 공유 기능 활성화
- "disabled"로 설정된 경우: 공유 비활성화 (Export Modal에 Raw Diff 탭만 표시)
소스 코드 위치: apps/hook/server/index.ts:44, apps/opencode-plugin/index.ts:50
브라우저 호환성 확인:
# 브라우저 콘솔에서 실행
const stream = new CompressionStream('deflate-raw');
console.log('CompressionStream supported');CompressionStream supported가 출력되면 브라우저가 지원하는 것입니다. 최신 브라우저(Chrome 80+, Firefox 113+, Safari 16.4+)는 모두 지원합니다.
따라하기
1단계: 계획 검토 완료
왜 필요한가 공유 전에 먼저 주석 추가를 포함한 검토를 완료해야 합니다.
작업:
- Claude Code 또는 OpenCode에서 계획 검토 트리거
- 계획 내용 확인, 수정이 필요한 텍스트 선택
- 주석 추가 (삭제, 교체, 코멘트 등)
- (선택) 이미지 첨부 업로드
예상 화면:
┌─────────────────────────────────────────────────────────────┐
│ Plan Review │
├─────────────────────────────────────────────────────────────┤
│ │
│ # Implementation Plan │
│ │
│ ## Phase 1: Setup │
│ Set up WebSocket server on port 8080 │
│ │
│ ## Phase 2: Authentication │
│ Implement JWT authentication middleware │
│ ┌─────────────────────┐ │
│ ━━━━━━━━━━━━━━━━│ Replace: "implement" │ │
│ └─────────────────────┘ │
│ │
│ Annotation Panel │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ REPLACE: "implement" → "add" │ │
│ │ JWT is overkill, use simple session tokens │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ [Approve] [Request Changes] [Export] │
└─────────────────────────────────────────────────────────────┘2단계: Export Modal 열기
왜 필요한가 Export Modal은 공유 URL 생성 진입점을 제공합니다.
작업:
- 오른쪽 상단의 Export 버튼 클릭
- Export Modal이 열릴 때까지 대기
예상 화면:
┌─────────────────────────────────────────────────────────────┐
│ Export × │
│ 1 annotation Share | Raw Diff │
├─────────────────────────────────────────────────────────────┤
│ │
│ Shareable URL │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ https://share.plannotator.ai/#eJyrVkrLz1... │ │
│ │ [Copy] │ │
│ │ 3.2 KB │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ This URL contains full plan and all annotations. │
│ Anyone with this link can view and add to your annotations.│
│ │
└─────────────────────────────────────────────────────────────┘URL 크기 힌트
오른쪽 하단에 URL의 바이트 수가 표시됩니다(예: 3.2 KB). URL이 너무 길면(8 KB 초과) 주석 수나 이미지 첨부를 줄이는 것을 고려하세요.
3단계: 공유 URL 복사
왜 필요한가 URL을 복사하면 어떤 메신저(Slack, Email, 카카오톡 등)에든 붙여넣을 수 있습니다.
작업:
- Copy 버튼 클릭
- 버튼이 **Copied!**로 변경될 때까지 대기
- URL이 클립보드에 복사됨
예상 화면:
┌─────────────────────────────────────────────────────────────┐
│ Shareable URL │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ https://share.plannotator.ai/#eJyrVkrLz1... │ │
│ │ ✓ Copied │ │
│ │ 3.2 KB │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘자동 선택
URL 입력 필드를 클릭하면 전체 내용이 자동으로 선택되어 수동 복사가 편리합니다(Copy 버튼을 사용하지 않는 경우).
4단계: 협업자에게 URL 공유
왜 필요한가 협업자가 URL을 열면 계획과 주석을 볼 수 있습니다.
작업:
- URL을 메신저(Slack, Email 등)에 붙여넣기
- 팀원에게 전송
예시 메시지:
안녕하세요 @팀,
이 구현 계획 검토 부탁드립니다:
https://share.plannotator.ai/#eJyrVkrLz1...
2단계에 교체 주석을 추가했는데, JWT가 너무 복잡하다고 생각합니다.
피드백 부탁드립니다. 감사합니다!5단계: 협업자가 공유 URL 열기 (수신자 측)
왜 필요한가 협업자가 브라우저에서 URL을 열어 내용을 확인해야 합니다.
작업 (협업자가 수행):
- 공유 URL 클릭
- 페이지 로딩 대기
예상 화면 (협업자 시점):
┌─────────────────────────────────────────────────────────────┐
│ Plan Review Read-only │
├─────────────────────────────────────────────────────────────┤
│ │
│ # Implementation Plan │
│ │
│ ## Phase 1: Setup │
│ Set up WebSocket server on port 8080 │
│ │
│ ## Phase 2: Authentication │
│ Implement JWT authentication middleware │
│ ┌─────────────────────┐ │
│ ━━━━━━━━━━━━━━━━│ Replace: "implement" │ │
│ │ └─────────────────────┘ │
│ │ This annotation was shared by [Your Name] │
│ │
│ Annotation Panel │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ REPLACE: "implement" → "add" │ │
│ │ JWT is overkill, use simple session tokens │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ [View Only Mode - Approve and Deny disabled] │
└─────────────────────────────────────────────────────────────┘읽기 전용 모드
공유 URL을 열면 인터페이스 오른쪽 상단에 "Read-only" 라벨이 표시되고, Approve와 Deny 버튼이 비활성화됩니다. 협업자는 계획과 주석을 볼 수 있지만 결정을 제출할 수 없습니다.
압축 해제 과정
협업자가 URL을 열면 브라우저가 자동으로 다음 단계를 실행합니다(useSharing Hook에 의해 트리거):
window.location.hash에서 압축 데이터 추출- 역순으로 Base64 디코딩 → deflate 압축 해제 → JSON 파싱 실행
- 계획과 주석 복원
- URL hash 제거 (새로고침 시 재로딩 방지)
소스 코드 위치: packages/ui/hooks/useSharing.ts:67
체크포인트 ✅
공유 URL 유효성 검증:
- 공유 URL 복사
- 새 탭이나 시크릿 모드에서 열기
- 동일한 계획과 주석이 표시되는지 확인
읽기 전용 모드 검증:
- 협업자가 공유 URL 열기
- 오른쪽 상단에 "Read-only" 라벨이 있는지 확인
- Approve와 Deny 버튼이 비활성화되었는지 확인
URL 길이 검증:
- Export Modal에서 URL 크기 확인
- 8 KB를 초과하지 않는지 확인 (초과 시 주석 줄이기 고려)
자주 발생하는 문제
문제 1: URL 공유 버튼이 표시되지 않음
현상: Export Modal에 Share 탭이 없고 Raw Diff만 있음.
원인: PLANNOTATOR_SHARE 환경 변수가 "disabled"로 설정됨.
해결 방법:
# 현재 값 확인
echo $PLANNOTATOR_SHARE
# 제거하거나 다른 값으로 설정
unset PLANNOTATOR_SHARE
# 또는
export PLANNOTATOR_SHARE=enabled소스 코드 위치: apps/hook/server/index.ts:44
문제 2: 공유 URL을 열면 빈 페이지가 표시됨
현상: 협업자가 URL을 열었는데 페이지에 내용이 없음.
원인: URL hash가 복사 과정에서 손실되거나 잘림.
해결 방법:
- 완전한 URL을 복사했는지 확인 (
#및 그 뒤의 모든 문자 포함) - 단축 URL 서비스 사용 금지 (hash가 잘릴 수 있음)
- 수동 복사 대신 Export Modal의 Copy 버튼 사용
URL hash 길이
공유 URL의 hash 부분은 보통 수천 개의 문자가 있어 수동 복사 시 누락되기 쉽습니다. Copy 버튼을 사용하거나 복사 → 붙여넣기 두 번으로 완전성을 확인하세요.
문제 3: URL이 너무 길어서 전송할 수 없음
현상: URL이 메신저의 문자 제한을 초과함 (예: 카카오톡, Slack).
원인: 계획 내용이 너무 길거나 주석 수가 너무 많음.
해결 방법:
- 불필요한 주석 삭제
- 이미지 첨부 줄이기
- Raw Diff로 내보내서 파일로 저장 고려
- 코드 리뷰 기능 사용 (diff 모드의 압축률이 더 높음)
문제 4: 협업자가 내 이미지를 볼 수 없음
현상: 공유 URL에 이미지 경로가 포함되어 있지만 협업자가 열면 "Image not found" 표시.
원인: 이미지가 로컬 /tmp/plannotator/ 디렉토리에 저장되어 있어 협업자가 접근할 수 없음.
해결 방법:
- Plannotator의 URL 공유는 기기 간 이미지 접근을 지원하지 않음
- Obsidian 연동 사용 권장, 이미지를 vault에 저장 후 공유 가능
- 또는 스크린샷을 찍어 주석에 텍스트 설명으로 포함
소스 코드 위치: packages/server/index.ts:163 (이미지 저장 경로)
문제 5: 공유 후 주석을 수정했는데 URL이 업데이트되지 않음
현상: 새 주석을 추가했는데 Export Modal의 URL이 변경되지 않음.
원인: shareUrl 상태가 자동으로 새로고침되지 않음 (드문 경우, 보통 React 상태 업데이트 문제).
해결 방법:
- Export Modal 닫기
- Export Modal 다시 열기
- URL이 자동으로 최신 내용으로 업데이트되어야 함
소스 코드 위치: packages/ui/hooks/useSharing.ts:128 (refreshShareUrl 함수)
이 강의 요약
URL 공유 기능으로 백엔드 서버 없이 계획과 주석을 공유할 수 있습니다:
- ✅ 백엔드 없음: 데이터가 URL hash에 압축되어 서버에 의존하지 않음
- ✅ 프라이버시 안전: 데이터가 업로드되지 않고 로컬과 협업자 사이에서만 전달
- ✅ 간편하고 효율적: 원클릭으로 URL 생성, 복사 붙여넣기로 공유
- ✅ 읽기 전용 모드: 협업자가 주석을 보고 추가할 수 있지만 결정을 제출할 수 없음
기술 원리:
- Deflate-raw 압축: JSON 데이터를 약 60-80% 압축
- Base64 인코딩: 바이너리 데이터를 텍스트로 변환
- URL 안전 문자 치환:
+→-,/→_,=→'' - Hash 파싱: 프론트엔드가 자동으로 압축 해제하고 내용 복원
설정 옵션:
PLANNOTATOR_SHARE=disabled: 공유 기능 비활성화- 기본 활성화: 공유 기능 사용 가능
다음 강의 예고
다음 강의에서는 **Obsidian 연동**을 배웁니다.
배울 내용:
- Obsidian vault 자동 감지
- 승인된 계획을 Obsidian에 저장
- frontmatter와 태그 자동 생성
- URL 공유와 Obsidian 지식 관리 결합
다음 강의 예고
다음 강의에서는 **Obsidian 연동**을 배웁니다.
배울 내용:
- Obsidian 연동 설정 방법, 계획을 vault에 자동 저장
- frontmatter와 태그 생성 메커니즘 이해
- backlink를 활용한 지식 그래프 구축
부록: 소스 코드 참조
소스 코드 위치 보기
업데이트 시간: 2026-01-24
| 기능 | 파일 경로 | 행 번호 |
|---|---|---|
| 데이터 압축 (deflate + Base64) | packages/ui/utils/sharing.ts | 30-48 |
| 데이터 압축 해제 | packages/ui/utils/sharing.ts | 53-71 |
| 주석 형식 변환 (간소화) | packages/ui/utils/sharing.ts | 76-95 |
| 주석 형식 복원 | packages/ui/utils/sharing.ts | 102-155 |
| 공유 URL 생성 | packages/ui/utils/sharing.ts | 162-175 |
| URL hash 파싱 | packages/ui/utils/sharing.ts | 181-194 |
| URL 크기 포맷팅 | packages/ui/utils/sharing.ts | 199-205 |
| URL 공유 Hook | packages/ui/hooks/useSharing.ts | 45-155 |
| Export Modal UI | packages/ui/components/ExportModal.tsx | 1-196 |
| 공유 스위치 설정 (Hook) | apps/hook/server/index.ts | 44 |
| 공유 스위치 설정 (OpenCode) | apps/opencode-plugin/index.ts | 50 |
주요 상수:
SHARE_BASE_URL = 'https://share.plannotator.ai': 공유 페이지 기본 도메인
주요 함수:
compress(payload: SharePayload): Promise<string>: payload를 base64url 문자열로 압축decompress(b64: string): Promise<SharePayload>: base64url 문자열을 payload로 압축 해제toShareable(annotations: Annotation[]): ShareableAnnotation[]: 전체 주석을 간소화 형식으로 변환fromShareable(data: ShareableAnnotation[]): Annotation[]: 간소화 형식을 전체 주석으로 복원generateShareUrl(markdown, annotations, attachments): Promise<string>: 완전한 공유 URL 생성parseShareHash(): Promise<SharePayload | null>: 현재 URL의 hash 파싱
데이터 타입:
interface SharePayload {
p: string; // plan markdown
a: ShareableAnnotation[];
g?: string[]; // global attachments
}
type ShareableAnnotation =
| ['D', string, string | null, string[]?] // Deletion
| ['R', string, string, string | null, string[]?] // Replacement
| ['C', string, string, string | null, string[]?] // Comment
| ['I', string, string, string | null, string[]?] // Insertion
| ['G', string, string | null, string[]?]; // Global Comment