Skip to content

Conversation

@hanseul524
Copy link

@hanseul524 hanseul524 commented Nov 9, 2025

과제 체크포인트

배포 링크

https://hanseul524.github.io/front_7th_chapter2-1/

기본과제

상품목록

상품 목록 로딩

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

상품 목록 조회

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

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

  • 드롭다운에서 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로 다시 구현한다.
  • 이 과정에서 직접 가공하는 것은 최대한 지양한다.

과제 셀프회고

저는 이번 과제를 통해 제일 배울점이 많은 사람이 아닐까 라고 스스로 생각합니다. 리액트에 익숙하지 않아서 리액트가 어떻게 동작하는지, SPA가 어떤 방식으로 페이지를 새로고침 없이 이동하는지 개념을 잡을 수 있는 과제였습니다.

리액트를 실무에서 써본 경험도 없고 혼자 공부하면서 배웠던 개념들이 조금은 막연하게 느껴졌었는데 직접 라우터와 상태관리를 구현해보면서 아직 100% 이해했다고 말할 순 없지만 (ㅜㅜ) ... 어느정도 감을 잡은 것 같습니다.

기술적 성장

1. 옵저버 패턴을 이용한 상태관리 로직 구현

  • 리액트의 상태관리를 Vanilla JS로 구현하는 방법 중 옵저버 패턴에 대해서 배우게 되었습니다. 옵저버 패턴이란 말을 .. 처음 접해보고 예제 코드를 보고도 이해가 가지 않아서 일단 만들고 observer - store - router - render 순으로 한통의 코드를 일단 작성하고 작동하는것을 보면서 이해했습니다. 결국 옵저버 패턴의 역할은 각 페이지가 필요한 state만 구독하고 있다가 state가 변경되면 해당 페이지의 render 함수를 호출하여 페이지를 업데이트하는 것이었습니다.
// observer pattern
export const createObserver = () => {
  const listeners = new Map(); // stateKey별 리스너 저장소

  return {
    subscribe(fn, stateKey = null) {
      // stateKey가 없으면 전체 구독, 있으면 특정 상태만 구독
      if (!listeners.has(stateKey)) {
        listeners.set(stateKey, new Set());
      }
      listeners.get(stateKey).add(fn);
    },
    unsubscribe(fn, stateKey = null) {
      // 이벤트 리스너 제거
      if (listeners.has(stateKey)) {
        listeners.get(stateKey).delete(fn);
      }
    },
    notify(stateKey) {
      const notifyListeners = new Set();

      if (listeners.has(null)) {
        listeners.get(null).forEach((fn) => notifyListeners.add(fn));
      }

      if (stateKey && listeners.has(stateKey)) {
        listeners.get(stateKey).forEach((fn) => notifyListeners.add(fn));
      }

      notifyListeners.forEach((fn) => fn());
    },
  };
};

2. localStorage를 이용한 장바구니 구현

  • 처음에는 state에 값을 담아서 장바구니 데이터를 유지하고 관리하려고 생각했는데 새로고침 시에도 데이터가 유지되어야 하기 때문에 localStorage를 사용하였습니다. localStorage에 데이터를 담아 관리하니 코드가 더 간결해지고 유지보수 측면에서 더 쉬워지지 않을까 ... 하고 생각합니다.
// localStorage 키
const CART_STORAGE_KEY = "shopping_cart";

// 장바구니에 상품 추가
export const addToCart = (product, quantity = 1) => {
  const cart = loadCartFromStorage();

  // 이미 장바구니에 있는 상품인지 확인
  const existingItemIndex = cart.findIndex((item) => item.productId === product.productId);

  if (existingItemIndex > -1) {
    // 이미 있으면 수량만 증가
    cart[existingItemIndex].quantity += quantity;
    console.log(`📦 기존 상품 수량 증가: ${product.title} (${cart[existingItemIndex].quantity}개)`);
  } else {
    // 없으면 새로 추가
    cart.push({
      ...product,
      quantity,
    });
    console.log(`✨ 새 상품 추가: ${product.title} (${quantity}개)`);
  }

  saveCartToStorage(cart);
  return cart;
};

// localStorage에 장바구니 데이터 저장하기
export const saveCartToStorage = (cartItems) => {
  try {
    localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(cartItems));
    console.log("✅ 장바구니 저장 완료:", cartItems);
  } catch (error) {
    console.error("장바구니 데이터 저장 실패:", error);
  }
};

3. 상태관리 리팩토링

  • 기존에 main.js에서 전역 구독을 하고 필요한 state가 아닌 모든 state를 변경하고 렌더링을 하는 방식으로 코드를 작성했습니다. 그걸 의도하고 짠게 아니고 ... 개념을 완벽하게 이해하지 못한 상태에서 냅다 작성했기 때문에 전역에서 상태 관리를 하고 있다는걸 인지하지 못하고 있었습니다 ^^ ...
// main.js
const main = () => {
  router.setup({
    "/": {
      component: HomePage,
      onEnter: () => {
        store.fetchProducts();
      },
    },
    "/products/:id": {
      component: DetailPage,
      onEnter: (props) => {
        store.fetchProductDetail(props.productId);
      },
    },
    "*": {
      component: NotFoundPage,
    },
  });
  // 상태 변경시 렌더
  store.subscribe(render);
  router.subscribe(render);
  • 각 페이지가 자신이 필요한 state만 구독하고 페이지를 떠날 때 구독 해제를 하는 방식으로 리팩토링을 했습니다.
// src/pages/HomePage.js

let renderFn = null;

export const HomePage = {
  init(render) {
    renderFn = render;
    
    // 이 페이지에서 필요한 state만 구독
    store.subscribe(renderFn, 'list.products');
    store.subscribe(renderFn, 'list.loading');
    store.subscribe(renderFn, 'list.error');
    store.subscribe(renderFn, 'list.categories');
    
    // 데이터 가져오기
    store.fetchProducts();
  },
  
  // 🟢 페이지 정리 (다른 페이지로 이동 시 실행)
  destroy() {
    if (renderFn) {
      store.unsubscribe(renderFn, 'list.products');
      store.unsubscribe(renderFn, 'list.loading');
      store.unsubscribe(renderFn, 'list.error');
      store.unsubscribe(renderFn, 'list.categories');
      renderFn = null;
    }
  },
  
  // 🟢 렌더링 (state 변경 시마다 실행)
  render() {
    const { loading, products, categories, filters, pagination } = store.state.list;
    
    return PageLayout({
      children: `
        ${SearchForm({ filters, pagination })}
        ${ProductList({ loading, products })}
      `,
    });
  },
  
  // 🟢 렌더링 후 실행 (이벤트 리스너 등록)
  mounted() {
...
};

개선이 필요하다고 생각하는 코드

  • main.js 이벤트 핸들러를 다 작성하는 방식이 아닌 각 컴포넌트에서 필요한 이벤트를 등록하고 해제하는 방식으로 이벤트 핸들러를 관리했습니다. 각 컴포넌트 별로 독립적으로 이벤트를 관리하기 위함이였는데 이벤트를 따로 분리해 관리하고 컴포넌트에서는 호출해서 사용하는 것이 더 구조적으로 나은 방식이 아닐까 하고 생각합니다.
// src/pages/DetailPage.js
let renderFn = null;
let eventHandlers = [];

export const DetailPage = {
  productId: null,

  // 페이지 초기화
  init(render, props) {
    renderFn = render;
    this.productId = props.productId;

    // detail state만 구독
    store.subscribe(renderFn, "detail.product");
    ...

    this.setupEventListeners();
    this.loadProductAndRelated();
  },

  // 이벤트 위임 함수
  setupEventListeners() {
    // 장바구니 아이콘 클릭 이벤트
    const cartIconHandler = (e) => {
      if (e.target.closest("#cart-icon-btn")) {
        console.log("장바구니 모달 열기");
        openCartModal();
      }
    };
    document.addEventListener("click", cartIconHandler);
    eventHandlers.push({ type: "click", handler: cartIconHandler });

  ... // 이벤트 핸들러 등록 로직
  },

  // 페이지 정리
  destroy() {
    console.log("🔴 DetailPage destroy 호출");
    if (renderFn) {
      ...// 구독 해제 로직
      // 이벤트 핸들러 해제
      eventHandlers.forEach(({ type, handler }) => {
        document.removeEventListener(type, handler);
      });
      eventHandlers = [];
    }
  },

  // 렌더링
  render() {
 ... // 상품 상세 페이지 렌더링 로직
  },

  // 렌더링 후 실행
  mounted() {
    console.log("✨ DetailPage mounted 호출");
  },
};

학습 효과 분석

과제 피드백

리뷰 받고 싶은 내용

4팀 코드리뷰

  • 장바구니 로직 구현할 때 cart.js, CartModal.js로 나누어서 관리하도록 설계했는데 결국 역할 분리는 store와의 분리가 목적이였습니다. cart.js에서는 받은 데이터를 처리하고 안에 있는 로직은 store를 거치지 않고 바로 호출할 수 있도록 짰는데 뭔가 불필요한 분리였는지 ..? 구조적으로 어떤지, 다른 분들은 어떻게 짜셨는지 궁금해용
  • 이벤트를 각 컴포넌트단에서 처리하도록 했는데 다른 분들은 이벤트 처리 어떻게 하셨는지 궁금합니다. 나름 분리해서 컴포넌트 독립적으로 운영되도록 했는데 이 방법이 어떤지 봐주세용

@JunilHwang JunilHwang force-pushed the main branch 3 times, most recently from c1cdc92 to 9b09aa3 Compare November 10, 2025 11:59
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 형태의 쇼핑몰 구현을 위해 다수의 핵심 기능(상품 목록, 검색 및 필터, 상품 상세, 장바구니, 토스트 메시지, 라우팅, 상태 관리 등)을 체계적으로 분리하고 구현한 점이 긍정적입니다.

특히, 전역 상태 관리(store)와 라우터(router), 그리고 컴포넌트 단위(UI 렌더링+이벤트)가 명확히 분리되어 있습니다. 또한 상태 옵저버 패턴 도입으로 필요한 부분만 리렌더링할 수 있도록 해 사용자 경험을 고려했습니다.

다만, 확장성이 요구되는 환경에서 상태 관리가 단일 store 내에 집중되어있고, 이벤트를 전역(document) 단계에서 관리하는 등 유지보수 및 성능 면에서 개선 여지가 있습니다.

권장 설계 개선 사항

  • 상태 관리는 도메인별(slice)로 분리하여 독립성, 재사용성 향상
  • 비즈니스 로직과 UI 상태 변경을 분리하여 책임을 명확히
  • 이벤트 처리는 하위 DOM에 위임하여 필요 범위 내에서만 이벤트 바인딩
  • 라우터 경로 처리 로직은 더 확장 가능하도록 정규표현식 또는 라우팅 라이브러리 도입 고려

이러한 개선으로 향후 추가 기능 출시, 유지보수, 다른 UI 프레임워크 적용 등 확장 요구 시 더 효과적으로 대응할 수 있습니다.## 장바구니 로직의 역할 분리 관련

cart.js에서 로컬 스토리지와 순수 상태 변환 로직만 처리하고, store에서 상태를 관리하는 방식은 나쁜 구조가 아닙니다. 이렇게 함으로써 순수 데이터 조작과 상태 관리 책임을 분리하여 각자 역할이 명확해집니다. 다만 실무에서는 다음 점들을 고려합니다:

  • cart.js 내 함수는 순수 함수로 유지하여 store와 연동 시 부작용 없이 결과만 반환하는 게 좋습니다.
  • 상태 변경은 store가 일괄 관리를 책임져서 상태 불일치 위험을 방지해야 합니다.
  • 만약 cart.jsstore 없이 독립 호출 가능하도록 하려면, 완전한 상태 저장소 역할을 하게 설계하는 게 필요합니다.

대부분 프로젝트에서 store 혹은 상태 관리 라이브러리(Redux, MobX 등)가 중추적인 역할을 수행하며 cart.js는 도우미 함수로 제한하여 유지보수가 편합니다.

이벤트 처리 방식에 관하여

컴포넌트 단위에서 이벤트를 처리하는 것은 좋은 시도입니다. 독립성 향상과 모듈성이 증대되기 때문입니다. 다만 다음을 추천합니다:

  • 이벤트 위임(event delegation)을 적극 활용하여 DOM 요소가 동적으로 교체되어도 이벤트 관리가 쉽도록 하세요.
  • 글로벌(document)대신 컴포넌트 루트 엘리먼트에 이벤트를 바인딩하세요.
  • 이벤트 핸들러 등록과 해제를 명확하게 하여 메모리 누수를 방지하고 의도치 않은 이벤트 중복 발생을 막으세요.
  • 이벤트 로직이 복잡해질 때는 개별 컴포넌트 내부에서 별도의 메서드나 클래스로 분리하는 것도 좋습니다.

총평하면, 지금처럼 컴포넌트 중심으로 이벤트를 모으되 위임과 함께 DOM 구조에 맞춘 바인딩과 해제 전략을 세우는 것이 가장 좋은 방법입니다.

filters: [],
pagination: [],
error: null,
limit: 20,
Copy link
Contributor

Choose a reason for hiding this comment

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

1. 문제상황 제시

구체적인 문제 상황:
추가 요구사항이나 기능 확장 시, 현재 store 내 상태가 깊은 중첩이 많고, 일부 상태 키별로 중복 코드가 많아 유지보수가 복잡해질 수 있습니다. 특히 상품 목록 필터, 페이징, 검색 조건 등이 list 하위에 모두 포함되어 있는데, 이것들이 모두 한 페이지에 종속적이어서 다른 페이지나 신규 기능에서 재사용하기 어렵습니다.

현재 코드의 한계:

  • 상태 트리 분리가 명확하지 않아 기능별로 독립적인 상태 관리가 어렵다.
  • 특정 상태 변경 후 fetchProducts 등 비즈니스 로직이 store 내부에 혼재되어 있음.
  • 상태 변경 함수 구현 시, 단일 책임 원칙 위배로 변경마다 수정을 요구할 가능성이 크다.

2. 근본 원인

핵심 문제:
전역 상태 관리가 기능별 모듈화 없이 단일 store 객체 내에서 복합 상태와 비즈니스 로직을 모두 처리하고 있어서 확장이 어려움.

왜 문제인가:
이 구조는 한 부분 수정이 전체 store에 영향을 미치고, 새로운 기능 추가나 코드 리팩토링이 번거워집니다. 또한 단일 상태 트리는 구독 관리 시에도 불필요한 컴포넌트 리렌더링을 유발할 수 있어 성능 저하를 불러올 수 있습니다.

3. 개선 구조

  • 현재는 store가 모든 상태와 로직을 관리하는 단일 모놀리식 구조입니다.
  • 개선안은 상태를 기능별로 분리된 slice 또는 모듈로 나누고, 각 모듈이 독립적으로 상태와 비즈니스 로직을 관리하도록 함.
  • 비즈니스 로직도 사이드 이펙트가 필요한 부분과 순수 상태 변경을 분리.

현재 구조:

store
 ├─ state
 │    ├─ list { products, categories, loading, filters, error, ... }
 │    ├─ detail { product, loading, relatedProducts, error }
 │    └─ cart { items, total, count }
 ├─ fetchProducts, fetchProductDetail, ... (비즈니스 로직과 상태 변경 혼합)
 └─ setState

개선된 구조:

store/
  ├─ listStore.js (상품 목록 관련 상태+로직)
  ├─ detailStore.js (상품 상세 상태+로직)
  └─ cartStore.js (장바구니 상태+로직)

각 모듈이 옵저버 패턴을 자체 관리하거나 중앙 store에서 합쳐짐.
비즈니스 로직 함수는 외부 서비스/API 호출과 순수 상태 변경을 분리.

개선 사항:

  • 상태를 기능 단위로 분리하여 모듈화 및 유지보수 용이
  • 비즈니스 로직은 비동기 API 호출과 상태 변경을 분리
  • 필요한 컴포넌트만 구독하도록 상태 키를 상세하게 분할
  • 상태 변경 함수의 재사용성 및 테스트 용이성 향상
// ❌ 현재 방식
setState(key, value) {
  const [topKey, nestedKey] = key.split('.');
  if (nestedKey) {
    this.state[topKey] = { ...this.state[topKey], [nestedKey]: value };
  } else {
    this.state[key] = value;
  }
  observer.notify(key);
}

// ✅ 개선 예시
class ListStore {
  constructor() {
    this.state = { products: [], categories: [], loading: false, ... };
    this.observer = createObserver();
  }

  setLoading(status) {
    this.state.loading = status;
    this.observer.notify('loading');
  }

  async fetchProducts() {
    this.setLoading(true);
    // fetch logic
    this.setLoading(false);
  }

  subscribe(fn, key) {
    this.observer.subscribe(fn, key);
  }
}

export const listStore = new ListStore();

@@ -0,0 +1,95 @@
// localStorage 키
Copy link
Contributor

Choose a reason for hiding this comment

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

1. 문제상황 제시

구체적인 문제 상황:
현재 cart.js는 로컬 스토리지에 특화된 장바구니 로직(불러오기, 저장, 수량 조절 등)만 수행합니다. store는 이들을 호출해 상태를 관리합니다. 하지만 cart.js가 저장소와 매우 밀접하여 단독으로 호출하는 구조가 실제로는 store와 굉장히 긴밀히 연결됨에도 역할이 모호해보입니다.

현재 코드의 한계:

  • cart.js가 저장소 역할과 비슷해 보이며, store와 명확한 역할 분리가 어렵습니다.
  • cart.js 함수들이 상태 변경 후 임의로 저장까지 하므로, 여러 참조점에서 상태 불일치 가능성이 존재합니다.
  • 다른 모듈에서 바로 호출하면 store.state와의 동기화 이슈가 발생할 수 있음.

2. 근본 원인

핵심 문제:
장바구니 저장소와 상태 관리(reactive 상태 또는 전역 상태)가 분리되었으나, 역할 경계가 명확하지 않고 동시에 store 상태와 로컬스토리지 둘을 동시에 관리하려다보니 중복과 복잡성이 증가.

왜 문제인가:
이 구조는 추후 상태 관리 방식 변경 시 불편함이 따르고, 여러 소스에서 저장소를 조작하면 store.state와 로컬스토리지 간 불일치 위험이 큽니다. cart.js를 완전히 독립된 저장소 모듈로 설계하거나 store 내부 장바구니 관리만 담당하도록 통합하는 것이 합리적입니다.

3. 개선 구조

현재 구조:

cart.js: 로컬스토리지 조작 및 장바구니 데이터 변형
store.js: cart.js 함수 호출해 상태 동기화, UI에 반영

양쪽 모두 장바구니 상태와 관련된 역할 수행

개선된 구조:

  • cart.js는 단순한 데이터 변환 및 로컬스토리지 관리 기능만 담당, 상태 관리 함수 호출하지 않도록 순수 함수화
  • store.js 내 장바구니 상태 및 비즈니스 로직 총괄 관리, 외부에서 직접 cart.js를 호출하지 않고 store를 통해서만 변경
  • 혹은 cart.js를 완전 독립된 State 관리 저장소로 재설계 후 store가 해당 모듈을 구독하는 역할로 변경

개선 사항:

  • cart.js는 순수 기능 함수 모듈로 유지
  • 상태는 store가 담당하고, cart.js의 함수 결과를 받아서 한 번에 상태 갱신
  • 꼭 필요한 경우 storecart.js 내부 상태를 직접 관리하게 리팩토링

document.addEventListener("click", addToCartHandler);
eventHandlers.push({ type: "click", handler: addToCartHandler });

// 상품 카드 클릭 이벤트 (상품 이미지나 정보 클릭 시)
Copy link
Contributor

Choose a reason for hiding this comment

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

1. 문제상황 제시

홈페이지 이벤트 핸들러가 setupEventListeners 메서드에 모두 모여 있고, 이 이벤트들을 찾고 해제하는 로직이 handler 배열에 저장되어 있습니다. 이렇게 이벤트를 페이지 단위에서 모아 관리하는 방법과 각 컴포넌트가 독립적으로 이벤트를 관리하는 방식이 혼용될 수 있는데, 유지보수와 확장성 측면에서 다소 복잡할 수 있습니다.

현재 코드의 한계:

  • 이벤트가 모두 글로벌(document)에 바인딩되어, 특정 UI 컴포넌트가 분리된 독립성을 가지기 어려움
  • 이벤트 핸들러 중복 혹은 충돌 가능성 존재
  • 이벤트 바인딩, 제거가 수동이며 실수하기 쉬움

2. 근본 원인

핵심 문제:
DOM 이벤트 처리가 전역(document) 단위에서 집합적으로 관리되어, 컴포넌트 독립성과 재사용성을 해침

왜 문제인가:
컴포넌트 단위 재사용 및 확장 시 이벤트가 컴포넌트 내부에서 캡슐화되지 않아 전체 애플리케이션 영향을 받을 수 있는 구조가 됨. 또한 이벤트 처리 변경 시 로직 분리와 테스트가 어려울 수 있습니다.

3. 개선 구조

현재 구조:

HomePage.setupEventListeners() {
  document.addEventListener('click', ...);
  document.addEventListener('change', ...);
  // etc
}

개선된 구조:
컴포넌트 단위 혹은 DOM subtree 단위로 이벤트 위임과 핸들러를 등록하고, 컴포넌트가 필요로 하는 이벤트만 등록/해제

개선 사항:

  • 컴포넌트 단위 내부 이벤트 위임 및 핸들러 분리
  • DOM container를 기준으로 이벤트 위임 (예: #products-grid 내에서만 처리 등)
  • 이벤트 핸들러는 메서드 혹은 클래스로 모듈화
  • 이벤트 등록 및 해제 전용 유틸 함수 도입 가능
// ✅ 개선 예시
const productsGrid = document.getElementById('products-grid');
productsGrid.addEventListener('click', (event) => {
  if (event.target.matches('.add-to-cart-btn')) {
    // 처리
  }
});

eventHandlers = [];
}
},

Copy link
Contributor

Choose a reason for hiding this comment

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

4. 가독성 문제 및 기타 개선 사항

  • 상태 구독 함수의 반복적인 나열은 중복이므로 배열 등으로 관리하여 반복문으로 개선하면 가독성이 좋아집니다.
  • 이벤트 핸들러 등록 시 eventHandlers 배열 사용은 관리는 좋으나, 클로저에 의존하지 말고 메서드 형태로 관리하는 게 유지보수에 더 좋습니다.
  • 각 이벤트 처리 함수에서 e.target.closest(...) 검사를 반복하는데, 이벤트 위임 대상으로 묶어서 처리하고 함수 분리 필요.
  • console.log 디버깅용 코드는 배포 전 제거하거나, 환경별 로깅 유틸을 도입하는 게 바람직합니다.
  • HTML 생성 시 템플릿 내 긴 문자열과 복잡한 로직을 분리해 가독성 향상 가능.
  • 네임스페이스를 고려해 컴포넌트 내 변수명, 이벤트 데이터 속성명을 일관되게 관리.
  • parseInt에 진수를 명시해주면 의도하지 않은 오류를 막을 수 있습니다.
  • event.stopPropagation() 위치가 적절한지 검토 필요합니다.
  • 미사용 변수와 주석 정리 필요.

// router 초기화
export const router = {
routes: {},
currentPath: getPathWithoutBase(),
Copy link
Contributor

Choose a reason for hiding this comment

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

5. 라우터 경로 분기 개선

  • getPageConfig() 내에서 라우팅 경로를 문자열 직접 비교하는 방식은 확장성과 관리성이 떨어집니다.
  • 라우트별 파라미터 추출 로직을 분리하고 정규표현식 혹은 라우팅 라이브러리 스타일로 개선하면 유지보수가 쉬워집니다.
  • 현재 코드는 경로가 많아질 경우 분기가 복잡해질 위험이 있습니다.

6. URL 쿼리 파라미터 관리 개선

  • 쿼리 파라미터 관리 코드가 중복되거나 약간의 기능별로 여러 메서드가 분산되어 있는데, 유틸 함수의 추가 추상화로 가독성 향상 가능.
  • 쿼리 파라미터 상태 동기화 시 pushState, replaceState의 사용이 혼용되어 불일치 발생 가능성 있음.
    상태 관리 측에서 명확히 제어하는 것이 좋습니다.


// 카테고리 브레드크럼 이벤트
const breadcrumbClickHandler = (e) => {
const breadcrumbLink = e.target.closest(".breadcrumb-link");
Copy link
Contributor

Choose a reason for hiding this comment

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

7. 이벤트 관리 및 퍼포먼스 개선

  • 현재 이벤트 리스너가 모두 전역 document에 붙는 방식임
  • 비슷한 이벤트들을 묶고, 필요에 따라 이벤트 위임 방식을 사용하여 콜백 개수 최소화 가능
  • 이벤트 핸들러 배열 관리와 제거 방식은 좋으나, 여러 페이지가 공존할 경우 각각 이벤트 간섭이 있을 수 있으니 유의 필요
  • 이 부분을 컴포넌트 별 독립적 마운트/언마운트로 개선 시 메모리 누수 감소 및 유지보수 편리

8. 수량 변경 동작

  • 수량 증감 UI와 데이터 일관성 체크할 때 UI 상 한도(최소1, 최대재고 등)와 실제 상태 관리가 잘 매칭되어야 함
  • 현재는 UI input의 disabled 처리도 고려 가능

focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
Copy link
Contributor

Choose a reason for hiding this comment

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

9. 선택된 카테고리 UI 표시 개선

  • 검색 폼 내 브레드크럼 UI 렌더링 시 중복된 조건 부가 표시가 많음
  • 조건별 변수를 미리 계산해 중복방지 및 코드 단순화 가능
  • 2depth 카테고리 존재 여부나 선택 상태에 따른 CSS 변경을 좀 더 체계적으로 처리 가능

</div>
`;
};

Copy link
Contributor

Choose a reason for hiding this comment

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

10. 상품 카드 UI 성능 및 접근성 개선

  • 상품 이미지에 alt 속성 잘 사용하였으나 이미지가 커질 경우 lazy-loading 기본만으로는 렌더링 시 렉 발생 가능
  • IntersectionObserver 기반 lazy-loading 보완 추천
  • 상품명, 가격 등의 텍스트 데이터는 명확히 문서 구조에 맞게 마크업되어 있어 접근성에 긍정적
  • 버튼에 aria-label 또는 role 추가하면 더 좋음

<p class="text-sm font-medium">${message}</p>
<button class="toast-close-btn flex-shrink-0 ml-2 text-white hover:text-gray-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
Copy link
Contributor

Choose a reason for hiding this comment

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

11. 사용자 경험(UX) 개선 제안

  • 현재 토스트는 3초 후 자동 사라짐과 닫기 버튼 제공함
  • 다중 토스트 메시지 동시 표시 시 토스트 간 적절한 spacing과 중첩 방지 로직 도입 권장
  • 토스트 애니메이션에 접근성 고려해 motion 개수를 줄이거나, 사용자가 애니메이션 제한 설정 시 대응 가능하도록 하면 좋음

@@ -0,0 +1,37 @@
// observer pattern
Copy link
Contributor

Choose a reason for hiding this comment

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

12. 옵저버 패턴 구현 개선

  • 옵저버가 listeners를 Map of Set으로 적절히 구현하여 중복 등록을 방지한 점 훌륭합니다.
  • 다만 notify 메서드가 현재는 동기 실행인데, 비동기 처리가 필요한 경우 Promises를 활용 고려 가능
  • 이벤트 버스 패턴과의 구분 및 한계점 명확히 하는 주석 추가 권장

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에서는 Vanilla JS로 직접 SPA 라우터, 옵저버 패턴 기반 전역 상태관리, 컴포넌트 단위 UI 구현 등이 잘 통합된 구조로 구현되었고, 특히 상태별 구독 관리와 이벤트 위임을 각 페이지에서 체계적으로 관리한 점이 인상적입니다.👍

한슬님은 옵저버 패턴, localStorage 활용 장바구니 구현, 페이지별 상태 구독 분리 등 많은 기술적 성장을 이루셨습니다.바닐라 JS로 리액트 핵심 개념을 체득하는 매우 좋은 경험이 되셨을 겁니다.

추가질문에서 언급한 cart.jsCartModal.js 역할 분리에 대해서는, 현재의 분리 방식이 책임 분리 원칙에서는 적절하나 장바구니 상태를 메모리 캐시에 유지하며 불필요한 I/O를 줄이는 방향으로 리팩토링을 고려하는 것이 좋겠습니다. 이벤트 핸들링 역시 각 컴포넌트 단위에서 이벤트 위임이나 그룹핑 등으로 관리 수준을 더 높이면 유지보수성과 확장성에 도움이 될 것입니다.

전반적으로 관심사 분리와 모듈화가 잘 되어 있고, SPA 특성에 맞게 URL과 상태를 연동해 설계되어 있어 현실적인 프로젝트에서 유지보수·확장성 측면에서도 튼튼한 구조로 평가됩니다.

질문에대한 답변

1. 질문 요약

한슬님께서는 cart.jsCartModal.js로 역할을 분리한 구조가 적절한지, 그리고 이벤트 핸들링을 각 컴포넌트 단위에서 관리하는 방식이 좋은지에 대해 고민 중이십니다.

2. 현재 선택의 장단점

  • 장바구니 로직 분리: cart.js는 로컬스토리지 입출력과 데이터 조작에 집중하고, store가 UI 상태를 관리하는 분리 방식은 책임 분리 원칙에 부합하며 유지보수가 용이합니다. 다만, 매번 로컬스토리지를 읽고 쓰는 방식은 빈번한 I/O로 성능에 영향을 줄 수 있습니다.
  • 이벤트 처리 분리: 각 컴포넌트가 독립적으로 이벤트를 등록해 관리하는 것은 컴포넌트 재사용과 유지보수에 긍정적입니다. 그러나 이벤트 핸들러가 중복 등록되거나, 전역 document에 지나치게 많이 등록될 경우 관리가 어려워질 수 있습니다.

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

  • 장바구니 상태 관리: store 내 메모리 상태를 중심으로 조작하고, 로컬스토리지 동기화는 델리게이션 하여 빈번한 저장/불러오기를 줄이는 구조 권장합니다. 즉, cart.js는 순수 로직과 저장 기능만 제공하고, 상태는 store에서 일원 관리합니다.

  • 이벤트 관리: 이벤트 위임 방식을 적극 활용하거나, 컴포넌트별로 이벤트 핸들만 모아 관리하는 별도의 이벤트 매니저 패턴 적용을 고려합니다. React와 같은 프레임워크는 가상 DOM과 이벤트 버블링을 최적화하므로, Vanilla JS에서도 이 원칙을 참고하면 좋습니다.

  • 추가 구조 개선 제안

    • [개선사항1] 장바구니 상태를 store가 메모리 단위로 가지고, 변동 시에 로컬스토리지에 한 번만 저장.
    • [개선사항2] UI 이벤트는 컴포넌트 범위 내에 위임하여 등록, 불필요한 이벤트 리스너 최소화.
    • [개선사항3] 이벤트 등록/해제를 헬퍼 등 공통 모듈로 분리하여 중복 감소.

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

  • 상태와 UI 관련 로직을 엄격히 분리하면, 코드 재사용성과 테스트 용이성이 높아집니다.
  • 이벤트는 최대한 델리게이션(위임)으로 처리하여 메모리 누수와 성능 저하를 방지할 수 있습니다.
  • 상태 변경 시 번복되지 않는 불변성을 유지하면 예기치 않은 부작용을 예방할 수 있습니다.
  • SPA 특성에 맞게 URL과 상태를 자연스럽게 동기화하고, 상태 복원 로직을 명확히 하면 사용자 경험이 더 좋아집니다.

한슬님, 과제 진행 고생 많으셨고, 이번 경험이 SPA 및 상태관리 개념 이해에 큰 도움이 되었으리라 생각합니다. 앞으로 구조적 설계 경험을 계속 쌓으며 리팩토링과 코드 유지보수에 집중해보시면 좋겠습니다!

@@ -0,0 +1,95 @@
// localStorage 키

Choose a reason for hiding this comment

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

한슬님, cart.js에서 순수 장바구니 로직(상품 추가, 삭제, 수량 변경, 로컬 스토리지 입출력)을 분리하신 점이 책임 분리 측면에서 적절합니다! 👍 데이터 조작과 저장만 담당하고 store의 상태 업데이트는 호출하는 곳에서 처리하도록 분리해 사용성도 좋습니다.

다만, cart.js 함수들이 매번 로컬스토리지에서 데이터를 불러오고 저장하는 방식을 개선하면 좋을 것 같아요. 예를 들어, 매번 loadCartFromStorage를 호출하면 빈번한 I/O가 발생할 수 있는데, 이런 부분을 store가 메모리 상태를 갖고 있으니 그 상태를 주로 조작하고, 저장은 한 곳에서 일괄 처리하는 방식도 생각해볼 수 있습니다.

이 부분을 개선하면 상태 일관성 유지가 더 쉬워지고, 성능 측면에서도 이점이 있을 것 같아요.

@@ -0,0 +1,355 @@
import { store } from "../core/store.js";

Choose a reason for hiding this comment

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

이벤트 핸들러를 Modal 컴포넌트 내에 독립적으로 등록하고 해제하는 방식이 깔끔하게 이벤트 관리를 분리하는 좋은 접근이에요. 👍

하지만 모든 DOM 이벤트를 직접 등록·해제하는 코드가 좀 길고 복잡하게 느껴질 수 있으니, 필요하다면 헬퍼 함수로 반복되는 로직(예: 이벤트 등록 배열 관리)을 추상화해보는 것도 고려해보시면 좋겠습니다.

그리고 모달 관련 이벤트가 많기 때문에 이벤트 위임 방식을 활용하면 개별 요소별로 중복된 이벤트 등록을 줄일 수 있어요. (예: 모달 내 컨테이너에 하나의 클릭 이벤트를 걸고 e.target으로 분기 처리). 이 방법은 이벤트 리스너 수를 줄이고 성능과 유지보수 관점에서 효과적입니다.

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

Choose a reason for hiding this comment

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

한슬님께서 HomePage 내부에서 필요한 상태들만 구독하고, 페이지 초기화 시점에 URL과 상태 동기화 로직을 잘 구현하셨네요. 👍 특히, 쿼리 파라미터를 상태와 연동하여 SPA URL 관리가 잘 되어 확장성을 잘 고려한 설계입니다.

추가로 고민해볼 점은, 이벤트 핸들러가 페이지 스코프에 전역으로 등록되어 있는데, 이 경우 이벤트 핸들러들이 많아지면 관리가 어려울 수 있습니다. 위에서 언급한 이벤트 위임 및 컴포넌트 단위 이벤트 핸들링과 함께, 컴포넌트별로 그룹핑해서 등록/해제가 좀 더 체계적으로 이루어지면 유지보수가 쉬워질 겁니다.

@@ -0,0 +1,159 @@
import { ProductDetail } from "../components/index.js";

Choose a reason for hiding this comment

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

DetailPage에서 필요한 상태만 골라서 구독하고, 페이지 라이프사이클에 맞춰 이벤트를 등록/해제하는 부분이 매우 적절합니다👍. 특히 eventHandlers 배열을 활용해 등록한 이벤트를 체계적으로 해제하는 점이 인상적이에요.

다만 loadProductAndRelated 메서드 내부에서 관련 상품 로드 후 상태 관리를 store에서 분리하는 것도 점검하면 좋겠습니다. 예를 들어, 관련 상품이 많아지면 페이징 처리가 필요할 수 있고, 이 때 비동기 흐름 처리를 개선할 필요가 있을 거예요.

이벤트 핸들링 측면에서도 마찬가지로, 이벤트 위임과 분리된 UI 컴포넌트 단위로 관리하는 방향을 계속 발전시켜보시면 좋겠습니다.

},

navigate(path) {
const base = import.meta.env.BASE_URL || "/";

Choose a reason for hiding this comment

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

라우터에서 URL 쿼리 파라미터를 잘 다루고, SPA 라우팅과 라이프사이클 관리(이전 페이지 destroy, 새 페이지 init 호출)를 명확히 구현해서 사용성, 유지보수성에 좋습니다👍.

다만 추후 요구사항 확장 (예: 권한 관리, 비동기 라우팅, 라우트별 미들웨어) 시 확장성을 조금 더 고려해 라우트 구성을 객체형이나 함수형으로 만드는 방법도 생각해볼 수 있겠어요.

@@ -0,0 +1,147 @@
// 전역 상태 관리

Choose a reason for hiding this comment

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

옵저버 패턴 기반으로 상태를 세부 키 단위로 분리하여 구독/알림하는 구조가 잘 잡혀 있습니다. 👍

한 가지 아쉬운 점은 setState 내에서 상태 변경 시 불변성을 완벽히 지키지 않고 합치는 부분인데, 예를 들어 중첩 상태 객체를 다룰 때 깊은 복사가 미흡할 수 있습니다. 이는 향후 상태가 더 복잡해졌을 때 의도치 않은 부작용을 유발할 수 있으니, immer같은 라이브러리를 참고하거나 깊은 복제 혹은 immutable 방식 적용을 고민해봐도 좋습니다.

또한, fetchProductsfetchCategories등 비동기 함수에서 에러 상태 관리를 잘 처리한 점이 좋네요.

const Loading = /* html */ `
<div class="text-center py-4">
<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">

Choose a reason for hiding this comment

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

상품 리스트와 스켈레톤 로딩 UI가 명확히 구분되어 있어 사용자 경험 측면에서 좋아 보입니다👍.

다만 ProductItem 함수 내 HTML에 중복된 클래스명과 이벤트 위임이 중복될 가능성이 있으니, 추후에 이벤트 리스너를 컴포넌트 단위로 분리하거나 프레임워크 기반으로 관리하게 된다면 유지보수가 더욱 용이해질 것입니다.

@@ -0,0 +1,100 @@
// 토스트 컴포넌트

Choose a reason for hiding this comment

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

토스트 메시지 시스템이 타입별 스타일링과 자동 제거 기능까지 잘 구현되어 사용자의 피드백 경험을 세심하게 다룬 점 좋습니다👍.

버튼 이벤트 등록 시 중복 등록 체크 또는 단일 인스턴스 제어 같은 확장성을 위해 좀 더 세밀한 관리가 가능하면 좋겠으나, 현재는 과제 요구에 충분히 부합합니다.

@@ -0,0 +1,128 @@
const categoryLoading = /* html */ `

Choose a reason for hiding this comment

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

검색 및 카테고리 필터 UI가 명확하고, 상태에 따른 렌더링 조건과 선택 상태 조작이 잘 설계되어 있습니다👍.

추가로, 이벤트 모음이 많아질 경우 초반부터 각 필터별 컴포넌트로 분리해 이벤트 담당을 명확히 하면 좋겠는데, 현재는 적절한 수준입니다.

또한 카테고리 옵션 렌더링에 대한 성능 최적화(예: memoization, 렌더링 최소화)도 장기적으로 고려할 수 있겠습니다.

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