2개월간 코드잇 스프린트에 참여하며 '개발자를 위한 투두 리스트 앱'을 기획부터 구현까지 경험했습니다. 프론트엔드 간 협업, JWT 인증 구현, PR 리뷰 등을 통해 한 단계 성장할 수 있었습니다. 본 글은 그 과정을 회고한 글입니다.
이전에 6개월 부트캠프 과정을 수료한 경험이 있었지만, 해당 과정에서는 아쉽게도 다른 프론트엔드 개발자와의 협업 경험을 제대로 쌓지 못했습니다. 당시 백엔드 지원자가 압도적으로 많았고, 최종 프로젝트에서는 프론트엔드 팀원이 초반에 이탈하면서, 사실상 백엔드 개발자들과의 협업에 가까웠습니다
또한, 디자이너가 없는 프로젝트 환경에서는 프론트엔드 개발자가 디자인까지 맡는 경우가 많았기 때문에, 이번에는 전담 디자이너가 있는 프로젝트를 경험해보고 싶다는 욕구도 컸습니다.
이러한 니즈를 모두 충족시켜준 것이 바로 코드잇 스프린트였습니다. 해당 부트캠프에 끌렸던 이유는 다음과 같습니다.
수료 이후 제공되는 혜택도 있었지만, 저에게는 다른 프론트엔드 개발자들과 소통하며 한 단계 성장할 수 있는 기회라는 점이 가장 크게 다가왔습니다.
참여를 위해 AI 면접을 진행했고, 포트폴리오 제출로 별도의 인터뷰는 생략되었습니다. 초반 2주간은 강의 위주의 커리큘럼이 진행되었고, 이후 본격적인 프로젝트를 시작하게 되었습니다.
프로젝트 기획안은 총 두 가지가 제공되었으며, 저는 그중 투두 리스트 앱을 선택했습니다. 다른 하나는 커뮤니티 서비스였는데, 기획력이 상당히 탄탄하지 않은 이상 실제 운영으로 이어가기 어렵겠다는 판단이 들었습니다.
반면, 투두 앱은 혼자서도 사용할 수 있는 서비스로, 주변 지인들의 피드백을 받아가며 발전시킬 수 있을 것 같아 이 주제를 선택하게 되었습니다.
다만 초기 기획안은 너무 범용적이었기 때문에, 팀원들과 논의 끝에 더 명확한 사용자 타깃을 설정하고, 그에 맞는 기능을 추가해보기로 했습니다. 제가 제안한 "개발자를 위한 투두 리스트 앱"이라는 방향성에 팀원들도 동의해, 해당 컨셉으로 기획을 확정하였습니다.
개발자에게 익숙한 GitHub 잔디 스타일의 스트릭 보드 시각화, 그리고 늘 컴퓨터와 붙어 살기 때문에 주기적으로 거북목 주의 알림 기능까지 추가하여 단순한 투두리스트가 아닌 실제로 활용할 만한 도구로 만들고자 했습니다.
1차 기획
위와 같이 컨셉을 최대한 단순하게 정리한 뒤, 팀원들과 함께 기능 명세서를 작성하고 프로젝트 일정을 수립했습니다. 그리고 각자 도전해보고 싶은 기능을 중심으로 역할 분담을 진행했습니다.
기능 명세서
역할 분담
저는 그동안 주로 Session 기반 인증 방식을 사용해왔기 때문에, 이번에는 직접 JWT 인증을 구현해보고자 회원가입/로그인 기능과 Input, Button, Progress Bar 컴포넌트, 목표 상세 페이지, 웹 푸시 알림 기능, 프로플 카드 관련 기능 구현을 맡았습니다.
이후 백엔드 개발자, 디자이너와도 기획 방향을 공유하며 피드백을 주고받았고, 본격적으로 프로젝트를 시작하게 되었습니다.
기능 명세를 마친 후, 본격적인 개발에 앞서 협업 환경을 먼저 구축하였습니다.
이러한 사항들은 대부분 팀원들과 함께 협의하여 결정하였으며, 저는 Jira 설정과 Notion 문서화를 주도적으로 담당하였습니다. 프로젝트 초기 구조 세팅 역시 VSCode의 Live Share 확장 기능을 활용해 팀원들과 함께 실시간으로 구성하였습니다.
Jira
Notion
다만 이 시점에 코드 컨벤션과 관련해 "ESLint와 Prettier, Husky를 도입했으니 코드 스타일은 유연하게 가져가자"는 합의를 하였는데, 지금 돌이켜보면 그 결정이 나중에 일정한 스타일을 유지하는 데 다소 아쉬움으로 남게 되었습니다.
JWT 인증에 대한 개념만 알고 있었던 저는, 가장 먼저 다양한 JWT 인증 구현 사례를 구글링을 통해 수집했습니다. JWT 방식은 Session 기반 인증에 비해 서버의 부담이 적다는 장점이 있지만, 암호화된 토큰이 탈취될 가능성을 완전히 배제할 수 없기 때문에, 보안상의 위험을 최소화하기 위한 방향으로 구현을 시작했습니다.
초기에는 다음과 같은 방식으로 구현했습니다.
secure: false
, httpOnly: true
(로컬 테스트용)secure: true
, httpOnly: true
, sameSite=strict
, CSRF 토큰 사용 (보안 강화)Authorization
헤더에 토큰 포함
하지만 이 과정에서 큰 실수가 하나 있었습니다. Next.js의 서버 환경을 고려하지 않고, 브라우저 메모리에 토큰을 저장했던 것입니다. 이 방식은 클라이언트 사이드에서만 토큰을 사용할 수 있기 때문에, 서버 컴포넌트에서 해당 토큰에 접근할 수 없는 문제가 발생했습니다.
저를 믿고 맡겨주신 팀원분들께 문제를 초기에 인식하지 못한 점이 아쉬웠고, 인증 구현이 계획된 스프린트를 초과하게 되어 많이 죄송했습니다.😭
결국, 로그인 성공 시 응답 헤더로 받은 Access Token을 Server Action을 통해 쿠키에 저장하는 방식으로 변경했습니다. 이때 쿠키 속성은 secure: true
, httpOnly: true
, sameSite=strict
로 설정하여 보안을 강화했습니다.
한편, 이 작업을 진행하면서 "어차피 쿠키를 쓸 거라면 처음부터 서버에서 Access Token도 쿠키로 관리하는 게 더 자연스럽지 않을까?"라는 생각이 들었고, 실제로 멘토님도 그런 방식이 있다고 조언해주셨습니다.
하지만 이미 백엔드 개발자 분께서 Access Token을 응답 헤더로 전달하는 구조로 작업해두신 상태였고, 전체 구조를 바꾸는 데 드는 리소스를 고려해 결국 현재 방식을 유지하기로 결정했습니다.
+) 추가로, Next.js의 middleware를 활용하여 Access Token이 없는 상태에서는 인증이 필요한 페이지에 접근하지 못하도록 구현했습니다.
또 다른 문제는 로컬 환경에서 Refresh Token을 활용한 Access Token 재발급이 되지 않았던 점입니다.
아래는 당시 제가 팀원분들께 보냈던 메시지입니다.
토큰 재발급 관련 메시지
로컬 환경에서는 도메인이 localhost
이다 보니, 보안을 위해 설정한 sameSite=None
속성 때문에 브라우저가 쿠키를 서버에 전송하지 않았습니다.
결과적으로 배포 환경에서는 문제 없이 동작하지만 로컬에서는 로그인이 풀리는 불편한 상황이 반복되었고, 테스트 및 개발에 불편을 주는 원인이 되었습니다.
당시에는 다른 작업들도 동시에 진행하고 있어서 이 문제에 더 이상 시간을 쓸 수 없었지만, 지금 생각해보면 테스트 환경에서는 인증이 없어도 요청이 가능하도록 임시 우회 로직을 두는 것도 방법이었겠다는 아쉬움이 남습니다.
이번 JWT 인증 구현을 통해 클라이언트/서버 환경 분리에 따른 토큰 저장 위치 설계의 중요성과, 보안 설정(sameSite, secure, httpOnly)에 따른 테스트 환경 이슈까지 깊이 있게 다룰 수 있었습니다.
저는 저희 프로젝트에서 사용할 Input, Button, Progress 컴포넌트를 작업했습니다.
Button 컴포넌트 디자인
Button 컴포넌트의 디자인은 위와 같이 디자이너님께서 작업해 주셨고, 다양한 크기와 배경/테두리/텍스트 색상을 지원하기 위해서 cva
라이브러리를 활용했습니다.
아래는 Button 컴포넌트 코드입니다.
import { ButtonHTMLAttributes, ReactNode } from "react";
import { cva, VariantProps } from "class-variance-authority";
import { cn } from "@/lib/cn";
const ButtonVariants = cva(
"flex items-center justify-center px-18 py-8 font-semibold",
{
variants: {
variant: {
solid:
"bg-dark-blue-500 hover:bg-dark-blue-600 active:bg-dark-blue-800 disabled:bg-slate-400 text-white",
outlined:
"border border-dark-blue-500 text-dark-blue-500 hover:border-dark-blue-600 hover:text-dark-blue-600 active:border-dark-blue-800 active:text-dark-blue-800 disabled:border-slate-400 disabled:text-slate-400",
},
shape: {
square: "rounded-xl",
round: "rounded-3xl",
},
size: {
lg: "min-w-291 min-h-48 text-base",
md: "min-w-150 min-h-44 text-base",
sm: "min-w-84 min-h-36 text-sm",
},
},
defaultVariants: {
variant: "solid",
shape: "square",
size: "md",
},
},
);
export interface ButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof ButtonVariants> {
children: ReactNode;
}
export default function Button({
type = "button",
variant,
size,
shape,
className,
children,
disabled,
...props
}: ButtonProps) {
return (
<button
type={type}
className={cn(ButtonVariants({ variant, shape, size }), className)}
aria-disabled={disabled}
disabled={disabled}
{...props}
>
{children}
</button>
);
}
기본 button
태그에 디자인 시스템 스타일만 입히고, 컴포넌트를 사용하는 쪽에서도 클래스를 쉽게 수정할 수 있도록 className
을 props로 받고, 나머지 props 또한 사용해주었습니다.
실제로 프로젝트에서 버튼 컴포넌트의 형태가 미리 정의해뒀던 형태를 벗어나는 경우가 있었고, 그런 경우에 유용하게 활용하였습니다.
그리고 코드를 보시면 tailwind class의 단위가 이상하다고 느껴지실 수 있습니다. 디자이너 분께서 모든 작업을 px
단위로 진행해주셨는데, 이에 저희 팀은 px 단위를 그대로 사용하면 rem
단위로 변환하여 적용해주는 tailwind-preset-px-to-rem
프리셋을 사용했습니다.
const config: Config = {
presets: [pxToRem],
}
덕분에 Tailwind 클래스 작성의 편의성을 누리면서도, 반응형 디자인과 접근성을 강화할 수 있었습니다.
프로젝트 후반부에 가장 집중했던 기능은 웹 푸시 알림 기능이었습니다. 이전 프로젝트에서 MSW를 활용하며 서비스 워커에 대해 공부했었는데, 이때 서비스 워커를 통해 웹 푸시 알림을 구현할 수 있다는 사실을 알게 되어 실제로 적용해보고 싶었기 때문입니다.
먼저 백엔드 개발자와의 협의를 통해 푸시 서버를 직접 구축하는 데에는 최소 2~3개월이 소요된다는 점을 확인했고, 프로젝트 일정상 Firebase Cloud Messaging(FCM) 을 활용하는 방향으로 결정했습니다.
프론트엔드에서는 크게 세 가지 작업을 맡았습니다.
공식 문서와 레퍼런스가 잘 정리되어 있어 1번과 2번은 비교적 수월하게 구현할 수 있었습니다. 하지만 알림 수신이 간헐적으로 동작하는 문제가 발생했고, 이 원인을 파악하는 데 시간이 꽤 걸렸습니다.
Firebase 콘솔에서 직접 푸시 메시지를 보내 테스트했을 때는 알림이 잘 도착했지만, 백엔드 연동 후에는 알림이 수신되지 않는 경우가 종종 발생했습니다. 작성한 코드를 다시 찬찬히 살펴보던 중, 서비스 워커가 백그라운드에서 작동한다는 특성을 떠올렸고, 문제의 원인이 바로 여기에 있다는 것을 알게 되었습니다.
onMessage(messaging, (payload) => {
const notificationTitle =
payload?.notification?.title || "거북목 주의보";
const notificationOptions = {
body: payload.notification?.body,
};
if (Notification.permission === "granted") {
new Notification(notificationTitle, notificationOptions);
}
});
위와 같이 onMessage
와 Notification API
를 함께 사용하여, 앱이 포그라운드 상태일 때도 알림을 직접 생성하도록 구현함으로써 이 문제를 해결했습니다. 결과적으로 포그라운드/백그라운드 여부에 관계없이 알림이 일관적으로 노출되도록 개선할 수 있었습니다.
하지만 연동 이후에도 두 가지 문제가 추가로 발생했습니다.
회원가입 시점에 알림을 허용하지 않은 경우, FCM 토큰이 발급되지 않음
→ 브라우저는 알림 허용 요청을 단 한 번만 노출하며, 사용자가 이를 거부한 경우 개발자가 다시 요청을 띄울 수 없습니다. 따라서 FCM 토큰이 발급되지 않은 경우 거북목 주의보 알림 설정을 켜도 알림 수신이 제대로 되지 않았습니다. 이를 해결하기 위해 토큰 발급 여부를 사용자 정보와 함께 로컬 스토리지에 저장해 두고, 알림 설정 변경 시 토큰 존재 여부를 확인하여 다음과 같이 분기 처리했습니다.
불규칙한 FCM 토큰 재발급 주기로 인한 서버와의 토큰 비동기화 문제
→ 공식 문서에서는 FCM 토큰이 자주 변경되지 않는다고 명시되어 있었지만, 실제 테스트 결과 30분~1시간 사이에도 토큰이 재발급되는 경우가 확인되었습니다. 이로 인해 서버에 저장된 이전 토큰과의 불일치로 알림 수신이 누락되는 문제가 발생했습니다.
이에 따라, 거북목 주의보 알림의 on/off 설정이 변경될 때마다 최신 토큰을 확인해 서버에 재전달하는 로직을 추가했습니다. 이를 통해 항상 최신 토큰을 기반으로 푸시 알림을 수신할 수 있도록 개선했습니다.
거북목 주의보 알림 기능 시연 영상
이 경험을 통해 단순히 기능을 구현하는 것을 넘어, 클라이언트와 서버 간 데이터 동기화의 중요성과 사용자에게 어떤 피드백을 언제, 어떻게 전달해야 하는지에 대해 깊이 고민해볼 수 있었습니다. 특히, 예상치 못한 FCM 토큰 재발급 문제나 알림 권한 거부 상황처럼, 사용자의 행동이나 브라우저 환경에 따라 발생할 수 있는 변수들까지 고려한 설계와 대응이 얼마나 중요한지 체감했습니다. 단순히 기술적인 해결뿐 아니라, 사용자에게 적절한 안내를 제공하여 스스로 문제를 인지하고 해결할 수 있도록 돕는 경험을 설계하는 것이 좋은 UX라는 점도 다시 한 번 느꼈습니다.
기능을 구현하며 기술적인 고민을 많이 했지만, 그와 동시에 협업 방식에 대해서도 많은 시행착오를 겪었습니다. 특히 팀원 대부분이 PR 리뷰를 처음 진행해보았기 때문에, 리뷰 문화나 프로세스를 정립해나가는 과정 자체가 하나의 도전이었던 것 같습니다.
프로젝트 초기에는 리뷰어를 별도로 지정하지 않고, 각자 작업하면서 틈틈이 PR을 확인해 리뷰를 다는 방식으로 진행하기로 합의했습니다. 하지만 시간이 지나면서 리뷰를 자주 하는 사람만 하게 되는 문제가 발생했고, 특히 저희 팀은 2명 이상의 승인 후에만 머지가 가능하도록 규칙을 설정해둔 상태였기 때문에, PR이 빠르게 머지되지 못하고 쌓이는 상황이 생겼습니다.
Problem
Try
이 문제는 2주차 주간 회고에서 'Problem' 항목으로 다루어졌고, 다음과 같은 방식으로 개선을 시도해보기로 했습니다.
이때까지는 CodeRabbit을 사용하고 있었는데, 실제로 사용해보니 몇 가지 한계가 보였습니다.
우선, CodeRabbit의 리뷰어 추천 로직이 지나치게 일관적이었습니다. 예를 들어, A가 PR을 올리면 매번 B와 C가 추천되는 경우가 많았고, 이는 팀원 간 리뷰 부담이 분산되기를 원했던 저희의 의도와는 맞지 않았습니다.
또한, CodeRabbit이 PR 생성 직후 바로 리뷰를 달기 때문에 코드 수정이 잦은 상황에서는 오히려 비효율이 발생했습니다. 리뷰를 작성하는 도중에 코드가 변경되어 리뷰를 다시 해야 하는 경우가 생기면서 생산성이 떨어졌고, 팀원 입장에서는 리뷰에 대한 피로감도 커졌습니다.
이에 따라, 일차적으로는 CodeRabbit의 리뷰 코멘트를 먼저 반영한 후, 직접 리뷰어에게 PR 요청 메시지를 보내는 방식으로 전환해봤습니다.
하지만 일주일 정도 이 방식을 사용해본 결과, CodeRabbit의 리뷰가 프로젝트 전체 맥락을 고려하지 못한다는 한계를 체감했습니다. 접근성 관련 이슈나 문법적인 지적은 도움이 되었지만, 흐름을 파악하지 못한 채 반복적인 설명을 요구하거나, 의미 없는 코멘트를 남기는 경우가 있었습니다.
리뷰 예시 1
리뷰 예시 2
예를 들어, 위 사진과 같이 임시로 작성한 요청 로직에 대해 의미 없는 지적이 달리는 상황도 있었고, 이는 팀원 입장에서 리뷰 코멘트를 확인할 때 혼란과 피로를 유발했습니다. 결국, 팀원 간 커뮤니케이션이 중심이 되어야 한다는 점을 강조하며 CodeRabbit 사용을 중단하자고 제안했고, 팀원들도 이에 공감해주셨습니다.
이후에는 다음과 같은 방식으로 리뷰 프로세스를 재정비했습니다.
이러한 과정을 거치며, 저희 팀은 자동화 도구를 맹신하기보다는, 팀 내부의 맥락을 잘 이해하고 있는 구성원 간의 커뮤니케이션이 더 중요하다는 점을 다시 한번 느끼게 되었습니다. PR 리뷰는 단순히 문법 오류나 버그를 찾는 것을 넘어, 프로젝트의 방향성과 일관성을 유지하기 위해 활용된다는 것 또한 느꼈습니다.
길지 않은 기간이었고, 비교적 간단한 서비스를 만들었다고 생각했는데 돌아보니 그 과정에서 얻은 것이 꽤 많은 것 같습니다.
✅ JWT 인증, 웹 푸시 알림 등 새로운 기능에 도전
여러 번의 시행착오를 겪으며 문제를 해결했고, 그 과정에서 Next.js의 서버 환경, 쿠키의 동작 방식, 브라우저 설정 등 다양한 새로운 개념을 접하고 이해할 수 있었습니다.
✅ PR 리뷰 경험
코드 리뷰와 PR 과정을 통해 코드의 가독성과 유지보수성을 개선하는 방법을 익혔고, 내가 작성한 코드를 논리적으로 설명하며 피드백을 수용하고 반영하는 경험을 할 수 있었습니다. 또한, 다른 사람의 코드를 읽고 의도를 파악하는 데에도 익숙해졌습니다.
✅ 주간 회고를 통한 점검과 개선
이번 글에 모두 담지는 못했지만, 매주 KPT 방식으로 회고를 진행하며 다양한 문제를 인식하고 개선하려는 시도를 꾸준히 이어갔습니다. 문제를 의식적으로 찾아내고 해결 방법을 고민했던 경험은 앞으로의 성장에 중요한 밑거름이 될 것 같습니다.
✅ 더 나은 협업을 위한 환경 만들기
PR 템플릿 작성, 회고 문화 제안, Query Key Factory 패턴 등 팀이 더 효율적으로 작업할 수 있는 환경을 만들기 위해 능동적으로 아이디어를 제안하고 적용해보았습니다. 이는 협업의 질을 높이는 데 큰 도움이 되었고, 앞으로도 꾸준히 실천하고 싶은 부분입니다.
이번 코드잇 스프린트를 통해 다양한 협업 도구와 실제 서비스 개발 경험을 쌓을 수 있었고, 특히 프론트엔드 개발자로서 한 단계 성장할 수 있는 좋은 기회였습니다. 아쉬움도 있었지만, 그만큼 많이 배운 시간이었다고 생각합니다. 앞으로도 이번 경험을 바탕으로 꾸준히 나아가는 개발자가 되겠습니다.