갭로그(Gaplog) : 공백기 청년을 위한 커리어 회복 플랫폼 개발 회고
프로젝트 기간 : 2025.04 ~ 2025.07
서비스 유형 : Web
역할 : 팀장 / 프론트엔드 개발
인원 : 백엔드 1명 AI 1명 프론트엔드 2명 (총 4명)
서비스 소개 영상 : youtube
들어가며
2025년 고용노동부 공공데이터 공모전에 참여하여,
공백기 청년을 위한 AI 기반 진로 설계 및 직무 추천 서비스를 기획·개발하고 장려상을 수상했습니다.
개발 직무로 전환한 이후 처음으로 경험한 공모전이었으며,
팀장 겸 프론트엔드 개발자로서 기획, 개발, 발표까지 전 과정을 주도하며
실제 프로덕트 개발과 유사한 경험을 쌓을 수 있었습니다.
짧은 기간 내 서비스 완성도를 높이기 위해 집중했던 지점과,
그 과정에서의 기술적 고민, 그리고 배운 점들을 공유하고자 합니다.
1. 프로젝트 개요
갭로그(Gaplog)란?
갭로그(Gaplog)는 취업 공백기를 겪는 청년의 커리어 회복을 지원하는 AI 기반 플랫폼입니다.
갭로그는 2025년 고용노동부 공공데이터 활용 공모전의 주제에 맞춰,
청년 고용과 직무 탐색 과정에서 발생하는 구조적 문제를
공공데이터를 통해 어떻게 완화할 수 있을지에 대한 고민에서 출발했습니다.


이미지 출처 이투데이 | 중앙 일보
최근 고용노동부 통계와 언론 보도에서도 나타나듯,
이른바 ‘쉬었음 청년’ 문제는 단순한 미취업 상태를 넘어
장기화된 공백으로 인한 경력 단절 인식, 진로 혼란, 심리적 부담으로 이어지고 있습니다.
특히 공백기 청년들은 공백 기간 동안의 경험과 활동을
체계적으로 정리하고 설명할 수 있는 기준이 부족하다는 한계를 지니고 있다고 판단했습니다.

이러한 배경을 바탕으로 ‘공백(Gap)’ 기간의 활동을 ‘로그(Log)’로 기록하고 의미화하는 주제로,
사용자 입력 데이터와 고용·직무 관련 공공데이터를 결합해
LLM 분석을 통해 개인의 현재 상황에 맞는
커리어 로드맵과 직무 방향을 제안하고,
공백기 동안의 경험과 상태를 함께 정리할 수 있는 서비스를 기획, 개발했습니다.
2. 왜 이 서비스를 만들게 되었나요?
1) 공백기 청년에 대한 문제 인식
고용노동부에서 제공하는 청년 고용 관련 공공데이터와 실태조사를 살펴보며,
공백기 청년이 단순히 ‘미취업 상태’가 아니라
다음과 같은 복합적인 문제를 동시에 겪고 있음을 확인했습니다.
- 방향성 부족: 직무, 산업준비 항목에 대한 기준이 없어 막막함을 느낌
- 강점 정리의 어려움: 공백기 동안의 경험을 이력서와 자기소개서로 설명하기 어려움
- 정보 탐색의 단절: 신뢰할 수 있는 데이터 기반 정보와 실질적인 조언에 접근하기 어려움
- 심리적·정신적 부담: 장기 취업 준비로 인한 번아웃, 불안감, 자기 효능감 저하
공공데이터와 실태조사를 통해 확인한 점은,
이러한 심리적 문제들이 단순한 개인 감정의 문제가 아니라
진로 탐색 의욕 저하와 준비 중단으로 이어질 수 있는 구조적 문제라는 것이었습니다.
2) 기존 취업 관련 서비스를 사용하면서 느낀 한계
한편, 잡코리아, 사람인, 자소설닷컴 등 기존 취업 플랫폼을 직접 사용하며 느낀 점은,
채용 공고 탐색과 지원 기능에는 강점이 있지만,
아래와 같은 부족한 부분들이 있었습니다.
- 사용자의 현재 상태를 진단해주는 기준
- 공백기를 설명 가능한 경험으로 정리해주는 구조
- 심리적 회복을 고려한 UX
그 결과 공백기 청년은
객관적인 기준 없이 정보 탐색과 실패를 반복하게 되고,
공백기에 대한 불안은 더욱 누적되는 구조라고 느꼈습니다.
3) 그래서 어떻게 접근했는가?
저희 팀 역시 모두 취업을 준비하는 과정에 있었기에
이러한 문제에 깊이 공감할 수 있었고,
"공백기 청년에게 지금 정말 필요한 서비스는 무엇일까?"
라는 질문을 핵심 문제로 설정했습니다.
그 과정에서
고용노동부의 청년 고용·직무 관련 공공데이터와
사용자가 직접 입력하는 경험 데이터를 결합하고,
LLM을 활용한다면
개인의 상황에 맞는 진단과 경로 설계가 가능하다고 판단했습니다.



따라서 갭로그는
‘무엇을 준비할지’ 정보를 제공하는 것 이상으로,
‘지금 나의 상태가 어떤지’를 함께 진단하는 서비스를 목표로
다음과 같은 핵심 가치를 중심으로 갭로그를 기획했습니다.
- 데이터 기반 맞춤형 진단: 사용자의 상황을 분석하여 구체적인 방향 제시
- 실제 극복 사례 공유: 커뮤니티를 통한 정보 교류 및 동기 부여
- 심리적 회복 지원: 감정 일기를 통한 정서적 안정감 제공
- 멘토링 연결: 직무별 전문가의 실질적인 조언 제공
3. 프로젝트 정보
3-1) 핵심 기능
(1) 공백기 후기 커뮤니티 (담당 개발)






공백기를 극복하고 취업에 성공한 사용자들이 자신의 여정을 공유하는 커뮤니티형 게시판입니다.
실제 경험과 전략을 통해 다른 공백기 청년들에게 정보와 동기 부여를 제공합니다.
(2) 멘토링 게시판 (담당 개발)



멘토와 멘티가 직무와 경험을 기반으로 질문과 답변을 주고받는 커뮤니티입니다.
직무별 태그와 답변 상태 표시를 통해 효율적인 정보 탐색을 지원합니다.
(3) 맞춤형 로드맵

갭로그는 사용자가 입력한 경험·상태 데이터를 기반으로
고용노동부 공공데이터와 큐넷(Q-net) 자격 정보 데이터를 결합해
동일 직무를 준비 중인 사용자들의 경로를 비교·분석합니다.
이를 통해
현재 단계에 적합한 자격증 추천, 부트캠프 및 학습 경로 제안,
그리고 실행 가능한 개인 맞춤형 커리어 로드맵을 제공합니다.
(4)AI 포트폴리오


복잡한 문서 작성 없이
간단한 정보 입력만으로 AI가 포트폴리오를 자동 생성하며,
결과물을 HTML 형태로 내보낼 수 있도록 설계해
실제 취업 준비 과정에서 바로 활용할 수 있도록 했습니다.
(5) 감정 일기 (마음 일기) (담당 개발)


페니베이커의 '표현적 글쓰기' 이론에 기반하여,
사용자가 감정·수면 시간·날씨를 함께 기록하며 심리적 회복을 돕는 서비스입니다.
시각화된 데이터를 통해 취업 준비 기간 중 정서 변화를 확인할 수 있습니다.
3-2) 담당 역할 및 기여
담당 역할
- 프로젝트 팀장
- 프론트엔드 리드 개발
- 공통 컴포넌트 UI 개발
- 감정일기 UX/UI 기획 및 개발
- 커뮤니티 핵심 기능 구현
| 구분 | 담당 내용 및 기여 부분 |
| 1) 프로젝트 기획 | 공백기 청년 지원을 위한 서비스 컨셉 정의 및 전체 사용자 여정 설계 |
| 커뮤니티, 감정 기록 기능을 포함한 서비스 핵심 기능 구체화 | |
| 2) 프론트엔드 개발 | TypeScript, React 기반 프론트엔드 아키텍처 설계 및 구축 |
| 프로젝트 디렉토리 구조, ESLint/Prettier 설정 등 개발 환경 구성 | |
| 홈 화면, 커뮤니티, 감정 일기 등 핵심 기능 개발 담당 | |
| 사용자 로그인/데이터 보유 상태에 따른 동적 화면 구성 | |
| 3) 공통 컴포넌트 UI 개발 | 전체 화면 구조 및 UI 규격 설계, 반응형 컴포넌트 시스템 구축 (아래 코드 1 참고) |
| Button, Card, Tab, Modal 등 공통 UI 컴포넌트 설계 및 구현 | |
| Tailwind CSS 기반 디자인 시스템 구축 | |
| 컴포넌트 재사용을 고려한 구조 설계로 코드 재사용률 50% 이상 확보 | |
| 4) 감정일기 UX/UI 기획 및 개발 | 감정 기록, 회고로 이어지는 사용자 흐름 설계 |
| 감정, 수면,날씨 데이터 관련 공통 컴포넌트 설계 및 재사용을 통해 중복 구현 최소화 | |
| ApexCharts를 활용한 주간/월간 감정 추이 그래프 구현 | |
| 감정 타입 모듈화를 통해 상태별 시각적 피드백 제공 및 UI 재사용성 향상 | |
| 5) 커뮤니티 기능 구현 | 공백기 후기 게시판 및 멘토링 게시판 CRUD 기능 구현 |
| 게시글 작성, 댓글, 좋아요, 즐겨찾기 등 사용자 상호작용 기능 개발 | |
| 6) 프로젝트 운영 | 팀장으로서 원격 협업 환경에서의 일정 관리 및 업무 분배 |
| 코드 리뷰 및 기술 공유를 통한 팀 역량 향상 | |
| 팀원들의 커뮤니케이션 및 요구사항 조율 | |
| 공모전 제출용 시연 영상 제작 및 최종 발표 참여 |
*(코드 1) 웹 기준 반응형 설계
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import Aside from '@/components/Aside';
interface LayoutProps {
children: React.ReactNode;
aside?: React.ReactNode;
}
export default function Layout({ children, aside }: LayoutProps) {
return (
<div className="flex flex-col w-full h-full ">
{/* Header */}
<Header />
{/* Body */}
<div className="flex flex-1 justify-center w-full pt-40">
<div className="w-full px-10 pt-14">
<div className="flex justify-between w-full min-w-[1024px] max-w-[1440px] mx-auto">
<main className="w-[65%]">{children}</main>
{aside && <Aside>{aside}</Aside>}
</div>
</div>
</div>
<Footer />
</div>
);
}
기술 스택
| 구분 | 기술 스택 |
| Frontend | React, TypeScript |
| 패키지 관리 | npm |
| 상태 관리 | Zustand |
| Styling | Tailwind, ApexChart(그래프) |
4. 기술 선택 과정
갭로그는 단순 CRUD 서비스가 아니라
감정 데이터, 커뮤니티 활동, 커리어 정보가 동시에 흐르는 서비스였습니다.
이처럼 서로 성격이 다른 정보가 하나의 서비스 안에서 유기적으로 연결되기 때문에,
기술 선택의 기준은 ‘유행’이나 개인적인 선호가 아니라 다음 질문에 맞춰 설정했습니다.
- 이 서비스의 복잡한 사용자 상태를 구조적으로 표현할 수 있는가
- 팀 단위 개발에서 안정성과 생산성을 동시에 확보할 수 있는가
- 짧은 기간 안에 완성도 있는 UX를 구현할 수 있는가
이 기준을 바탕으로 갭로그의 프론트엔드 기술 스택을 결정했습니다.
4-1) 복잡성을 관리하기 위한 선택 – React
갭로그의 화면을 구성하는 컴포넌트 대부분은단일 역할을 수행하지 않았습니다.
공백기 후기, 멘토링 게시판, 감정일기처럼 목적과 맥락이 전혀 다른 콘텐츠가
하나의 서비스 흐름 안에 공존했기 때문입니다.


각 기능은 목적과 데이터 구조가 전혀 달랐지만,
사용자 입장에서는 이 모든 정보가 하나의 피드 안에서
같은 맥락의 ‘카드형 정보’로 자연스럽게 인식되어야 했습니다.
문제는 기능별로 카드 UI를 각각 구현할 경우 였습니다.
카드 스타일과 레이아웃의 중복이 빠르게 증가하고
스타일 변경 시 여러 컴포넌트를 동시에 수정해야 하는 구조가 될 가능성이 컸습니다.
또한, 서비스가 확장될수록 UI 관리 비용이 늘어날 것이라 판단했습니다.
선택
이러한 문제를 해결하기 위해
UI 구조를 역할 단위로 분리하고, 조합을 통해 확장할 수 있는 방식이 필요했고,
그에 가장 적합한 선택이 React였습니다.
React의 컴포넌트 기반 구조를 활용해
카드 UI의 공통 책임(레이아웃, 테두리, 그림자)은 하나로 묶고,
각 서비스는 그 안에서 필요한 정보만 조합하는 방식으로 설계했습니다.
이를 통해 “UI는 하나지만, 내용은 다르다”는 요구사항을
컴포넌트 구조로 자연스럽게 표현하고자 했습니다.
적용
아래는 공통으로 사용한 기본 Card 컴포넌트입니다.
// src/components/Card.tsx
// 기본 Card 컴포넌트 - Compound Component 패턴
interface CardProps {
children: React.ReactNode;
className?: string;
}
interface CardHeaderProps {
children: React.ReactNode;
className?: string;
}
interface CardContentProps {
children: React.ReactNode;
className?: string;
}
export const Card: React.FC<CardProps> = ({ children, className = '' }) => (
<div
className={`bg-white rounded-lg border border-gray-200 shadow-sm ${className}`}
>
{children}
</div>
)
export const CardHeader: React.FC<CardHeaderProps> = ({
children,
className = '',
}) => (
<div className={`px-6 py-4 border-b border-gray-200 ${className}`}>
{children}
</div>
)
export const CardContent: React.FC<CardContentProps> = ({
children,
className = '',
}) => <div className={`px-6 py-4 ${className}`}>{children}</div>;
이 기본 구조를 바탕으로,
각 서비스는 자신의 목적에 맞게 카드 내부를 구성했습니다.
아래는 멘토링 카드의 실제 코드 일부로,
공통 Card 레이아웃을 재사용하면서도
멘토링 서비스에 필요한 정보(직무, 작성자, 답변 상태 등)를
서비스 로직에 맞게 배치하였습니다.
// src/features/Mentor/MentorCard.tsx
interface MentorCardProps {
article: Mentor;
type: CardColor;
}
export default function MentorCard({ article, type }: MentorCardProps) {
const {
mentorId,
title,
content,
category,
major,
replies,
} = article;
const isAnswered = replies.length > 0;
return (
<Card className="h-[320px]">
<CardHeader>
<span>{category}</span>
<span>{major}</span>
</CardHeader>
<CardContent>
<h3>{title}</h3>
<p>{content}</p>
<Tag
type={isAnswered ? 'green' : 'pink'}
label={isAnswered ? '답변 완료' : '답변 대기'}
/>
</CardContent>
</Card>
);
}
결과
이러한 구조를 통해 UI의 일관성은 유지하면서도,
각 서비스의 목적에 맞는 정보 표현을 유연하게 구성할 수 있었습니다.
이 방식의 핵심은 컴포넌트 수를 줄이는 것이 아니라,
컴포넌트가 책임지는 범위를 명확히 제한한 것이었습니다.
Card 자체는 단순하지만, 그 위에 쌓이는 조합은 서비스 특성에 따라 유연하게 확장될 수 있었습니다.
4-2) 감정 데이터에 ‘실수’가 생기지 않게 – TypeScript
서비스 전반에서 다루는 데이터는 단순하지 않았습니다.
감정 데이터는 감정 타입, 수면 시간, 날씨 등 정형 데이터와 함께 텍스트 기록을 포함했고,
멘토링과 공백기 후기는 서로 다른 메타 정보를 가지고 있었습니다.




특히 감정 일기의 경우
감정 타입과 시각적 표현이 항상 일관되게 매핑되어야 했고,
누락이나 오타는 UX에 직접적인 영향을 줄 수 있었습니다.
선택
이를 런타임 로직으로 처리하기보다는,
컴파일 단계에서 구조적으로 강제하는 방식이 필요했고
그 해답이 TypeScript였습니다.
TypeScript를 통해 서비스의 핵심 개념을 타입으로 먼저 정의하고,
그 위에 UI와 로직을 쌓는 방식으로 개발을 진행했습니다.
적용
// src/types/emotion.ts
export type Emotion = '기쁨' | '평온' | '불안' | '슬픔' | '화남';
export type EmotionTagType = 'yellow' | 'pink' | 'green' | 'skyblue' | 'primary';
export const emotionColorMap: Record<Emotion, EmotionTagType> = {
기쁨: 'yellow',
평온: 'green',
불안: 'primary',
슬픔: 'skyblue',
화남: 'pink',
};
결과
이 구조를 통해 감정 타입이 추가되거나 변경되더라도
반드시 대응되는 UI 표현이 함께 정의되도록 강제할 수 있었고,
누락된 경우 컴파일 단계에서 즉시 오류를 확인할 수 있어 개발 안정선 측면에서도 긍정적이었습니다.
특히 공통 컴포넌트를 사용하는 구조에서는 Props 타입이 곧 컴포넌트 사용 설명서 역할을 했기 때문에,
팀원 간 협업에서도 의사소통 비용을 크게 줄일 수 있었습니다.
4-3) 스타일링과 디자인 시스템을 코드로 고정하기 – Tailwind CSS
갭로그는 감정 일기, 커뮤니티 카드, 커리어 정보 등
서로 다른 성격의 UI가 하나의 서비스 안에서 공존하는 구조였습니다.
이 과정에서 단순히 “예쁘게 스타일링”하는 것보다,
UI의 일관성을 어떻게 유지할 것인가가 중요한 과제가 되었습니다.

디자인은 Figma를 기반으로 정리되어 있었고,
색상·여백·강조 규칙이 비교적 명확했기 때문에
이를 매번 CSS 파일로 옮기는 방식보다는 디자인 규칙 자체를 코드로 고정할 수 있는 방법이 필요했습니다.
선택
Tailwind CSS는 유틸리티 기반 클래스라는 특성 덕분에
컴포넌트 단위에서 스타일과 구조를 함께 읽을 수 있었고,
설정 파일을 통해 디자인 토큰을 중앙에서 관리할 수 있다는 점이 프로젝트 성격과 잘 맞았습니다.
특히 감정 일기처럼 감정 타입에 따라 반복적으로 색상이 사용되는 UI에서는
클래스 조합이 아닌, 디자인 시스템 차원에서 색상을 정의해 두는 방식이 유지보수에 유리하다고 판단했습니다.
적용
Tailwind 설정 파일에 서비스 전반에서 사용하는 컬러와 감정별 색상 체계를 정의했습니다.
이를 통해 “기쁨은 항상 이 색”, “평온은 항상 이 톤”이라는 규칙을 코드로 고정했습니다.
// tailwind.config.ts - 디자인 시스템 구축
import type { Config } from 'tailwindcss';
const config: Config = {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
colors: {
primary: '#8B5CF6',
'primary-active': '#A855F7',
'primary-action': '#9333EA',
'primary-primary-background': '#F3E8FF',
background: '#F9FAFB',
border: '#E2E8F0',
// 감정별 색상 체계
yellow: '#F59E0C', // 기쁨
pink: '#EC4899', // 화남
green: '#22C55E', // 평온
skyblue: '#06B6D4', // 슬픔
},
backgroundImage: {
// 그라데이션 배경 정의
'gd-point-purple': 'linear-gradient(to right, #F3E8FF, #FCE8F4)',
'gd-point-blue': 'linear-gradient(to right, #DAEAFE, #CFFAFE)',
},
},
},
};
컴포넌트에서는 감정 타입만 전달받아, 미리 정의된 색상 규칙을 그대로 사용하도록 구성했습니다.
export const Tag: React.FC<TagProps> = ({ emotion, children }) => {
const colorClass = {
기쁨: 'bg-yellow-100 text-yellow-800',
평온: 'bg-green-100 text-green-800',
불안: 'bg-primary-primary-background text-primary',
슬픔: 'bg-skyblue-100 text-skyblue-800',
화남: 'bg-pink-100 text-pink-800',
}[emotion];
return (
<span className={`px-3 py-1 rounded-full text-sm ${colorClass}`}>
{children}
</span>
);
};
결과

디자인 수정이 발생하더라도 설정 파일만 변경하면 전체 UI에 반영되는 구조를 만들 수 있었고,
감정 일기, 감정 태그, 차트 등 서로 다른 기능에서도 일관된 감정 표현 UX를 유지할 수 있었습니다.
또한 스타일 정의가 분산되지 않아 UI 관련 의사결정이 훨씬 단순해졌습니다.
4-4) 필요한 만큼만 사용하는 전역 상태 관리 – Zustand
갭로그에서 전역 상태로 관리해야 할 정보는
로그인 여부, 사용자 정보, 역할 구분 등 비교적 명확하고 제한적이었습니다.
복잡한 상태 흐름이나 미들웨어가 필요한 상황은 아니었기 때문에,
무거운 상태 관리 도구는 오히려 과하다고 판단했습니다.
선택
Zustand는 최소한의 API로 전역 상태를 정의할 수 있고,
TypeScript와의 결합이 자연스럽다는 점에서 프로젝트 성격과 잘 맞았습니다.
특히 보일러플레이트 코드 없이 상태와 로직을 함께 정의할 수 있다는 점이
팀원들의 학습 곡선을 고려하였을 때 장점이라고 판단했습니다.
적용
인증 관련 전역 상태를 하나의 스토어로 정의하고,
컴포넌트에서는 훅 형태로 직접 접근하도록 구성했습니다.
// src/stores/useAuthStore.ts
import { create } from 'zustand';
interface AuthState {
user: User | null;
isLoggedIn: boolean;
login: (user: User) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>((set) => ({
user: null,
isLoggedIn: false,
login: (user) => set({ user, isLoggedIn: true }),
logout: () => set({ user: null, isLoggedIn: false }),
}));
결과
전역 상태가 필요한 부분과 그렇지 않은 부분이 명확히 구분되었고,
상태 관리 로직이 UI 구조를 복잡하게 만들지 않았습니다.
결과적으로 전역 상태가 “편의 기능”이 아니라
서비스의 흐름을 드러내는 최소한의 구조로 유지될 수 있었습니다.
4-5) 데이터를 경험으로 보여주는 그래프 라이브러리 – ApexCharts
갭로그의 감정 일기는 단순한 기록 기능이 아니라,
사용자가 자신의 상태 변화를 인식하도록 돕는 장치에 가까웠습니다.
하루하루의 감정 데이터를 나열하는 것만으로는
“최근 내가 어떤 흐름에 있는지”를 직관적으로 이해하기 어려웠고,
사용자가 스스로 변화를 발견할 수 있는 시각적 장치가 필요했습니다.
따라서 감정 데이터를 단순히 차트로 그리는 것이 아니라,
시간에 따른 변화와 흐름이 자연스럽게 읽히는 방식이 중요했습니다.
선택
ApexCharts는 React 환경에서 비교적 설정이 단순하면서도,
라인 차트, 영역 차트 등 감정의 연속성과 흐름을 표현하기에 적합한 시각화를 제공했습니다.
JavaScript Chart Examples & Samples Demo – ApexCharts.js
Get a glimpse of ApexCharts by exploring all the samples created using the library. You can download these JavaScript Chart Examples and use them freely.
apexcharts.com
특히 반응형 지원과 커스터마이징 범위가 넓어
모바일·데스크톱 환경 모두에서 동일한 UX를 유지할 수 있었고,
차트 자체에 불필요한 인터랙션을 강요하지 않는 점도 감정 데이터의 성격과 잘 맞았습니다.
적용
감정 타입을 수치 값으로 변환해,
날짜별 감정 흐름이 부드럽게 이어지도록 라인 차트로 구성했습니다.
이때 감정 값은 “정확한 수치”보다는
상대적인 흐름을 인식하기 위한 지표로 활용했습니다.
import ReactApexChart from 'react-apexcharts';
export const EmotionChart: React.FC<{ data: EmotionData[] }> = ({ data }) => {
const chartOptions = {
chart: {
type: 'line' as const,
responsive: true,
toolbar: { show: false },
},
xaxis: {
categories: data.map((d) => d.date),
},
stroke: {
curve: 'smooth' as const,
width: 2,
},
};
const series = [
{
name: '감정 흐름',
data: data.map((d) => {
const emotionValues: Record<Emotion, number> = {
기쁨: 5,
평온: 3,
불안: 2,
슬픔: 1,
화남: 4,
};
return emotionValues[d.emotion];
}),
},
];
return (
<ReactApexChart
options={chartOptions}
series={series}
type="line"
height={240}
/>
);
};
결과
사용자는 단순한 감정 기록 목록이 아니라,
“최근 감정이 점점 안정되고 있는지”,
“특정 시점 이후 감정 변화가 컸는지”를 한눈에 파악할 수 있게 되었습니다.
감정 차트는 데이터를 분석하기 위한 도구라기보다,
사용자가 자신의 상태를 돌아보는 계기로 작동했고,
감정 일기의 목적을 UI 차원에서 보완해주는 역할을 했습니다.
5. 기술적 핵심 과제
5-1) 날짜 기반 감정 데이터 시각화 : 정확성과 성능의 균형
문제상황
감정 일기 기능에서 가장 까다로웠던 부분은
캘린더에서 선택한 날짜와 차트 데이터를 정확히 동기화하는 것이었습니다.

사용자가 날짜를 변경할 때 마다 해당 날짜를 기준으로 2주간의 감정 데이터를 즉시 조회하고
변화 정도를 ApexCharts 라이브러리로 시각화하는 과정에서 가장 신경 썼던 부분은
날짜 변경, 데이터 조회, 차트 렌더링이 항상 같은 순서와 기준으로 동작하도록 유지하는 것이었습니다.
초기에는 날짜 상태와 차트 데이터가 느슨하게 연결되어 있어,
- 날짜를 빠르게 변경할 경우 차트 렌더링 타이밍이 불안정해지는 문제
- 날짜 변경과 무관한 렌더링까지 발생해 성능이 불필요하게 소모되는 문제
가 있었습니다.
“날짜 하나만 바꿨을 뿐인데, 왜 이렇게 많은 렌더링이 일어날까?”
라는 의문이 들었습니다.
해결 과정 1
단일 소스로 날짜 상태 관리
문제의 원인은 날짜 상태가 차트와 데이터 조회 로직의
명확한 기준점 역할을 하지 못하고 있다는 점이라고 판단했습니다.
그래서 날짜를 단일 state로 관리하고,
모든 데이터 조회 로직이 이 값을 기준으로 실행되도록 변경했습니다.
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
// 날짜 변경 시 데이터 재조회
useEffect(() => {
const fetchEmotionData = async () => {
const data = await getEmotionByDate(selectedDate);
setEmotionData(data);
};
fetchEmotionData();
}, [selectedDate]);
이후 날짜가 변경될 때만 데이터가 갱신되도록 흐름이 단순해졌고,
렌더링 순서를 추적하기도 훨씬 쉬워졌습니다.
해결 과정 2
차트 옵션 메모이제이션으로 렌더링 비용 줄이기
차트 옵션 객체가 렌더링마다 새로 생성되면서,
실제 데이터 변경과 상관없는 차트 재렌더링이 발생하고 있었습니다.
이를 해결하기 위해 useMemo를 활용해
감정 데이터가 변경될 때만 차트 옵션이 다시 계산되도록 최적화했습니다.
// ApexCharts Option
const chartOptions = useMemo(() => ({
series: emotionData.map(d => ({
name: d.emotion,
data: d.scores,
})),
chart: { type: 'line' },
xaxis: { categories: weekDays },
}), [emotionData]);
이후 날짜를 변경하지 않는 한 차트는 다시 계산되지 않았고,
렌더링 흐름이 훨씬 안정적으로 동작했습니다.
아쉬운 점 및 개선 방향
차트의 기본 시각화 안정성에 집중하다 보니,
특정 날짜를 클릭했을 때 해당 감정 일기 상세로 바로 이어지는 인터랙션까지는 구현하지 못했습니다.
다음 단계에서는 차트가 단순한 정보 전달에 그치지 않고,
사용자의 행동을 자연스럽게 다음 화면으로 연결하는 역할까지 수행하도록
인터랙션을 확장하여 서비스 경험을 향상시키고 싶습니다.
5-2) 초기 이탈을 줄이기 위한 화면 설계 : 사용자 상태별 UI 분기
문제 상황
홈 화면은 사용자 상태에 따라 전혀 다른 역할을 해야 했습니다.
비로그인 사용자에게는 서비스 소개가 필요했고,
로그인한 사용자에게는 개인화된 경험을 제공해야 했습니다.



특히 로그인은 했지만 아직 감정 일기나 게시글을 작성하지 않은 사용자의 경우,
아무런 정보가 없는 화면을 마주하면
“무엇을 해야 할지 모른 채 이탈할 가능성이 높다”고 판단했습니다.
단순히 데이터를 조건부로 렌더링하는 것이 아니라,
각 상태에서 사용자가 다음으로 취해야 할 행동을 명확히 보여주는 UI 설계가 필요했습니다.
해결 과정
사용자 상태를 다음과 같이 명확히 구분하고,
각 상태마다 화면의 목적을 다르게 정의했습니다.
- 로딩 중: 빈 화면 대신 구조를 예측할 수 있는 UI 제공
- 비로그인 사용자: 서비스 맥락을 이해할 수 있는 랜딩 화면 노출
- 로그인했지만 데이터 없음: 첫 행동을 유도하는 메시지와 CTA 제공
- 데이터 보유 사용자: 활동 요약과 추천 중심의 대시보드 구성
이를 코드 레벨에서도 조건 분기가 한눈에 읽히도록 구성하여,
각 상태에 맞는 UI와 CTA를 다음과 같이 제공했습니다.
// src/components/UserCard.tsx
export default function UserCard() {
const { user, isLoggedIn, logout } = useAuthStore();
const navigate = useNavigate();
return (
<>
{isLoggedIn && user ? (
// 로그인 상태: 프로필 + 마이페이지/로그아웃
<Card className="py-5">
<CardContent className="flex flex-col justify-center items-center w-full gap-4">
<div className="w-full flex gap-6 mb-2">
<Avatar alt="유저 이미지" sx={{ width: 80, height: 80 }} />
<div className="flex flex-col gap-3">
<div className="w-full flex gap-2">
<Tag type="info" label="갭로거" />
{user.isMentor && <Tag type="primary" label="멘토" />}
</div>
<p className="typo-strong text-secondary">{user.name}</p>
<p className="typo-small text-disabled">{user.email}</p>
</div>
</div>
<button onClick={() => navigate('/mypage')}>마이페이지</button>
<button onClick={() => logout()}>로그아웃</button>
</CardContent>
</Card>
) : (
// 비로그인 상태: 로그인 유도
<Card className="py-5">
<CardContent className="flex flex-col justify-center w-full items-center gap-4">
<h3 className="typo-subheading text-secondary">
로그인하고 기록을 시작하세요!
</h3>
<ActionButton size="large" onClick={() => navigate('/login')}>
로그인 / 회원가입
</ActionButton>
</CardContent>
</Card>
)}
</>
);
}
// src/features/Care/ui/RecentCareCard.tsx
export default function RecentCareCard() {
const navigate = useNavigate();
const { user, isLoggedIn } = useAuthStore();
return (
<Card>
<CardHeader className="flex justify-between items-center">
<p className="typo-subheading text-title">감정로그</p>
{emotionLogs.length === 0 ? (
<MoveButton type="primary" onClick={() => navigate('/care')}>
이동하기
</MoveButton>
) : (
<MoreButton onClick={() => navigate('/care')} />
)}
</CardHeader>
<CardContent>
{user && isLoggedIn ? (
// 로그인 + 데이터 있음/없음
<RecentCareContent data={emotionLogs} />
) : (
// 비로그인
<div className="w-full flex gap-4 p-4 border-0 border-l-4 border-l-primary">
<SmilePlus className="text-primary" />
<div>
<p className="typo-text text-secondary">
당신의 하루, 감정과 함께 기록해보세요!
</p>
<p className="typo-small text-main">
감정 일기 작성은 로그인이 필요합니다.
</p>
</div>
</div>
)}
</CardContent>
</Card>
);
}
// RecentCareContent 내부: 데이터 없음 시 작성 유도
function RecentCareContent({ data }: { data: EmotionLog[] }) {
if (data.length === 0) {
return (
<div className="w-full flex gap-4 p-4 border-0 border-l-4 border-l-primary">
<SmilePlus className="text-primary" />
<div>
<p className="typo-text text-secondary">
아직 작성한 감정 일기가 없습니다.
</p>
<p className="typo-small text-main">
지금부터 기록을 시작하면, 나만의 감정 히스토리가 만들어져요.
</p>
</div>
</div>
);
}
const selectedData = data.slice(0, 3);
return (
<div className="w-full flex flex-col gap-6">
{selectedData.map((item) => (
<div key={item.careId} onClick={() => navigate('/care')}>
<Tag type={emotionColorMap[item.emotion]} label={item.emotion} />
<p className="typo-strong text-title">{item.title}</p>
<p className="typo-small text-main line-clamp-2">{item.content}</p>
</div>
))}
</div>
);
}
// src/features/Home/HomeAside.tsx
export default function HomeAside() {
return (
<>
<UserCard /> {/* 비로그인 vs 로그인 분기 */}
<TopReviewCard /> {/* 데이터 있을 시에만 표시 */}
<RecentCareCard /> {/* 상태별 다른 UI 표시 */}
<ServiceInfoCard /> {/* 항상 표시 */}
</>
);
}
이 과정을 통해 각 기능별 화면을
“정보를 보여주는 화면”이 아니라
사용자의 상태에 따라 역할이 달라지는 진입 지점으로 정의할 수 있었습니다.
아쉬운 점 및 개선 방향
현재 구조에서는 모든 상태 분기 로직이 각 기능별 컴포넌트 내부에 집중되어 있습니다.
향후 사용자 유형이나 조건이 추가될 경우 컴포넌트의 책임이 과도하게 커질 가능성이 있습니다.
다음 단계에서는 다음과 같은 개선을 고려하고 있습니다.
- 사용자 상태를 명확히 표현하는 UserViewState와 같은 파생 상태 도입
- 상태 판단 로직을 커스텀 훅으로 분리하여 UI 컴포넌트의 책임 축소
- 홈 화면을 “상태 선택 → 화면 렌더링” 구조로 단순화
이를 통해 조건문이 늘어나는 컴포넌트가 아니라,
의도가 드러나는 상태 기반 UI 구조로 발전할 수 있을 것이라 생각합니다.
6. 프로젝트 회고
6-1) Keep - 계속 유지하고 싶은 점
(1) 프로젝트 초기 설계에 시간을 충분히 투자한 점
프로젝트 초반에 Figma 디자인을 기준으로 Tailwind CSS 설정, 공통 색상 토큰, TypeScript 타입 구조를 먼저 정리했습니다.
동시에 컴포넌트 네이밍 규칙과 폴더 구조를 팀 내에서 합의하여,
각자 작업하더라도 코드 스타일과 UI 결과물이 크게 흔들리지 않도록 했습니다.
팀 작업 환경이 각자 다른 지방에서 온라인으로 진행되었기 때문에,
이러한 초기 합의는 소통 비용을 줄이는 데 큰 도움이 되었습니다.
또한 다른 팀원이 작성한 코드를 빠르게 이해하고 이어서 작업할 수 있는 기반이 되었습니다.
이 경험을 통해 초기 설계를 탄탄하게 할수록 개인의 개발 생산성뿐 아니라
팀 전체의 협업 효율과 결과물의 완성도를 함께 끌어올릴 수 있다는 점을 배울 수 있었습니다.
(2) 공통 컴포넌트 중심의 UI 설계
프로젝트 전반에서 반복적으로 사용되는 UI 패턴을 공통 컴포넌트로 정리하여,
전체 코드 기준 약 50% 이상의 재사용률을 확보했습니다.
공통 컴포넌트 구조를 기반으로 각 팀원이 서로 다른 기능을 병렬로 개발하더라도,
UI 구조와 코드 스타일이 크게 흔들리지 않았습니다.
특히 디자인 수정이나 정책 변경이 발생했을 때, 개별 화면을 수정하는 대신
공통 컴포넌트 단위에서 일괄 대응할 수 있어 유지보수 비용을 줄일 수 있었습니다.
(3) 예측 가능한 상태 관리와 데이터 흐름 설계
감정 일기 기능을 기획하고 개발하며 사용자 데이터를 단순히 나열하는 것이 아니라,
의미 있는 흐름으로 시각화하는 경험을 집중적으로 쌓을 수 있었습니다.
어떤 유형의 그래프가 감정 변화에 가장 적합한지,
데이터의 범위를 어디까지 노출하는 것이 과도하지 않은지 등을 고민하며
시각화는 구현보다 의사결정의 영역에 가깝다는 점을 체감했습니다.
이 과정에서 프론트엔드 개발자는 데이터를 그대로 전달하는 역할이 아니라,
정보를 해석 가능한 형태로 가공하는 책임을 가진다는 인식을 갖게 되었습니다.
또한 ApexCharts 활용과 useMemo를 통한 React 최적화는
이전 프로젝트에서 회고로만 정리해 두었던 기술이었는데,
이번 프로젝트에서는 필요성을 스스로 판단하고 즉시 적용할 수 있었습니다.
이를 통해 단순히 “알고 있는 기술”이 아니라
상황에 맞게 꺼내 쓸 수 있는 경험 기반의 기술로 전환되고 있음을 느낄 수 있었습니다.
6-2) Problem - 아쉬웠던 점
(1) 커뮤니티 서비스 UX 디테일에 대한 고려 부족
첫 커뮤니티 서비스 개발이다 보니,
게시글 작성·조회와 같은 핵심 기능 구현에 우선적으로 집중하게 되었습니다.
그 과정에서 스크롤 위치 유지, 버튼 클릭 시 피드백, 호버 상태와 같은
세부적인 UX 요소를 충분히 고려하지 못한 한계가 있었습니다.
기능 자체는 정상적으로 동작했지만,
사용자가 연속적으로 콘텐츠를 탐색하거나 상호작용하는 흐름에서는
작은 불편이 누적될 수 있겠다는 점을 개발 이후에 인식하게 되었습니다.
특히 커뮤니티 서비스에서는 이러한 미세한 인터랙션 차이가
체감 품질과 서비스 신뢰도에 직접적인 영향을 준다는 점을 체감했습니다.
이 경험을 통해 프론트엔드 개발에서의 완성도는
“기능이 동작하는지”를 넘어 “사용자가 어떻게 느끼는지”까지 포함된다는 것을 배웠습니다.
이후 프로젝트에서는 기능 구현 단계에서부터
UX 디테일을 체크리스트 형태로 함께 점검하는 방식으로 개선하고자 합니다.
(2) 컴포넌트 책임 경계의 모호함
공통 컴포넌트 재사용 자체는 효과적이었지만,
하지만 컴포넌트를 여러 화면에서 조합해 사용하다 보니,
표현과 상태 처리의 책임이 명확히 분리되지 않은 채 확장되었습니다.
초기에는 재사용을 우선하다 보니,
컴포넌트 내부에서 데이터 가공, 조건 분기, UI 표현이 함께 처리되는 구조가 되었고,
기능이 추가될수록 컴포넌트의 역할을 한눈에 파악하기 어려워졌습니다.
그 결과, 수정 시 의도치 않은 사이드 이펙트를 고려해야 하는 지점도 늘어났습니다.
이 경험을 통해 재사용성 확보와 유지보수성은 동일한 목표가 아니며,
공통 컴포넌트일수록 책임을 더 엄격하게 나누는 설계 관점이 필요하다는 점을 인식하게 되었습니다.
이후에는 표현 중심 컴포넌트와 상태를 관리하는 컨테이너를 분리하는 방향이
더 적절하다는 기준을 갖게 되었습니다
(3) 팀장으로서 원격 협업 환경에서의 리딩 한계
공모전 특성상 서류 통과 이후 바로 프로젝트 발표가 진행되어
서비스를 유지·개선할 수 있는 시간이 제한적이었습니다.
또한 모든 팀원이 취업 준비를 병행하고 있어,
비대면 환경에서 각자의 작업 시간과 리듬이 서로 다른 상황이었습니다.
이로 인해 작업 진행 상황이 실시간으로 공유되지 않는 경우가 있었고,
기능 단위의 의사결정이나 우선순위 조율이 늦어지는 순간도 발생했습니다.
결과적으로 기술적인 난이도보다는,
팀장으로서 작업 기준과 의사결정 흐름을 더 명확히 제시하지 못한 점이
협업 효율에 영향을 주었다고 느꼈습니다.
이 경험을 통해 단순히 작업을 분배하는 역할이 아니라,
공유 기준, 결정 시점, 커뮤니케이션 규칙을 명확히 만드는 것이
팀장의 중요한 책임이라는 점을 배웠습니다.
다음 프로젝트에서는 초기 단계에서 더 명확한 작업 가이드와 합의 구조를
먼저 설계하는 방향으로 개선하고자 합니다.
6-3) Try - 다음에 시도할 점
(1) 데이터 조회 및 필터링 로직에 대한 이해 확장
검색·필터링과 같은 기본적인 데이터 조회 기능을 구현하며,
단순한 프론트엔드 상태 처리만으로는 성능과 확장성에 한계가 있다는 점을 느꼈습니다.
특히 데이터 양이 늘어날수록 필터링 기준, 상태 관리 방식, API 설계에 따라
사용자 경험과 성능 차이가 크게 발생할 수 있음을 체감했습니다.
앞으로는 클라이언트 사이드 상태 관리뿐 아니라,
백엔드와의 역할 분리를 고려한 데이터 조회 구조를 학습하고자 합니다.
필요한 경우 서버 사이드 렌더링(SSR)이나 쿼리 단위 캐싱 전략 등을 함께 고민하며,
프론트엔드 개발자로서 데이터 흐름을 이해하고 설계할 수 있는 기준을 갖추는 것을 목표로 합니다.
(2) UX 개선을 위한 벤치마킹과 기록 중심의 학습 방식 도입
다음 프로젝트에서는 기능 구현 이후에 UX를 보완하는 방식이 아니라,
초기 설계 단계부터 실제 서비스의 UX 패턴을 참고하며 화면 흐름과 인터랙션을 함께 설계해보고자 합니다.
스크롤 처리, 화면 전환 시 사용자 피드백, 버튼 상태 변화 등
자주 사용되는 인터랙션 패턴을 사전에 정리하고,
이를 컴포넌트 설계와 UI 구현에 의도적으로 반영하는 연습을 진행할 계획입니다.
단순한 구현 경험에 그치지 않고,
“왜 이 UX가 필요한지”, “어떤 상황에서 사용자에게 도움이 되는지” 를
기록으로 남기는 습관을 통해 UX를 함께 고려할 수 있는 프론트엔드 개발자로 성장하고자 합니다.
(3) 공통 컴포넌트 구조에 대한 설계 역량 강화
공통 컴포넌트를 활용한 재사용 경험은 개발 효율을 높이는 데 효과적이었지만,
동시에 컴포넌트 책임 분리와 확장성에 대한 설계 고민이 필요하다는 점도 함께 느꼈습니다.
다음 프로젝트에서는 표현과 상태의 책임을 명확히 나누는 구조를 의식적으로 설계하고,
Compound Component 패턴이나 역할 중심 컴포넌트 설계 방식을 적용해보고자 합니다.
이를 통해 재사용성과 유지보수성을 동시에 고려하는
프론트엔드 컴포넌트 설계 역량을 강화하는 것을 목표로 합니다.
마치며
갭로그 프로젝트에서 가장 아쉬웠던 부분은
데이터 조회와 검색·필터링과 같은 기능을 충분히 깊게 다뤄보지 못했다는 점입니다.
특히 쿼리 구조 설계나 URL 상태를 활용한 검색,
서버 사이드 렌더링(SSR)을 통한 데이터 전달 흐름을 실험해보지 못한 점이 아쉬움으로 남았습니다.
그래서 이러한 아쉬움은 이후 트리토리 프로젝트에서 의식적으로 보완하고자 했습니다.
Next.js를 활용한 서버 사이드 렌더링, 검색/필터링 관련 API를 백엔드와 협업하여 설계하는 방식으로
데이터 조회 흐름과 상태 관리에 대한 경험을 쌓을 수 있었습니다.
해당 내용은 아래 트리토리 회고 링크에서 더 자세히 다루었습니다.
[프로젝트 회고] 크리스마스 트리 꾸미기 서비스, 트리토리
들어가며. 이번 회고는 2025년 크리스마스 시즌에 운영을 목표로 개발했던 트리토리 프로젝트에 대한 기록이며약 2개월간 5명의 팀원들과 함께 완성한 프로젝트입니다. 팀장으로 프로젝트를 이
jignonne.tistory.com
한편, 공통 컴포넌트와 디자인 시스템 설계에 대한 아쉬움은 이후에도 계속 고민하고 있는 주제입니다.
관련 내용을 다룬 토스 기술 블로그 글 등 현업 선배들의 전략과 고민을 참고하며 학습을 이어가고 있고,
아직 완전히 체화되지는 않았지만 의식적으로 이해하고 적용해보려 노력하고 있습니다.
디자인 시스템 다시 생각해보기
"어떻게 하면 더 많은 팀이 우리 시스템을 잘 사용하게 할 수 있을까?" TDS가 이 질문에 답하기 위해 고민했던 과정을 공유합니다.
toss.tech
여담이지만, 갭로그는 개인적으로 가장 아쉬움이 많이 남는 프로젝트이기도 합니다.
공모전 진행 방식상 서류 합격 이후 바로 최종 발표까지 이어지는 일정이었고,
모든 팀원이 취업 준비를 병행하고 있었기 때문에
기술적인 선택지를 충분히 탐색하거나 서비스를 단계적으로 발전시킬 시간적 여유가 제한적이었습니다.
그럼에도 불구하고 제한된 시간과 환경 속에서
각자의 역할을 책임감 있게 수행해준 팀원들에게 감사한 마음이 큽니다.
완벽하지는 않았지만,
짧은 기간 안에서도 설계, 협업, 의사결정의 중요성을 체감할 수 있었던 프로젝트로 기억에 남을 것 같습니다.
개인 github 링크
jung-jinyoung - Overview
jung-jinyoung has 17 repositories available. Follow their code on GitHub.
github.com
'my projects > 2025' 카테고리의 다른 글
| [프로젝트 회고] 관광데이터 활용 공모전 참여 서비스, 섬띵(SumThing) (0) | 2026.02.24 |
|---|---|
| [프로젝트 회고] 교육 공공데이터 분석활용대회 참여 서비스, 찾-다(Chat-da) (0) | 2026.02.23 |
| [프로젝트 회고] 크리스마스 트리 꾸미기 서비스, 트리토리 (2) | 2026.01.30 |