지난 5일간 솔로 프로젝트로 'Coz shopping'을 제작하였다. 사실 나는 리액트를 사용해서 처음 프로젝트 셋팅부터 개발까지 프로젝트를 만들어 본 경험이 없었다. 따라서 이번 솔로프로젝트는 나에겐 새로운 도전이었다.
프로젝트 계획
개발에 들어가기에 앞서 프로젝트의 요구사항을 분석하고 플래닝하는 단계를 가졌다.
애자일 방법론에 대해 학습하고 그 중 스크럼 방식을 채택하여 프로젝트 계획을 짰다. 도구로는 깃허브에서 제공하는 기능을 사용하였다.
사실 계획을 짜는데 있어서 카드를 생성할 때 어느정도로 기능을 나누어 작성해야하는지 , 어떤 기준으로 카드를 나누어야 하는지 감이 잡히지 않았다. 그래서 일단 페이지 별로 카드를 먼저 나누고 그 안에서 중복적으로 페이지에 들어갈 기능들은 따로 빼서 카드로 작성했다. ( 헤더 ,푸터 , 타입 컴포넌트 등.. )
이렇게 스크럼보드를 작성하고 카드별로 브랜치를 생성하여 작업을 시작하였다.
0. 프로젝트 설명
기초 요구사항 : react 를 사용해 주어진 요구사항을 만족하는 상품리스트 페이지와 사용자가 북마크 한 상품들을 조회할 수 있는 북마크 페이지를 포함하는 SPA 앱을 구현
상세 요구사항 :
Main page(메인 페이지)
- path: /
- Header와 Footer를 갖고 있으며, 해당 GNB와 Footer는 어느 페이지를 가더라도 항상 존재해야 한다.
- Header 내 햄버거 버튼 존재, 햄버거 버튼 펼칠 시 내부에
- Header는 페이지 내 스크롤이 발생하더라도 항상 상단에 붙어있어야 한다.
- 메인로고 → 클릭하면 / 페이지로 이동
- 햄버거 버튼
- 상품리스트 → 클릭하면 /products/list 페이지로 이동
- 북마크페이지 → 클릭하면 /bookmark 페이지로 이동
- Footer
- 일련의 텍스트 정보들
- Header 내 햄버거 버튼 존재, 햄버거 버튼 펼칠 시 내부에
- 해당 메인페이지에서는
- 모든 타입의 상품 정보를 4개 보여준다 (필터기능 없이)
- 보이는 상품의 타입은 혼합되어 있을 수 있다 (상품, 카테고리, 기획전, 브랜드)
- 모든 타입의 북마크 된 상품 정보를 4개 보여준다 (필터기능 없이)
- 보이는 상품의 타입은 혼합되어 있을 수 있다 (상품, 카테고리, 기획전, 브랜드)
- 모든 타입의 상품 정보를 4개 보여준다 (필터기능 없이)
Products list page(상품리스트 페이지)
- path: /products/list
- 서버에서 제공하는 상품 리스트들을 확인할 수 있는 페이지이며 무한 스크롤을 통해 상품들을 계속 보여줄 수 있어야 한다.
- 무한스크롤은 쿼리파라미터를 통한 매번 api call이 아닌, 최초 1번 api call을 통해 전체 데이터를 받아온 후 적용합니다.
- 상품은 각 상품별로 타입이 존재한다. (상품, 카테고리, 기획전, 브랜드)
- 상단의 필터 버튼을 통해 상품을 타입별로 조회해 보여줄 수 있어야 한다.
- 각 상품을 클릭하면 해당 상품의 사진을 보여주는 모달을 띄울 수 있어야 한다.
- 각 상품에 존재하는 북마크 버튼을 눌러 원하는 상품을 북마크 할 수 있어야 한다.
- 이미 북마크 된 상품의 경우, 북마크 버튼에 표시를 해주어야 하며 다시 한번 북마크 버튼을 클릭 시 해당 상품을 북마크에서 삭제한다.
- 북마크 버튼을 클릭하여 북마크에 추가할 때 그리고 삭제할 때는 사용자에게 알림 토스트가 표시되어야 한다.
Bookmark page(북마크 페이지)
- path: /bookmark
- 사용자가 북마크 한 상품들을 확인할 수 있는 페이지로 무한 스크롤이 가능해야 한다.
- 상품리스트 페이지에 존재하는 필터링 버튼과 같은 버튼을 이용해 상품을 타입별로 필터해 보여줄 수 있어야 한다.
API
- Swagger API 문서
1. Header
처음 먼저 생성한 브랜치는 헤더 브랜치로 요구사항을 모두 만족하였다.
import React from 'react';
import styled from 'styled-components';
import { Link } from 'react-router-dom';
import { useState ,useEffect,useRef} from 'react';
const HeaderDiv = styled.div`
width:100vw;
height: 80px;
box-shadow: 0px 8px 8px 0px #0000001A;
position: sticky;
top: 0px;
display: flex;
align-items: center;
justify-content: space-between;
background-color: white;
`
const LogoTitleDiv = styled.div`
display: flex;
justify-content: left;
align-items: center;
margin-left:76px;
&:hover{
cursor: pointer;
}
`
const LogoImg = styled.img `
width: 55px;
height: 30px;
`
const LogoTitle = styled.p`
font-size: 26px; // 글자 크기 변경
font-weight: 700;
`
const HamburgerDiv = styled.div`
margin-right: 78px;
&:hover{
cursor: pointer;
}
`
const Hamburger = styled.img`
width: 30px;
height: 20px;
`
const DropdownDiv = styled.div`
width: 200px;
height: 150px;
position: absolute;
left: 80%;
right: 0%;
top: 70%;
bottom: 0%;
border-radius: 12px;
background-color: white;
box-shadow: 0px 8px 8px 0px #0000001A;
`
const DropdownUl = styled.ul`
width: 200px;
height: 150px;
background-color: white;
padding-left: 0;
margin: 0;
border-radius: 12px;
`
const DropdownList = styled.li`
font-size: 16px;
list-style: none;
border-bottom: 0.5px solid rgba(0, 0, 0, 0.1);
width: 200px;
height: 50px;
line-height: 50px;
`
const DropdownListIconDiv = styled.div`
line-height: 50px;
margin-left: 30px;
`
const DropdownListIcon = styled.img`
width: 20px;
height: 20px;
vertical-align: middle;
margin-bottom: 5px;
margin-right: 8px;
`
const StyledLink = styled(Link)`
text-decoration: none;
color: black;
`
function Header(){
const [isActive, setActive] = useState(false);
const dropdownRef = useRef(null);
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setActive(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
function clickBtn(){
setActive(!isActive);
}
return (
<HeaderDiv>
<LogoTitleDiv>
<StyledLink to='/'>
<LogoImg src="../logo.png" alt="로고" />
</StyledLink>
<StyledLink to='/'>
<LogoTitle>COZ Shopping</LogoTitle>
</StyledLink>
</LogoTitleDiv>
<HamburgerDiv>
<Hamburger src="../hamburger.png" alt="드롭다운 메뉴" onClick={clickBtn}/>
{isActive === true ?
<DropdownDiv ref={dropdownRef}>
<DropdownUl>
<DropdownList>
<DropdownListIconDiv>
OOO님, 안녕하세요
</DropdownListIconDiv>
</DropdownList>
<DropdownList>
<StyledLink to='/products/list' >
<DropdownListIconDiv>
<DropdownListIcon src="../goods.png" alt="선물모양 아이콘"/>
상품리스트 페이지
</DropdownListIconDiv>
</StyledLink>
</DropdownList>
<DropdownList>
<StyledLink to='/bookmark'>
<DropdownListIconDiv>
<DropdownListIcon src="../bookmark.png" alt="북마크 아이콘"/>
북마크 페이지
</DropdownListIconDiv>
</StyledLink>
</DropdownList>
</DropdownUl>
</DropdownDiv>
: undefined}
</HamburgerDiv>
</HeaderDiv>
)
}
export default Header;
헤더를 진행하면서 겪었던 가장 큰 어려움은 드롭다운 창 외부를 클릭했을 시 창이 닫히는 것을 구현하는 것이었다.
이 부분을 페어분과 함께 고민을 많이 해보았지만 원하는대로 기능이 구현되지 않았다.
(... 결국 GPT의 도움을 받아 ) useRef 와 useEffect 를 사용하면 문제점을 해결 할 수 있었다.
dropdownRef라는 참조(ref)가 존재하고, 이 참조가 가리키는 엘리먼트 외부를 클릭한 경우에는 setActive(false)를 호출하여 상태를 업데이트 -> 이는 클릭된 엘리먼트가 드롭다운 영역 외부인 경우에 드롭다운을 닫는 기능을 수행
document 객체에 mousedown 이벤트 리스너를 등록 -> 이는 페이지 전체에서 마우스 버튼을 눌렀을 때 이벤트가 발생하도록 함 ->이벤트가 발생하면 handleClickOutside 함수가 호출
컴포넌트가 언마운트되거나 업데이트되기 전에 등록한 이벤트 리스너를 제거하기 위해, useEffect 함수의 반환값으로 함수를 반환함 -> 이 반환된 함수는 이펙트(clean-up) 함수로, 컴포넌트가 소멸되기 전에 실행됨
여기서는 document.removeEventListener를 호출하여 등록한 mousedown 이벤트 리스너를 제거
마지막 매개변수로 빈 배열([])을 전달하여, 이 효과(effect)가 컴포넌트의 마운트와 언마운트 시에만 실행되도록 설정
이렇게 함으로써 이벤트 리스너는 컴포넌트가 처음 마운트될 때 한 번만 등록되고, 컴포넌트가 언마운트될 때 한 번만 해제되게 됨.
2. Footer 구현
푸터는 가장 간단하게 구현하였다. 따로 가장 아래에 고정시키지 않고 푸터 컴포넌트를 만들어 App.js 에서 가장 아래부분에 렌더링 되도록 가장 하단에 배치하여 마무리 하였다.
import React from "react";
import styled from "styled-components";
const FooterDiv = styled.div`
width:100vw;
height: 58px;
text-align: center;
`
const FooterText = styled.div`
color: #888888;
font-size: 12px;
`
function Footer(){
return(
<FooterDiv>
<FooterText>
개인정보 처리방침 | 이용 약관
</FooterText>
<FooterText>
All rights reserved @ Codestates
</FooterText>
</FooterDiv>
)
}export default Footer;
사실 헤더와 푸터 구현은 그렇게 오래 걸리지 않았지만,, 타입에 대한 컴포넌트 개발과 북마크에 관한 부분은 매우 오래걸렸다. 이부분에 관한 내용은 다음 포스트에 작성할 것이다.
'코드스테이츠44기 프론트엔드' 카테고리의 다른 글
만족스럽지 못했던 프리프로젝트를 마치고 쓰는 회고 ,, (0) | 2023.08.01 |
---|---|
Section 4 기술 면접 준비 (0) | 2023.06.08 |
cmarket_redux 풀어보기 (1) | 2023.04.25 |
[리액트] Cmarket (Hooks 버전 ) 구현하기 (0) | 2023.04.22 |
섹션2를 마무리하며 KTP회고 (0) | 2023.04.10 |