WuMo(우리들의 모임) 프로젝트 회고
2023.09.20
Table of Contents
2023.02.16-03.15 약 한 달간의 집중 개발 기간을 마치고 최종발표와 함께 데브코스의 수료식도 함께 진행되었다.
백엔드와 함께 진행하는 프로젝트인 만큼 걱정도 있었지만 좋은 팀원들을 만나게 되어 큰 트러블 없이 프로젝트를 끝마칠 수 있었다.
프로젝트 소개
모임 계획, 추억 관리를 위한 프라이빗 서비스
기획
평소 여행이나 모임을 계획할 때 어떻게 하시나요?
주요 기능
- 모임을 만들어 친구, 가족을 초대한다.
- 후보지를 정하고 의견을 공유한다.
- 마음에 드는 후보지를 일정으로 등록하면 타임라인 형식으로 모임 루트를 계획할 수 있습니다.
- 다녀온 장소의 피드를 작성하여 추억을 기록할 수 있습니다.
- 영수증 기능을 활용하여 정산할 때 도움을 받을 수 있습니다.
- 다른 사람들의 추천 루트를 확인할 수 있고 공유할 수 있습니다.
기술 스택
Chakra-UI, Framer-Motion
지난 프로젝트를 통해 CSS 작업에 소요되는 시간이 많다는 걸 느끼게 되었고 이번 프로젝트에서 구현해야 할 내용이 많았기에 과감하게 Chakra-UI를 채택하였다. styled-components와 함께 사용하여 전체적인 레이아웃과 약간의 수정으로 원하는 디자인을 제작할 수 있었다. 또한 Framer-Motion과 쉽게 조합할 수 있어 애니메이션 효과를 구현할 수 있었다.
Axios, JWT
JWT Token에 대한 내용을 구현하며 이번 기회에 Axios의 장점을 잘 활용했다고 느꼈다. Axios Interceptor를 활용해서 요청, 응답, 에러 상황시 통신이 이뤄지기 이전에 처리를 통해 다른 팀원들이 토큰 관련 고민을 하지 않아도 앱이 동작하도록 구현하였다.
Access Token과 Refresh Token을 활용하여 Access Token 짧은 시간에 만료시키는 방법으로 보안성이 향상했다.
React-Hook-Form
Formik과 React-hook-form 중 후자를 택한 이유는 이전에 학습을 위한 프로젝트를 진행할 때 form을 상태로 관리할 때 내용이 변경될 때마다 re-rendering이 발생하는 문제를 겪어 uncontrolled Input으로 이를 해결할 수 있는 라이브러리를 적용하게 되었다.
Tanstack-Query (React-Query)
기존 useEffect를 활용하여 서버와 통신을 통해 상태값을 관리하는 경우 불편함을 많이 느꼈었다. 꽤 긴 코드를 매번 작성해야 하고 useEffect 안의 함수를 빼기 위해서 useCallback을 또 사용해야 하는 상황이 자주 생겼다. 이와는 다르게 캐싱과 데이터 변경 시 refetch를 쉽게 사용할 수 있다는 것이 해당 라이브러리를 사용한 가장 큰 이유였다. 또한 isLoading, isError와 같은 분기처리로 각 상황에 맞는 처리를 구현하는 것도 훨씬 간단하였다. 결과적으로 코드 가독성이 좋아지고 더 적은 코드로 같은 기능을 구현할 수 있었다.
ETC..
Recoil은 이번 프로젝트에서는 불가피한 상황이 아니라면 Global state 사용을 자제하였다. 모임 생성, 후보지 생성과 같은 각 컴포넌트 간의 props drilling이 일어나는 경우를 효과적으로 해결할 수 있었다.
Vite의 빠른 번들링으로 오래 기다리지 않아도 앱이 실행되어 개발 시간을 단축할 수 있었다.
구현한 기능
- 로그인, 회원가입 기능
- 유저 프로필, 수정 기능
- 나의 모임 목록
고민한 부분들
사용자 인증, 인가
로그인, 회원가입을 구현하며 다른 팀원들의 편의를 위해 v1부터 v3까지 로그인을 구현하게 되었다.
- v1에서는 회원가입 후 로그인이 동작하는 것에 초점을 맞추어 팀원들의 작업이 최대한 딜레이 되지 않도록 노력했다.
- v2에서는 JWT 토큰을 재발행하여 보안을 높이는 작업을 진행하기 위해 Axios Interceptor, 보안 관련 학습을 병행하며 구현했다.
- v3를 진행하게 된 이유는 이번에 가장 깊게 생각한 이슈인 accessToken이 만료되었을 때 재발행하는 로직때문이였다. 초기에는 해당 에러코드를 받게 되면 reissue를 진행하였으나 이 경우 이전에 요청했던 통신 개수만큼 reissue를 진행하는 문제가 발생했다. 이를 해결하기 위해 flag를 설정하고 Promise를 배열에 담아 보관하고 재발행된 토큰을 이용하여 배열에 담긴 요청들을 처리하였다.
let lock = false; let subscribers: ((token: string) => void)[] = []; const subscribeTokenRefresh = (cb: (token: string) => void) => { subscribers.push(cb); }; const onRrefreshed = (token: string) => { subscribers.forEach((cb) => cb(token)); }; const getRefreshToken = async (): Promise<string | void> => { const tokens = localStorage.getItem('tokens'); if (tokens) { try { const response = await axiosInstance.post('/members/reissue', JSON.parse(tokens)); const { accessToken, refreshToken } = response.data; lock = false; onRrefreshed(accessToken); subscribers = []; localStorage.setItem('tokens', JSON.stringify({ accessToken, refreshToken })); return accessToken; } catch (error) { lock = false; subscribers = []; localStorage.removeItem('tokens'); location.replace(ROUTES.LANDING); } } };
UseController 활용
React hook form과 Chakra UI를 함께 사용하며 컴포넌트로 분리하여 재사용성을 높이기 위해 노력하고 리팩토링을 진행했다.
초기 register method를 활용하여 form이 있는 컴포넌트에서 바로 input들을 제어하였으나 관리해야할 Input들이 늘어나자 가독성이 떨어지는 문제가 발생하였다.
- register method 대신 control method를 사용하여 form과 input을 분리할 수 있도록 했다.
- useController 훅을 사용하여 컴포넌트를 분리했다.
- resetField, setError, trigger와 같은 method들과 form에서 관리해야 할 상태 값들을 props로 전달하여 각 input들에서 form의 기능들을 사용할 수 있도록 하였다.
- 결과적으로 73 lines에서 9 lines로 코드를 줄이고 가독성을 높혔다.
React Hook Form과 Tanstack Query 혼용
Tanstack Query와 React hook form을 같이 활용할 때 문제가 발생했다.
- 가져온 데이터의 stale 시간이 짧을 경우, 사용자가 작성한 내용이 초기화되었고 이를 활용하기 위해 useEffect에 dependency를 통해 이를 해결하였다.
useEffect(() => { if (!myProfileInfo) return; setImageValues({ ...imageValues, imageBase64: myProfileInfo.profileImage }); setValue('nickname', myProfileInfo.nickname); }, [myProfileInfo]);
- 프로필 사진 수정 시 한 곳에서 사진의 변화로 모든 변경사항을 표현했다. 이를 처리하기 위해 Data를 상태 값으로 추가하고 각 조건을 고려하여 구분한 뒤 이를 코드로 표현하여 구현했다.
const getImage = async () => { if (myProfileInfo.profileImage === imageValues.imageBase64) return myProfileInfo.profileImage; if (myProfileInfo.profileImage === null && imageValues.imageBase64 !== null) { const imageUrl = await onSubmitImageFile(imageValues.imageFile); return imageUrl; } if (myProfileInfo.profileImage !== null && imageValues.imageBase64 === null) { await deleteImage(myProfileInfo.profileImage); return null; } if (myProfileInfo.profileImage !== null && imageValues.imageBase64 !== null) { await deleteImage(myProfileInfo.profileImage); const imageUrl = await onSubmitImageFile(imageValues.imageFile); return imageUrl; } return myProfileInfo.profileImage; };
프로젝트를 마치고
느낀 점
3번의 스프린트로 힘들 수도 있었지만 한 팀으로 목표로 했던 기능들과 QA를 통해 원하는 결과물을 만들어 냈다는 것에 뿌듯함을 느낀다. 또한 백엔드 교육생분들과 협업을 진행하며 개발자로서 현업에 투입되었을 때 어떠한 과정을 거칠지 조금이나마 경험해 본 것이 이 프로젝트의 목표 중 하나였고 많은 점을 배운 것 같다.
아쉬웠던 점
문서화
문서화의 중요성을 프로젝트 중반 정도에 느끼게 되었다. 백엔드 팀원과 함께 프로젝트를 진행하다 보니 많은 이야기가 오갔고 시간이 지나 두 번 질문하는 경우도 발생했다. 개선을 위해 이후에는 간단하게라도 일지를 작성하였고 본인뿐만 아니라 다른 팀원들의 일지들도 확인하며 서로의 로직에 대한 이해도를 높일 수 있었다. 프로젝트 초반부터 진행했더라면 더 좋았을 내용이기에 다음부터는 습관화하는 노력을 들여야겠다.
최적화
QA를 진행하며 시간이 많이 촉박해졌고 최적화를 잘 진행하지 못하였다. Lighthouse를 통해 확인해 보면 준수한 성능을 보여주지만 여전히 이미지, 폰트 최적화와 같은 부분이 필요하기 때문에 리팩토링을 통해 개선해보고 싶다.
앞으로의 WuMo
회고를 작성하기 이전에도 몇 가지 리팩토링을 진행했다. 비밀번호 관련 버그와 위에 언급했던 폼 관련 컴포넌트를 분리하는 작업을 했다.
앞으로의 리팩토링 계획은 다음과 같다.
- OAuth 구현: 기존 목표는 이메일 인증을 통한 자유로운 회원가입이었으나 OAuth를 통해 진입장벽을 낮게 설정하는 필요성을 느끼게 되었다. 백엔드 팀원이 이미 진행해 준 내용이 있기 때문에 이를 토대로 학습하고 프로젝트에 구현해 볼 계획이다.
- 알림 기능(혹은 채팅 기능): 현재 프로젝트에서는 댓글과 같은 형식으로 의견을 주고받는다. 하지만 어디서 어떤 댓글을 달았는지 확인하려면 직접 들어가야 하기 때문에 사용성을 개선하고자 구현해보려고 한다. 기존 채팅 기능을 구현하고 싶다는 생각을 하였으나 채팅보다 우선되어야 할 기능이라고 느낀다.
- 모임 목록 무한스크롤 구현: 모임 리스트가 많아진다면 로딩 시간이 점점 길어지기 때문에 구현 우선도가 높은 기능이라고 생각된다. Tanstack-Query의 Infinite Queries를 활용하여 이를 구현해 볼 계획이다.
- 이미지, 폰트 최적화: 이미지 사이즈, 이미지 Encode와 같은 부분들을 개선하고 폰트 파일의 용량을 줄이는 방식을 통해 완성도 높은 프로젝트를 완성하고자 한다.
마지막으로 1달간 프로젝트를 위해 좋은 분위기를 유지해 주고 더 좋은 방향이라면 긍정적으로 의견을 받아준 백엔드 팀원들과 책임감 있게 끝까지 애써준 프론트엔드 팀원들에게 감사함을 전하고 싶다.
배포 링크 🔗
FE 깃허브