Yuyeol 님의 상세페이지[8팀 정유열] Chapter 1-1. 프레임워크 없이 SPA 만들기

과제 체크포인트

배포 링크

https://yuyeol.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의 매끄러운 페이지 전환이 동작하는지에 대해 뜯어보는 기회를 가질 수 있었습니다.

    • a 태그로 페이지 이동을 하면 화면에 플리커가 발생하며 문서를 통째로 다시 불러오지만, e.preventDefault()로 동작을 막고 history.pushState()로 URL만 바꾼 뒤 같은 render()를 발생시키는 디테일한 과정을 경험했습니다.
    • 또한 SPA 페이지 이동을 구현하더라도 앞·뒤로가기에 대한 대응이 불가했던 상황에서 popState 이벤트를 이용한 render()가 필수라는 것도 알게 되었습니다.
  • 테스트코드를 처음 접해보아서 생소한 부분이 많았지만, 촘촘하게 작성된 테스트를 바탕트로 코드를 작성해본 것은 귀한 경험이었습니다.

    • 실제로 테스트코드를 경험해보니 왜 명세서 역할을 할 수 있는지, 어떤점에서 편의성을 주는지 실감할 수 있었습니다.
    • 비슷한 기능을 하는듯한 각 테스트함수들도 세밀한 의도에따라 전략적으로 사용할 수 있어야만 좋은 테스트코드를 짤 수 있겠다는 생각이 들었습니다.

자랑하고 싶은 코드

  • 상태 관리 라이브러리 없이 구독/알림 기반 경량 스토어를 구현했습니다.(@/stores/cart-store.ts)

    • 배경: 페이지 범위를 넘나드는 장바구니 상태 특성상 로컬스토리지에 데이터를 저장해야 했고, 로컬스토리지만으로는 상태 변경에 따른 리렌더링이 발생하지 않아 외부 스토어를 만들어 리렌더를 가능하게 했습니다.
    • 구독/알림 패턴: subscribe() 메서드로 컴포넌트들이 상태 변화를 구독하고, 상태가 변경될 때마다 notify()를 통해 리렌더링되도록 구현했습니다.
    • 로컬스토리지 연동: 상태 변경 시 자동으로 persist()를 통해 로컬스토리지에 동기화되도록 구현했습니다.
    • 테스트 독립성 강화: __resetForTest() 메서드를 통해 테스트 간 상태 격리를 보장하고, 연속 테스트 실행 시 발생할 수 있는 side effect를 가드했습니다.
  • 페이지 모듈과 컴포넌트 모듈의 명확한 역할 분리를 통한 아키텍처 설계(@/router.ts)

    • 배경: SPA에서 페이지 전환 시 HTML 생성과 이벤트 바인딩을 체계적으로 관리하기 위해 역할을 분리했습니다.
    • 페이지 모듈: render()로 초기 HTML 템플릿을 생성하고, mount()에서 실제 DOM 조작과 이벤트 리스너 등록을 담당하도록 구현했습니다. (templete()과 hydration라고 표현하는것이 컨셉에 맞을 지도 모르나, 리액트 친화적인 속성명으로 표현하였습니다.)
    • 컴포넌트 모듈: 순수하게 HTML 문자열 조합과 재가공만을 담당하여 재사용성 확보를 의도했습니다.

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

  • 장바구니 컴포넌트 모듈(@/components/cart/index.ts): 비즈니스로직과 렌더링 로직이 강결합되어 의도한 만큼 깔끔한 코드를 작성하지는 못했습니다.
  • 토스트(@/components/toast/index.ts)와 모달(@/components/modal/index.ts) 위치에 대한 고민
    • 토스트는 페이지 이동에 간섭되지 않아야 하므로 도큐먼트에 부착하였고, 모달은 각 페이지들과 결합도가 있고 장바구니 상태를 가지고있기때문에 사이클을 함께 가져가기 위해 루트에 부착했습니다.
    • 둘다 포탈과 유사한 메커니즘으로 트리 최상위에 배치되는데 부착되는 위치가 달라서 일관성이 깨지는것이 아쉬웠습니다.

학습 효과 분석

  • 라이프사이클 직접 설계, 검증해보는 기회였습니다: 테스트 코드를 통과시키는 과정에서 제가 설계한 페이지 모듈의 render()mount() 라이프사이클이 의도대로 동작하는지 지속적으로 검증하고 개선하기 위해 많은 노력을 쏟을 수 있었습니다.

  • 테스트 코드 작성 역량 확보: 다양한 시나리오와 엣지 케이스를 다루는 테스트들을 통해 어떤 범위를, 어떤 방식으로 테스트해야 하는지에 대한 실전적인 샘플을 얻었고 실무에서도 조금씩 테스트 코드를 도입할 수 있겠다는 자신감이 생겼습니다.

  • 상태 관리 툴 직접 구현해보기: 구독/알림 패턴 기반의 스토어를 직접 구현하면서 Redux, Zustand 같은 상태 관리 라이브러리들이 내부적으로 어떤 메커니즘으로 동작하는지 체감할 수 있어 흥미를 가지고 몰입하여 학습할 수 있었습니다.

과제 피드백

  • 직장을 병행하고 있어서 개인적으로는 과제의 분량이 많게 느껴졌습니다.
  • 과제를 완료한 시점에서 명세의 변경이나 추가가 있어서 급하게 코드를 수정하는 상황이 발생했습니다.
  • 자바스크립트 공부 뿐만 아니라 테스트코드 까지 공부하는 기회를 주셔서 습득하는 경험이 알찼습니다.
  • 특정 렌더링 사이클이나 비동기 특성에 의해 충돌 되는 테스트가 존재해서 코드 작성에 어려움이 있었습니다.
  • 코치님께서 고심하시고 정성을 쏟은 흔적이 과제에 묻어나는 것 같아 열심히 과제를 수행할 수 있었습니다.

AI 활용 경험 공유하기

  • HTML string의 비교 및 분리 작업은 AI에게 전권 위임했습니다.
  • 전체적인 SPA 구조 설계에 대한 조언과 계획을 구하고 그에 따라 코드를 작성했습니다.
  • 한꺼번에 많은 컨텍스트를 던지기보다는 질문을 더 쉽게 추상화하여 그 아이디어를 차용하여 로직을 작성했습니다.
  • 어려운 문제의 경우 즉시 구현을 요구하기보다는 분석 내용에 대해 자세하게 설명을 요구하고 토른 결과에 따라 구현과 디버깅을 수행했습니다.

리뷰 받고 싶은 내용(위에서 기술했던 자랑하고싶은 코드, 아쉬웠던 코드와 거의 동일한 내용입니다.)

  • 페이지 모듈과 컴포넌트 모듈의 역할 분리를 했으나 만족스러운 구조가 나오지 못한것 같습니다. 어떻게 하면 더 나은 구조를 만들 수 있었을까요?
    • 페이지 모듈은 명확한 책임을 담당하고 있지만 핸들러 바인딩이 비대해지는 단점을 가지고있고, 렌더와 마운트, 언마운트 정도의 라이프사이클 밖에 흉내내지 못했습니다.
    • 컴포넌트 모듈은 html return만을 목표로 설계하였지만 비즈니스로직과 렌더링과 개별 api 호출로직이 곳곳에 섞여버려서 본래의 목적을 잃어버린것 같습니다.
  • 장바구니 스토어 구현(@/stores/cart-store.ts)
    • 외부 상태관리 스토어로써 적절한 기능을 하도록 잘 구성하였는지 궁금합니다.
  • 장바구니 컴포넌트 모듈(@/components/cart/index.ts)
    • 비즈니스로직과 렌더링 로직이 강결합되어 의도한 만큼 깔끔한 코드를 작성하지는 못했는데 어떻게하면 더 좋은 코드를 작성할 수 있었을까요?
  • 토스트(@/components/toast/index.ts)와 모달(@/components/modal/index.ts) 위치에 대한 고민
    • 토스트는 페이지 이동에 간섭되지 않아야 하므로 도큐먼트에 부착하였고, 모달은 각 페이지들과 결합도가 있고 장바구니 상태를 가지고있기때문에 사이클을 함께 가져가기 위해 루트에 부착했습니다.
    • 둘다 포탈과 유사한 메커니즘으로 트리 최상위에 배치되는데 부착되는 위치가 달라서 일관성이 깨지는것이 아쉬운데 어떻게 하는것이 최선이었을까요?