Skip to content

Conversation

@ongsim0629
Copy link

@ongsim0629 ongsim0629 commented Nov 10, 2025

과제 체크포인트

배포 링크

https://ongsim0629.github.io/spa/

기본과제

상품목록

상품 목록 로딩

  • 페이지 접속 시 로딩 상태가 표시된다
  • 데이터 로드 완료 후 상품 목록이 렌더링된다
  • 로딩 실패 시 에러 상태가 표시된다
  • 에러 발생 시 재시도 버튼이 제공된다

상품 목록 조회

  • 각 상품의 기본 정보(이미지, 상품명, 가격)가 카드 형태로 표시된다

한 페이지에 보여질 상품 수 선택

  • 드롭다운에서 10, 20, 50, 100개 중 선택할 수 있으며 기본 값은 20개 이다.
  • 선택 변경 시 즉시 목록에 반영된다

상품 정렬 기능

  • 상품을 가격순/이름순으로 오름차순/내림차순 정렬을 할 수 있다.
  • 드롭다운을 통해 정렬 기준을 선택할 수 있다
  • 정렬 변경 시 즉시 목록에 반영된다

무한 스크롤 페이지네이션

  • 페이지 하단 근처 도달 시 다음 페이지 데이터가 자동 로드된다
  • 스크롤에 따라 계속해서 새로운 상품들이 목록에 추가된다
  • 새 데이터 로드 중일 때 로딩 인디케이터와 스켈레톤 UI가 표시된다
  • 홈 페이지에서만 무한 스크롤이 활성화된다

상품을 장바구니에 담기

  • 각 상품에 장바구니 추가 버튼이 있다
  • 버튼 클릭 시 해당 상품이 장바구니에 추가된다
  • 추가 완료 시 사용자에게 알림이 표시된다

상품 검색

  • 상품명 기반 검색을 위한 텍스트 입력 필드가 있다
  • 검색 버튼 클릭으로 검색이 수행된다
  • Enter 키로 검색이 수행된다
  • 검색어와 일치하는 상품들만 목록에 표시된다

카테고리 선택

  • 사용 가능한 카테고리들을 선택할 수 있는 UI가 제공된다
  • 선택된 카테고리에 해당하는 상품들만 표시된다
  • 전체 상품 보기로 돌아갈 수 있다
  • 2단계 카테고리 구조를 지원한다 (1depth, 2depth)

카테고리 네비게이션

  • 현재 선택된 카테고리 경로가 브레드크럼으로 표시된다
  • 브레드크럼의 각 단계를 클릭하여 상위 카테고리로 이동할 수 있다
  • "전체" > "1depth 카테고리" > "2depth 카테고리" 형태로 표시된다

현재 상품 수 표시

  • 현재 조건에서 조회된 총 상품 수가 화면에 표시된다
  • 검색이나 필터 적용 시 상품 수가 실시간으로 업데이트된다

장바구니

장바구니 모달

  • 장바구니 아이콘 클릭 시 모달 형태로 장바구니가 열린다
  • X 버튼이나 배경 클릭으로 모달을 닫을 수 있다
  • ESC 키로 모달을 닫을 수 있다
  • 모달에서 장바구니의 모든 기능을 사용할 수 있다

장바구니 수량 조절

  • 각 장바구니 상품의 수량을 증가할 수 있다
  • 각 장바구니 상품의 수량을 감소할 수 있다
  • 수량 변경 시 총 금액이 실시간으로 업데이트된다

장바구니 삭제

  • 각 상품에 삭제 버튼이 배치되어 있다
  • 삭제 버튼 클릭 시 해당 상품이 장바구니에서 제거된다

장바구니 선택 삭제

  • 각 상품에 선택을 위한 체크박스가 제공된다
  • 선택 삭제 버튼이 있다
  • 체크된 상품들만 일괄 삭제된다

장바구니 전체 선택

  • 모든 상품을 한 번에 선택할 수 있는 마스터 체크박스가 있다
  • 전체 선택 시 모든 상품의 체크박스가 선택된다
  • 전체 해제 시 모든 상품의 체크박스가 해제된다

장바구니 비우기

  • 장바구니에 있는 모든 상품을 한 번에 삭제할 수 있다

상품 상세

상품 클릭시 상세 페이지 이동

  • 상품 목록에서 상품 이미지나 상품 정보 클릭 시 상세 페이지로 이동한다
  • URL이 /product/{productId} 형태로 변경된다
  • 상품의 자세한 정보가 전용 페이지에서 표시된다

상품 상세 페이지 기능

  • 상품 이미지, 설명, 가격 등의 상세 정보가 표시된다
  • 전체 화면을 활용한 상세 정보 레이아웃이 제공된다

상품 상세 - 장바구니 담기

  • 상품 상세 페이지에서 해당 상품을 장바구니에 추가할 수 있다
  • 페이지 내에서 수량을 선택하여 장바구니에 추가할 수 있다
  • 수량 증가/감소 버튼이 제공된다

관련 상품 기능

  • 상품 상세 페이지에서 관련 상품들이 표시된다
  • 같은 카테고리(category2)의 다른 상품들이 관련 상품으로 표시된다
  • 관련 상품 클릭 시 해당 상품의 상세 페이지로 이동한다
  • 현재 보고 있는 상품은 관련 상품에서 제외된다

상품 상세 페이지 내 네비게이션

  • 상품 상세에서 상품 목록으로 돌아가는 버튼이 제공된다
  • 브레드크럼을 통해 카테고리별 상품 목록으로 이동할 수 있다
  • SPA 방식으로 페이지 간 이동이 부드럽게 처리된다

사용자 피드백 시스템

토스트 메시지

  • 장바구니 추가 시 성공 메시지가 토스트로 표시된다
  • 장바구니 삭제, 선택 삭제, 전체 삭제 시 알림 메시지가 표시된다
  • 토스트는 3초 후 자동으로 사라진다
  • 토스트에 닫기 버튼이 제공된다
  • 토스트 타입별로 다른 스타일이 적용된다 (success, info, error)

심화과제

SPA 네비게이션 및 URL 관리

페이지 이동

  • 어플리케이션 내의 모든 페이지 이동(뒤로가기/앞으로가기를 포함)은 하여 새로고침이 발생하지 않아야 한다.

상품 목록 - URL 쿼리 반영

  • 검색어가 URL 쿼리 파라미터에 저장된다
  • 카테고리 선택이 URL 쿼리 파라미터에 저장된다
  • 상품 옵션이 URL 쿼리 파라미터에 저장된다
  • 정렬 조건이 URL 쿼리 파라미터에 저장된다
  • 조건 변경 시 URL이 자동으로 업데이트된다
  • URL을 통해 현재 검색/필터 상태를 공유할 수 있다

상품 목록 - 새로고침 시 상태 유지

  • 새로고침 후 URL 쿼리에서 검색어가 복원된다
  • 새로고침 후 URL 쿼리에서 카테고리가 복원된다
  • 새로고침 후 URL 쿼리에서 옵션 설정이 복원된다
  • 새로고침 후 URL 쿼리에서 정렬 조건이 복원된다
  • 복원된 조건에 맞는 상품 데이터가 다시 로드된다

장바구니 - 새로고침 시 데이터 유지

  • 장바구니 내용이 브라우저에 저장된다
  • 새로고침 후에도 이전 장바구니 내용이 유지된다
  • 장바구니의 선택 상태도 함께 유지된다

상품 상세 - URL에 ID 반영

  • 상품 상세 페이지 이동 시 상품 ID가 URL 경로에 포함된다 (/product/{productId})
  • URL로 직접 접근 시 해당 상품의 상세 페이지가 자동으로 로드된다

상품 상세 - 새로고침시 유지

  • 새로고침 후에도 URL의 상품 ID를 읽어서 해당 상품 상세 페이지가 유지된다

404 페이지

  • 존재하지 않는 경로 접근 시 404 에러 페이지가 표시된다
  • 홈으로 돌아가기 버튼이 제공된다

AI로 한 번 더 구현하기

  • 기존에 구현한 기능을 AI로 다시 구현한다.
  • 이 과정에서 직접 가공하는 것은 최대한 지양한다.

과제 셀프회고

과거에는 그래도 프로젝트를 진행하면서 설계를 해보려고 했었던 거 같은데, 입사한 이후에는 그냥 개발 요청이 들어온 사항들에 대해서만 처리를 하게 되다 보니까 설계에 대해서 아예 잊고 있었는ㄴ데 설계의 중요성에 대해서 다시 한 번 생각하게 된 시간인 것 같습니다!
그리고 커밋 꼭 중간중간 하는 습관을 들이자....! 하하...! 그리고 깃 공부도 열심히 해야할 것 같다 기본적인 사용법만 알고 있는 느낌,,!

기술적 성장

멘토링을 진행하면서 설계 중 디자인 패턴에 대해서 설명을 듣게 되고 한 번 자세히 공부해야겠다는 생각이 들어서 기존에 정보처리기사를 준비할 때도 디자인패턴은 버리고 다른 부분을 공부하기도 했었었는ㄴ데 디자인 패턴에 대해서 좀 더 심도있게 공부할 수 있었던 시간이었습니다!!!

자랑하고 싶은 코드, 개선이 필요하다고 생각하는 코드

image

깃 ci 보면서 이러지마만 30번 외쳤습니다 ^0^.....

저 무수한 컨플릭트 때문인 것 같기도한데 메인에서 풀 받으니까 변경할 게 없다고 뜨고 아아아아 ㅠㅠㅠㅠ

라우터 기반이나 store 등 준일 코치님이 강조하셨던 부분의 기능 구현을 못한 것도 아쉽기도 하지만 기존에 장바구니 부분, 무한 스크롤 부분 테스트 3개를 제외하고 전부 통과했었는데 lint 에러 때문에 깃에 커밋 자체가 안돼서 커밋하려고 수정하면서 뭔가 엄청난 빅 이슈가 발생하면서 ㅋㅋ큐ㅠㅠ 아무 테스트도 통과 못 한 뒤로 ai랑 제가 열심히 망친 코드라 부탁 드릴게 없습니다 ㅜㅜ,...

학습 효과 분석

간략하게 정리한 느낌이라서 성호 코치님이 추천해주신 책 기반으로 좀 더 깊이 공부하면 더 좋을 것 같다!!!

과제 피드백

과제는 좋았지만 router나 store 등을 구현하기엔 시간이랑 내 능력이 부족했다 ㅜㅜ...

AI 활용 경험 공유하기

제가 린트 에러 고치겠다고 코드 날려 먹은 건 맞지만 자꾸 깃에 저장한 거 없냐고 물어보는 ai가 너무너무너무 짜증났ㄹ습니다 ㅠㅠㅠㅠㅠㅠ

리뷰 받고 싶은 내용

@ongsim0629 ongsim0629 changed the title Init: PR용 빈 커밋 [7팀 신수빈] Chapter2-1. 프레임워크 없이 SPA 만들기 Nov 10, 2025
@ongsim0629 ongsim0629 marked this pull request as draft November 14, 2025 22:12
@ongsim0629 ongsim0629 marked this pull request as ready for review November 14, 2025 22:13
Copy link
Contributor

@JunilHwang JunilHwang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 피드백은 n8n + ai (gpt-5-mini)를 활용하여 자동으로 생성된 내용입니다.

전체적인 요약

이번 PR에서 SPA 기반의 상품/상세/장바구니 UI를 한 번에 구현하고자 했던 점은 잘 와 닿습니다. 그러나 main.js가 API 호출, 렌더, 이벤트 핸들링을 한 덩어리로 담고 있어서, 추가 필터(브랜드/가격대)나 키보드 접근성 기능을 넣을 때마다 변화가 기하급수적으로 될 위험이 큽니다. 특히 render() 내부에서 이벤트 핸들러를 정의·등록하는 패턴은 재랜더링 시 중복 리스너를 만들고 addToCart 결과를 잘못 해석하게 하는 버그까지 만들었습니다.

설계 피드백

  • 고수준에서 state를 분리해서 renderHome, renderDetail, bindEvents 등을 작은 책임 단위로 나누면 새 요구사항이 생겨도 render() 전체를 건드리지 않아도 됩니다.
  • 장바구니 모달과 페이지 전체 레이아웃을 관리하는 PageLayout에는 cartCount 같이 공유해야 하는 상태를 props로 내려주어 햄버거 게이지가 모든 페이지에서 일관되게 반영되도록 해주세요.
  • 이벤트 핸들러는 모듈 수준에서 한 번만 정의해서 중복 등록을 막고, 모달/키보드와 같은 전역 행동은 개별 함수(예: handleEsc)를 openCartModal/closeCartModal에서만 등록/삭제하는 식으로 개선해 보세요.

질문에대한 답변

추가 질문이 없어 답변할 내용이 없습니다.

}
showToast("구매 기능은 준비 중입니다.", "info");
});
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

문제 상황

브랜드 필터처럼 새로운 UI에서 click/change 이벤트를 처리하려면 handleClick, handleChange, handleKeyPress를 매번 렌더링 기준으로 붙이고 떼는 구조를 손봐야 합니다. 하지만 현재 render() 안에 이 핸들러들을 선언하고, 제거할 때도 같은 새 함수 참조로 제거하려 하기 때문에 이전 리스너가 절대로 제거되지 않고 누적됩니다.

현재 코드의 한계

  • handleClick/handleChange/handleKeyPress가 렌더링마다 새로 선언되므로 document.body.removeEventListener("click", handleClick)는 아무런 효과가 없습니다.
  • 필터를 바꿀 때마다 렌더가 다시 실행되어 중복 등록된 리스너가 계속 증가하면 복잡한 사용자 플로우에서 의도치 않은 동작(예: 검색 한 번에 여러 번 실행)과 메모리 누수가 생깁니다.
  • 브랜드 필터 등 추가 이벤트를 붙이게 되면 이 누적 문제가 더 심해집니다.

개선 구조

  • 핸들러는 모듈 수준에 한 번만 선언하고, [renderedClickHandler] 등 전역 참조에 보관하여 이전에 등록했던 리스너만 제거합니다.
  • render()에서는 addEventListener를 할 때 항상 같은 함수 참조(document.body.addEventListener("click", renderedClickHandler))를 사용합니다.
  • 이렇게 하면 새로운 필터 처리 로직을 handler 안에 추가하는 것만으로도 중복 없이 동작하고, popstate/스크롤 등에서도 안정적으로 제거할 수 있습니다.

코드 비교

// ❌ 현재 방식 (항상 새로운 함수)
const handleClick = (e) => { ... };
document.body.removeEventListener("click", handleClick);
document.body.addEventListener("click", handleClick);

// ✅ 개선 방식
const handleClick = (e) => { ... };
if (renderedClickHandler) {
  document.body.removeEventListener("click", renderedClickHandler);
}
renderedClickHandler = handleClick;
document.body.addEventListener("click", renderedClickHandler);

loadingIndicator.innerHTML = `
<div class="inline-flex items-center">
<svg class="animate-spin h-5 w-5 text-blue-600 mr-2" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

문제 상황

상품 카드에서 장바구니에 담으면 항상 실패 토스트가 뜹니다. /api/cart가 아니라 로컬 addToCart를 쓰고 있는데, 그 함수는 카트 배열을 반환할 뿐 success 속성을 포함하지 않습니다. 즉, result.successundefined가 되므로 항상 실패 분기로 빠집니다.

현재 코드의 한계

  • 성공/실패 조건이 if (result.success)인데, addToCart의 반환값에는 success 필드가 없습니다.
  • 따라서 실제로는 상품이 카트에 추가되어도 유저에게 "장바구니 추가에 실패했습니다." 라고 표시됩니다.
  • UI에서 상품 정렬이나 검색과 함께 호출되는 E2E에서 이 경고가 자주 뜨면 사용자 신뢰성과 QA 커버리지 모두 위축됩니다.

개선 구조

  • addToCarttrue/false 형태로 결과를 알려주도록 하거나, 여기에서는 Array.isArray(result) 또는 result.length 등으로 성공 여부를 판단합니다.
  • refreshCartModal/updateCartBadge를 바로 호출해서, 모달이 열려 있을 때도 최신 상태가 반영되도록 합니다.

코드 비교

// ❌ 현재 방식
const result = addToCart(product);
if (result.success) {
  showToast("장바구니에 추가되었습니다", "success");
} else {
  showToast("장바구니 추가에 실패했습니다.", "error");
}

// ✅ 개선 방식
const cart = addToCart(product);
if (Array.isArray(cart)) {
  showToast("장바구니에 추가되었습니다", "success");
  if (isCartModalOpen) refreshCartModal(true);
  updateCartBadge();
} else {
  showToast("장바구니 추가에 실패했습니다.", "error");
}

const badgeHtml = `<span id="cart-badge" class="absolute -top-1 -right-1 bg-red-500 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center">${count > 99 ? "99+" : count}</span>`;
cartBtn.insertAdjacentHTML("beforeend", badgeHtml);
}
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

문제 상황

장바구니 모달을 열면 Esc 키로 닫을 수 있도록 handleEsc를 붙이지만, 모달을 닫는 다른 경로(닫기 버튼·배경 클릭·페이지 이동 등)를 타면 이 리스너가 제거되지 않습니다. 모달을 여러 번 열 때마다 같은 키 이벤트가 다시 등록되면서 closeCartModal()이 여러 번 호출될 수 있습니다.

현재 코드의 한계

  • document.addEventListener("keydown", handleEsc)는 열릴 때마다 실행되지만, closeCartModal()이나 클릭으로 닫을 때 document.removeEventListener("keydown", handleEsc)를 호출하지 않습니다.
  • ESC로 닫을 경우에만 제거되기 때문에, 다른 방법으로 모달을 닫았으면 리스너가 남아있어 다음 모달에서 또 실행됩니다.
  • 키보드 단축(예: M 키를 눌러 모달 토글)같은 추가 요구사항도 이 구조를 그대로 따라하면 리스너가 더 쌓이기만 합니다.

개선 구조

  • handleEscopenCartModal에서만 등록하고, closeCartModal 내부에서 항상 document.removeEventListener("keydown", handleEsc)를 호출하도록 합니다.
  • 혹은 handleEsc를 모듈 레벨에 한 번만 선언해 isCartModalOpen 확인 후 실행하게 해서 중복 등록을 막습니다.

코드 비교

// ❌ 현재 방식
document.addEventListener("keydown", handleEsc);
const handleEsc = (e) => {
  if (e.key === "Escape" && isCartModalOpen) {
    closeCartModal();
    document.removeEventListener("keydown", handleEsc);
  }
};

// ✅ 개선 방식
const handleEsc = (e) => {
  if (e.key === "Escape" && isCartModalOpen) {
    closeCartModal();
  }
};

const openCartModal = () => {
  document.addEventListener("keydown", handleEsc);
};

const closeCartModal = () => {
  document.removeEventListener("keydown", handleEsc);
  ...
};

<h3 class="text-sm font-medium text-gray-900 line-clamp-2 mb-1">
${product.title}
</h3>
<p class="text-xs text-gray-500 mb-2">${product.brand || ""}</p>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

문제 상황

무한 스크롤 중 로딩 인디케이터/스켈레톤을 보여주기 위해 ProductListisLoadingMore prop을 준비해두셨는데, 이 값은 정작 render()에서 한 번도 true로 설정되지 않습니다. 그 결과 ProductList 자체의 로딩 UI는 절대 등장하지 않고, 스크롤 로딩은 DOM을 직접 조작하고 있습니다.

현재 코드의 한계

  • isLoadingMore가 실제 상태와 연결되지 않아서 컴포넌트 내의 loading/isLoadingMore 블록은 사용할 수 없습니다.
  • 나중에 IntersectionObserver 기반 스크롤 트리거나 skeleton 레이아웃을 추가하려면 render()에서 수동으로 DOM을 조작하는 현재 구조를 다시 손봐야 합니다.
  • 무한 스크롤 로직을 ProductList 안으로 옮겨 재사용하고 싶은 경우, 현재처럼 isLoadingMore가 무시되면 확장이 어렵습니다.

개선 구조

  • render()에서 추가 데이터를 요청하기 직전에 isLoadingMoreState = true/false를 유지하고, HomePage에게 isLoadingMore={isLoadingMoreState}를 넘겨서 <ProductList>가 스켈레톤을 알아서 렌더링하게 만듭니다.
  • 무한 스크롤 상태를 ProductList 내부로 추상화해서 scroll-triggerIntersectionObserver로 바꾸면 DOM 대신 isLoadingMore 상태만 바꾸면 됩니다.

코드 비교

// ❌ current
<ProductList {...} isLoadingMore={false} />

// ✅ improvement
const [isLoadingMore, setIsLoadingMore] = useState(false);
setIsLoadingMore(true);
const data = await getProducts(...);
setIsLoadingMore(false);
<ProductList {...} isLoadingMore={isLoadingMore} />

import { Header, Footer } from "../components/index.js";

export const PageLayout = ({ children }) => {
return /*HTML*/ `
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

문제 상황

PageLayout은 모든 페이지의 공통 레이아웃인데, Header()를 호출할 때 cartCount를 전달하지 않아서 상세 페이지(DetailPage)나 404 페이지에서는 장바구니 뱃지가 항상 0으로 보입니다. render()에서 커스텀 카운트를 받아도 PageLayout으로 전달되지 않습니다.

현재 코드 한계

  • HomePage에서는 Header(cartCount)라고 직접 넘기고 있지만, DetailPage/NotFoundPagePageLayout을 통해 같은 Header를 렌더링합니다.
  • PageLayout에서 Header()cartCount를 넣지 않기 때문에, 브라우저 뒤로/앞으로 이동해도 카운트가 갱신되지 않습니다.
  • 향후 cartCount를 다른 곳에서도 보여주고 싶다면 PageLayoutHeader 계층 구조를 건드려야 합니다.

개선 구조

  • PageLayoutcartCount prop을 추가하고, DetailPage/NotFoundPage로부터 render()가 전달하도록 합니다.
  • Header를 호출할 때 ${Header(cartCount)}처럼 항상 현재 카운트를 명시적으로 주입합니다.

코드 비교

// ❌ 현재 방식
export const PageLayout = ({ children }) => {
  return `
    <div>
      ${Header()}
      ...
    </div>
  `;
};

// ✅ 개선 방식
export const PageLayout = ({ children, cartCount = 0 }) => {
  return `
    <div>
      ${Header(cartCount)}
      ...
    </div>
  `;
};

(cat1) => /*HTML*/ `
<button
data-category1="${cat1}"
class="category1-filter-btn text-left px-3 py-2 text-sm rounded-md border transition-colors
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

문제 상황

handleClick에서는 search-button을 누르면 검색이 실행되도록 분기해놨는데, 실제 SearchForm markup에는 검색 버튼이 없습니다. Enter 키로는 검색이 되지만, spec대로 버튼 클릭을 지원해야 할 때 이 분기는 절대 통과하지 않습니다.

현재 코드 한계

  • document.body.addEventListener("click", ...)에서 if (e.target.id === "search-button")조건은 항상 false입니다.
  • 버튼을 추가하려면 레이아웃을 수정해야 하고, 버그 없이 버튼을 활성화하려면 기존 스크립트와 연동하는 작업이 뒤섞여 번거롭게 됩니다.

개선 구조

  • 입력폼에 <button id="search-button" type="button">검색</button>을 추가하고, 버튼 클릭 시 현재 search-input 값을 읽어 같은 로직을 재사용합니다.
  • 같은 동작을 handleClick에서만 처리하기보다는 submit 이벤트로 정리하면 검색 UI를 확장할 때도 쉽게 재사용할 수 있습니다.

코드 비교

<!-- ❌ 현재 없음 -->
<input .../>

<!-- ✅ 버튼 추가 -->
<div class="flex">
  <input ... />
  <button id="search-button" type="button">검색</button>
</div>

@@ -0,0 +1,120 @@
export const SearchForm = ({ filters = {}, categories = {} }) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

문제 상황

SearchForm-new.jsSearchForm.js가 거의 동일한 구현체인데 두 파일이 동시에 존재하고 둘 다 빌드에 남아 있습니다. 명확하게 어떤 파일이 참조되는지 파악하기 어려워 유지보수가 복잡해집니다.

현재 코드 한계

  • components/index.js에서는 export * from "./SearchForm.js", 하지만 SearchForm-new.js가 같은 디렉터리에 남아 있어서 실수로 잘못된 파일을 수정하거나 문서/테스트가 다른 버전을 참조할 수 있습니다.
  • 중복 파일은 번들 사이즈에 영향을 주고, 리뷰어나 새 동료가 어느 파일이 실제로 쓰이는지 찾기 어렵습니다.

개선 구조

  • 사용하지 않는 SearchForm-new.js를 삭제하거나 SearchForm.js에 통합한 다음, 코드베이스 전체에서 한 군데만 검색 폼을 유지합니다.
  • SearchForm을 완전한 컴포넌트로 캡슐화해서 HomePage가 props만 넘기도록 만듭니다. 그런 다음 새로운 필터(예: 브랜드)도 이 컴포넌트 안에서 처리하도록 하면 스페셜한 로직을 여러 곳에 흩어 놓지 않아도 됩니다.

const badgeHtml = `<span id="cart-badge" class="absolute -top-1 -right-1 bg-red-500 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center">${count > 99 ? "99+" : count}</span>`;
cartBtn.insertAdjacentHTML("beforeend", badgeHtml);
}
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

문제 상황

render() 함수 안에서 API 호출, 템플릿 생성, 이벤트 바인딩, URL 업데이트까지 모두 처리하고 있어 기능 추가(예: 브랜드 필터, 카테고리 태그) 때마다 메소드가 길어지고 이해하기 어려워집니다. 이 구조는 추후 GraphQL로 전환하거나 서버 렌더링을 도입할 때 큰 장벽이 됩니다.

현재 코드의 한계

  • 하나의 함수에서 getProducts, HomePage 렌더, setupDetailPageEvents, handleScroll을 모두 처리하고 있어서 변수를 공유할 수밖에 없습니다.
  • 샘플 추가 요구사항(브랜드 필터, 복수 카테고리 선택, 페이징 상태 공유 등)을 넣으려면 render()를 계속 크게 수정해야 하고, 단위 테스트를 작성하기도 어려워집니다.

개선 구조

  • render()fetchProducts(), renderHome(), bindHomeEvents()처럼 역할 별로 나눠서 각 함수가 하나의 책임만 가지도록 분리합니다.
  • 상태 관리(필터, 페이지, 카트)는 state 객체로 묶고, setState() 식으로 변경될 때마다 renderHome()만 호출하도록 해서 로직을 재활용할 수 있습니다.

결과 기대

이렇게 분리하면 스크롤 처리/URL 업데이트/항목 개수 선택을 각각의 모듈로 빼서 브랜드 필터나 정렬 조건을 추가할 때 엮이는 코드가 줄고, 테스트 용이성도 확보됩니다.

@@ -0,0 +1,98 @@
export const CartModal = ({ items = [], isOpen = false }) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

문제 상황

장바구니 모달은 div로 감싼 오버레이만 있고 role="dialog"/aria-modal="true" 같은 접근성 속성이 빠져 있습니다. Esc로 닫히긴 하지만 키보드 사용자나 스크린리더에게 "모달이 열렸다"는 신호가 전달되지 않습니다.

현재 코드의 한계

  • #cart-modal-overlayrole="dialog", aria-labelledby, aria-describedby를 대입하지 않아 스크린리더가 모달을 인식하지 못합니다.
  • 포커스 트랩이나 초기 포커스도 없어서 키보드만 사용하는 경우 body에 머물러 버립니다.
  • 향후 보조 키(예: Shift+Tab/Tab 순환)나 접근성 요구사항이 생기면 지금 구조로는 추가 비용이 많이 듭니다.

개선 구조

  • 모달에 role="dialog", aria-modal="true", aria-labelledby/aria-describedby를 추가하고, 닫기 버튼에 aria-label을 넣습니다.
  • 모달을 열 때 첫 포커스를 cart-modal-close-btn이나 첫 입력 칸으로 이동시키고, 닫을 때 이전 포커스를 복원해 줍니다.
  • document.addEventListener("focus", ...)를 붙여 Tab 체인을 모달 내부로 한정하는 식으로 키보드 포커스 순환을 적용하면 접근성을 유지하면서도 기능 확장이 수월해집니다.

이런 개선은 SPA 접근성을 충족시키고, 나중에 키보드 단축(예: M 누르면 모달 열림/닫힘)이나 내비게이션을 추가할 때도 기반을 제공합니다.

Copy link

@devchaeyoung devchaeyoung left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 피드백은 n8n + ai (gpt-5-mini)를 활용하여 자동으로 생성된 내용입니다.

이번 PR에서는 상품 목록, 장바구니, 상품 상세 페이지 등 주요 컴포넌트와 API가 기능적 요구사항에 맞춰 전반적으로 잘 구현된 점이 돋보입니다. 특히, 장바구니 로컬 스토리지 활용과 모달 UI, 무한 스크롤 로딩 상태에 대한 처리가 안정적으로 이뤄져 사용자 경험 개선에 기여하고 있습니다.👍

수빈님은 과거 설계 경험이 있으셨고, 이번 과제를 통해 디자인 패턴과 상태 관리에 대한 중요성 및 필요성을 다시 느끼셨습니다. 특히 설계에 대한 고민과 함께 Git 사용, 커밋 습관 개선에 노력을 기울이고 계신 점도 자연스러운 성장 과정이라 생각합니다. 리뷰에서 주요 포인트는 "상태와 UI가 명확하게 분리되어 있고 이벤트 위임이 적절히 활용된 점"과 "장바구니 API와 모달 UI 구조가 관심사 분리에 준하는 완성도를 갖췄다"는 점입니다.

구조적 총평으로써, 현재는 함수 단위 모듈화와 이벤트 위임이 장점이나, 렌더링 로직과 상태 관리, 이벤트 핸들러가 한 파일(src/main.js)에 다소 집중되어 있습니다. 이는 규모가 커질수록 유지보수와 확장성을 저해할 수 있으니, 앞으로는 컴포넌트 별로 렌더링, 상태, 이벤트를 명확히 분리하고 설계 패턴(예: MVVM, Observer) 적용을 고민해보시면 좋겠습니다.

질문에대한 답변

1. 질문 요약

수빈님께서는 최근 프로젝트에서 설계에 대한 고민이 줄고 단순 개발 요청 대응으로 제한되면서 설계 중요성에 대해 다시 생각하셨고, 디자인 패턴 공부 의욕이 생겼음을 말씀해주셨습니다. 또한 Git 사용 중 겪었던 충돌, 리팩토링 과정에서 난항을 겪은 경험을 공유해주셨으며, 멘토링과 학습 방향에 대한 피드백도 요청하셨습니다.

2. 현재 선택의 장단점

  • 장점: 프로젝트 기능 구현이 요구사항에 충실하고, 상태 관리와 API 분리가 어느 정도 잘 되어 있습니다. MSW를 통한 API mocking과 테스트 자동화도 적용되어 학습 효과가 큽니다.
  • 단점: 주요 로직이 src/main.js에 집중되어 있어 관심사 분리가 덜 된 느낌이고, 이벤트 핸들러가 각 UI 컴포넌트와 결합되어 있어 코드 복잡도가 증가할 소지가 큽니다. Git 충돌 이슈는 주로 병합 전략과 협업 플로우 운영 문제일 수 있습니다.

3. 실무에서라면 이렇게 설계할 것 같아요

  • 모듈화 및 관심사 분리: 렌더링, 상태 관리, 이벤트 처리를 분리하여 컴포넌트 단위로 책임을 명확히 구분합니다. 예를 들어 ProductList 컴포넌트는 렌더링에 집중하고, 상태 변경은 외부 store나 상태관리 모듈이 담당하도록 합니다.
  • 상태 관리 도입: 옵저버 패턴이나 간단한 Pub/Sub 구조를 도입해 상태 변경 시 UI가 자동 업데이트되도록 하면, 코드 복잡도를 줄이고 테스트 가능성을 높일 수 있습니다.
  • 라우팅과 상태 연동: SPA 라우터를 별도 모듈로 분리하여 URL 변경과 상태 복원을 관리하면 사용자 경험이 좋아지고 유지 관리도 편해집니다.
  • Git과 협업: 정기적 커밋과 PR 전략, rebase와 병합 전략 학습, 팀 내 코드 소유권 분배 등을 숙지하여 충돌 최소화 및 생산성 향상을 도모합니다.

4. 앞으로 구조를 잡을 때 참고하면 좋은 포인트

  • 컴포넌트와 상태 분리: UI 컴포넌트는 상태를 읽기만 하고, 상태 변경은 별도 컨트롤러가 담당하는 구조를 연습해보세요.
  • 함수형 프로그래밍과 순수함수 활용: UI 렌더링은 순수 함수로 작성해 사이드이펙트를 줄여보시면 좋습니다.
  • 디자인 패턴 공부: Observer, Mediator, Command, State 패턴을 간단한 SPA 구조에 적용해보면서 이해도를 높이세요.
  • 테스트가 용이한 구조: 각 기능별 유닛 테스트를 작성하기 좋은 모듈 구조, 독립적인 함수 구성을 지향하세요.
  • Git 활용: 브랜치 전략(Git Flow, GitHub Flow), 충돌 해결과 리베이스 등 기본기를 꾸준히 익히시면 작업 효율이 크게 올라갈 것입니다.

설계 공부를 하시면 개발 속도도 빨라지고, 코드 품질 및 유지보수성도 크게 개선될 거예요. 앞으로도 꾸준히 고민하고 기록해보시면 쌓이는 경험이 큰 자산이 될 것입니다. 조금 느리더라도 위 내용들 참고하시면서 차근차근 개선해 보시길 추천드립니다!


const rootElement = document.getElementById("root");
if (rootElement) {
Array.from(rootElement.children).forEach((child) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수빈님, 장바구니 모달을 열 때 modalHtmlroot에 직접 삽입하고, 기존 모달이나 다른 자식 요소는 숨기는 방식으로 처리한 점은 접근성 측면까지 고려한 좋은 시도입니다 👍 다만, 모달 삽입과 제거 시 DOM 조작이 반복되고 있어 별도 컴포넌트로 분리하거나 상태 관리와 일관성 있게 조작할 수 있도록 구조 개선을 생각해보시면 좋을 것 같아요.

document.addEventListener("keydown", handleEsc);

document.querySelectorAll(".quantity-increase-btn").forEach((btn) => {
btn.addEventListener("click", (e) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

홈페이지 렌더링 함수 내 이벤트 위임 방식 구현이 되어 있어서 중복적으로 여러요소에 이벤트를 바인딩하지 않고 전체 문서에서 이벤트를 처리하는 점이 깔끔해 보입니다. 특히 무한 스크롤 로딩 표시를 직접 DOM에 추가하는 부분도 UX를 고려하신 부분이에요 👍 다만, 이벤트 핸들러가 render 함수에 중복 정의되는 부분은 메모리 누수 우려가 있으니, 핸들러 함수들을 컴포넌트 외부나 클래스 메소드로 분리해보시면 좋습니다.

document.getElementById("cart-modal-checkout-btn")?.addEventListener("click", () => {
const cartItems = getCart();
if (cartItems.length === 0) {
showToast("장바구니에 상품이 없습니다.", "error");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상품 상세 페이지에서 수량 조절 버튼과 인풋 이벤트 핸들링 및 UI 업데이트 로직에서 상태와 UI가 적절히 동기화되어 있어서 사용자 경험 측면에서 안정적이에요. 다만, for 루프를 이용해 수량만큼 addToCart를 호출하는 방식은 장바구니 API에 수량 전달 기능과 통합하여 한 번의 호출로 처리할 수 있다면 성능과 로직 간결성이 좋아질 수 있어요.

@@ -0,0 +1,127 @@
const CART_STORAGE_KEY = "shopping_cart";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로컬 스토리지 기반 장바구니 API를 별도의 파일로 분리한 점이 좋습니다 👍 각 함수가 단일 책임 원칙에 맞게 잘 구성되어 있어 테스트와 유지보수에 용이해 보입니다. 다만 에러 메시지에 일부 한글 깨짐 현상이 있어, 콘솔 메시지를 UTF-8 인코딩에 맞게 수정해주시면 가독성 향상에 도움이 될 것 같아요.

@@ -0,0 +1,98 @@
export const CartModal = ({ items = [], isOpen = false }) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CartModal 컴포넌트가 받은 데이터(items)로 상태 계산(전체 선택 여부, 총합 등)을 내부에서 처리하며 HTML 문자열을 반환하는 구조가 깔끔합니다 👍 UI 상태에 따른 조건 렌더링이 잘 구현되어 있습니다. 다만, 이벤트 핸들러는 외부에서 따로 관리하고 있어 관심사 분리가 잘 되어있는데, 추후 상태 변경 시 뷰를 갱신하는 코드와 더욱 밀접하게 결합하면 재사용성과 확장성이 개선될 수 있어요.

@@ -0,0 +1,27 @@
export const Header = (cartCount = 0) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Header 컴포넌트에서 장바구니 아이콘 옆에 배지 형태의 장바구니 개수를 조건부로 렌더링하는 방식이 직관적이고 UI 상태 반영이 용이해 좋습니다. 👍 다만 cartCount가 기본값 없이 파라미터로 들어오는데 기본값을 명시하거나, 객체 파라미터로 변경해 가독성을 향상시키는 방법도 고려해보시면 좋을 것 같아요.

@@ -0,0 +1,82 @@
export const ProductList = ({ products = [], totalCount = 0, loading = false, isLoadingMore = false }) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상품 목록에 로딩 상태 시 스켈레톤 UI를 표시하고, 정상 로딩 시 상품 카드를 반복 렌더링하는 방식이 사용자 경험에 긍정적이에요. 👍 또한 무한 스크롤 로딩 시 로딩 인디케이터를 조건부로 넣은 부분도 좋습니다. 추후 성능과 유지보수를 위해 함수 또는 클래스 컴포넌트화하여 이벤트 핸들러와 상태를 결합하는 방향도 고려해보시면 좋습니다.

@@ -0,0 +1,43 @@
import { Header } from "../components/Header.js";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오류가 발생할 경우 에러 UI를 보여주고 재시도 버튼을 제공하여 UX를 고려한 점이 좋네요. 👍 다만 Header 호출 시 CartCount를 괄호없이 바로 넘기고 있는데, Header 컴포넌트가 객체 파라미터를 기대한다면 불일치가 있을 수 있어 확인해보시면 좋겠습니다.

@@ -0,0 +1,166 @@
import { PageLayout } from "./PageLayout";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상품 상세 페이지에서 별점과 관련 상품까지 조건부 렌더링이 잘 처리되어 있고, 브레드크럼과 수량 조절 UI 구성 등 UX 요소가 충실히 구현되어 있어 깔끔합니다. 👍 다만, 브레드크럼의 버튼 핸들링 등 이벤트 바인딩이 분리되어 있어 추후 유지보수와 테스트를 위해 컴포넌트화 또는 모듈화를 더 진행해보시면 좋겠습니다.

@@ -0,0 +1,40 @@
export function showToast(message, type = "success") {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사용자 피드백을 위한 토스트 메시지 기능이 단일 책임 원칙에 맞게 독립 함수로 잘 분리되어 있습니다. 👍 종류별 스타일과 닫기 버튼, 자동 사라짐 기능도 충실히 구현되어 있어 UX 측면에서 매우 긍정적입니다. 향후 여러 토스트 메시지가 겹치는 경우를 대비하거나, 공통 상태 관리 체계와 연결하면 더 발전할 수 있을 것 같아요.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants