[5팀 손승현] Chapter2-1. 프레임워크 없이 SPA 만들기 - #28
Conversation
c1cdc92 to
9b09aa3
Compare
| const existingProducts = Array.isArray(state.products) ? state.products : []; | ||
| const shouldReuseProducts = existingProducts.length > 0; | ||
|
|
||
| if (!shouldReuseProducts) { | ||
| appStore.resetPagination(); | ||
| } | ||
| appStore.setFetchingNextPage(false); | ||
|
|
||
| const { filters } = appStore.getState(); |
There was a problem hiding this comment.
필터 캐시 재사용 조건 모호하다. 홈으로 돌아올 때 existingProducts가 있으면 재사용하는데, 필터가 바뀐 경우에도 동일한 데이터를 보여줄 가능성이 있다.
There was a problem hiding this comment.
appStore에 마지막 사용한 필터에 필터 상품을 저장하고 비교하는 방식???? 을 사용하면 어떨까..
| const forceInclude = {}; | ||
| if (Object.prototype.hasOwnProperty.call(partialFilters ?? {}, "limit")) { | ||
| forceInclude.limit = true; | ||
| } | ||
| if (Object.prototype.hasOwnProperty.call(partialFilters ?? {}, "sort")) { | ||
| forceInclude.sort = true; | ||
| } |
There was a problem hiding this comment.
기본값(limit=20)도 쿼리에 강제로 남기는 부분은 나중에 정책이 바뀌면 수정하기 번거로울 수 있다.
There was a problem hiding this comment.
@sonsonsh1125 걍 상수들 모아놓고 거기서 컨트롤 하는 방법밖에 없지않나ㅠ모르겠어요 정책바꾸면 혼내줘야죠
|
|
||
| const root = document.getElementById("root") ?? document.body; | ||
|
|
||
| overlay = document.createElement("div"); |
There was a problem hiding this comment.
이정도 DOM 요소의 직접접근..! 방법이 없다. 이부분 뭔가 더 좋은 방법이 생각이 안남
There was a problem hiding this comment.
<div id="overlay"></div>말씀하신거랑은 좀 다를수잇는데 직접접근이 아니고 직접생성이 문제라면 정적 DOM을 미리 준비하는거도 방법일거같아요
| products: shouldReuseProducts ? existingProducts : [], | ||
| filters, | ||
| categories: categoriesCache ?? {}, | ||
| pagination: state.pagination, |
There was a problem hiding this comment.
이 부분으로 인해서 장바구니 버튼이 깜빡이는것 같은데 pageLayout에서 매번 새헤더를 불러내서..? 장바구니 깜빡이는거 너무 거슬리네!!!
There was a problem hiding this comment.
@sonsonsh1125 장바구니에 담긴 상품 개수 불러오는걸 Header.js 컴포넌트에서 호출하면 해당 이슈는 해결될거같아요!
JunilHwang
left a comment
There was a problem hiding this comment.
이 피드백은 n8n + ai (gpt-5-mini)를 활용하여 자동으로 생성된 내용입니다.
전체 요약
이번 PR의 코드는 다양한 요구사항을 충실히 반영하여, 상품 목록, 상세 페이지, 장바구니, 검색, 필터, 토스트 메시지, SPA 네비게이션 등 기능을 잘 구현했습니다. 특히 상태관리와 URL 동기화를 적절히 분리하여 SPA 경험과 상태 복원이 원활한 점이 긍정적입니다.
그러나 전반적으로 DOM 조작을 직접 수행하며 상태와 UI가 다소 결합된 부분이 있고, 이벤트 핸들링과 상태 업데이트가 분리되어 관리되지 않은 곳이 있어 확장성과 유지보수성 측면에서 개선이 필요해 보입니다.
주요 설계 피드백
- 상태 관리 및 의존성 주입: 장바구니나 상세 수량 제어 같은 상태 관리 객체를 전역(window)에서 독립된 모듈로 분리하고, 필요한 곳에 명시적으로 주입하는 방향을 권장합니다.
- 이벤트 위임과 부분 렌더링: 장바구니 모달 등에서는 innerHTML 재할당 방식보다 이벤트 위임 기법과 최소한의 DOM 업데이트 전략을 사용해 성능과 유지보수를 개선할 수 있습니다.
- UI 컴포넌트 분리와 재사용성: SearchForm의 카테고리 렌더링 로직 다듬기, 통화 포맷팅 유틸 분리, 공통 컴포넌트 import 관리 등 모듈화와 재사용 향상을 고려하는 것이 좋습니다.
- 성능 최적화: 무한 스크롤 이벤트에 throttle/debounce 적용과 애니메이션을 사용하는 토스트 메시지 관리에서 과도한 DOM 조작을 제어할 필요가 있습니다.
- 템플릿 문법 오류 수정:
PageLayoutHTML 템플릿 닫는 태그 누락 등은 작은 실수지만 큰 문제를 일으키므로 집중 점검 바랍니다.
현실적인 확장 요구사항 예시
상품 상세 페이지에 여러 상품을 동시에 표시하거나, 장바구니가 복수 뷰를 갖는 복잡한 UI 구성이 추가된다면, 현재의 DOM 직접 조작/innerHTML 교체 방식은 심각한 성능 저하와 이벤트 관리 오류를 유발할 수 있습니다.
이를 대비해 상태와 UI를 명확히 분리한 컴포넌트 기반 구조, 이벤트 위임, 상태 변화만 최소한으로 DOM 조작하는 방식을 도입하면 확장성, 유지보수성, 성능이 크게 향상될 것입니다.추가 질문이 없으므로, 제출하신 코드에 대한 상세 리뷰와 설계 개선 방향을 드렸습니다. 앞으로도 상태관리와 UI 분리, 이벤트 처리 최적화에 좀 더 신경 쓰시면서 리팩토링해 보시면 좋겠습니다. 질문이 생기시면 언제든 도움을 요청해 주세요!
| } | ||
| updateFiltersWithNavigation({ sort: nextSort }); | ||
| render(); | ||
| }, |
There was a problem hiding this comment.
1. 문제상황 제시
구체적인 문제 상황:
현재 src/main.js에서는 상품 상세 페이지 내에서 수량 변경 UI를 직접 DOM 이벤트 핸들러로 바인딩하고 있습니다. 만약 향후 기능이 확장되어 여러 상품 상세 컴포넌트가 동시에 존재하거나, SPA에서 복잡한 상태 관리가 요구될 경우 이러한 DOM 직접 조작 방식은 유지보수가 어려워질 수 있습니다.
현재 코드의 한계:
- DOM 요소와 로직 결합이 강해 테스트 및 유지보수가 어렵습니다.
- 수량 변경 로직이 수량 입력 박스에 종속적이며, 상태가 분산되어 관리됩니다.
- 같은 로직을 상세 페이지 외 컴포넌트에서 재사용하기 어렵습니다.
2. 근본 원인
핵심 문제:
UI 이벤트 핸들링 로직이 DOM에 직접 바인딩되어 있고, 상태와 비즈니스 로직이 분리되지 않았습니다.
왜 문제인가:
이 구조는 코드 확장과 유지보수 시 비효율적이며, 향후 상태관리 패턴 (예: 상태머신, 리액티브 패턴) 도입을 어렵게 만듭니다.
3. 개선 구조
현재 구조:
- DOM 이벤트 핸들러가 상세페이지 내에서 직접 수량 조절과 관련된 로직 처리
개선된 구조:
- 수량 조절 로직을 별도의 상태관리 함수로 분리
- 이벤트 핸들러는 상태관리자를 호출하는 형태로 변경
- 상태 관리를 중앙화하여 다른 기능과 재사용 가능하게 구조화
개선 사항:
- 수량 관련 로직을 한 곳에 모아 재사용성 증가
- 검증 로직도 상태 관리자에서 처리해 입력 밸리데이션 명확화
- 코드 가독성 및 유지보수성 향상
코드 비교:
// ❌ 현재 방식
const quantityInput = document.getElementById("quantity-input");
const decreaseBtn = document.getElementById("quantity-decrease");
decreaseBtn.addEventListener("click", () => {
adjustDetailQuantity(quantityInput, -1);
});
// ✅ 개선된 방식
// 상태관리 함수
function changeQuantity(productId, delta) {
const item = getProductById(productId);
if (!item) return;
const newQuantity = Math.max(1, item.quantity + delta);
updateProductQuantity(productId, newQuantity);
renderQuantity(newQuantity);
}
// 이벤트 바인딩
const decreaseBtn = document.getElementById("quantity-decrease");
decreaseBtn.addEventListener("click", () => changeQuantity(productId, -1));| search: state.filters.search, | ||
| category1: state.filters.category1, | ||
| category2: state.filters.category2, | ||
| page: nextPage, |
There was a problem hiding this comment.
2. 문제상황 제시
구체적인 문제 상황:
장바구니 기능이 window.cartManager 전역 객체로 관리되는 구조로 되어 있습니다. 이로 인해 다른 모듈이 장바구니 상태에 직접 접근하거나 수정할 수 있는 위험이 있고, 협업 시 충돌 가능성이 있습니다.
현재 코드의 한계:
- 전역 상태 관리로 인한 의존성 및 사이드 이펙트 위험
- 모듈 간 결합도가 높아 유지보수성 저하
- 테스트 작성이 어렵고, 상태 변경 추적이 비효율적
2. 근본 원인
핵심 문제:
중앙 집중식 상태 관리 없이 전역 객체에 상태와 메서드를 노출하는 방식.
왜 문제인가:
이러한 설계는 앱이 확장될수록 상태동기화 문제를 야기하며, 모듈 단위 테스트 및 코드 분리가 어려워집니다.
3. 개선 구조
현재 구조:
- 전역 객체에 상태 및 메서드 직접 노출
개선된 구조:
- 상태 관리 라이브러리 혹은 자체 스토어(예:
createCartStore) 도입 - 구독/발행 패턴 적용하여 상태 변경 감지
- API만 노출하여 외부에서 직접 상태 변경 불가하게 제한
개선 사항:
- 장바구니 상태 관리 모듈화
- 외부 접근은 공개 API만 가능하게 제한
- 상태 변경은 정해진 액션을 통해서만 가능하게 설계
코드 비교:
// ❌ 현재 방식
window.cartManager = {
getState: getCartSnapshot,
subscribe: subscribeCart,
addItem: addProductToCart,
...
};
// ✅ 개선된 방식
function createCartStore() {
let state = { items: [] };
const listeners = new Set();
function subscribe(listener) {
listeners.add(listener);
listener(state);
return () => listeners.delete(listener);
}
function addItem(productId, quantity) {
// ... update state immutably
// ... notify listeners
}
return { subscribe, addItem, ... };
}
const cartStore = createCartStore();
export default cartStore;| } | ||
|
|
||
| const categorySummaryWhenNone = category1Keys.map((category1) => escapeHtml(category1)).join(" "); | ||
|
|
There was a problem hiding this comment.
3. 문제상황 제시
구체적인 문제 상황:
카테고리 선택 UI를 렌더링할 때 여러 중첩된 조건과 문자열 조작이 섞여 가독성이 떨어집니다. 카테고리 상태가 복잡해질수록 유지보수가 어려워질 수 있습니다.
현재 코드의 한계:
- 가독성이 낮아 수정하기 어려움
- 카테고리 버튼 생성 로직이 분리되지 않아 재사용성 부족
- 상태 변경 시 렌더링 로직 수정이 어려움
2. 근본 원인
핵심 문제:
UI 생성 로직과 상태 처리 로직이 분리되지 않고, HTML 문자열 템플릿 안에 조건부 처리들이 섞여 있음
왜 문제인가:
코드가 길어지고 중첩될수록 의도 파악과 디버깅이 어려워지고, 작은 변경에도 버그 발생 가능성이 높아집니다.
3. 개선 구조
현재 구조:
- 카테고리 렌더링 로직이 단일 함수 내 중첩 HTML 문자열 템플릿과 조건부 처리 혼합
개선된 구조:
- 카테고리 렌더링을 작은 컴포넌트 함수로 분리
- 상태와 UI 분리를 명확히 하여 구조화
- 재사용 가능한 컴포넌트/함수로 제작
개선 사항:
- 카테고리 버튼 및 선택 영역을 별도 함수로 추출
- 명확한 상위/하위 카테고리 컴포넌트 분리
- 상태 변경 시 UI 상태만 교체
코드 예시:
function renderCategoryButton(name, isActive) {
return `<button class="${isActive ? activeClass : baseClass}">${escapeHtml(name)}</button>`;
}
function renderCategoryList(categories, selected) {
return categories.map(name => renderCategoryButton(name, selected === name)).join("");
}| keydownHandler = (event) => { | ||
| if (event.key === "Escape") { | ||
| event.preventDefault(); | ||
| closeCartModal(); |
There was a problem hiding this comment.
4. 문제상황 제시
구체적인 문제 상황:
장바구니 모달에서 UI 상태 업데이트 시 전체 HTML을 innerHTML로 덮어쓰기 때문에 이벤트 핸들러가 초기화되고 다시 붙여야 하는 구조입니다. 만약 상품 수가 많아질 경우 DOM 재생성 비용이 상승하고, 이벤트 중복 바인딩 관리가 어려울 수 있습니다.
현재 코드의 한계:
- innerHTML 재할당으로 기존 이벤트 핸들러 소멸
- 상태 변화마다 DOM 전체 재렌더링으로 퍼포먼스 저하
- 이벤트 위임 패턴 미적용으로 코드 복잡도 상승
2. 근본 원인
핵심 문제:
DOM에 직접 innerHTML로 전체 내용을 교체하는 방식으로 상태 및 이벤트 바인딩 관리 미흡
왜 문제인가:
이 구조는 복잡도가 증가할수록 코드 유지보수가 어려워지고, UI 반응 속도가 저하됩니다.
3. 개선 구조
현재 구조:
- 전체 컨테이너 innerHTML 재할당 + 이벤트 리바인딩
개선된 구조:
- 이벤트 위임 방식 적용: 상위 컨테이너에 단일 이벤트 리스너 등록
- 변경된 부분만 DOM 업데이트 (예: 수량, 선택 상태)
- 상태 관리 분리 및 최소한의 DOM 조작 유도
개선 사항:
- 이벤트 위임으로 이벤트 핸들러 일원화
- 데이터 속성과 상태를 DOM에 명확히 연결
- 필요한 부분만 최소한으로 업데이트
코드 예시:
// 이벤트 위임 예시
container.addEventListener('click', (event) => {
const target = event.target;
if(target.matches('.quantity-increase-btn')) {
const productId = target.dataset.productId;
cartManager.updateQuantity(productId, /* newQuantity */);
}
// 기타 이벤트 처리
});
// 상태 변화 시 필요한 부분만 업데이트
function updateQuantityDisplay(productId, quantity) {
const input = container.querySelector(`input[data-product-id="${productId}"]`);
if (input) input.value = quantity;
}| const snapshot = manager.getState(); | ||
|
|
||
| if (overlay) { | ||
| updateModalContent(snapshot, manager); |
There was a problem hiding this comment.
5. 문제상황 제시
구체적인 문제 상황:
전역 window.cartManager 객체에 의존하여 장바구니 상태를 가져오는데, 모듈 간 의존성이 높아 추후 모듈화나 테스트가 어렵습니다.
현재 코드의 한계:
- 전역 변수 의존성으로 인한 테스트 어려움
- 모듈간 결합도가 높아서 재사용성과 유지보수성 저하
2. 근본 원인
핵심 문제:
장바구니 상태 및 동작을 전역 객체에 밀접하게 의존함
왜 문제인가:
전역 변수 사용은 명확한 의존성 주입 패턴이 없고, 상태 추적과 디버깅이 어렵습니다.
3. 개선 구조
현재 구조:
- 전역 객체 직접 접근 및 상태 사용
개선된 구조:
- 함수나 클래스의 생성 시점에 상태 관리자를 주입받는 방식 채택
- DI(의존성 주입) 패턴 사용으로 테스트 및 재사용성 향상
개선 사항:
openCartModal등 함수에 상태 관리자를 파라미터로 받거나 컨텍스트 모듈에서 주입- 테스트용 목 상태 관리자 교체 용이
코드 예시:
export function openCartModal(manager) {
const snapshot = manager.getState();
// ...
}
// 모듈 외부에서
import { cartManager } from './cartStore';
openCartModal(cartManager);| type="button" | ||
| id="toast-close-btn" | ||
| class="flex-shrink-0 ml-3 text-white hover:text-gray-200 transition-colors" | ||
| aria-label="토스트 닫기" |
There was a problem hiding this comment.
6. 문제상황 제시
구체적인 문제 상황:
showToast 함수가 호출될 때 매번 새 DOM 요소를 생성하고, 애니메이션 끝나면 제거하는 방식인데, 많은 알림이 빠르게 호출되면 애니메이션 충돌이나 메모리 누수가 발생할 수 있습니다.
현재 코드의 한계:
- 중복 알림 생성 가능성이 높음
- 애니메이션 상태 관리가 이벤트 기반이며 중첩 호출에 취약
2. 근본 원인
핵심 문제:
토스트 알림이 개별 DOM 및 애니메이션 타이머에 의존하여 종합적인 관리가 어렵다.
왜 문제인가:
빠른 연속 호출 시 과도한 DOM 조작과 애니메이션 충돌이 발생할 수 있어 자원 낭비 우려
3. 개선 구조
현재 구조:
- 매 호출마다 독립적인 DOM 토스트 생성 및 제거
개선된 구조:
- 토스트 메시지를 큐로 관리해 일정 갯수 이상 생성하지 않음
- 애니메이션 완료 후 콜백으로 정리
- 상태 관리 변수를 이용해 중복 호출 방지
개선 사항:
- 내부 큐 구현 및 중복 메시지 필터링
- 동시 알림 갯수 제한
코드 예시:
const toastQueue = [];
function showToast(message, type) {
if (toastQueue.length > MAX_TOASTS) return;
// ...
toastQueue.push(toast);
// 애니메이션 후 삭제 및 큐에서 제거
}| ${children} | ||
| </main> | ||
| ${Footer()} | ||
| </div |
There was a problem hiding this comment.
7. 문제상황 제시
구체적인 문제 상황:
PageLayout 컴포넌트의 리턴 문자열에 문법 오류가 있어 실제 적용 시 의도한 레이아웃이 깨질 위험이 있습니다.
현재 코드의 한계:
- 닫는 div 태그에 닫는 꺽쇠(>)가 빠져 있음
2. 근본 원인
핵심 문제:
템플릿 문자열 내 HTML 태그 작성 오류
왜 문제인가:
이로 인해 HTML 파싱 오류가 발생하며, 레이아웃이 왜곡되거나 자바스크립트가 정상 작동하지 않을 수 있습니다.
3. 개선 구조
개선된 구조:
- 문자열 마지막 부분 닫는 태그에
>추가
코드 비교:
// ❌ 현재
return `
<div class="min-h-screen bg-gray-50">
${Header()}
<main class="max-w-md mx-auto px-4 py-4">
${children}
</main>
${Footer()}
</div
`;
// ✅ 개선
return `
<div class="min-h-screen bg-gray-50">
${Header()}
<main class="max-w-md mx-auto px-4 py-4">
${children}
</main>
${Footer()}
</div>
`;| <p class="text-sm font-medium text-gray-900">${total ?? price}</p> | ||
| ${ | ||
| controls | ||
| ? `<button class="cart-item-remove-btn mt-1 text-xs text-red-600 hover:text-red-800" data-product-id="${productId}"> |
There was a problem hiding this comment.
8. 문제상황 제시
구체적인 문제 상황:
Cart 컴포넌트 템플릿 내에서 가격, 총합 등의 통화 단위를 문자열로 표시하는 로직이 여러 곳에 분산되어 중복되고 있습니다.
현재 코드의 한계:
- 통화 단위 포매팅 로직이 각 컴포넌트에서 중복
- 변경 시 일괄 수정이 어려움
2. 근본 원인
핵심 문제:
뷰 컴포넌트에서 통화 포매팅 로직이 별도 함수나 유틸을 통해서가 아닌 직접 구현됨
왜 문제인가:
중복 코드로 인해 유지보수 비용 증가 및 일관성 없는 표시 가능성 존재
3. 개선 구조
개선된 구조:
- 통화 포맷팅 유틸 함수를 별도로 작성해 재사용
- 컴포넌트에서는 유틸 호출 형태로 간결화
코드 비교:
function formatPrice(value) {
return `${Number(value).toLocaleString()}원`;
}
// 사용 예시
priceLabel: formatPrice(item.price),| continue; | ||
| } | ||
| const normalized = parsed.map((item) => ({ | ||
| productId: item.productId, |
There was a problem hiding this comment.
9. 문제상황 제시
구체적인 문제 상황:
무한 스크롤 로직이 스크롤 이벤트에서 직접 페이지네이션 로딩 여부를 체크 후 데이터를 불러오는 방식인데, 스크롤 이벤트 빈번 호출로 성능 문제 가능성이 있습니다.
현재 코드의 한계:
- 스크롤 이벤트에 디바운스 또는 스로틀 처리 없음
- 불필요한 함수 반복 호출 가능성
2. 근본 원인
핵심 문제:
이벤트 핸들러 최적화 미흡으로 과도한 호출 발생
왜 문제인가:
불필요한 렌더링, API 호출 부담 증가
3. 개선 구조
개선된 구조:
- 스로틀링 또는 디바운싱 적용하여 호출 빈도 제한
개선 사항:
- lodash 같은 외부 라이브러리 사용 혹은 직접 스로틀 함수 작성
- 이벤트 청취 시 옵션에 passive: true 유지
코드 예시:
window.addEventListener('scroll', throttle(() => {
if (shouldLoadMore()) {
loadMoreProducts();
}
}, 200), { passive: true });| export { SearchForm } from "./SearchForm.js"; | ||
| export { Skeleton } from "./ProductList.js"; | ||
| export { Header } from "./Header.js"; | ||
| export { Footer } from "./Footer.js"; |
There was a problem hiding this comment.
10. 문제상황 제시
구체적인 문제 상황:
src/components/index.js에서 Loading 컴포넌트를 ProductList.js로부터 별도 export 하도록 했는데, 이는 컴포넌트가 ProductList와 결합되는 의존성이 높아질 수 있는 구조입니다.
현재 코드의 한계:
- 다수 컴포넌트를 한 파일에서 export하면 재사용성 및 변경 관리 어려움
2. 근본 원인
핵심 문제:
컴포넌트 경로나 파일이 자주 변경될 경우 import 경로 관리가 복잡해짐
왜 문제인가:
모듈 별 분리가 어렵고, 중복 import 발생 가능성
3. 개선 구조
개선된 구조:
- 컴포넌트별로 별도의 독립파일 구성 유지
- index.js는 주요 컴포넌트만 export하여 API 명확화
권장사항:
- Loading을 ProductList 내부 컴포넌트로 취급하거나 별도 내포 컴포넌트로 관리
- 불필요한 export 최소화
eveneul
left a comment
There was a problem hiding this comment.
승현 님 이번 주차도 고생 많으셨습니다! 많이 기대하시고기다리던 자바스크립트 / 리액트 챕터인데 어떠셨나요! 많은 도움이 되셨을까여..
관련이 있는 값들은 하나의 객체로 묶어 주고, 오탈자 늘 신경 써 주세요!
이번 주도 넘 고생 많으셨고 다음 주부터 다시 파이팅해봅시다!
|
|
||
| const clearBtn = container.querySelector("#cart-modal-clear-cart-btn"); | ||
| if (clearBtn) { | ||
| clearBtn.addEventListener("click", () => { |
There was a problem hiding this comment.
클릭 이벤트가 여러군데 쓰이는데 차라리 이걸 함수로 하면 어떨까요?
const addEvent = (eventType, element, callback) => { ... }
| </svg> | ||
| `, | ||
| }; | ||
| } |
There was a problem hiding this comment.
여러 상태를 가지고 있는 컴포넌트는 switch문으로 관리하신 것 좋습니다!
| </div> | ||
| `; | ||
|
|
||
| const ProductItem = ({ title, image, productId, lprice, brand }) => { |
There was a problem hiding this comment.
Item은 개별적인 컴포넌트로 봐도 되지 않을까..! 라는 제 생각
왜냐하면 상품 상세에서도 관련 상품으로 ProductItem이 쓰이거든요!
|
|
||
| const categorySummaryWhenNone = category1Keys.map((category1) => escapeHtml(category1)).join(" "); | ||
|
|
||
| const category1Buttons = |
There was a problem hiding this comment.
Category 버튼을 1뎁스, 2뎁스를 따로따로 만드는 것보다는 하나의 컴포넌트로 합치는 건 어떨까요? 저도 이번 챕터 문제를 풀 때 똑같은 고민을 했다가 성민 님처럼 두 개로 나눈 경험이 있는데요, 지금 생각해 보면 카테고리 뎁스가 더 많아질 때는 어떻게 하지? 생각이 들어서요.
제가 생각한 코드는..
const Category = (attrs, label, isSelected) => /* HTML */ `
<button
${Object.entries(attrs)
.map((k, v) => `data-${k}=${v}`)
.join(" ")}
class="px-3 py-2 text-sm rounded-md border transition-color
${isSelected ? "bg-blue-100 border-blue-300 text-blue-800" : "bg-white border-gray-300 text-gray-700 hover:bg-gray-50"}
>${label}</button>
`;이렇게 만들고, Category 컴포넌트를 사용할 때는,
.map(product => Category({ category1: product }, product, false).map(product => Category({ category1: selectedCategory1, category2: product },
product,
product === selectedCategory2
)
)이런 식으로 해도 좋을 것 같아용
| </div> | ||
| `; | ||
|
|
||
| const STAR_PATH = |
There was a problem hiding this comment.
rating component도 따로 분리하면 좋을 듯 해요!
/components/product/Rating.js
| const ratingValue = Number(product.rating ?? 0); | ||
| const normalizedRating = Number.isFinite(ratingValue) ? ratingValue : 0; | ||
| const reviewValue = Number(product.reviewCount ?? 0); | ||
| const normalizedReviewCount = Number.isFinite(reviewValue) ? reviewValue : 0; |
There was a problem hiding this comment.
const productInfo = {
pridct: Number(product.lprice ?? product.price ?? 0).toLocaleString(),
stock: Number ..... 후략
}
이렇게 했을 때 객체로 접근하면 가독성이 쪼금 더 좋지 않을까ㅏ...! 라는 제 생각
| ${children} | ||
| </main> | ||
| ${Footer()} | ||
| </div |
과제 체크포인트
배포 링크
https://sonsonsh1125.github.io/front_7th_chapter2-1/
기본과제
상품목록
상품 목록 로딩
상품 목록 조회
한 페이지에 보여질 상품 수 선택
상품 정렬 기능
무한 스크롤 페이지네이션
상품을 장바구니에 담기
상품 검색
카테고리 선택
카테고리 네비게이션
현재 상품 수 표시
장바구니
장바구니 모달
장바구니 수량 조절
장바구니 삭제
장바구니 선택 삭제
장바구니 전체 선택
장바구니 비우기
상품 상세
상품 클릭시 상세 페이지 이동
/product/{productId}형태로 변경된다상품 상세 페이지 기능
상품 상세 - 장바구니 담기
관련 상품 기능
상품 상세 페이지 내 네비게이션
사용자 피드백 시스템
토스트 메시지
심화과제
SPA 네비게이션 및 URL 관리
페이지 이동
상품 목록 - URL 쿼리 반영
상품 목록 - 새로고침 시 상태 유지
장바구니 - 새로고침 시 데이터 유지
상품 상세 - URL에 ID 반영
/product/{productId})상품 상세 - 새로고침시 유지
404 페이지
AI로 한 번 더 구현하기
과제 셀프회고
배포문제(25.11.10)
작업을 상세페이지까지 작업하던 도중 왠지 추후에 배포에서 에러가 나서 시간이 많이 소요될 것을 감지해 배포 작업을 진행하게 되었다.
당연히 스무스 하게 진행될 것이라 생각했는데 아뿔싸.. 정말 꼬박 3시간은 걸린것 같다. 남들이면 이정도는 소요 안될 것 같은데
내 적은 뇌용량 및 체력 때문이다.
배포 세팅 관련
gh-pages를 npm을 통해 설치를 안했다. 근데 이거는 deploy.yml 설정해 놓으면 배포상 문제는 없음deploy.yml을 peaceiris/actions-gh-pages 사용해놓고 환경설정을 Github Page 액션사용 하는 방법으로 해놓음.코드 수정 → git push → GitHub Actions 자동 실행 → deploy.yml 워크플로우 실행 → 배포 완료vite.config.js에서 빌드 설정을 했어야 했음baseL: "/"프로덕션에서는base "base: "/front_7th_chapter2-1/"를 사용하고 alias 설정을 통해서 현재 설정 파일 위치를 기준으로 절대 경로를 안전한게 만들었다. ->jsconfig.js추가 작성해야한다.getNormalizedPathname함수를 추가해서 URL에서 basePath를 제거하여 정규화된 경로를 추출 -> 근데 너무 복잡해 진듯 하여 vite의 base를/로 유지하고 404.html을 추가하는 방법을 고려중(단점은 해쉬가 꼴뵈기 싫음)DOM 직접 조작을 하지 않고 처리하기(25.11.11)
직접 조작시 문제점
바닐라 JS에서 DOM 직접 조작 대안
단방향 데이터 흐름에 주의 하자 : 상태변경 -> 렌더링
현재 코드의 문제점 진단 및 store로 상태관리 처리하기(25.11.12)
현재 코드의 문제점
render()를 호출해야 한다bindFilters()도 매번 다시 연결해야 한다render()안 부르면 화면 안바뀜innerHTML로 DOM을 교체하면 이벤트 리스너가 사라진다store로 관리해야할 상태들
새로 알게 된 함수(25.11.12)
history.pushState페이지 새로고침 없이 브라우저 URL을 변경하는 API
E2E 테스트 작업 중 수정 사항(25.11.13)
로딩 상태 미표시 문제
CI에서는 Mock API 응답이 너무 빨라
"카테고리 로딩 중..."문구가 렌더되기 전에 데이터가 도착. 로컬에서는 보였지만, CI 테스트가 실패하면서 로딩 단계가 비동기 속도에 의존한다는 사실이 드러남→ Mock API에 지연을 추가해 “로딩 중” 상태가 재현 되도록 처리
장바구니 모달의 선택자 불일치
.cart-modal-overlay,.cart-modal등 테스트가 찾는 클래스가 실제 DOM에 없거나, 모달이#root밖에 렌더링되어 테스트가 접근하지 못함(내가 처음부터 구조를 이상하게 했다)→ 클래스명을 테스트 기대대로 추가하고, 모달을
#root안에 붙여 구조를 일치토스트 닫기 버튼 선택 불가
테스트는
#toast-close-btn을 클릭하려 했지만 해당 ID가 없었다.→ 닫기 버튼에 ID를 부여해 테스트가 안정적으로 DOM 요소를 찾도록 함.
필터 상태·URL 싱크 불일치
페이지당 개수, 정렬, 검색어가 변경돼도 URL 쿼리에 반영되지 않아 새로고침/직접 접근 시 상태가 복원되지 않았고, 테스트는 URL에 특정 쿼리를 기대했기 때문에 실패.
→ 스토어-URL 간 양방향 동기화를 구축해 필터 변경 시 쿼리를 갱신하고, 쿼리로 진입 시 필터를 복원하도록 수정.
- URL 쿼리 → 스토어를 초기화 (parseFiltersFromUrl()),
- 스토어 변경 → URL 갱신 (appStore.subscribe),
- 필터 UI 이벤트 → 스토어 & URL 동시에 갱신 (updateFiltersWithNavigation()),
- 뒤로가기/앞으로가기에서도 render()가 호출되면서 URL 기반 필터가 다시 복원
상세 페이지에서 뒤로가기 시 리스트 사라짐
상세 페이지 진입 시
appStore.clearProducts()로 기존 목록을 비워버려 뒤로가기 직후 홈 화면에 데이터가 없어 테스트가 간헐적으로 실패.→ 상세 진입에서도 캐시를 유지하고, 홈으로 돌아올 때 기존 데이터를 즉시 재사용하도록 리팩터링.
404 페이지에서
<main>중복404 컴포넌트가
<main>을 중첩해서 렌더링해 ARIA strict 모드에서 두 개의main이 감지되었습니다.→
NotFound컴포넌트 내부의<main>을 제거하여PageLayout이 제공하는 단일<main>만 사용하도록 수정기술적 성장
자랑하고 싶은 코드
updateFiltersWithNavigation() 도입
개선이 필요하다고 생각하는 코드
필터 동기화, 라우팅, 제품 캐시 관리가 모두 한 파일에 몰려 있어 가독성이 떨어집니다. 필터-URL 동기화, 라우터, 데이터 페치 등을 모듈화하면 이해와 유지보수가 쉬울 것 같습니다.
상세 페이지에서 홈으로 돌아올 때 캐시를 재사용하도록 했지만, 필터가 바뀌었는지 여부에 따라 캐시를 초기화해야 하는 상황을 더 정교하게 다듬을 필요가 있습니다.
학습 효과 분석
과제 피드백
AI 활용 경험 공유하기
cusor를 사용하여 기본적인 컴포넌트 분리 및 작성된 코드와 기존 과제 탬플릿의 차이점 관련하여 구별할 수 있게 처리하였습니다.
또한 E2E 테스트 중 DOM 구조 잡기 같은 기본적인 부분들을 위임해서 작업했습니다.
리뷰 받고 싶은 내용