← cd /projects
project : 똑DDOK - 지도 기반 프로젝트·스터디 매칭 플랫폼
period : 2025.08 - 2025.09
role : Team Lead, Fullstack Developer, Infra
stack :
ReactTypeScriptViteSpring BootJava 17PostgreSQLRedisWebSocketKakao Maps APIDockerAWSElasticsearchSentryCoolSMS
// key metrics
- ↗ 7명 팀 프로젝트 팀장 (풀스택 + 인프라 담당)
- ↗ 2개월 기획 및 개발 완료
- ↗ 실시간 채팅 및 알림 시스템 구현
$ open demo.mp4
문제 정의
개발자들이 스터디나 사이드 프로젝트를 찾을 때 겪는 어려움:
- 오픈톡/커뮤니티에서 지역/역할 매칭이 어려움
- 참여 의사 표현 후 연락 두절, 책임감 부재
- 팀 구성 후 협업 도구(채팅, 일정 관리)가 분산됨
목표: 지도 기반으로 주변 스터디/프로젝트를 찾고, 원클릭 참여부터 팀 협업까지 한 곳에서 해결하는 플랫폼
역할과 범위
역할: 팀장 / 풀스택 개발자 / 인프라 담당
범위:
- 프론트엔드: React UI/UX, 카카오맵 연동, 실시간 채팅 클라이언트
- 백엔드: Spring Boot API, 인증, 채팅, 신뢰도 시스템
- 인프라: AWS 배포 (EC2, S3, CloudFront, RDS), Docker, CI/CD
아키텍처
┌─────────────────────────────────────────────────────────────┐
│ Frontend (React + Vite) │
│ React + TypeScript + TanStack Query + Zustand + Kakao Map │
└─────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ Backend (Spring Boot) │ │ AWS Infrastructure │
│ │ │ │
│ - REST API │ │ - EC2 (Backend) │
│ - WebSocket/STOMP Chat │ │ - S3 + CloudFront (FE) │
│ - Spring Security + JWT │ │ - RDS (PostgreSQL) │
│ - Spring Batch │ │ - ElastiCache (Redis) │
└───────────┬─────────────┘ └─────────────────────────┘
│
┌───────┴───────┐
▼ ▼
┌────────┐ ┌────────────┐
│ Redis │ │ PostgreSQL │
│ (Cache)│ │ (Main DB) │
└────────┘ └────────────┘
레포지토리 구성
| 서비스 | 기술 스택 | 역할 |
|---|---|---|
| ddok-fe | React, TypeScript, Vite | 프론트엔드 SPA |
| ddok-be | Spring Boot, Java 17 | API 서버, 채팅, 인증 |
핵심 구현
1. 카카오맵 기반 지역 탐색
카카오맵 SDK와 클러스터링을 활용한 스터디/프로젝트 위치 시각화:
// 카카오맵 마커 클러스터링
const MapContainer = () => {
const { data: projects } = useQuery({
queryKey: ['projects', bounds],
queryFn: () => fetchProjectsInBounds(bounds)
});
return (
<Map center={center} level={5} onBoundsChanged={handleBoundsChange}>
<MarkerClusterer
averageCenter={true}
minLevel={4}
calculator={[10, 30, 50]}
>
{projects?.map((project) => (
<MapMarker
key={project.id}
position={{ lat: project.lat, lng: project.lng }}
onClick={() => openProjectDetail(project)}
>
<ProjectInfoWindow project={project} />
</MapMarker>
))}
</MarkerClusterer>
</Map>
);
};
2. WebSocket STOMP 실시간 채팅
팀 생성 시 자동으로 채팅방 생성, 실시간 메시지 전송:
@Controller
public class ChatController {
@MessageMapping("/chat/{teamId}")
@SendTo("/topic/team/{teamId}")
public ChatMessageDto sendMessage(
@DestinationVariable Long teamId,
@Payload ChatMessageDto message,
@AuthenticationPrincipal UserDetails user
) {
message.setSender(user.getUsername());
message.setTimestamp(LocalDateTime.now());
// 메시지 저장 및 알림 발송
chatService.saveAndNotify(teamId, message);
return message;
}
}
// 프론트엔드 STOMP 클라이언트
const useChatConnection = (teamId: number) => {
const stompClient = useRef<Client | null>(null);
const { addMessage } = useChatStore();
useEffect(() => {
const client = new Client({
brokerURL: `${WS_URL}/ws`,
onConnect: () => {
client.subscribe(`/topic/team/${teamId}`, (message) => {
const chatMessage = JSON.parse(message.body);
addMessage(chatMessage);
});
},
});
client.activate();
stompClient.current = client;
return () => client.deactivate();
}, [teamId]);
const sendMessage = (content: string) => {
stompClient.current?.publish({
destination: `/app/chat/${teamId}`,
body: JSON.stringify({ content }),
});
};
return { sendMessage };
};
3. 신뢰도(온도) 시스템
프로젝트 완주율, 팀원 평가를 기반으로 한 사용자 신뢰도 계산:
@Service
public class ReputationService {
private static final double BASE_TEMPERATURE = 36.5;
public double calculateTemperature(Member member) {
// 프로젝트 완주율
double completionRate = calculateCompletionRate(member);
// 팀원 평가 점수
double evaluationScore = getAverageEvaluation(member);
// 활동 기간 보정
double activityBonus = calculateActivityBonus(member);
return BASE_TEMPERATURE
+ (completionRate * 5) // 완주율 최대 +5도
+ (evaluationScore * 3) // 평가 최대 +3도
+ activityBonus; // 활동 보너스 최대 +1도
}
}
// Spring Batch로 일간 랭킹 갱신
@Configuration
public class RankingBatchConfig {
@Bean
public Job rankingJob() {
return jobBuilderFactory.get("rankingJob")
.start(calculateRankingStep())
.next(updateRedisCacheStep())
.build();
}
}
4. Kakao OAuth2 소셜 로그인
@Service
public class KakaoOAuthService {
public LoginResponse loginWithKakao(String authCode) {
// 1. 인가 코드로 액세스 토큰 발급
KakaoTokenResponse tokenResponse = requestToken(authCode);
// 2. 사용자 정보 조회
KakaoUserInfo userInfo = getUserInfo(tokenResponse.getAccessToken());
// 3. 회원 조회 또는 생성
Member member = memberRepository.findByKakaoId(userInfo.getId())
.orElseGet(() -> createMember(userInfo));
// 4. JWT 토큰 발급
String jwt = jwtProvider.createToken(member);
String refreshToken = jwtProvider.createRefreshToken(member);
// 5. Redis에 리프레시 토큰 저장
redisTemplate.opsForValue().set(
"refresh:" + member.getId(),
refreshToken,
Duration.ofDays(14)
);
return new LoginResponse(jwt, refreshToken, member);
}
}
트러블슈팅
이슈 1: 카카오맵 마커 렌더링 성능 저하
현상: 수백 개의 스터디/프로젝트 마커 동시 렌더링 시 지도 조작이 버벅거림
원인: 모든 마커를 개별 DOM 요소로 렌더링하여 리플로우 발생
해결:
- 마커 클러스터링으로 화면 내 마커 수 제한
- 지도 bounds 변경 시 debounce 적용
- 뷰포트 외 마커는 렌더링 제외
const debouncedFetch = useMemo(
() => debounce((bounds: LatLngBounds) => {
queryClient.prefetchQuery({
queryKey: ['projects', bounds],
queryFn: () => fetchProjectsInBounds(bounds)
});
}, 300),
[]
);
결과: 지도 조작 시 프레임 드롭 해소, 부드러운 UX 제공
이슈 2: WebSocket 연결 유지 문제
현상: 모바일 환경에서 채팅 연결이 자주 끊기고 메시지 유실
원인:
- 모바일 백그라운드 전환 시 WebSocket 연결 해제
- 네트워크 불안정 시 재연결 로직 부재
해결:
const client = new Client({
brokerURL: `${WS_URL}/ws`,
reconnectDelay: 5000, // 재연결 딜레이
heartbeatIncoming: 10000, // 하트비트 설정
heartbeatOutgoing: 10000,
onDisconnect: () => {
// 연결 끊김 시 로컬 상태 유지
setConnectionStatus('disconnected');
},
onStompError: (frame) => {
console.error('STOMP error:', frame);
// 에러 발생 시 자동 재연결
}
});
// 페이지 visibility 변경 시 재연결
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible' && !client.connected) {
client.activate();
}
});
재발 방지:
- 미전송 메시지 로컬 큐에 저장 후 재연결 시 재전송
- 연결 상태 UI 표시로 사용자 피드백 제공
이슈 3: N+1 쿼리 문제
현상: 프로젝트 목록 조회 시 응답 시간 2초 이상
원인: 프로젝트별 팀원, 기술스택, 위치 정보를 개별 쿼리로 조회
해결: Fetch Join과 EntityGraph 활용
@EntityGraph(attributePaths = {"members", "techStacks", "location"})
@Query("SELECT p FROM Project p WHERE ST_DWithin(p.location, :point, :radius)")
List<Project> findProjectsWithinRadius(
@Param("point") Point point,
@Param("radius") double radius
);
결과: 조회 성능 2초 → 200ms로 개선 (10배 향상)
성과
| 지표 | 결과 |
|---|---|
| 개발 기간 | 2개월 (기획 ~ 배포) |
| 팀 구성 | 7명 (FE 4, BE 2, 풀스택 1) |
| 담당 역할 | 팀장, 풀스택, 인프라 |
| 주요 기여 | 채팅 시스템, 인증, 인프라 구축 |
| 서비스 배포 | AWS 기반 프로덕션 배포 완료 |
주요 기능
- 지도 기반 탐색: 카카오맵에서 주변 스터디/프로젝트/플레이어를 한눈에 확인
- 포지션 매칭: 역할/경험/시간대 기반 맞춤 필터와 추천
- 원클릭 참여: 오픈톡/댓글 없이 클릭 한 번으로 신청/취소
- 팀 협업: 팀 생성 시 자동 채팅방, 일정 조율(캘린더), 팀 ReadMe
- 신뢰도 시스템: 온도(완주율/기여도), 배지/랭킹으로 책임감과 지속 참여 유도
프론트엔드 기여
- 카카오맵 통합: 클러스터링, 커스텀 오버레이, 위치 기반 필터링
- 실시간 채팅 UI: STOMP 클라이언트, 메시지 상태 관리, 읽음 표시
- 폼 검증: React Hook Form + Zod 스키마 기반 유효성 검사
- 상태 관리: Zustand (전역) + TanStack Query (서버 상태)
회고 및 다음 개선
잘한 점
- 팀장으로서 7명 팀원 역할 분배 및 일정 관리 성공
- 실시간 채팅 및 알림 시스템 완성
- 카카오맵 기반 차별화된 UX 제공
개선할 점
- 테스트 코드 커버리지 부족
- 모니터링/로깅 체계 강화 필요
- 성능 최적화 여지 존재 (이미지 최적화 등)
다음 개선 계획
- Kubernetes 기반 컨테이너 오케스트레이션
- 프론트엔드 E2E 테스트 (Playwright) 도입
- 실시간 알림 고도화 (FCM 푸시 알림)