joooii

[회고] 유레카 부트캠프 프론트엔드 과정: 미니 프로젝트 핏로그 - 프론트엔드 본문

회고

[회고] 유레카 부트캠프 프론트엔드 과정: 미니 프로젝트 핏로그 - 프론트엔드

joooii 2025. 12. 24. 17:14

 
지난번에 미니프로젝트 핏로그의 백엔드 부분이 끝나고, 최종적으로 프론트엔드 작업을 시작하게 되었다.
 

💼 FE Github

GitHub - FitLog-ureca/FE: FitLog FE 레포지토리입니다.

FitLog FE 레포지토리입니다. Contribute to FitLog-ureca/FE development by creating an account on GitHub.

github.com

 
 

📌 주제

하루의 운동 목표를 투두리스트 형태로 계획하고 완료 여부를 기록하며, 운동 기록을 잔디형 시각화로 제공해 성취감을 높이는 운동 관리 웹 프로젝트
 

⚙️ 프론트엔드 기술스택

  • React `^19.2.1`
  • Next.js `^16.0.7`
  • TypeScript `^5`
  • Tailwind css `^4`
  • Redux Toolkit `^2.11.1`
  • Tanstack Query `^5.90.12`
개발환경

 
 

🛠️ 시스템 아키텍처

시스템 아키텍처

 
 

🏃🏻 유저 플로우차트

로그인 -> 메인화면
세부화면 -> 메인화면

 
 

🌆 피그마 - UI 구성

 

 

🗂️ 프로젝트 구조

src
├─ actions 		// 서버액션
├─ api			// API 
├─ app
│  ├─ api		// Next API (운동 시작 여부 판단을 위한)
│  │  └─ todos
│  ├─ login
│  ├─ signup
│  └─ todos
├─ assets		// 이미지 파일
├─ components		// 컴포넌트
│  ├─ auth
│  ├─ main
│  ├─ main-left
│  ├─ main-right
│  ├─ navbar
│  ├─ todos
│  └─ ui
├─ hooks		// 커스텀 훅
├─ lib			// 라이브러리 (tanstack)
│  ├─ interceptor
│  └─ tanstack
├─ store		// 상태관리 (redux toolkit)		
│  └─ redux	
│     └─ features
└─ types		// 타입

 
 

👾 내가 맡은 개발 파트

▪︎ Input, ActionButton, CheckBox 공용 컴포넌트 개발

이전 프로젝트를 진행할 때는 공용 컴포넌트 개발 없이 그때그때 필요한 컴포넌트를 만들어서 나중에 하나의 컴포넌트로 분리해 버리는 방식으로 개발을 했었다.
사실 이전에 해왔던 것이 컴포넌트 재사용이라 생각했었으나, 다른 팀이 한 것을 보니 내가 했던 작업은 사실상 컴포넌트 분리에 가까웠던 것 같았다.
그래서 이번 프로젝트를 진행할 때는 피그마 디자인과 동일하게 자주 사용하는 input, button 같은 공용 컴포넌트는 `clsx` 유틸리티 라이브러리를 사용하여 다른 컴포넌트에서 동일하지만 css 가 약간 다른 컴포넌트에서 사용할 때 조건적으로 css를 적용할 수 있도록 하여 재사용성을 향상시켰다.

InputActionButtonCheckBox

 

▪︎ 로그인, 회원가입 (Access Token, Refresh Token)

백엔드에서 JWT 토큰을 사용하여 로그인 과정을 구현하는 로직을 구현했었기 때문에, 프론트엔드에서도 로그인 기능을 맡아서 작업하였다. 지난 백엔드 시간에는 refresh token을 따로 구현하지 않고, access token만으로 구현했었으나, 이번에 refresh token 백엔드 작업을 수행하여서 프론트엔드에서 토큰 저장하는 작업이 필요했다.
이때 고민했던 것이 token의 저장 위치였다.
결론적으로는 Accss Token은 JS 메모리에, Refresh Token은 HttpOnly Cookies에 저장하여 관리하였다. (트러블슈팅에서 계속)
이에 따라 interceptor와 proxy (=middleware)를 사용하여 로그인 인증이 필요한 도메인에서는 로그인 인증이 되어야 접근할 수 있도록 구현하였다.

로그인 화면회원가입 화면
Refresh Token 쿠키 저장

 

▪︎ 세부화면 (운동 목표 완료 체크, 휴식 관련 타이머 기능)

1) 운동 목표 완료 체크
해당 부분은 투두리스트 체크 기능과 동일하게 작업하였다. 이때 Redux Toolkit을 사용하여 투두 상태를 관리하였다. 

체크 기능투두리스트 체크 상태 관리

 
 
2) 휴식 체크 시에 타이머 기능 
운동 완료 체크 여부에 따라 개별 세트의 휴식 버튼 활성화 여부가 결정되는 기능이며, 휴식 버튼을 누르면 타이머가 활성화되는 기능이다.
이는 개별 세트 완료할 때마다 일정 시간 휴식을 취한 후, 다음 세트를 들어가기 때문에 구현한 기능이다.
 
이때, 체크한 해당 세트의 휴식을 어떻게 기억하고, 시간을 어떻게 기록해야 할지 엄청난 고민에 빠졌었다.
 
도저히 감을 못 잡아서 claude에게 물어본 결과, Redux로 타이머 상태(활성화 여부, 남은 시간, todoId)를 관리하고, 마지막으로 체크한 todoId를 sessionStorage에 저장하여 새로고침 후에도 어떤 운동의 휴식이었는지 복원할 수 있도록 구현하는 방법이 있었다.
 
이를 기반으로 아래와 같이 `timerSlice.ts`를 작성하였다.

import { Timer } from "@/types/timer";
import { createSlice, PayloadAction } from "@reduxjs/toolkit";

// sessionStorage에서 todoId 복원
const getInitialTodoId = (): number | null => {
  if (typeof window === "undefined") return null;
  const saved = sessionStorage.getItem("lastTodoId");
  return saved ? parseInt(saved, 10) : null;
};

const initialState: Timer = {
  isActive: false,
  duration: 0,
  todoId: getInitialTodoId(),
};

const timerSlice = createSlice({
  name: "timer",
  initialState,
  reducers: {
    // 휴식 시작
    startRest(
      state,
      action: PayloadAction<{ duration: number; todoId: number }>
    ) {
      state.isActive = true;
      state.duration = action.payload.duration;
      state.todoId = action.payload.todoId;

      // sessionStorage에 마지막으로 선택된 todoId 저장
      if (typeof window !== "undefined") {
        sessionStorage.setItem("lastTodoId", action.payload.todoId.toString());
      }
    },
    // 휴식 스탑
    stopRest(state) {
      state.isActive = false;
      state.duration = 0;
    },
    // 타이머 초기화
    clearTimer(state) {
      state.isActive = false;
      state.duration = 0;
      state.todoId = null;
      // sessionStorage에서 제거
      if (typeof window !== "undefined") {
        sessionStorage.removeItem("lastTodoId");
      }
    },
  },
});

export const { startRest, stopRest, clearTimer } = timerSlice.actions;
export default timerSlice.reducer;

 

타이머 비활성화타이머 활성화 (휴식 버튼 클릭)휴식 시간 기록
마지막으로 체크한 todoId 저장

 

▪︎ 프로필  수정 기능

이름, 생년월일, 자기소개, 프로필 이미지를 수정할 수 있는 기능을 개발하였다.
프로필 이미지 관련해서는 Base64 형태로 DB에 직접 저장하는 방식으로 구현하였다.
처음 기획했을 때는 AWS S3로 이미지를 관리하고자 하였는데, 비용 대비 효율이 낮다고 판단하여 Base64 형태로 DB에 직접 저장하는 방식이 현실적인 선택이라 판단했다.
DB에 저장할 때도 그대로 저장하는 것이 아니라, 이미지의 최대 크기를 300 * 300px로 제한하였고, JPEG 압축을 적용하여 2MB -> 약 30~70KB 수준으로 감소시켜 관리하였다.
 
이에 관련해서는 아래 기록해 두었다.

[74일차] Spring Boot에서 이미지 파일 관리하기

미니 프로젝트를 진행하면서 프로필 이미지 업로드 / 삭제 기능을 구현하게 되었다.프론트엔드에서는 이미지 파일 관리를 해보았던 경험이 있었으나, Spring Boot에서 이미지 파일을 어떻게 관리

joooii.tistory.com

프로필 수정프로필 조회

 

▪︎ 운동 시작 여부에 따른 세부화면 접근 제어

기존엔 운동 시작 여부에 상관없이 세부화면 (/todos)에 접근할 수 있었다. 이를 위해 API를 구현하기에는 오버 스펙일 것 같다는 생각이 들어서 Next의 API 기능 (`next/server`)을 사용하여 '운동 시작' 버튼을 누르면 쿠키에 `canEnterTodos = true`를 저장하여 true일 때만 접근이 가능하도록 하였다. 또한 '운동 완료' 버튼을 누르면 쿠키에서 canEnterTodos가 삭제되어 세부화면에 접근하지 못하고, 접근 시 메인화면으로 강제 리다이렉트 되도록 구현하였다.

// app/api/todos/start/route.ts 
// 운동 시작시

import { NextResponse } from "next/server";

export async function POST() {
  const res = NextResponse.json({ ok: true });
  res.cookies.set("canEnterTodos", "true", {
    httpOnly: true,
    path: "/",
    sameSite: "lax",
  });
  return res;
}
// app/api/todos/end/route.ts
// 운동 완료시

import { NextResponse } from "next/server";

export async function POST() {
  const res = NextResponse.json({ ok: true });
  res.cookies.set("canEnterTodos", "", { maxAge: 0, path: "/" });
  return res;
}

 

canEnterTodos 저장
start APIend API

 
 

📹 시연영상

 
 

💫 회고 

🌝 좋았던 점

▪︎ 처음으로 팀장을 맡았던 프로젝트
그동안 팀장을 하기에는 리더십이 부족하고, 실력도 부족하다고 생각했어서 쉽게 도전해보지 않았던 직책이다. 그렇기에 팀장을 잘할 수 있을까?라는 고민이 컸다. 
그래서 일정 관리부터 파트 분배까지 팀장의 역할을 최대한 수행하고자 노력하였다. 확실히 팀원으로 참여했을 때와 팀장으로 참여했을 때의 무게감은 차원이 달랐던 것 같다. 그래도 팀원이 잘 도와줘서 프로젝트를 깔끔하게 마무리할 수 있었다.
 
▪︎ 백엔드의 이해
프론트엔드 반에서 진행한 프로젝트였기에 백엔드까지 전부 다 구현해야 했다. 백엔드는 예전 express 제외하고는.. spring boot는 처음이었기 때문에 폴더 구조부터 API 구현 방법, 로그인 처리 등 1부터 시작해서 쉽지 않았다. 그렇기에 강의와 책을 통해 추가 학습을 진행하면서 백엔드에 대한 어느 정도의 감이 생겼다고 생각한다. (남이 볼 땐 모르겠지만)
사실 프론트엔드만 하게 되면 백엔드 코드가 어떻게 구성되는지 알 방법이 없다. 이번 기회에 폴더 구조와 코드를 보는 방법을 익힐 수 있어서 좋았다. 
 
 

🌚 아쉬웠던 점

▪︎ AI 도구의 높은 의존성
이번 프로젝트를 하면서 사실상 백엔드는 AI가 전부 다 구현했다고 .. 해도 할 말이 없다. AI를 적당히 활용하면 득이 되는 것을 알고 있지만, 생각보다 활용을 너무 많이 해서 약간의 실이 된 것 같아 아쉬웠다. 이 의존성을 좀 줄여놔야 득이 되는 데 그게 쉽지가 않다..
 
▪︎ 한정된 기술 스택
해당 부트캠프에서 제시한 기술 스택을 사용해야만 했던 부분이 조금 아쉽다. 예를 들면 Redux나 MyBatis를 사용했어야만 해서 약간의 기술 스택 선택에 있어 자유롭지 못했던 것이 조금 아쉽다. Zustand를 써보고 싶었는데, 이건 종합 프로젝트에 가서 사용해 봐야겠다. 후회는 없다.


마치며 ..

10월 달부터 진행했던 미니 프로젝트가 드디어 성황리에 막을 내렸다. 생각보다 프론트엔드 작업을 하면서 예상치 못한 문제들이 많아서 백엔드 수정을 많이 하게 되었다. 이 과정에서 역시 완벽한 설계란 없구나라는 생각이 들었다.
이번 프로젝트에서 Redux Toolkit과 Tanstack Query를 반드시 사용했어야 해서 이번 기회에 정환님 인프런 강의를 들으면서 초고속으로 사용 방법을 익혔다. 어찌저찌 사용해서 구현을 하긴 했으나, 다음 종합 프로젝트에서는 조금 더 완벽하게 사용해보고 싶다.
그래도 이번 프로젝트를 하면서 백엔드를 좀 더 잘해보고 싶다는 생각이 들었고, 인프런에 나와있는 영한님 무료 강의를 들어보고 종프때는 AI 툴에 적당히 의존하고 싶다 ..
 
이번에 공식문서를 많이 보려고 노력하였고, 이를 기반으로 proxy(middleware)의 사용 방법을 익히니 이해가 더욱 잘 되었고, 공식문서의 중요성을 또 한 번 느꼈다
 
연말 전에 깔끔하게 마무리할 수 있어서 좋다 !
메리 크리스마스이옵니다