hyunzsu 님의 상세페이지[8팀 현지수] Chapter 1-1. 프레임워크 없이 SPA 만들기

과제 체크포인트

배포 링크

https://hyunzsu.github.io/front_6th_chapter1-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를 구현하는 것이 처음이어서 처음에 어떤 것부터 시작해야 될지 몰랐고, 막막했습니다. 상태관리 → 라우터 → 컴포넌트 순서로 차근차근 접근하면서 전체적인 SPA 구조를 이해할 수 있었습니다.

기술적 성장

  • 테스트 코드
  • 이벤트 위임 방식
  • 브라우저 API 활용

자랑하고 싶은 코드

1. Redux-like 상태 관리 패턴: 순수 함수 기반 리듀서와 액션 생성자 구현

복잡한 상태 관리를 어떻게 효율적이게 구현할 수 있을지 고민이 많았습니다. 초기에는 전역 변수나 DOM 직접 조작으로 상태를 관리하려 했지만, 컴포넌트 간 데이터 공유와 상태 동기화 문제가 발생했고, 애플리케이션이 복잡해질수록 상태 변화를 추적하기 어려워졌습니다.

// stores/index.js - 단방향 데이터 흐름을 위한 Store 구현
export const createStore = (reducer, initialState) => {
  let state = initialState;
  const listeners = [];
  
  const getState = () => state;
  const dispatch = (action) => {
    state = reducer(state, action);
    listeners.forEach(listener => listener());
  };
  const subscribe = (listener) => {
    listeners.push(listener);
    return () => { /* unsubscribe */ };
  };
  
  return { getState, dispatch, subscribe };
};

설계 이유:

  • 상품 목록, 상품 상세, 라우팅, 로딩 상태 등 모든 애플리케이션 상태를 한 곳에서 관리하여 데이터 일관성을 보장하고 상태를 한눈에 파악할 수 있게 했습니다.
  • 모든 상태 변경이 action을 통해서만 일어나도록 하여 디버깅과 추적이 용이하며, 특히 상품 로딩, 무한 스크롤, 페이지 전환 등 복잡한 상태 변화를 명확하게 관리할 수 있습니다.
  • Action → Reducer → State → View 순서로 데이터가 흐르도록 하여 상태 변화의 원인을 쉽게 추적할 수 있습니다.

실제 적용 사례:

// stores/reducer.js - 무한 스크롤 상품 추가 처리
case ACTION_TYPES.APPEND_PRODUCTS: {
  const newProducts = [...state.products, ...action.payload.products];
  return {
    ...state,
    products: newProducts,
    pagination: {
      ...state.pagination,
      currentPage: action.payload.pagination.page,
      hasNext: action.payload.pagination.hasNext ?? newProducts.length < state.total,
    },
    isLoadingMore: false, // 로딩 상태 자동 해제
  };
}

// stores/actions.js - 타입 안전한 액션 생성자
export const actions = {
  appendProducts: (products, pagination) => ({
    type: ACTION_TYPES.APPEND_PRODUCTS,
    payload: { products, pagination },
  }),
};

2. 무한 스크롤 (Intersection Observer API)

// events.js 
export const registerInfiniteScroll = (productService) => {
  if (infiniteScrollObserver) {
    infiniteScrollObserver.disconnect();
    infiniteScrollObserver = null;
  }

  const sentinel = document.querySelector("#scroll-sentinel");
  if (!sentinel) return;

  const options = {
    root: null,
    rootMargin: "200px", // 200px 전에 미리 로딩
    threshold: 0.01,
  };

  infiniteScrollObserver = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        productService.loadMoreProducts();
      }
    });
  }, options);

  infiniteScrollObserver.observe(sentinel);
};
// events.js 
// pages/ProductListPage.js - 센티넬 요소 렌더링
const InfiniteScrollLoader = ({ isLoadingMore, hasNext, isInitialLoad }) => {
  // 초기 로드 중에는 센티넬 렌더링하지 않음
  if (isInitialLoad) return "";

  // 더 이상 로드할 상품이 없으면 완료 메시지
  if (!hasNext && !isLoadingMore) {
    return `<div class="text-center py-8 text-sm text-gray-500">
      모든 상품을 확인했습니다
    </div>`;
  }

  // 로딩 중일 때만 인디케이터 표시
  if (isLoadingMore) {
    return `<div class="text-center py-4">로딩 인디케이터</div>`;
  }

  // 센티넬 요소 (보이지 않는 감시 요소)
  return `<div id="scroll-sentinel" style="height: 1px; margin-top: -1px;"></div>`;
};

구현 이유:

  • scroll 이벤트는 스크롤할 때마다 수십 번 발생하여 성능을 저하시키지만, Intersection Observer는 요소가 뷰포트에 들어올 때만 한 번 실행되어 성능 개선됩니다.
  • 로딩 상태를 체크하여 동시에 여러 번 API 호출이 발생하는 것을 방지합니다.

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

전반적으로 모두 개선이 필요하다고 생각해요.....🥲 상태 관리와 URL 중심 접근 사이의 경계가 모호해서 같은 데이터를 여러 곳에서 관리하는 문제가 심각한 것 같아요.

AI 활용 경험 공유하기

  • cursor
  • claude

리뷰 받고 싶은 내용

  1. 현재 Redux 패턴을 모방해서 Store-Reducer-Action 구조로 상태관리를 구현했습니다. 이런 방식으로 구현한 것이 바닐라 JavaScript 환경에서 적절한 선택이었는지 궁금합니다. 특히 매번 전체 렌더링이 발생하는 부분이 성능상 문제가 될 것 같아 고민입니다.
  2. 현재 렌더링 시마다 이벤트 리스너를 재등록하는 구조로 되어 있습니다. 이 방식이 메모리 누수를 유발할 수 있다고 생각되는데, 바닐라 JavaScript 환경에서는 어떤 식으로 이벤트 생명주기를 관리하는 것이 좋을까요?
  3. JavaScript로 SPA를 구현할 때 실무에서 자주 사용하는 패턴이나 베스트 프랙티스가 있는지 궁금합니다!
  4. 현재 코드가 많이 꼬여있고 리팩토링을 진행하고 싶은데, 어떤 순서로 진행하는게 좋을까요?
  • 어떤 부분부터 우선적으로 개선해야 할까요?
  • 기존 구조를 유지하면서 점진적으로 개선할지, 아니면 전면적인 재설계가 필요할까요?