- 코드잇 스프린트 프론트엔드 18기 과정에서 2주간 진행한 팀 프로젝트
- 배우지 않은 기술과 외부 라이브러리 사용을 최소화하고 가능한 직접 구현하며 학습한 기술들의 숙련도 향상을 목표로 진행
- UI : React, Styled components
- Routing : React Router
- Network : Axios
- Fetch API를 사용해서 공통 네트워크 모듈을 개발하는 대신 팀원들에게 더 익숙한 axios 사용
- Build Tool : Vite
- Deloyment : Vercel
- 향후 학습할 Next.js와 관련된 배포 환경을 미리 체험해 보기 위해 Vercel로 배포 진행
GitHub repository의 branch ruleset 설정
- 목표 : 아직 git에 익숙하지 않은 팀원들이 부담 없이 개발에 집중할 수 있는 환경 설정
- 활동
main
및develop
branch에 direct push, force push, branch 삭제 금지 설정- PR 병합을 위한 approve 조건 설정 (최소 2명)
- 아직 개발에 익숙하지 않은 팀원들의 코드를 안전하게 병합하고, 다른 팀원들에게 코드 리뷰 경험을 장려하기 위함
- PR 생성 시 자동으로 reviewer를 추가하는 'auto assign app'을 설정해서 리뷰 요청 자동화
- 성과
GitHub의 issue tracking 기능을 활용한 프로젝트 일정 관리
- 목표 : 짧은 기간 안에 프로젝트를 완성하기 위해 팀원 별로 담당한 작업의 진행 현황과 병목 지점을 파악하고 대처
- 활동
- GitHub issue에 개발할 action item을 등록
- GitHub project board에 팀원 별로 개발 진행 현황 파악
- 성과 : 매일 팀 미팅을 진행하면서 project board의 작업 현황을 바탕으로 개발 일정을 유동적으로 조정할 수 있었음
GitHub template을 통한 일관된 문서 작성
- 목표 : Issue 및 PR을 생성할 때 다른 팀원들이 작업 내용을 쉽게 이해할 수 있는 항목들로 본문을 작성할 수 있도록 유도
- 활동 :
- 일관된 형식으로 issue를 작성하기 위한 issue template 설정
- 일관된 형식으로 PR을 작성하기 위한 PR template 설정
- 성과
- Issue 및 관련된 PR의 실제 작업 내용을 살펴보지 않아도 어떤 작업을 진행했고 어떤 결과물이 나왔는지 쉽게 파악 가능
- 실제 개발에 들어가기 전에 해야 할 작업을 간략하게 정리하는 시간을 갖게 되어 요구사항 이해도를 높이는 데에 도움
공통 컴포넌트 개발 (관련 issue)
- 목표 : 팀원들이 각자 맡은 화면을 개발할 때 활용할 수 있는 공통 컴포넌트 개발
- 활동
- 성과
- 팀원들이 각자 맡은 화면을 빠르게 개발할 수 있었음
- 재사용성 및 확장성을 고려한 공통 컴포넌트 설계 경험
React portal을 활용하여 컴포넌트를 별도의 layer에 render (관련 PR)
- Portal : React component tree 구조는 유지하면서도 DOM tree 상에 임의의 위치에 component를 render 해 주는 것
- Dropdown, Modal, Popover 등 화면 전체를 덮어야 하는 component를 portal을 활용해서 별도의 layer에 render
- 구현 방식
- React Context API를 활용해서 component를 portal로 render할 수 있는 scope를 제공하는
PortalProvider
component 구현 - Portal로 rendering할 요소를 결정하는
Portal
component 구현 - Portal로 rendering할 요소의 rendering 조건을 결정하는
usePortal()
custom hook 구현 - Portal로 rendering한 component의 mount/unmount를 animation의 시작/끝 시점과 동기화 시키기 위한
useAnimatedPortal()
custom hook 구현
- React Context API를 활용해서 component를 portal로 render할 수 있는 scope를 제공하는
- 구현 예시 :
Modal
componentModal
component를Portal
component로 감싸서 구현Modal
을 animation과 함께 열고 닫기 위해useAnimatedPortal()
custom hook을 활용한useModal()
custom hook 구현
Component를 animation 종료 후 unmount 하는 custom hook 구현 (source code)
- React는 component가 화면에서 사라지는 방식을 조건부 rendering으로 구현
- 닫는 animation을 추가하려면 animation이 동작하는 동안에는 component가 mount 되어 있어야 하고, animation이 끝난 뒤 unmount 해야 함
- 이것을 구현하기 위해 component에 두 가지 상태 필요
- Mount or unmount
- Show or hide (animation)
- 위 두 가지 상태를 관리하는
useAnimatedMount()
custom hook 구현-
useState()
를 두 번 사용해서 두 가지 상탯값 관리function useAnimatedMount() { const [isMount, setMount] = useState(false); const [isOpen, setOpen] = useState(false); ... }
-
Component가 animation과 함께 나타나고 사라지는 코드를 추상화한
setShows
setter 구현function useAnimatedMount() { ... const setShows = (shows) => { if (shows) { // `true`를 전달하면 component를 mount하고 열리는 animation 시작 setMount(true); setOpen(true); } else { // `false`를 전달하면 닫는 animation 시작 // 이 때, `isMount`는 `true`로 component가 mount된 상태 setOpen(false); } }; ... }
-
닫는 animation이 종료되면 component를 unmount 시키기 위한
onAnimatedEnd
handler 구현function useAnimatedMount() { ... const onAnimationEnd = () => { // Open animation이 종료되는 경우는 무시 if (isOpen) return; setMount(false); }; ... }
-
이 custom hook은 네 가지 값을 반환
function useAnimatedMount() { ... return { isMount, isOpen, setShows, onAnimationEnd }; }
isMount
: Component의 mount/unmount 제어 (조건부 rendering)isOpen
: Component의 open/close animation 제어setShows
: Component가 animation과 함께 나타나고 사라지는 코드 추상화onAnimationEnd
: Component에서 animation이 종료되었을 때 unmount 시키기 위한 handler
-
Skeleton loading animation 구현 (관련 PR)
- Image loading 중 placeholder로 사용할 수 있는
SkeletonLoading
component 구현 - Background에 linear gradient를 넣고, gradient를 좌에서 우 방향으로 animate 시켜서 loading animation 구현
const StyledSkeletonLoading = styled.div` width: 100%; height: 100%; background: linear-gradient( to left, ${Colors.gray(200)} 40%, white 50%, ${Colors.gray(200)} 60% ); background-size: 300% 100%; background-repeat: no-repeat; animation: ${({ $isLoading }) => ($isLoading ? LoadingAnimation : "none")} 2s infinite; `;
Avatar
component 등 loading 중 placeholder를 보여줘야 하는 곳에서 활용 (source code)<StyledAvatar $size={size} $color={color}> <SkeletonLoading isLoading={isLoading}> <img src={source ?? defaultAvatarImage} alt="사용자 사진" onLoad={handleImageLoad} /> </SkeletonLoading> </StyledAvatar>
IntersectionObserver
를 활용한 무한 스크롤 구현 (관련 PR)
- 구현 방식
- 서버에 첫 번째 page의 데이터를 요청하고 render
- List의 끝까지 스크롤하면 서버에 다음 page의 데이터를 추가 요청
- 다음 page 데이터를
useState
가 반환하는 setter를 통해 이전 state에 이어붙이고 component를 re-render
IntersectionObserver
API 활용- List 맨 아래에 observing을 위한
<div>
요소를 추가 - 이 요소를
IntersectionObserver
가 observe - 요소가 viewport에 들어오거나 나갈 때마다 callback 실행
- Observer callback으로 받는
IntersectionObserverEntry
의isIntersecting
값이true
일 때 다음 page 데이터 요청
- List 맨 아래에 observing을 위한
- React component에서 사용하기 위해
useIntersectionObserver
custom hook으로 구현
matchMedia()
method를 활용하여 JavaScript에서 media query matching 감지 (source code)
- JavaScript에서 media query를 감지할 때
matchMedia()
method를 사용할 수 있음 - 이 method를 React component에서 사용하기 위한 custom hook 구현
- Desktop, tablet, mobile size 변화를 감지하기 위한
useMedia()
custom hook 구현 예시-
각 size 별로
matchMedia(queryString)
을 실행하여MediaQueryList
생성function useMedia() { const desktop = useRef(matchMedia(mediaQueryString.desktop)).current; const tablet = useRef(matchMedia(mediaQueryString.tablet)).current; const mobile = useRef(matchMedia(mediaQueryString.mobile)).current; ... }
- 이 때,
matchMedia()
가 생성하는MediaQueryList
객체는 한 번만 생성하면 됨 - 최초 한 번만
MediaQueryList
객체가 생성되도록useRef()
hook 사용
- 이 때,
-
각 size 별로 활성화 여부를 상탯값으로 관리
function useMedia() { ... const [matches, setMatches] = useState({ isDesktop: desktop.matches, isTablet: tablet.matches, isMobile: mobile.matches, }); ... }
-
useEffect()
안에서MediaQueryList
에change
event를 감지하면 상탯값을 변경해서 component를 re-renderfunction useMedia() { ... useEffect(() => { const handleDesktopMatch = (event) => { ... }; const handleTabletMatch = (event) => { ... }; const handleMobileMatch = (event) => { ... }; desktop.addEventListener("change", handleDesktopMatch); tablet.addEventListener("change", handleTabletMatch); mobile.addEventListener("change", handleMobileMatch); return () => { desktop.removeEventListener("change", handleDesktopMatch); tablet.removeEventListener("change", handleTabletMatch); mobile.removeEventListener("change", handleMobileMatch); }; }, [desktop, tablet, mobile]); }
-
카카오톡 공유하기 기능 개발 (관련 PR)
- KakaoTalk JavaScript API를 연동하고 custom message template를 사용하여 공유하기 기능 개발
- 상용 환경과 개발 환경을 구분하여 JavaScript API key 및 template ID를 환경 변수로 관리
테스트 데이터 관리 페이지 개발 (관련 PR)
-
개발 서버에 주입할 테스트 데이터를 생성, 조회, 삭제 등 관리하기 위한 별도의 페이지 개발
-
팀원들이 편리하게 테스트 데이터를 관리할 수 있도록 하여 API 연동 개발 시 생산성 향상에 기여함
-
배포 환경에 따라 다르게 사용될 값을 환경 변수 파일로 관리
.env.production
: Vite가 production mode에서 사용할 환경 변수.env.development
: Vite가 development mode에서 사용할 환경 변수
-
배포 자동화를 위한 GitHub Actions workflow 작성
- Vercel은 organization repository에 대해 유료 plan을 사용해야 하므로 forked repository를 배포용 repository로 사용
- Upstream repository의
develop
branch에 commit이 push 또는 merge되면 forked repository로 push하는 workflow 작성
- React portal
- Clipboard API
IntersectionObserver
- Animation이 끝났을 때 component를 unmount 시키기
Vercel 배포 시 root 이외 경로에 접근하면 404 error가 발생하는 문제 (관련 PR)
- 문제
- React 프로젝트를 Vercel로 배포한 뒤, root(
/
) 이외의 경로로 접근하면 404 status error가 반환되는 문제
- React 프로젝트를 Vercel로 배포한 뒤, root(
- 원인
- React는 SPA이기 때문에 기본적으로 root HTML 1개만 가짐.
#root
id를 가진 요소에 JavaScript로 UI를 그리는 구조. - Vercel 등 정적 웹사이트 배포 서비스에 React 프로젝트를 배포했을 때, root 이외의 경로로 접근하면 해당 경로의 HTML 파일을 서버에 요청함
- 하지만, React는 단일 HTML 파일만 가지고 있으므로 요청한 경로에 HTML 파일이 없어서 error가 발생한 것
- React app에서 어떤 URL을 요청하면 웹 서버에 요청을 보내는게 아니라, URL 경로에 해당하는 component를 rendering 함
- React는 SPA이기 때문에 기본적으로 root HTML 1개만 가짐.
- 해결
vercel.json
설정 파일에서 root(/
) 이외의 경로로 접근하면 항상 root로 redirect
PR 생성 시 결과물을 바로 확인하기 (관련 PR)
- 목표 : PR을 만들 때마다 Vercel에 배포해서 reviewer가 프로젝트를 실행하지 않고 결과를 확인하여 생산성을 향상
- 시도 : PR이 생성되면 Vercel CLI를 사용해서 source branch를 기준으로 Vercel에 preview 배포하는 GitHub Actions workflow 작성
- 문제 : Forked repository에서 trigger된 pull request event에 의해 실행되는 workflow는 upstream repository에 설정된 secrets variables에 접근할 수 없으므로 workflow를 실행할 수 없음
- 결과 :
- Forked repository를 사용하지 않는 협업 방식을 사용했다면 쉽게 구현할 수 있었음
- 개발 중간에 upstream repository 하나만 사용하는 방식으로 변경하는 것은 투입하는 리소스에 비해 효과는 작을 것으로 판단하여 보류
- 가능하다 하더라도, upstream repository가 조직 계정에 묶여 있어서 Vercel 배포가 어려운 상황임
- HTML/CSS/JavaScript와 React의 기초를 학습하고 있는 상황에서 외부 라이브러리를 최소로 사용하고 가능한 직접 구현해 보면서 숙련도를 높이는 데 집중한 것
- GitHub 설정, Vercel 배포, 카카오톡 공유 템플릿 제작 등 코드만 작성하지 않고 더 넓은 시야를 갖고 프로젝트를 진행한 점
- 개발 초기에 기획 및 요구사항을 제대로 분석하지 않고 바로 개발에 들어간 점
- 그로 인해, 사용자의 관점에서 생각할 시간이 부족해지고 급하게 개발을 진행하면서 디테일한 부분을 놓친 점
- 개발을 시작하기 전에 전체적인 요구사항을 먼저 파악하고 설계하는 과정을 갖는다.
- 완벽하게 설계하기보다 먼저 대략적으로 설계한 다음 세부적인 내용을 지속적으로 개선해 나간다.