Hwirin-Kim 님의 상세페이지[1팀 김휘린] Chapter 1-1. 프레임워크 없이 SPA 만들기

과제 체크포인트

배포 링크

https://hwirin-kim.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를 구현함으로써 라우팅, 상태 관리, 컴포넌트 기반 렌더링, 렌더 트리거 등 기존에 리액트가 추상화해주던 작업들을 직접 구현하면서, 왜 리액트가 등장했고 어떤 문제를 해결하려 했는지를 더 깊이 이해할 수 있었다. (물론 리액트처럼 구현하고싶었지만.. 마음대로 되지 않았다)

자랑하고 싶은 코드

export const createStore = (initialState) => {
  return {
    state: { ...initialState, isLoading: false, error: null },
    listeners: [],
    setState(newState) {
      this.state = { ...this.state, ...newState };
      this.listeners.forEach((fn) => fn(this.state));
    },
    subscribe(fn) {
      this.listeners.push(fn);
    },
  };
};

의도한 동작은 파라미터로 초기 상태 객체(initialState)를 받아서 내부적으로 상태(state)를 생성하고, 여기에 isLoading, error 같은 공통 상태 필드를 자동으로 시킨다.
setState 메서드는 새로운 상태를 병합하여 업데이트하고, 등록된 모든 리스너 함수들을 호출하여 상태 변경을 구독 중인 UI 또는 로직에 반영되도록 한다.
subscribe 메서드는 상태 변경 시 실행될 콜백을 등록할 수 있어, 상태 변경에 따른 render를 바로 하게 해준다.
현재 내가 만든 과제에서는 render함수가 전체 페이지 렌더밖에 없어서 잘 동작하는지 확인하지 못해서 좀 아쉬웠다.

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

이번 과제를 진행하면서 가장 개선이 필요하다고 느낀 부분은 컴포넌트 구조와 상태-렌더링 간 연결 방식이다.

SPA 형태로 페이지와 컴포넌트를 함수 단위로 나누긴 했지만, 실제 구현해보니 다음과 같은 아쉬움이 있었다:

  1. 컴포넌트 내부에서 render를 직접 제어할 수 없는 구조
  • 현재 구조에서는 모든 렌더링이 renderHtml()로 통합되어 있어, 어떤 컴포넌트가 어떤 이유로 다시 렌더링되는지 추적이 어렵고, 부분 렌더링이 불가능해서 매번 전체 페이지를 리렌더링하게 된다.
    이 때문에 Router 단위로 모든 상태 변화에 반응하게 되어, 성능 저하나 불필요한 렌더가 발생할 수밖에 없는 구조다.
  • 만약 컴포넌트를 클래스 단위로 정의하고 this.render(), this.setState() 같은 메서드를 가지게 했다면, 각 컴포넌트가 자기 상태에만 반응하도록 구조화할 수 있었을 것이다.
  1. 렌더 트리거와 상태 변화의 연결의 불안정함
  • store.subscribe(renderHtml) 형태로 전체 렌더링을 구독시키는 구조다 보니, 작은 부분의 상태와 렌더링 연결이 어려웠고, 로딩 상태만 바뀌어도 전체 렌더링이 발생하는 구조를 만들었다.
  • 하위 컴포넌트에서 데이터를 받으려 하면, 스토어에 저장시키는 과정에서 또 렌더링을 유발시키고, 그 렌더링이 다시 데이터를 받으려고 하는 무한 렌더링 문제가 발생하기도 했다. 해결을 위해 조건문으로 무한 렌더링을 막는 코드가 추가했지만, 원인을 해결하지 못하고 문제만 해결해 놓은 상태이다..
  • 나중에는 결국 renderHtml() 호출을 fetchProductDetail() 내부에서 수동 호출하기도 했는데, 이런 코드를 작성할 때마다 후회가 들었다..

학습 효과 분석

  1. 첫 번째로, 렌더링을 어떻게 효율적으로 만드는지가 왜 중요한지, 그리고 그게 단순히 함수 호출만 잘하면 되는 수준이 아니라는 걸 제대로 체감했다.

  2. 두 번째로, 리액트가 왜 등장했는지, 그리고 어떤 문제를 해결하려 했는지를 몸소 경험하면서 이해하게 되었다. 특히 상태 관리, 컴포넌트 간 통신, 렌더 트리거 등 복잡한 흐름들을 내가 직접 구현해보니, 리액트의 Virtual DOM이나 useEffect, Context API 같은 기능들이 얼마나 많은 고민 끝에 나온 건지 알게 되었다.

  3. 세 번째로, SPA에서 페이지 구조를 어떻게 나누고, 상태를 어떻게 전역으로 관리해야 할지 고민하면서, 구조화의 중요성을 배웠다. 단순히 기능이 동작하는 코드보다, 구조적으로 유지보수가 가능한 형태로 만들기 위해 고민하는 과정이 정말 중요하다는 걸 알게 됐다.

과제 피드백

  • 일단 과제의 모든 부분을 완성하고싶었지만 단순히 easy테스트만 통과한것에 대해 반성한다.
  • 기능구현도 기능구현이지만 좀더 SPA를 만들기 위해 기초를 탄탄히 만들어놨다면 기능구현도 더 빠르게 하지 않았을까 하는 생각이 들었다.

AI 활용 경험 공유하기

  • 사실 과제 막판까지 AI를 많이 사용하지 않았다. 그 이유는 처음에 사용해본 결과, 생각보다 원하는 답이 나오지 않았고, 너무 원론적인 방법만 알려주기도 했다. (나한테 거의 리액트를 만드는 수준으로 알려주려고 한다.)
  • 나는 Cursor를 주로 사용했는데, 이 부분에 있어서는 단순한 기능이나 util함수등을 만드는데는 매우 도움이 많이 되었다.
  • 명확하고 작은 함수 하나를 만드는데는 적합하지만, 과제 자체를 날먹? 할 순 없다고 생각한다.
  • 따라서 먼저 내가 어떻게 개발 할 것인지, 명확하고 상세한 플랜이 있다면 AI와 함께 매우 빠르게 개발 할 수 있다고 생각한다.

리뷰 받고 싶은 내용

내 코드 동작

  • renderHtml이라는 함수로 Router자체를 리턴시켜서 렌더하고 있습니다.
  • 그리고 상태가 변할때 마다 renderHtml을 다시 실행시켜 최신 상태를 UI에 반영시키는 구조로 만들었습니다.

리뷰 받고 싶은 내용

  • 저는 작은 컴포넌트 단위에서 해당 컴포넌트만 다시 렌더링을 시킬 수 있는지 궁금합니다. (작은 컴포넌트 단위의 예시로는 장바구니 아이콘의 숫자 라던지, 조그만한 로딩창과 같은 부분 등 각각의 컴포넌트를 의미합니다!)
  • 제가 현재 이해한 바로는 리액트는 가상돔이 있어서 돔트리를 비교해서 반영하지만, 가상돔이 없는 제 코드에서는 그게 불가능 하다고 생각하는데, 혹시 가상돔이 없어도 구현할만한 방법이 있는지 궁금합니다.