온라인 도서 정보 쇼핑몰 ( BookStore )
- 프론트엔드 : React
- 백엔드 : node.js(express), Mysql
- 팀 : 1인 개발
- 깃허브 : https://github.com/changchangwoo/BookStore
- Redux-Tool-Kit을 활용하여 중앙 저장소를 통한 상태 관리
- Emotion을 활용하여 CSS in JS 방식의 스타일 관리
- 작성한 API를 별다른 수정 없이 그대로 활용할 수 있는 프론트엔드 구조 설계
- 재사용성을 고려한 컴포넌트 설계
- 이미 구현된 데이터베이스와 API를 활용해야하기에 OpenAPI 데이터가 아닌,데이터 삽입이 필요했다.
- Cheerio 라이브러리를 활용하여 yes24 도서 정보를 추출할 수 있었다
- 해당 내용은 node.js환경에서 cheerio를 활용한 웹 스크래핑 에서 정리하였다
- 로그인 성공 시, 회원 전용 컴포넌트(네비게이션 변경, 버튼 활성화 등)들을 전역 변수를 통해 식별하고 관리하려고 설계하였다
- 하지만 리덕스 상태는 브라우저 메모리에 저장되기에 새로고침 또는 새 탭이 열릴시 상태가 초기화되어지는 문제점을 발견했다
- 쿠키를 통한 JWT 통신으로 서버에서 회원 식별은 가능하지만, 컴포넌트가 회원 식별하는데에 어려움이 있었다
- 네비게이션 컴포넌트가 렌더링 될 때, 서버로부터 현재 유효한 쿠키를 가지고 있는지 식별하는 API를 구현하여 로그인 상태를 파악하도록 하여 해결했다.
- 하지만 이 경우에 사용자가 로그아웃을 통해 서버로 요청을 보내서 임의로 쿠키를 제거하지 않는 경우 로그인이 컴포넌트가 계속 출력된다는 한계점이 있다
- AccessToken-Refresh 토큰 학습의 필요성을 느끼게 되었다
한 페이지에서, 섹션마다 여러 데이터 호출 동작을하다보니까 too many connection 오류를 겪었다. 서버가 동시에 처리할 수 있는 데이터베이스의 최대 연결 수를 초과할 때 발생하는 문제였다. 서버의 connection 로직을 수정할 수 있었지만 최대한 그대로 활용하고싶어 MySQL의 경우에는 my.cnf 파일에서 max_connections 값을 증가시킴으로 해결할 수 있었다.
도서 상세페이지에서 동일한 페이지의 다른 도서 정보로 이동하는 경우, 페이지내에서 리렌더링이 일어나야한다. 하지만 이동하는 이벤트를 가지는 컴포넌트가 아주 깊은 자식이어서 부모의 리렌더링을 촉발시키기 어려웠던 문제가 있었다. 이 경우 리덕스를 통해 자식에서 부모 상태를 변경하는 트리거 변수를 생성하여 어렵지 않게 해결할 수 있었다.
- 배송지 내부 스크롤(자식 스크롤)이 끝나면 페이지 외부(부모 스크롤)로 포커스가 맞춰져 스크롤이 내려가는 문제가 있었다.
- css에서 overscroll-behavior 속성의 디폴트값인 auto가 아닌, contain으로 수정함으로 해결할 수 있었다.
/* GlobalStyles.jsx */
import { Global, css } from "@emotion/react";
import { useSelector } from "react-redux";
const GlobalStyles = () => {
const isDark = useSelector((state) => state.user.isDark);
const globalStyles = css`
:root {
--mainBG: ${isDark ? "#23272b" : "#ffffff"};
--subBG: ${isDark ? "#282C30" : "#f9f9f9"};
--fontColor : ${isDark ? "#ffffff" : "#000000"};
--outLine: ${isDark ? "#35393d" : "#e1e1e1"};
--reverseFontColor : ${isDark ? "#000000" : "#ffffff"};
--reverseMainBG : ${isDark ? "#f9f9f9" : "#282C30"};
// var(--MainBG);
// var(--SubBG);
// var(--fontColor);
// var(--outline);
}
`;
return <Global styles={globalStyles} />;
};
export default GlobalStyles;
const ReviewContents = ({ id }) => {
const [initialRender, setInitialRender] = useState(true);
const [reviews, setReviews] = useState([]);
const reviewsEndRef = useRef(null);
const fetchReviews = async () => {
try {
const response = await API.get("/reviews", {
params: { bookId: id },
});
setReviews(response.data);
} catch (err) {
console.error(err);
}
};
useEffect(() => {
fetchReviews();
setInitialRender(true)
}, [id]);
useEffect(() => {
if (!initialRender) {
scrollToBottom();
}
}, [reviews]);
const handleAddComment = (event) => {
setInitialRender(false);
setInputComment("");
API.post("/reviews", {
bookId: id,
comment: inputComment,
})
.then((res) => {
fetchReviews();
})
.catch((err) => {
console.log(err);
});
};
const scrollToBottom = () => {
reviewsEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
.
,
reivews를 의존성 배열로한다. 하지만 처음 렌더링 할 떄 서버로부터 리뷰를 호출한 후 출력하기 위해 reviews 상태를 변경하는데 이 때문에 페이지에 접근하면 리뷰 항목에 가장 하단으로 스크롤이 내려지는 문제점이 발생했다. 이 경우 처음 렌더링을 구분하는 initialRender state를 만들어 true인 경우 scrollBottom 동작이 호출되어지지 않고, 이후 addComment를 통해 review를 변경하는 경우에만 호출하도록 하여 문제를 해결하였다.
/* Search.jsx */
useEffect(() => {
window.scrollTo(0, 0);
const searchParams = new URLSearchParams(location.search);
const query = searchParams.get("query");
const categoryId = parseInt(searchParams.get("categoryId"));
const page = searchParams.get("page");
if (query) {
setResultKeyword(query);
}
if (categoryId !== null && !isNaN(categoryId)) {
const categoryName = category.find(item => item.category_id === categoryId)?.category_name;
setResultKeyword(null);
setCategoryName(categoryName);
if (!page) {
dispatch(
getSearchCategory({
categoryId: categoryId,
currentPage: 1,
totalCount: true,
})
);
}
}
}, [location.search]);
/* pagination.jsx */
const Pagination = ({ totalCount }) => {
const pageButtonRender = () => {
const startPage = (currentPageGroup - 1) * 5 + 1;
const endPage = Math.min(5 * currentPageGroup, pages);
const pageNumbers = [];
for (let i = startPage; i <= endPage; i++) {
pageNumbers.push(
<li key={i}>
<button
style={{backgroundColor : currentPage === i ? "var(--primaryHover)" : "var(--primary" }}
onClick={() => handleClickPage(i)}>{i}</button>
</li>
);
}
return pageNumbers;
};
/* DetailCard.jsx */
useEffect(() => { // 렌더링 시 좋아요 상태 확인 통신
API.get(`/likes/${id}`).then((response) => {
if (response.data.liked) {
setLikesCheck(true);
} else {
setLikesCheck(false);
}
})
.catch((err) => {
console.log(err);
});
}, []);
const likeHandler = () => {
if (likesCheck) { // 이미 좋아요가 눌린 상태일 때,
// 낙관적 업데이트를 반영한 좋아요 삭제 통신 및 UI 동기화
dispatch(likesCount({ type: "decrease" }));
setLikesCheck(false);
API.delete(`/likes/${id}`)
.then((response) => {
if (response.status !== 200) {
dispatch(likesCount({ type: "increase" }));
setLikesCheck(true);
}
})
.catch((err) => {
console.log(err);
dispatch(likesCount({ type: "increase" }));
setLikesCheck(true);
});
} else {
dispatch(likesCount({ type: "increase" }));
setLikesCheck(true);
API.post("/likes", { id: id })
// 좋아요가 눌리지 않은 상태일 때,
// 낙관적 업데이트를 반영한 좋아요 추가 통신 및 UI 동기화
.then((response) => {
if (response.status !== 200) {
dispatch(likesCount({ type: "decrease" }));
setLikesCheck(false);
}
})
.catch((err) => {
console.log(err);
dispatch(likesCount({ type: "decrease" }));
setLikesCheck(false);
});
}
};
![]() | ![]() |
---|
다방면으로 정말 많은 실력과 경험 키울 수 있었던 프로젝트였다.
또 아직 배워야 할 길은 멀구나.. 새삼 느낀다