soyalattee 님의 상세페이지[2팀 박소연] Chapter 1-1. 프레임워크 없이 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로 다시 구현한다.
  • 이 과정에서 직접 가공하는 것은 최대한 지양한다.

과제 셀프회고

기술적 성장

Pub/Sub 패턴 기반 상태 관리 시스템 구현

  • React의 useState와 유사하게 반응형 상태 관리를 바닐라 JS로 구현

  • subscribe/notify 패턴을 통해 상태 변경 시 자동 리렌더링

  • 여러 컴포넌트 간 상태 공유와 동기화에 대한 깊은 이해 획득 아쉬운점: 모든 상태를 전역 state하나로 구현함. 해당 페이지에서만 사용되는 상태도 state에 전부 있어 점점 state가 커짐

    커스텀 SPA 라우팅 시스템 개발

  • History API를 활용한 브라우저 네비게이션 구현

  • 정규식 기반 라우트 매칭과 파라미터 추출 로직 설계

  • URL 쿼리 파라미터를 통한 상태 관리 아쉬운점: 네비게이션으로 페이지 이동시, 전체적으로 로딩이필요한경우, 로딩 없이 일부 UI만 렌더가 필요한 경우가 나뉘는데 이부분에대한 고민을 깊게하지않고 모두 하나의 네비게이션 함수로 통일. 관련처리는 router에게 넘김(router에서 판단하여 렌더링분기)

    컴포넌트 생명주기 관리

  • 페이지 컴포넌트의 생성, 렌더링, 정리 과정으로 분리(create, render, clean)

  • 컴포넌트 인스턴스 생성해 page 변수에 할당 아쉬운점: clean 은 상품리스트 페이지에만 존재. (scrollEvent 해지를 위한 역할). 좀더 명확한 분리 필요

자랑하고 싶은 코드

Pub-Sub 패턴 상태 관리 (src/store/stateStore.js)

const listeners = new Set();

export function subscribe(callback) {
  listeners.add(callback);
  callback();
  return () => listeners.delete(callback);
}

export const setState = (partialState) => {
  Object.assign(state, partialState);
  notify();
};
  • React의 setState와 유사하게, 상태업데이트시 재렌더링 하기위해 Pub/Sub패턴을 사용하기로 결정 아쉬운점: 해지가 없다. 페이지를 수차례 이동하면 리스너가 계속 추가될지도 모르겠다.

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

장바구니 상태 관리의 이중화 문제

  • stateStore.js의 cart 상태와 cartStore.js의 분리된 관리
  • 두 store 간 동기화 코드의 복잡성이 올라감. 실수할 가능성 증가
  • cart 데이터는 state에 저장하지않고 cartStore에서만 관리했어야 할까?
  • cartStore 는 localstorage와 연결되어있음. 이것도 분리해도 좋을것 같다.

컴포넌트 간 의존성 주입의 복잡성

page = ProductListPage({ state, setState, openCartModal, addToCart, navigateTo });
  • 너무 많은 의존성을 props로 전달하는 구조
  • 의존성 주입 ? 어떻게 개선해야할지 모르겠다.

라우터 함수의 복잡성과 책임 과다

function router() {
  // 70여 줄의 복잡한 로직
  if (!page || page.pageType === "detail") {
    setState({ loading: true });
  }
  cartStore.loadFromStorage();
  const { openCartModal, updateQuantityInputs, addToCart, renderCartModal } = cartModal({ state, setState });
  subscribe(() => {
    /* 구독 1 */
  });
  setState({ cart: cartStore.state.cart });

  if (route.page === "list") {
    setState({
      /* 상태 설정 */
    });
    if (page && page.pageType === "list") {
      page.render();
      return;
    }
    page = ProductListPage({
      /* 의존성 주입 */
    });
    subscribe(() => {
      /* 구독 2 */
    });
    page.createPage();
    return;
  }
  // 유사한 패턴이 각 페이지마다 반복...
}
  • 라우팅, 상태 관리, 페이지 생성, 구독 관리 등 너무 많은 책임
  • 각 페이지마다 중복되는 패턴 (상태 설정 → 페이지 생성 → 구독 → 초기화) 묶을 수 있을텐데 하지 못함.
  • 구독 해지 없이 계속 누적되는 메모리 누수 문제
  • 전역 page 변수 사용으로 인한 상태 관리의 복잡함
  • 페이지별 특수 처리 로직이 하드코딩..

학습 효과 분석

가장 큰 배움이 있었던 부분

  • 프레임워크 없이 현대적인 웹 애플리케이션 패턴 구현
  • Pub/Sub 패턴의 실제 적용과 상태 관리 시스템 설계
  • SPA 라우팅과 브라우저 히스토리 API 활용
  • 테스트코드의 이해도가 올라감

추가 학습이 필요한 영역

  • 상태 관리 라이브러리 (Redux, Zustand) 패턴 학습
  • 컴포넌트 간 통신 최적화 방법

실무 적용 가능성

  • Pub/Sub 패턴을 활용한 상태 관리 개념
  • 컴포넌트 생명주기 관리 경험
  • 브라우저 API 활용 능력 향상..?

과제 피드백

과제에서 좋았던 부분

  • 단계별 요구사항으로 점진적으로 개발 해볼 수 있었음
  • 테스트 코드 작성을 통한 안정성 확보
    • 잘 돌아가던 테스트가 다른 코드 수정시 안되는 문제 다수 발생. 코드의 안정성을 보장받는다 느낌
  • 평소 라이브러리와 프레임워크에 의존해 구현하던 것들을 직접 해보니 고려해야할 것들이 많다는것을 느낌
    그러면서 평소 내가 개발하며 놓치고있었던 메모리관리와 설계에 대해서도 고민해보게됨

오랜만에 신입때처럼 몰두해서 개발했어요. 역시 난이도 있는 과제는 할땐 힘들어도 하고나면 뿌듯하고 성취감이 드는것 같습니다. 또 SPA, React useState 직접 구현해보자 해보자 말만하고 항상 쉽게 손이 가질 않는데 이번기회에 이렇게 좋은 과제로 주셔서 너무 재밌게 개발했어요!!! 힘들었지만 그만큼 얻어가는게 많은 과제였습니다 감사합니다. 고생하셨습니다.

과제에서 모호하거나 애매했던 부분

  • 로딩 순서 같은거.. 모호했어요 e.g) 상품 디테일 선로드 후 관련상품 로드. 기준이 '관련 상품' 이런.. UI 적인 모호함.. 하지만 테스트코드가 곧 요구사항이다 라고 생각하고 개발했습니다.

AI 활용 경험 공유하기

사용한 AI 도구 적극적으로 활용했습니다.

  • Claude (Cursor IDE)
  • ChatGPT (에러, 배포 관련 )

AI와 함께한 개발 과정

  • 복잡한 라우팅 로직의 정규식 패턴 최적화

AI가 일을 더 잘 하게 만든 방법

  • 구체적인 요구사항과 함께 현재 코드 컨텍스트 제공
  • 단계별 질문을 통한 점진적 문제 해결 ( 페이지만들어줘. 가 아니라 잘게 쪼개서 패치 구현, UI 로드, 서버에서 받아온 데이터 UI 반영 등 나누어 요청 )
  • 코드 리뷰 관점에서 개선점 도출 (두가지 중 고민이 될때 각각 입장을 가진 개발자의 토론을 보여달라 요청 e.g Pub/Sub vs Observer )

리뷰 받고 싶은 내용

1. 상태 관리 아키텍처 설계에 대한 피드백

현재 stateStore.jscartStore.js로 상태를 분리했지만, 장바구니 상태가 두 store에서 중복 관리되고 있습니다.

// stateStore.js
export const state = {
  cart: [], // 여기와
  // ... 다른 상태들
};

// cartStore.js
export const cartStore = {
  state: {
    cart: [], // 여기에 중복
  },
};

단일 store로 통합할지, 아니면 명확한 책임 분리를 통해 현재 구조를 개선할지 고민입니다. 또 localStorage 관련 로직을 별도 함수로 빼야했을지

2. 컴포넌트 의존성 주입 패턴 개선 방안

현재 페이지 컴포넌트 생성 시 많은 의존성을 전달하고 있습니다:

page = ProductListPage({ state, setState, openCartModal, addToCart, navigateTo });

이런 구조에서 의존성이 증가할 때마다 모든 컴포넌트를 수정해야 하는 문제가 있습니다. 바닐라 JS 환경에서 의존성 주입을 더 깔끔하게 처리할 수 있는 패턴이 있을까요?

3. Pub/Sub 패턴 구현의 성능 최적화

현재 상태 변경 시 모든 구독자에게 알림을 보내는 구조입니다:

export const setState = (partialState) => {
  Object.assign(state, partialState);
  notify(); // 모든 구독자에게 알림
};

특정 상태 변경에만 반응하는 선택적 구독 패턴이라던가.. 뭔가 성능 최적화 방법이 있을까요? React의 가상 DOM이나 Vue의 반응형 시스템과 비교했을 때 어떤 개선점이 있을까요?

4. Router 코드 역할 분리와 복잡성 개선

현재 main.js의 router 함수가 너무 많은 책임을 가지고 있어 복잡해졌습니다:

function router() {
  // 1. 로딩 상태 관리
  if (!page || page.pageType === "detail") {
    setState({ loading: true });
  }

  // 2. 장바구니 초기화
  cartStore.loadFromStorage();
  const { openCartModal, updateQuantityInputs, addToCart, renderCartModal } = cartModal({ state, setState });

  // 3. 구독 관리 (해지 없이 계속 누적)
  subscribe(() => {
    updateQuantityInputs();
    renderCartModal();
  });

  // 4. 상태 동기화
  setState({ cart: cartStore.state.cart });

  // 5. 라우팅 로직
  const route = getRoute();

  // 6. 페이지별 처리 (각 페이지마다 중복 패턴)
  if (route.page === "list") {
    setState({
      /* 복잡한 필터 상태 설정 */
    });
    if (page && page.pageType === "list") {
      page.render();
      return;
    }
    page = ProductListPage({
      /* 많은 의존성 */
    });
    page.pageType = "list";
    subscribe(() => {
      page.render();
    });
    page.createPage();
    return;
  }

  // 7. 정리 작업 (일부 페이지만)
  if (page && page.pageType === "list") {
    page.cleanupScrollInfinity();
  }

  // 8. 또 다른 페이지 처리...
}

구체적인 문제점들:

  • 70여 줄의 단일 함수: 라우팅, 상태 관리, 페이지 생성, 구독 관리 등 너무 많은 책임
  • 중복 패턴: 각 페이지마다 "상태 설정 → 페이지 생성 → 구독 " 패턴이 반복
  • 메모리 누수: 구독 해지 없이 subscribe() 호출이 계속 누적됨
  • 전역 변수 의존성: page 변수를 전역에서 관리하여 상태 추적이 어려움
  • 하드코딩된 분기: 페이지별 특수 처리 로직이 if-else로 하드코딩. 페이지별 특수 처리 로직 어떻게 해야할까요? 예를들면 상세 -> 디테일 페이지로 이동했을경우와 상세 -> 상세 페이지일 경우 처리가 다름

개선 방향 고민:

  1. router 함수를 어떻게 분리해야 할까요?
  2. navigateTo 함수와 router의 관계를 어떻게 정리하는 것이 좋을까요?

과제 피드백

안녕하세요 박소연님! 수고하셨습니다. :)

이번 과제는 프레임워크 없이 SPA의 핵심 기능들을 직접 구현하면서, 현대 프론트엔드 프레임워크들이 해결하고 있는 문제들을 깊이 이해하는 것이 목표였습니다. 우리가 그냥 쓰고 있는 도구들을 직접 만들어 보며 그 작동원리를 이해하면서 우리의 도구를 더 잘 쓸 수 있게 되기를 바랍니다.

과제를 보면서 특히 Pub/Sub 패턴을 활용한 반응형 상태 관리 시스템 구현이 인상적이었습니다. React의 useState와 유사한 방식으로 구현하신 점, 그리고 이에 대한 한계점까지 명확히 인지하고 계신 점이 훌륭합니다. 또한 테스트 코드를 모두 통과시키며 요구사항을 충실히 구현하신 점도 잘하셨습니다.

스스로 작성해둔 라우터의 복잡성 문제나 subscribe를 해제하는 부분이 없는 것에 대한 반성(?)도 좋았습니다. 만약 아는데 어떻게 해야할지 잘 모르겠다면, 그래도 한번 시도를 해보고 피드백을 받아 본다면 앞으로 과제할 때 조금 더 잘 피드백을 드릴 수 있겠네요

회고에서 "오랜만에 신입때처럼 몰두해서 개발했다"는 말씀이 인상 깊었습니다. 이런 열정과 도전 정신이 있다면 앞으로도 계속 성장하실 수 있을 것입니다. 2주차도 화이팅입니다! :)

다음은 질문 주신 내용에 대한 구체적인 답변입니다.


Q) router 함수를 어떻게 분리해야 할까요?

=> 이미 스스로 주석을 하나씩 달아두었고 스스로 평가하기를 너무 많은 책임을 가지고 있다고 말하는 것처럼 책임과 하나의 책임에 대한 적절한 크기감각은 이미 있는 것 같네요. 그런 쎄함(?)을 느끼면 우선 크기를 나눠줄 생각을 먼저 합시다. 책임을 너무 잘 분리하지 않아도 좋으니 우선 주석을 달아준 그 구간을 분리해서 함수로 빼내는 일을 먼저합니다.

=> 함수로 분리를 하는건 간단합니다. 우선 함수로 감싸고 밖으로 보낸다음 모든 인자만 연결하면 되죠. 심지어 이런건 VSCode에 기능으로도 제공하고 있습니다.

function outerModule() { // 원래 코드 } outerModule()

  1. 인자 맞춰주고 바깥으로.. function outerModule(a,b,c) { // 원래 코드 } outerModule(a,b,c)

=> 그렇게 잘라두고 나면 조금 더 잘 보입니다. 책임에 맞게 분리했는데 괜한 분리를 했는지. 그리고 모듈을 분리하면 해당 로직을 내부에 두는게 맞는지 외부에 위임해야 하는게 맞는지 알 수 있습니다. 특히나 내가 새롭게 라우트를 만드는데 불필요하게 또 적어줘야만 하는게 있는지 정말 내가 적어주지 않으면 안되는 것들로만 구성이 되어 있는지 말이죠..

설명이 엄청 길어질것 같아서 우선 다음번 과제에서는 이런 부분을 만나면 과감하게 함수를 분리해서 책임을 나눠보세요. 우선 시도를 해야 그 다음 피드백을 받을 수 있겠지요. 화이팅입니다 :)

Q) 컴포넌트 의존성 주입 패턴 개선 방안

=> 지금 주입받고 있는 공톨 모듈을 받아 줄 수 있는 Context 패턴을 고려해보세요. 의존성들을 하나의 context 객체로 묶어 전달하면 됩니다. 이런 경우 모두에게 의존하게 하는게 아니라 중간 담당자를 두면 변화를 한군데에서 관리를 할 수 있게 됩니다.

const context = { state, setState, actions: { openCartModal, addToCart, navigateTo } };
page = ProductListPage(context);

Q) Pub/Sub 패턴 구현의 성능 최적화

=> React의 경우 지금처럼 모든 상태를 1개씩 전달하나 불변성을 통해서 비교의 횟수를 줄이는 방법이고 사실 React는 성능을 희생하고 DX를 높이는 방법을 택했죠.

=> React가 아닌 Vue를 비롯한 다른 모든 프레임워크들은 각 key, path별로 여러개의 pub/sub를 통해서 최소한의 업데이트만 할 수 있도록 만들고 있습니다. 하나의 setState가 아니라 여려개의 signal을 만들수 있겠죠.

Q) Router 코드 역할 분리와 복잡성 개선

=> 라우터를 단순히 라우팅만 담당하도록 하고, 페이지별 초기화 로직은 각 페이지 컴포넌트의 생명주기 메서드로 이동시키세요. 예를 들어:

class Page {
  onMount() { /* 초기화 로직 */ }
  onUnmount() { /* 정리 로직 */ }
}

이렇게 하면 라우터는 단순히 페이지 전환과 생명주기 호출만 담당하게 됩니다.

다 아는 패턴이죠? 만들다보면 그렇게 나오는 고민들이 이미 세련된 해법으로 우리가 잘 쓰고 있죠. 모를때에는 내가 아는 정답(!)을 참고해보세요.

수고하셨습니다! 다음 주차도 화이팅입니다! :)