[TIL] React + TypeScript로 실패 분석 앱 만들기🔍

지현·2026년 7월 5일
post-thumbnail

1. 왜 만들었나 ?

나는 항상 벼락치기를 한다.
시험 날이 되면 매번 후회한다. "아 미리 할걸..."

근데 이게 왜 반복되는지 궁금했다.
그냥 의지력 부족인건지, 아니면 다른 이유가 있는건지.

그래서 실패를 기록하고 분석하는 앱을 만들어봤다.

처음에는 다른 사람들과 비교하는 기능도 기획했는데 결국 빼버렸다.
남들과 경쟁하고 싶은 게 아니라 그냥 어제의 나보다 나아지고 싶었기 때문이다.

그래서 이 앱의 핵심은 하나다.
"나는 언제, 왜 포기하는가"


2. 어떤 앱인가 ?

작심삼일 수사대는 목표 달성 과정에서 실패 원인을 기록하고, AI가 탐정처럼 반복되는 습관의 패턴을 분석해주는 서비스다.

기존 습관 앱들은 성공을 기록하는 데 집중하는데 이 앱은 반대로 실패에 집중했다.


3. 기술 스택

  • Language: TypeScript
  • Framework: React
  • Styling: Tailwind CSS
  • State Management: Zustand
  • HTTP: Axios
  • 차트: recharts
  • 달력: react-calendar
  • 백엔드: FastAPI + MongoDB
  • AI: Gemini API
  • 배포: Vercel + Azure

4. 주요 기능

1. 카카오 로그인

  • 로그인은 카카오 로그인 API 를 사용했고, JWT 토큰을 이용해 사용자 인증처리를 구현했다.

2. 목표 생성 및 기록

  • 목표를 생성하고 매일 성공/실패를 체크하는 기능이다.
    실패하면 이유를 입력하면 Gemini API가 자동으로 카테고리를 분류해준다.

3. 기록 달력

  • react-calendar를 커스텀해서 날짜별로 성공/실패/혼합 동그라미를 표시했다.

4. 분석 대시보드

  • recharts로 요일별 실패율 바 차트와 실패 원인 도넛 차트를 구현했다.

5. AI 수사 브리핑

  • 홈: 최근 7일 실패 기록 기반 동기부여 메시지

  • 분석: 요일별 실패율 + 실패 키워드 기반 인사이트

하루에 한 번만 생성하고 DB에 캐싱해서 API 비용을 절약했다.

6. 다크모드 / 반응형

  • CSS 변수를 재정의하는 방식으로 다크모드를 구현했다.
.dark {
    --color-bg-primary: #1f1b18;
    --color-text-primary: #f5f1eb;
}
  • 모바일에서는 하단 네비바로 전환했다.

5. Zustand로 전역 상태 관리

로그인 후 accessToken, nickname, theme을 어디서든 꺼내 쓸 수 있어야 했다.
Zustand를 사용해서 전역으로 관리했다.

import { create } from "zustand";
import { persist } from "zustand/middleware";

interface AuthStore {
    accessToken: string | null;
    nickname: string | null;
    theme: string | null;
    setAuth: (accessToken: string, nickname: string, theme: string) => void;
    clearAuth: () => void;
}

export const useAuthStore = create<AuthStore>()(
    persist(
        (set) => ({
            accessToken: null,
            nickname: null,
            theme: null,
            setAuth: (accessToken, nickname, theme) =>
                set({ accessToken, nickname, theme }),
            clearAuth: () =>
                set({ accessToken: null, nickname: null, theme: null }),
        }),
        {
            name: "auth-storage", // localStorage 키 이름
        },
    ),
);

persist 미들웨어를 사용해서 새로고침해도 로그인 상태가 유지되게 했다.
Redux보다 코드가 훨씬 간결해서 소규모 프로젝트에 적합했다.


6. react-calendar 커스텀

react-calendar 기본 스타일이 너무 밋밋해서 전부 커스텀했다.

Tailwind를 쓰고 있었는데 react-calendar 기본 스타일을 덮어쓰려면 별도 CSS 파일이 필요했다.

날짜 숫자가 abbr 태그로 감싸져 있어서 동그라미를 표시하려면 abbr 에 스타일을 적용해야 한다는 걸 한참 헤매다 알았다.

/* 성공한 날 → 초록 동그라미 */
.react-calendar__tile.tile-success abbr {
    display: inline-flex;
    justify-content: center;
    align-items: center;
    width: 36px;
    height: 36px;
    border-radius: 50%;
    background-color: #4caf50;
    color: white;
}

/* 실패한 날 → 빨간 동그라미 */
.react-calendar__tile.tile-fail abbr {
    background-color: #f44336;
}

/* 성공 + 실패 혼합 → 반반 동그라미 */
.react-calendar__tile.tile-partial abbr {
    background: linear-gradient(90deg, #4caf50 50%, #f44336 50%);
}

tileClassName prop으로 날짜마다 클래스를 붙여서 구현했다.

const tileClassName = ({ date }: { date: Date }) => {
    const status = getDateStatus(dateStr)
    if (status === 'all_success') return 'tile-success'
    if (status === 'all_fail') return 'tile-fail'
    if (status === 'partial') return 'tile-partial'
    return null
}

7. 아직 해결하지 못한 부분

날짜 처리

DB에는 UTC로 저장하고 응답할 때 KST로 변환하는 게 표준이라고 알고 있는데
기록을 등록할 때는 KST 기준으로 날짜를 저장해야 해서 어떤 곳은 UTC, 어떤 곳은 KST로 처리하다 보니 헷갈리는 부분이 많다.

예를 들면 새벽 1시에 기록하면 UTC로는 전날이 되어버리는 문제가 있었다.

// KST 기준으로 오늘 날짜 계산
const getKoreaToday = () => {
    const today = new Date()
    const koreaToday = new Date(today.getTime() + 9 * 60 * 60 * 1000)
    return koreaToday.toISOString().split('T')[0]
}

날짜 처리를 더 명확하게 정리할 필요가 있을 것 같다.


8. 마무리

그동안 배운 것들을 참고해 혼자서 화면을 구현해보니 확실히 어느 정도 감을 잡게 된 것 같다. 수업 시간에 이해가 되지 않았던 부분들도 직접 부딪혀보니 훨씬 잘 이해됐다.

특히 Tailwind는 처음에는 불편하다고 생각했는데 직접 프로젝트에 적용해보니 훨씬 편하게 느껴졌다.

아직 계획한 기능을 다 구현하지 못했고, 데이터 로딩도 느리고 사용자 경험이 불편한 부분도 많다. 앞으로 찬찬히 개선해나갈 예정이다!

0개의 댓글