안녕하세요! 오늘은 GitHub Spec Kit을 사용해서 “FeedLink”라는 소셜 미디어 앱을 처음부터 어떻게 설계하고 개발하는지 단계별로 보여드리겠습니다. 실제 프로젝트에서 작성할 법한 내용으로 구성했으니, 여러분의 프로젝트에 바로 적용해보세요!
프로젝트 개요
FeedLink는 개발자들을 위한 소셜 네트워크입니다. 코드 스니펫을 공유하고, 기술 아티클을 포스팅하며, 다른 개발자들과 소통할 수 있는 플랫폼입니다.
1단계: Constitution (프로젝트 헌법) 작성
먼저 프로젝트의 불변 원칙을 정의합니다. 이는 모든 개발 결정의 기준이 됩니다.
/constitution
constitution.md
# FeedLink 프로젝트 헌법
## 핵심 원칙
### 1. 기술 스택
- Frontend: React 18+ with TypeScript (strict mode)
- Backend: Node.js 20+ with Express
- Database: PostgreSQL 15+ (primary), Redis (cache)
- ORM: Drizzle ORM
- 파일 저장: AWS S3
- 실시간 통신: WebSocket (Socket.io)
### 2. 코드 품질
- TypeScript strict mode 필수
- ESLint + Prettier 적용
- 테스트 커버리지 최소 75%
- 모든 API는 Swagger 문서화 필수
- Git commit은 Conventional Commits 규칙 준수
### 3. 보안 원칙
- 모든 비밀번호는 bcrypt로 해싱 (salt rounds: 12)
- JWT 기반 인증 (access token: 15분, refresh token: 7일)
- HTTPS 통신 필수
- XSS, CSRF 방어 구현 필수
- 민감 정보는 환경변수로 관리
### 4. API 설계
- RESTful 원칙 준수
- API versioning: /api/v1/...
- 에러 응답 표준화 (RFC 7807)
- Rate limiting: 인증 사용자 100req/min, 비인증 20req/min
- 페이지네이션: cursor-based (offset 금지)
### 5. 성능 기준
- 페이지 로드: < 2초
- API 응답: < 200ms (95th percentile)
- 이미지 최적화: WebP 포맷, lazy loading
- 코드 스플리팅 적용
### 6. 접근성
- WCAG 2.1 AA 레벨 준수
- 키보드 네비게이션 지원
- 스크린 리더 호환
- 다크 모드 지원
### 7. 데이터베이스
- 모든 테이블에 created_at, updated_at 필수
- Soft delete 구현 (deleted_at)
- Foreign key constraints 필수
- Index 전략: 모든 조회 쿼리는 인덱스 사용
- Drizzle ORM 사용: SQL-like 쿼리, 타입 안정성 우선
### 8. 금지 사항
- any 타입 사용 금지
- console.log 프로덕션 코드 금지
- 하드코딩된 URL, API key 금지
- 동기 파일 I/O 금지
- SQL injection 가능한 raw query 금지
2단계: Specify (기능 명세 작성)
이제 첫 번째 주요 기능인 “포스트 작성 및 피드” 기능을 명세합니다.
/speckit.specify 개발자 소셜 네트워크 FeedLink - 포스트 작성, 피드 조회, 좋아요, 댓글 기능
specs/001-post-feed/spec.md
# Feature: 포스트 피드 시스템
## 1. 개요
개발자들이 텍스트, 코드 스니펫, 이미지를 포함한 포스트를 작성하고,
팔로우한 사용자들의 포스트를 피드에서 볼 수 있는 핵심 기능입니다.
## 2. 사용자 스토리
### US-001: 포스트 작성
As a 로그인한 사용자
I want to 텍스트, 코드, 이미지가 포함된 포스트를 작성하고
So that 나의 생각과 작업물을 커뮤니티와 공유할 수 있다
인수 기준:
- 최대 5,000자까지 입력 가능
- 마크다운 형식 지원 (제목, 굵게, 이탤릭, 링크, 코드블록)
- 코드 스니펫에 syntax highlighting 지원 (30개 언어)
- 최대 4장의 이미지 첨부 가능 (각 10MB 제한)
- 해시태그 추가 가능 (최대 10개)
- 임시저장 기능
- 작성 중 자동저장 (30초마다)
### US-002: 피드 조회
As a 로그인한 사용자
I want to 팔로우한 사용자들의 포스트를 시간순으로 보고
So that 커뮤니티의 최신 소식을 파악할 수 있다
인수 기준:
- 팔로우한 사용자의 포스트를 최신순으로 표시
- 무한 스크롤 방식 (한 번에 20개씩 로드)
- 각 포스트에 작성자, 작성시간, 내용, 이미지, 좋아요 수, 댓글 수 표시
- 코드 블록은 syntax highlighting 적용
- 이미지는 lazy loading
- 실시간 업데이트 (WebSocket)
- 새 포스트 알림 배너 ("3개의 새 포스트")
### US-003: 좋아요
As a 로그인한 사용자
I want to 포스트에 좋아요를 표시하고
So that 유용한 콘텐츠에 반응할 수 있다
인수 기준:
- 하트 아이콘 클릭으로 좋아요/취소
- 좋아요 수 실시간 업데이트
- 좋아요한 포스트 목록 조회 가능
- 중복 좋아요 방지
- 애니메이션 효과
### US-004: 댓글 작성
As a 로그인한 사용자
I want to 포스트에 댓글을 달고
So that 다른 개발자들과 토론할 수 있다
인수 기준:
- 최대 1,000자까지 입력 가능
- 마크다운 지원
- 코드 스니펫 포함 가능
- 댓글에 좋아요 가능
- 대댓글 지원 (1단계만)
- @mention 기능 (알림 발송)
- 최신순/인기순 정렬
## 3. 비기능 요구사항
### 성능
- 피드 로딩: < 1.5초
- 포스트 작성/댓글: < 500ms
- 실시간 업데이트 지연: < 100ms
- 이미지 최적화: 원본 + WebP + 썸네일(300x300)
### 확장성
- 1,000명 동시 접속자 지원
- 피드는 Redis 캐싱 (TTL: 5분)
- 이미지는 CDN을 통해 제공
### 보안
- XSS 방지: 모든 입력 sanitize
- 이미지 업로드: MIME 타입 검증, 바이러스 스캔
- Rate limiting:
- 포스트 작성: 10개/시간
- 댓글: 30개/시간
- 좋아요: 100개/분
## 4. UI/UX 요구사항
### 포스트 작성 화면
- 모달 또는 전체 페이지 에디터
- 실시간 프리뷰
- 드래그앤드롭 이미지 업로드
- 진행률 표시 (업로드 시)
- 작성 취소 확인 다이얼로그
### 피드 화면
- 3단 레이아웃: 사이드바(네비게이션) | 피드 | 추천 사용자
- 카드 형식 포스트
- Skeleton UI (로딩 상태)
- Pull-to-refresh (모바일)
- 맨 위로 가기 버튼
## 5. 데이터 모델 (개요)
### Posts
- id, user_id, content, created_at, updated_at, deleted_at
- like_count, comment_count
- is_draft
### PostImages
- id, post_id, image_url, thumbnail_url, order
### PostHashtags
- post_id, hashtag_id
### Likes
- user_id, post_id, created_at
### Comments
- id, post_id, user_id, parent_comment_id, content, created_at
## 6. 성공 지표
- 일간 활성 사용자(DAU) 추적
- 평균 포스트 작성 시간 < 3분
- 좋아요/댓글 참여율 > 15%
- 피드 이탈률 < 30%
3단계: Clarify (명확화)
/speckit.clarify
AI가 질문하고 답변하는 과정:
AI: “코드 스니펫의 syntax highlighting은 서버 사이드에서 처리할까요, 클라이언트 사이드에서 처리할까요?”
답변: “클라이언트 사이드에서 처리합니다. highlight.js 라이브러리를 사용하세요.”
AI: “이미지 압축은 어느 시점에 수행하나요?”
답변: “업로드 시 서버에서 Sharp 라이브러리로 처리합니다. 원본, WebP, 썸네일 3가지 버전을 S3에 저장합니다.”
AI: “실시간 업데이트 시 모든 사용자에게 브로드캐스트하나요?”
답변: “아니요. 팔로워에게만 선택적으로 전송합니다. Socket.io의 room 기능을 활용하세요.”
4단계: Plan (구현 계획)
/speckit.plan React + TypeScript frontend, Express.js backend, PostgreSQL + Drizzle ORM, Redis, Socket.io 실시간, AWS S3 이미지 저장
specs/001-post-feed/plan.md
# 구현 계획: 포스트 피드 시스템
## 아키텍처 개요
[React App] <--> [API Gateway] <--> [Express Server]
|
+--------------------+--------------------+
| | |
[PostgreSQL] [Redis] [Socket.io]
| | |
[Drizzle ORM] [Cache Layer] [Real-time Layer]
|
[S3 Bucket]
## Phase 0: 환경 설정
### 0.1 프로젝트 구조
feedlink/
├── client/ # React 앱
│ ├── src/
│ │ ├── components/
│ │ ├── pages/
│ │ ├── hooks/
│ │ ├── api/
│ │ └── utils/
│ └── package.json
├── server/ # Express 서버
│ ├── src/
│ │ ├── routes/
│ │ ├── controllers/
│ │ ├── services/
│ │ ├── db/
│ │ │ ├── schema/
│ │ │ ├── migrations/
│ │ │ └── index.ts
│ │ ├── middleware/
│ │ └── utils/
│ └── package.json
└── docker-compose.yml
### 0.2 기술 스택 설정
Frontend:
- Vite + React 18 + TypeScript + TailwindCSS
- State Management: Zustand
- Form: React Hook Form + Zod
- API: Axios + React Query
- Editor: React-Markdown + react-simplemde-editor
- Code Highlighting: highlight.js
Backend:
- Express 4 + TypeScript
- ORM: Drizzle ORM
- Validation: Zod
- Auth: jsonwebtoken + bcrypt
- File Upload: multer + sharp
- WebSocket: Socket.io
## Phase 1: 데이터베이스 설계
### 1.1 Drizzle 스키마 - users.ts
// server/src/db/schema/users.ts
import { pgTable, text, timestamp, varchar } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
username: varchar('username', { length: 50 }).notNull().unique(),
email: varchar('email', { length: 255 }).notNull().unique(),
password: text('password').notNull(),
avatarUrl: text('avatar_url'),
bio: text('bio'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
1.2 Drizzle 스키마 – posts.ts
// server/src/db/schema/posts.ts
import { pgTable, text, timestamp, integer, boolean, index } from 'drizzle-orm/pg-core';
import { users } from './users';
export const posts = pgTable('posts', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
userId: text('user_id').notNull().references(() => users.id),
content: text('content').notNull(),
isDraft: boolean('is_draft').default(false).notNull(),
likeCount: integer('like_count').default(0).notNull(),
commentCount: integer('comment_count').default(0).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
deletedAt: timestamp('deleted_at'),
}, (table) => ({
userIdCreatedAtIdx: index('posts_user_id_created_at_idx').on(table.userId, table.createdAt),
createdAtIdx: index('posts_created_at_idx').on(table.createdAt),
}));
1.3 Drizzle 스키마 – 기타 테이블
// server/src/db/schema/postImages.ts
import { pgTable, text, integer, index } from 'drizzle-orm/pg-core';
import { posts } from './posts';
export const postImages = pgTable('post_images', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
postId: text('post_id').notNull().references(() => posts.id, { onDelete: 'cascade' }),
imageUrl: text('image_url').notNull(),
thumbnailUrl: text('thumbnail_url').notNull(),
webpUrl: text('webp_url').notNull(),
order: integer('order').notNull(),
}, (table) => ({
postIdIdx: index('post_images_post_id_idx').on(table.postId),
}));
// server/src/db/schema/hashtags.ts
import { pgTable, text, integer } from 'drizzle-orm/pg-core';
export const hashtags = pgTable('hashtags', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
name: text('name').notNull().unique(),
postCount: integer('post_count').default(0).notNull(),
});
// server/src/db/schema/postHashtags.ts
import { pgTable, text, primaryKey } from 'drizzle-orm/pg-core';
import { posts } from './posts';
import { hashtags } from './hashtags';
export const postHashtags = pgTable('post_hashtags', {
postId: text('post_id').notNull().references(() => posts.id, { onDelete: 'cascade' }),
hashtagId: text('hashtag_id').notNull().references(() => hashtags.id),
}, (table) => ({
pk: primaryKey({ columns: [table.postId, table.hashtagId] }),
}));
// server/src/db/schema/likes.ts
import { pgTable, text, timestamp, primaryKey, index } from 'drizzle-orm/pg-core';
import { users } from './users';
import { posts } from './posts';
export const likes = pgTable('likes', {
userId: text('user_id').notNull().references(() => users.id),
postId: text('post_id').notNull().references(() => posts.id, { onDelete: 'cascade' }),
createdAt: timestamp('created_at').defaultNow().notNull(),
}, (table) => ({
pk: primaryKey({ columns: [table.userId, table.postId] }),
postIdIdx: index('likes_post_id_idx').on(table.postId),
}));
// server/src/db/schema/comments.ts
import { pgTable, text, timestamp, integer, index } from 'drizzle-orm/pg-core';
import { users } from './users';
import { posts } from './posts';
export const comments = pgTable('comments', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
postId: text('post_id').notNull().references(() => posts.id, { onDelete: 'cascade' }),
userId: text('user_id').notNull().references(() => users.id),
parentId: text('parent_id').references((): any => comments.id),
content: text('content').notNull(),
likeCount: integer('like_count').default(0).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
deletedAt: timestamp('deleted_at'),
}, (table) => ({
postIdCreatedAtIdx: index('comments_post_id_created_at_idx').on(table.postId, table.createdAt),
}));
// server/src/db/schema/follows.ts
import { pgTable, text, timestamp, primaryKey, index } from 'drizzle-orm/pg-core';
import { users } from './users';
export const follows = pgTable('follows', {
followerId: text('follower_id').notNull().references(() => users.id),
followingId: text('following_id').notNull().references(() => users.id),
createdAt: timestamp('created_at').defaultNow().notNull(),
}, (table) => ({
pk: primaryKey({ columns: [table.followerId, table.followingId] }),
followingIdIdx: index('follows_following_id_idx').on(table.followingId),
}));
1.4 Drizzle 설정
// server/src/db/index.ts
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from './schema';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
export const db = drizzle(pool, { schema });
1.5 마이그레이션 설정
// drizzle.config.ts
import type { Config } from 'drizzle-kit';
export default {
schema: './src/db/schema/*',
out: './src/db/migrations',
driver: 'pg',
dbCredentials: {
connectionString: process.env.DATABASE_URL!,
},
} satisfies Config;
1.6 Redis 캐시 전략
캐시 키 구조:
- feed:{userId} // TTL: 5분 - 사용자 피드
- post:{postId} // TTL: 10분 - 개별 포스트
- user:{userId}:posts // TTL: 5분 - 사용자 포스트 목록
- trending:hashtags // TTL: 1시간 - 트렌딩 해시태그
Phase 2: Backend API 구현
2.1 API 엔드포인트
POST /api/v1/posts # 포스트 작성
GET /api/v1/posts/feed # 피드 조회
GET /api/v1/posts/:id # 포스트 상세
PUT /api/v1/posts/:id # 포스트 수정
DELETE /api/v1/posts/:id # 포스트 삭제
POST /api/v1/posts/:id/like # 좋아요
DELETE /api/v1/posts/:id/like # 좋아요 취소
POST /api/v1/posts/:id/comments # 댓글 작성
GET /api/v1/posts/:id/comments # 댓글 조회
POST /api/v1/upload/images # 이미지 업로드
2.2 이미지 처리 파이프라인
server/src/services/imageService.ts 처리 순서:
1. multer로 업로드 받기 (메모리 저장)
2. MIME 타입 검증
3. Sharp로 이미지 처리:
- 원본: 최대 2048x2048
- WebP: 80% 품질
- 썸네일: 300x300 (cover)
4. S3에 업로드 (각각 다른 경로)
5. URL 반환
2.3 피드 알고리즘 (Drizzle 쿼리)
// server/src/services/feedService.ts
import { db } from '../db';
import { posts, users, follows } from '../db/schema';
import { eq, desc, lt, inArray, isNull, and } from 'drizzle-orm';
export async function getUserFeed(userId: string, cursor?: string, limit = 20) {
// 팔로잉 사용자 ID 조회
const followingUsers = await db
.select({ followingId: follows.followingId })
.from(follows)
.where(eq(follows.followerId, userId));
const followingIds = followingUsers.map(f => f.followingId);
if (followingIds.length === 0) {
return { posts: [], nextCursor: null };
}
// 피드 포스트 조회
const conditions = [
inArray(posts.userId, followingIds),
isNull(posts.deletedAt),
eq(posts.isDraft, false),
];
if (cursor) {
conditions.push(lt(posts.createdAt, new Date(cursor)));
}
const feedPosts = await db
.select({
post: posts,
author: {
id: users.id,
username: users.username,
avatarUrl: users.avatarUrl,
},
})
.from(posts)
.innerJoin(users, eq(posts.userId, users.id))
.where(and(...conditions))
.orderBy(desc(posts.createdAt))
.limit(limit + 1);
const hasMore = feedPosts.length > limit;
const resultPosts = hasMore ? feedPosts.slice(0, limit) : feedPosts;
const nextCursor = hasMore
? resultPosts[resultPosts.length - 1].post.createdAt.toISOString()
: null;
return { posts: resultPosts, nextCursor };
}
Phase 3: Frontend 구현
3.1 주요 컴포넌트 구조
components/
├── PostEditor/
│ ├── PostEditor.tsx # 메인 에디터
│ ├── MarkdownToolbar.tsx # 마크다운 도구바
│ ├── ImageUploader.tsx # 이미지 업로드
│ └── HashtagInput.tsx # 해시태그 입력
├── Feed/
│ ├── Feed.tsx # 피드 컨테이너
│ ├── PostCard.tsx # 포스트 카드
│ ├── InfiniteScroll.tsx # 무한 스크롤
│ └── NewPostsBanner.tsx # 새 포스트 알림
├── Post/
│ ├── PostContent.tsx # 포스트 본문
│ ├── CodeBlock.tsx # 코드 하이라이팅
│ ├── LikeButton.tsx # 좋아요 버튼
│ └── CommentSection.tsx # 댓글 섹션
3.2 상태 관리 (Zustand)
// stores/feedStore.ts
interface FeedStore {
posts: Post[]
cursor: string | null
hasMore: boolean
loadPosts: () => Promise<void>
addPost: (post: Post) => void
updatePost: (id: string, updates: Partial<Post>) => void
likePost: (id: string) => void
}
// stores/editorStore.ts
interface EditorStore {
draft: Draft | null
saveDraft: (content: string) => void
loadDraft: () => void
clearDraft: () => void
}
Phase 4: 실시간 기능 (Socket.io)
4.1 Socket 이벤트
// 서버 측
socket.on('join-feed', (userId) => {
socket.join(`feed:${userId}`)
})
io.to(`feed:${authorId}`).emit('new-post', postData)
io.to(`post:${postId}`).emit('like-update', { postId, likeCount })
// 클라이언트 측
socket.on('new-post', (post) => {
feedStore.addPost(post)
showNotification('새 포스트가 있습니다')
})
Phase 5: 테스트
5.1 Backend 테스트
- Unit: Service 로직 (Jest)
- Integration: API 엔드포인트 (Supertest)
- E2E: 주요 사용자 플로우
5.2 Frontend 테스트
- Unit: 유틸 함수, 훅 (Vitest)
- Component: React Testing Library
- E2E: Playwright
Phase 6: 배포
6.1 Docker 구성
services:
postgres:
image: postgres:15
redis:
image: redis:7
server:
build: ./server
ports:
- "3000:3000"
client:
build: ./client
ports:
- "5173:5173"
6.2 AWS 인프라
- EC2: 서버 호스팅
- RDS: PostgreSQL
- ElastiCache: Redis
- S3: 이미지 저장
- CloudFront: CDN
---
## 5단계: Tasks (작업 분해)
```bash
/speckit.tasks
specs/001-post-feed/tasks.md
# 작업 분해: 포스트 피드 시스템
## User Story 1: 포스트 작성
### [P] Task 1.1: 데이터베이스 스키마 생성
파일:
- server/src/db/schema/users.ts
- server/src/db/schema/posts.ts
- server/src/db/schema/postImages.ts
- server/src/db/schema/hashtags.ts
- server/src/db/schema/postHashtags.ts
- server/src/db/index.ts
- drizzle.config.ts
완료 기준:
- Drizzle 스키마 작성 완료
- drizzle-kit generate:pg 성공
- drizzle-kit push:pg 실행
- 테스트 데이터 시딩 가능
---
### [P] Task 1.2: 이미지 업로드 서비스 구현
파일: server/src/services/imageService.ts
작업 내용:
- Sharp를 사용한 이미지 처리
- S3 업로드 로직
- 에러 핸들링
완료 기준:
- 원본, WebP, 썸네일 생성
- S3 업로드 성공
- URL 반환
- Unit 테스트 통과
---
### Task 1.3: 포스트 작성 API 엔드포인트
파일:
- server/src/routes/postRoutes.ts
- server/src/controllers/postController.ts
- server/src/services/postService.ts
의존성: Task 1.1, 1.2
완료 기준:
- POST /api/v1/posts 구현
- Drizzle 트랜잭션으로 Post + Images + Hashtags 동시 생성
- 입력 검증 (Zod)
- Integration 테스트 통과
---
### Task 1.4: 임시저장 API 구현
파일: server/src/controllers/draftController.ts
작업 내용:
- Redis에 임시저장
- TTL 24시간 설정
의존성: Task 1.3
완료 기준:
- POST /api/v1/drafts
- GET /api/v1/drafts
- Redis 캐싱 동작
---
### Task 1.5: PostEditor 컴포넌트 구현
파일: client/src/components/PostEditor/PostEditor.tsx
작업 내용:
- React Hook Form 통합
- 마크다운 에디터
- 실시간 프리뷰
완료 기준:
- 5,000자 제한
- 마크다운 입력 가능
- 프리뷰 렌더링
- Component 테스트 통과
---
### Task 1.6: ImageUploader 컴포넌트
파일: client/src/components/PostEditor/ImageUploader.tsx
작업 내용:
- 드래그앤드롭
- 진행률 표시
- 4장 제한
의존성: Task 1.2
완료 기준:
- 이미지 업로드 가능
- 미리보기 표시
- 삭제 기능
- 진행률 표시
---
### Task 1.7: 자동저장 기능
파일: client/src/hooks/useAutoSave.ts
작업 내용:
- 30초마다 임시저장
- Debounce 처리
의존성: Task 1.4, 1.5
완료 기준:
- 30초 간격 자동저장
- 저장 상태 표시
- 페이지 이탈 시 저장 확인
---
## User Story 2: 피드 조회
### Task 2.1: 피드 조회 API
파일:
- server/src/controllers/feedController.ts
- server/src/services/feedService.ts
작업 내용:
- Drizzle ORM으로 복잡한 조인 쿼리
- Cursor-based pagination
- Redis 캐싱
의존성: Task 1.1
완료 기준:
- GET /api/v1/posts/feed
- 20개씩 페이지네이션
- 팔로잉 사용자 필터링
- Redis 캐싱 (5분 TTL)
- Drizzle 쿼리 최적화
---
### Task 2.2: Feed 컴포넌트 구조
파일: client/src/pages/Feed.tsx
작업 내용:
- React Query로 데이터 fetching
- 무한 스크롤
완료 기준:
- 초기 20개 로드
- 스크롤 시 추가 로드
- 로딩 스켈레톤 UI
---
### Task 2.3: PostCard 컴포넌트
파일: client/src/components/Feed/PostCard.tsx
작업 내용:
- 포스트 카드 UI
- 마크다운 렌더링
- 이미지 lazy loading
완료 기준:
- 카드 레이아웃 구현
- 마크다운 렌더링
- 이미지 lazy load
- 반응형 디자인
---
### Task 2.4: CodeBlock 컴포넌트
파일: client/src/components/Post/CodeBlock.tsx
작업 내용:
- highlight.js 통합
- 복사 버튼
완료 기준:
- 30개 언어 하이라이팅
- 복사 버튼 동작
- 다크 모드 테마
---
### [P] Task 2.5: Socket.io 실시간 연결
파일:
- server/src/socket/feedSocket.ts
- client/src/hooks/useSocket.ts
완료 기준:
- Socket.io 서버 설정
- 클라이언트 연결
- Room 관리 (feed:userId)
---
### Task 2.6: 새 포스트 실시간 알림
파일: client/src/components/Feed/NewPostsBanner.tsx
작업 내용:
- Socket 이벤트 수신
- 배너 표시
의존성: Task 2.5
완료 기준:
- 새 포스트 이벤트 수신
- 배너 표시
- 클릭 시 피드 새로고침
---
## User Story 3: 좋아요
### Task 3.1: 좋아요 API
파일: server/src/controllers/likeController.ts
작업 내용:
- 좋아요/취소 토글
- Drizzle 트랜잭션
- 좋아요 수 캐싱
의존성: Task 1.1
완료 기준:
- POST /api/v1/posts/:id/like
- DELETE /api/v1/posts/:id/like
- 중복 방지
- Drizzle 트랜잭션 처리 (post.likeCount 업데이트)
---
### Task 3.2: LikeButton 컴포넌트
파일: client/src/components/Post/LikeButton.tsx
작업 내용:
- 좋아요 토글 UI
- 애니메이션
의존성: Task 3.1
완료 기준:
- 하트 아이콘 토글
- 좋아요 수 표시
- 애니메이션 효과
- Optimistic update
---
### Task 3.3: 좋아요 실시간 업데이트
파일: server/src/socket/likeSocket.ts
작업 내용:
- Socket 이벤트 발송
의존성: Task 2.5, 3.1
완료 기준:
- 좋아요 시 이벤트 브로드캐스트
- 클라이언트에서 실시간 수 업데이트
---
## User Story 4: 댓글
### Task 4.1: 댓글 API
파일: server/src/controllers/commentController.ts
작업 내용:
- Drizzle ORM으로 댓글 CRUD
- 대댓글 지원
의존성: Task 1.1
완료 기준:
- POST /api/v1/posts/:id/comments
- GET /api/v1/posts/:id/comments
- PUT /api/v1/comments/:id
- DELETE /api/v1/comments/:id
- 1단계 대댓글 지원 (self-reference)
---
### Task 4.2: CommentSection 컴포넌트
파일: client/src/components/Post/CommentSection.tsx
작업 내용:
- 댓글 목록
- 댓글 작성 폼
의존성: Task 4.1
완료 기준:
- 댓글 목록 표시
- 최신순/인기순 정렬
- 대댓글 트리 구조
- @mention 하이라이팅
---
### Task 4.3: @mention 기능
파일: server/src/services/mentionService.ts
작업 내용:
- 사용자 태그 파싱
- 알림 발송
의존성: Task 4.1
완료 기준:
- @username 파싱
- 사용자 ID 추출
- 알림 생성
---
## Phase -1: 준비 작업
### Task 0.1: 프로젝트 초기화
- Vite React 프로젝트 생성
- Express TypeScript 프로젝트 생성
- ESLint, Prettier 설정
- Drizzle ORM 설치 및 설정
### Task 0.2: 개발 환경 설정
- Docker Compose 작성
- PostgreSQL, Redis 컨테이너
- 환경변수 설정
### Task 0.3: CI/CD 파이프라인
- GitHub Actions 워크플로우
- 테스트 자동화
- 배포 스크립트
---
## 추정 시간
- Phase 0: 4시간
- User Story 1: 16시간
- User Story 2: 20시간
- User Story 3: 6시간
- User Story 4: 12시간
총 추정: 58시간 (약 7.5일)
6단계: Analyze (일관성 검증)
/speckit.analyze
AI가 자동으로 검증하는 항목들:
Constitution 준수
- PostgreSQL + Drizzle ORM 사용 확인
- TypeScript strict mode 확인
- API versioning (/api/v1) 확인
명세-계획 일치
- 모든 사용자 스토리가 Tasks에 매핑됨
- 비기능 요구사항 충족 (성능, 보안)
작업 의존성
- Task 순서가 의존성을 고려함
- 병렬 실행 가능한 작업 식별
7단계: Implement (구현 실행)
/speckit.implement
이제 AI 에이전트가 Task 순서대로 코드를 생성합니다. 각 Task마다 리뷰 체크포인트가 있어서 검증 후 다음 단계로 진행됩니다.
✓ Task 1.1: 데이터베이스 스키마 생성 완료
→ 리뷰 필요: src/db/schema/*.ts
✓ Task 1.2: 이미지 업로드 서비스 구현 완료
→ 테스트 통과: imageService.test.ts (8/8)
진행 중: Task 1.3 - 포스트 작성 API...
마무리
GitHub Spec Kit을 사용하면 이렇게 체계적으로 AI와 협업할 수 있습니다:
장점
- 명확한 요구사항: 모호함 없이 “무엇을” 만들지 정의
- 일관된 아키텍처: Constitution으로 기술 스택 통일
- 검증 가능한 작업: 각 Task마다 완료 기준 명확
- 코드 품질: 작은 단위로 리뷰하므로 품질 향상
- 문서화 자동화: 명세 자체가 살아있는 문서
Drizzle ORM의 장점
- 타입 안정성: TypeScript-first 설계로 컴파일 타임 타입 체크
- SQL-like 문법: SQL에 익숙하면 쉽게 학습 가능
- 성능: Prisma보다 가볍고 빠른 쿼리 실행
- 마이그레이션: 명확한 SQL 마이그레이션 파일 생성
- 번들 크기: 작은 런타임 오버헤드
핵심 팁
- Constitution을 처음부터 잘 정의하세요 – 나중에 바꾸기 어렵습니다
- 명세는 기능 중심으로 – “어떻게”가 아닌 “무엇을”에 집중
- 작은 기능부터 – 한 번에 너무 많은 User Story를 넣지 마세요
- Clarify를 활용 – 애매한 부분은 반드시 명확히 하고 시작
- Drizzle 스키마 설계 신중히 – 관계 설정과 인덱스를 초기에 잘 잡아두세요

댓글 남기기