Sharepetment 회고
처음으로 구현을 마무리한 프로젝트
구현 작업 4주와 약 한 달간의 리펙토링을 통해서 첫 번째 팀 프로젝트를 마무리했습니다. FE 3명, BE 3명이 함께 팀을 이루어 4주간 기획, 디자인, 구현, 배포를 마무리했고, 이후 한 달 동안 프로젝트를 함께 진행한 FE 팀원 1분과 함께 개선 작업을 진행했습니다.
무엇이 좋았는가?
-
기획과 회의의 중요성 기획과 디자인, 코드 컨벤션과 린트, 노션 및 디스코드를 활용한 팀 워크스페이스 형성 등을 통해 팀 모집과, 개발을 진행 방식을 밑바닥부터 경험할 수 있었습니다. 특히 기획의 중요성을 느꼈습니다. 저희는 약 1주일 정도 기획 회의를 했습니다.. 해당 기간 산업 분야 설정, 아이템, 기능 구현 범위, MVP 구축을 설정했습니다. 이를 통해 팀원 모두 동의하는 개발 범위 설정과 주제 확립을 할 수 있었고 이후 실제 개발에서 근본적인 기획과 아이디어에 대한 의문 없이 작업을 진행할 수 있었습니다. 특히 기획 회의에서 한정된 시간 안에서 무엇을 구현하고, 무엇을 포기할지 아는 것이 중요하다는 것을 배웠습니다. 현재 역량과 주어진 시간 안에서 객관적으로 내가 할 수 있는 일은 어딘지? 어떤 부분이 도전적이고 성장을 도모할 수 있는지 파악해야 했습니다. 저를 포함한 FE 팀원에게 도전적인 부분은 react-query를 통한 서버 state 환경 구축, 이미지 처리 작업, react-hook-form을 활용한 효율적인 form 작업 처리, pwa 도입 등이 저희의 현재 역량에서 도전적이지만, 성공 가능한 작업으로 생각하고 해당 부분을 구현에 도입했습니다. 반면에 BE 팀과의 회의를 통해 socket을 활용한 실시간 채팅, 반려동물 이미지 검증 작업은 시간 안에 구현할 수 없다고 판단하고 프로젝트의 범위를 축소했습니다.
-
프로젝트의 마무리 기획 - 구현 - 배포를 통해 실제 프로그램을 배포할 수 있었습니다. 주어진 시간보다 4일 전에 초기 구현을 마쳤고 약 나 동안은 테스트를 통해 당장 급한 버그를 해결하기 위해 노력했습니다. 이 시간 동안 실제 프로덕트에서 마주친 문제들을 해결했습니다. Clipboard API 동작 문제 해결을 위해 https protocol로 수정했습니다. 파생되는 문제로 FE에서 https protocol을 사용하는 환경에서 서버 측의 http protocol에 접근할 때, 혼합 콘텐츠와 관련된 보안 위험 때문에 브라우저가 이러한 요청을 차단하는 상황이 발생했습니다. 해당 문제 해결을 위해 proxy로 우회하여 서버와 소통하는 방법을 마련해야 했습니다. 이처럼 개발 환경과 실제 운영 환경에 대한 이해와 문제 해결 방법을 찾을 수 있었습니다.
-
코드 리펙토링 짧은 시간 안에 프로젝트를 마무리하기 위해서 우선 실행되면 정상적으로 돌아가는 프로그램을 만들자는 생각으로 작업을 진행했습니다.! 그리고 실제 배포 환경에서 예상대로 동작하는 프로그램에 만족을 느꼈습니다. 하지만 실제 프로젝트 환경은 다음과 같았다고 생각합니다.
댓글 컴포넌트 이중 작성, 페이지 내에서 무분별하게 사용되는 useQuery, 조건문을 통한 동일한 컴포넌트의 반복, 낮은 LightHouse 점수, 영구적인 Access Token 등 돌아는 가지만, 내부 구조는 아름답지 않았습니다. 이를 해결하기 위해 한달 간의 리펙토링 작업을 진행했고 유의미한 성과를 보였습니다.
우선 페이지 내에 동일한 jsx 구문을 컴포넌트로 분리하여 유지 보수를 최적화할 수 있도록 수정했습니다. 또한, 각 페이지별 동일한 형식으로 작성되는 useQuery, useInfinityQuery 훅을 Custom Hook으로 한 번 더 감싸서 재사용성을 높였습니다.
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'; import { useInView } from 'react-intersection-observer'; import { useReadLocalStorage } from 'usehooks-ts'; import { SERVER_URL } from '@/api/url'; type Prop = { enabledValue: string | null; queryKey: | 'guestFeed' | 'hostFeed' | 'myFeed' | 'walkFeedList' | 'walkmateList' | 'walkmateListAdvertise'; fn: (url: string, token?: string | null) => Promise<any>; zip?: string; }; export default function UseInfinityScroll<T>({ enabledValue, queryKey, fn, zip, }: Prop) { const queryClient = useQueryClient(); const accessToken = useReadLocalStorage<string | null>('accessToken'); const { ref, inView } = useInView(); const { data, fetchNextPage, isSuccess, isError, isLoading, refetch } = useInfiniteQuery<T[]>({ queryKey: [queryKey], staleTime: queryKey === 'myFeed' || queryKey === 'walkmateList' || queryKey === 'walkmateListAdvertise' ? 0 : 600000, refetchOnMount: queryKey === 'myFeed' || queryKey === 'walkmateList' || queryKey === 'walkmateListAdvertise' ? 'always' : false, cacheTime: queryKey === 'myFeed' || queryKey === 'walkmateList' || queryKey === 'walkmateListAdvertise' ? 0 : 600000, queryFn: ({ pageParam = 0 }) => { let url = ``; switch (queryKey) { case 'guestFeed': { url = `${SERVER_URL}/feeds/all/list/random?page=${pageParam}&size=10`; break; } case 'hostFeed': { url = `${SERVER_URL}/feeds/list?page=${pageParam}&size=10`; break; } case 'myFeed': { url = `${SERVER_URL}/feeds/my-feed?page=${pageParam}&size=10`; break; } case 'walkFeedList': { url = `${SERVER_URL}/walkmates/my-walks?openFilter=false&&page=${pageParam}&size=10`; break; } case 'walkmateList': { url = `${SERVER_URL}/walkmates/walks?openFilter=false&location=${zip}&page=${pageParam}&size=10`; break; } case 'walkmateListAdvertise': { url = `${SERVER_URL}/walkmates/walks?openFilter=true&location=${zip}&page=${pageParam}&size=10`; break; } } return fn(url, accessToken); }, getNextPageParam: (_, allPages) => { const len = allPages.length; const totalLength = allPages.length; return allPages[totalLength - 1].length === 0 ? undefined : len; }, enabled: queryKey === 'guestFeed' ? enabledValue === null : !!enabledValue, onSuccess: () => { if (queryKey === 'hostFeed') { queryClient.invalidateQueries({ queryKey: ['contextApi'] }); } }, }); return { data, fetchNextPage, isSuccess, isError, isLoading, refetch, ref, inView, }; }또한, div tag를 무분별하게 사용한 것을 시멘틱 태그에 맞추어 수정하였고 필수적인 속성 사용 및 컴포넌트 작업을 통한 코드 최적화를 통해 최종적으로 LightHouse 점수를 60점에서 약 80점으로 끌어올릴 수 있었습니다!
무엇이 아쉬웠는가?
-
운영의 아쉬움 실제 프로덕트를 배포하는 것을 성공적으로 마칠 수 있었지만, 운영을 하지 못한 점이 매우 아쉬웠습니다. post가 1,000개가 작성되었을 때, 10명 이상의 유저가 한 번에 댓글을 작성한다면, 등등 실제 운영 단계에서 마주칠 수 있는 문제들을 다루지 못한 점이 아쉬움으로 남았습니다.
-
기획 및 구현의 아쉬움 구현과 리펙토링을 마친 후에 프로젝트를 되돌아보면서 이도 저도 아닌 프로젝트라고 느꼈습니다. SNS 서비스와 산책 매칭 서비스를 동시에 제공하지만, SNS와 산책 매칭 서비스 모두 한계가 명확했습니다. SNS 서비스지만, 채팅 기능을 제공하지 않고 있습니다. 산책 매칭 서비스이지만 산책 신청자와 산책 모집자를 확인하는 방법, 알림 등의 이후 서비스가 부재했습니다. 산책 페이지이지만, 일반 게시물 페이지와 별다른 차이점이 없었습니다. 매력적인 프로젝트가 되기 위해서는 자신이 해당 프로젝트를 사용할 만큼 매력적인지? 고민했어야 했는데, 이 부분을 간과했다는 것을 알았습니다.
-
코드 리뷰의 부재 FE 팀원 두 분과 함께 작업했지만, 서로의 코드를 리뷰 하는 시간을 가지지 못했습니다. 해당 코드를 main branch 병합 여부를 판단하는 조건은 화면이 디자인에 맞추어 정상적으로 동작하는지로 결정했습니다. 이로 인해 동작은 하지만, 문제가 산재하는 프로젝트로 마무리가 될 수 밖에 없었습니다. 또한, 해당 문제로 재사용성과 최적화를 고려하지 못 결과물이 탄생했다고 느꼈습니다. 다음 프로젝트에서는 코드 리뷰를 적극 도입해야 할 필요성을 느꼈습니다.
마무리
아쉬움이 산적한 프로젝트이지만, 처음으로 돌아가는 프로젝트를 만들었습니다. 기획에서 요구하는 기능을 작성하기 위해 무엇이 필요한지 고민하고 어떻게 구현할지 고민하면서 자바스크립트에 대한 이해와 react 환경에 대한 이해가 깊어졌음을 느꼈습니다. 무엇보다 할 수 있다는 자신감이 이번 프로젝트에서 얻은 가장 큰 요소가 아닌가 싶습니다.!
- 처음으로 구현을 마무리한 프로젝트
- - 무엇이 좋았는가?
- - 무엇이 아쉬웠는가?
- 마무리