Skip to content

Conversation

@piggggggggy
Copy link

@piggggggggy piggggggggy commented Nov 9, 2025

과제 체크포인트

배포 링크

배포 사이트 링크

기본과제

상품목록

상품 목록 로딩

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

상품 목록 조회

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

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

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

과제 셀프회고

할..말이... 너무 많은 프로젝트입니다.. (할말 정리 중..⏳..⌛️..)

전체적인 주요 작업의 흐름은,

  • 시작은 Vanilla JS를 이용한 csr 구현
  • 작업하면서 생기는 문제들을 해결하기 위해, 구조적으로 개선
    • SPA Router 구조 도입
    • 반응형 컴포넌트 시스템
    • 컴포넌트 시스템에 약식 라이프사이클 추가
    • 등...
      입니다.

TL;DR

  • Router + 반응형 컴포넌트 시스템 + 라이프사이클 구현
  • 문제 발견 → 해결 사이클을 반복하며 점진적으로 아키텍처 개선
  • 도구의 내부 동작을 이해하는 귀중한 경험 (+ 기본기 보강)

1) csr 라우트 이동을 구현하며, 중앙 집중식 구조가 필요함을 느껴 src/route.js선언적인 라우트 레지스트리를 구현

Vue Router를 사용할 때, 이런 Route Config 구조를 활용했었는데, 어떤 요구사항에서 이런 중앙 레지스트리 구조를 갖게 되었는지 느껴볼 수 있는 경험이었습니다. 입맛대로 새로운 설정도 추가해보고, 어떻게하면 더 잘 활용할 수 있을까? 확장성 측면에서는 어떤 부분을 고려해야할까? 등의 고민이 동반되었습니다. (흥미 단계)

1.5) Route 레벨의 데이터 loader 패턴 도입, src/route.jsloader 함수.

추가로, 중앙에서 Router 이동을 관리하는 구조에서 동적으로 렌더링 된 페이지에 알맞는 데이터를 넣어주기 어렵다고 느꼈습니다. 이를 해결하기 위해 Route 명세에 페이지 render 함수 뿐만아니라, 데이터 로딩을 Route 정의의 관심사로 옮기는 loader 함수 패턴을 활용하여, Route별로 사전에 필요한 데이터를 선언적으로 관리할 수 있는 구조를 도입했습니다.

const ROUTES = {
  home: {
    path: "/",
    render: HomePage.mount,
    loader: async () => { // 요기, 지금은.. 굳이 안쓰고 있긴합니다.
      const categories = await getCategories();
      return { categories };
    }
  }
}

현재는 딱히 활용하지 않음... (반응형 컴포넌트 시스템 도입으로..)

2) 전역에서 접근 가능한 싱글톤 패턴의 정적 Router, src/core/router/index.js

직관적이고 간단한 push, replace, goBack 메서드를 활용할 수 있고, App 초기 로드 시 주입된 Route Registry(routes)와 basePath 등을 전역에서 활용할 수 있도록 싱글톤 패턴으로 구성된 Router를 약식으로 구현했습니다.
구현 목적은
1. main.js에서 덕지덕지 붙은 Route 이동관련 로직과 이벤트 관리 분리,
2. 반응형 컴포넌트 시스템 도입 시, 메서드를 가져다 쓰는 패턴을 활용하기 위함
이었습니다.

class Router {
  static routes;
  static basePath;
  static container;
  ...

  static init(container, routes, basePath) {
    ...

    popstate 이벤트 등록
  }

  static push(path) { 
    ...

    Router.#render(...); 
  }

  static replace(path) { 
    ...

    Router.#render(...); 
  }

  static goBack() { 
    ...
  }

  ...
}

// 활용

const handleClickProductCard = (id) => {
  ...

  Router.push(`/product/${id}`);
}

3) 디테일한 상태 기반의 UI 업데이트를 위한 반응형 컴포넌트 시스템 도입, src/core/component/create-component.js

모든 상태를 중앙(처음엔 main.js에서 -> app-store.js로 옮겨감)에서 관리하다가, 매번 중앙에서 string html을 root에 innerHtml로 갈아 끼워주는 방식으로는 디테일한 기능과 요구사항에 적절한 구현은 어렵겠다는 생각이 스믈스믈 쌓여가던 차였습니다. (과제 수준에서는 정신적으로 편하긴 했음) 단순 함수로 컴포넌트를 쪼개 반복되는 UI의 재활용 자체는 쉬웠으나, 특정 컴포넌트에서의 작은 상태관리가 페이지 수준의 render를 유발하는 구조와 중앙에서 모든 상태를 관리하는 구조는 한계가 있었습니다.

  • 😨 컴포넌트 수준의 작은 상태 변경을 위해 페이지 수준의 render
  • 😇 main.js 혹은 app-store.js 에서 모든 상태 관리..
  • 🤮 하위의 특정 UI 변경을 위한 덕지덕지 식별자 붙이기

따라서 결국엔,,, 컴포넌트 시스템에 손을 대고야 말았습니다.

// 초기 의사코드

function createComponent({ 프롭스, 상태, templateFn, 이벤트 }) {
  const 리렌더를 위한 식별 엘리먼트;

  const { getter, setter, 구독하기 } = createState(상태)

  const render 함수 = () => {
    // ..잡도리 로직
    리렌더를 위한 식별 엘리먼트 <- 최신 상태 반영된 html 주입
  }

  구독하기(reder 함수);

  // 이벤트 등록과 setter 바인딩 (상위로 위임)
  const 이벤트 타입 후보 = ['click', 'change', ..];
  document.addEventListener(타입, () => {
    이벤트와 setter, getter 적절히 바인딩
  })

  return {
    mount: () => {
      return 초기 render html
    }
  }
}

// 반응형 컴포넌트 생성
const productList = createComponent({
  props: ...,
  state: {
    isLoading: false,
    productList: [],
  },
  eventHandlers: {
    이벤트1: () => {...},
    이벤트2: () => {...},
  },
  templateFn: (props, state, setState) => {
    return /* HTML */`
      <div class="...">
          ${state.productList.map((product) => { ... })}
          <button data-event="이벤트1" date-event-type="click" ... />
      </div>
    `
  }
});

// 활용
/* HTML */`
  <div>
    ${productList.mount({ props... })}
    ...
  </div>
`

굳이 이렇게 까지 해야했을까.... 왜...

하지만 그래도 의사코드 -> 구현 -> 영점 조절1 -> 영점 조절2 -> 영점 조절3 ... 진행하다보니, 그럴듯한 반응형 컴포넌트가 완성되었습니다. 처음엔 이를 활용해 쭉쭉 리팩터링과 분리를 시작했고, 전체 렌더를 막기 위한 반응형 컴포넌트 적용이 아주 성공적이었습니다.

순한 맛 고민들 😇

  • 왜 이벤트가 한번만 작동하지..?
  • 왜 이벤트가 여러번 등록되지..?
  • 왜 이벤트가 두번 작동하지..?
  • 상태는 변했는데 왜 render가 안돌지..?
  • 왜 상태 구독이 취소된거지..?
  • props 변경에 대한 render는 어떻게 유발하지...?

그렇게 방구석 프로메테우스가 되어 나만의 작은 프로젝트에 불을 지피던 찰나.. (여기서 멈췄어야 했습니다)

컴포넌트를_발견한_룡태

4.5) 라이프 사이클의 도입... src/core/component/create-component.js - effect와 onMount

자체 상태와 반응성을 가지고 동작하는 것을 넘어, 그것을 이용해 다양한 동작들을 녹이기 위해서는 결국 간소하게라도 라이프 사이클을 이용할 수 있는 인터페이스가 필요했습니다. 여기까지 했는데, 이것만 안하기에는 아쉬웠습니다...

최종적으로 onMount와 dependencies를 watch하는 cutom effect를 구현했습니다. 실제로 제대로 워킹하는 effect를 만드는 것은 아주 어렵지는 않았습니다만, 깔끔하지도 않았습니다.

무튼 구현하고 나니 이제서야 거의 대부분 작업을 Component System으로 처리할 수 있게되었습니다.

  • Component LC를 이용한 Data Fetching
  • 외부 상태를 로컬 상태와 연동하여, 외부의 상태에 반응성 갖게하기 (src/components/header.js 의 onMount)

5) 그 외..

  • 이벤트 위임
  • jsdoc를 활용한 type 활용
  • toast나 modal UI 활용을 위한 구조적 고민 등...

실제 작업 시, 충분히 도구를 활용해서 신경쓰지 않아도 되는 부분에 대한 고민들을 하는 시간이었습니다. 대단한 걸 하지는 않았지만, 하나하나 처리할 때마다 작은 효능감을 느꼈습니다..

전반적인 감상

위에서도 말했다시피 실제 현업이나 프로젝트를 진행할 때는 해보지 않아도 되는 고민들을 해보는 시간이었습니다. 지난 주차에서 현실에서 할법한 고민들을 밀도있게 고민해봤다면, 이번주에는 평소에 사용하는 도구를 깊게 까보고 도구 빌더의 입장에서 고민해볼 수 있는 경험이었다고 생각합니다.

과제 시작 -> 문제 발견 -> 문제 정의 -> (떠오르는 선례들.. 아~ 이래서 이게 필요했던 거구나..) -> 해결 방안 모색 -> 설계 -> 구현 -> 적용 -> 개선 -> 적용 의 작업 흐름이 정말 재미있었습니다. 설계할 때는 머리가 쥐가 났지만, 많은 고민 끝에 만들어낸 결과물이 매끄럽게 워킹할 때는 큰 효능감을 느끼기도 했습니다.

그 과정에서

  • 문제 정의와 분석을 통한 작업 정의의 중요성을 느꼈고,
  • 좋은 설계를 위해서는
    • 구조에 대한 공부와
    • 도구의 Raw 레벨 동작 원리에 대한 공부,
    • 그리고, 많은 경험이 필요하다는 것을 다시 한번 느꼈습니다.

특히, 대 AI시대에 부족한 기본기가 발목을 잡는 경험을 많이 했는데, 고런 부분을 밀도있게 다시 챙겨볼 수 있는 기회였다고 생각합니다.

추가로, 미리 gh page 배포를 하고 간단 배포 가이드를 팀원들에게 공유했다가 학습메이트들의 말에 떠밀려 잡담방에 다시 올리게 되었습니다. 뭐 문서의 퀄리티가 좋은 것은 아니지만, 누군가에게 공유해봐야만 느낄 수 있는 경험들이 재미있었습니다.

  • 문서가 잘못 작성되었거나 설명이 부족하여 이를 정정/추가하기 위해 추가로 더 공부해볼 수 있는 경험
  • 실제로 이 문서에 도움을 받는 누군가가 있어 뿌듯함을 느끼는 경험
    등.. 이를 계기로 앞으로 더 많은 공유를 시도해보려고 합니다.

아쉬운 점

  • 테스트가 몇 개 실패한다.... 😱😱😱 ... 로컬에서는 성공하는데, 이런저런 이유로 실패하네요...

  • 여전히.. 과제하는데 시간이 급급해 팀원들과 과제와 관려한 더 많은 대화를 나누지 못했다는 점이 아쉽습니다. 그래도 중간에 모각코도 하고 작업에 대한 공유도 했지만, 뭔가 아직은 아쉽습니다. 차주에는 또 다른 시도를 해봐야겠어요

  • 어디 내세울만한 코드 퀄리티는 아닌점.. 이번주에는 온전히 나를 위한 코드 작성이었다고 생각합니다. 내가 시스템을 만들고 내가 보기좋게, 나만 이해하면 되는 구조였던 것 같습니다. 평소에 DX를 위한 도구 인터페이스 설계를 하는 것을 좋아하는 데, 사실 이번주는 많이 내려놨습니다.. 그 덕에 조금 더 온전히 경험해볼 수 있는 것도 있었던 것 같습니다.

  • 아키텍쳐에 대한 지식 부족.. 이를 계기로 구조 공부에 시간을 더 써볼 예정입니다... 그동안은 알면서도 억지로 시간을 할애하지 못했지만, 제가 흥미를 크게 느끼는 영역이기도 하고 기대가됩니다.

    • Q) 추천해주실만한 책이 있으신가요~~?

AI 활용 경험 공유하기

AI활용을 해보지 않은 경험을 했습니다. 필요한 시간이었습니다.

리뷰 받고 싶은 내용

사실 코드 자체는 매우 부끄럽고 부족함을 알기 때문에, 아주 뾰족하게 리뷰 요청드리기 애매합니다 ㅠㅠ

1) PR에 남긴 제 작업과정에서 개선할 부분이 있을까요?

저는 가끔 제가 너무 비효율적으로 작업하는 습관을 가지고 있다고 생각합니다. 결과를 내는 것 보다, 그 과정에서 다양한 방향을 고민해보는 것에 집중하게 되는 것 같아요. 아마도 작업 시 목표지향적으로 움직이지 않아서인 것 같습니다. 물론 실제 업무와 이런 학습성 작업의 접근 방식은 다르긴 합니다만, 자꾸 이런 접근이 반복되고 사서 고생을 하는 것 같습니다. 무튼 정답이 어느정도 있는 질문이라서, 쓰고나니 애매한 질문이라는 느낌이네요.
그렇담 추가로, 코치님은 과제를 검토하시면서 어떤 PR이 읽기 좋으신가요? 4주동안 과제 제출 / 회고성 PR을 작성하면서 '어떤 방식으로 작성해야겠다' 등의 기준이 없었다보니, '내 PR이 잘 읽힐까?', '상황에 맞는 적절한 포멧/문체 일까?'라는 생각이 문득 들었습니다. 제 PR에 대한 피드백 부탁드립니다...🙏

2)

@piggggggggy piggggggggy force-pushed the feature-spa branch 2 times, most recently from c401f55 to 41835fc Compare November 10, 2025 16:14
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은 vanilla JS 컴포넌트 기반 SPA 구현으로, 요구사항인 상품 목록, 필터, 정렬, 무한 스크롤, 장바구니, 상품 상세, 토스트, 라우팅 등 주요 기능을 모두 구현하였습니다. 타입스크립트 설정과 DTO로 API 데이터 타입 안정성 보장, 상태 관리(store) 구축, 라우터 구성, 뷰모델로 UI 상태 분리 등 설계가 좋은 기반을 갖추고 있습니다.

주요 구조

  • createComponent 유틸리티로 선언적 컴포넌트 생성 및 라이프사이클, 이벤트 위임 관리
  • appStore 전역 상태 관리 및 로컬 스토리지 연동
  • Router 모듈로 SPA 라우팅 및 주소 관리
  • API함수를 DTO 매핑하여 API 변경에 유연하게 대응
  • ViewModel 계층으로 복잡한 UI 상태를 분리
  • 페이지별 컴포넌트(HomePage, ProductDetailPage)로 비즈니스 로직 캡슐화

개선 방향

  • 상태와 URL 쿼리를 단일 책임 원칙에 맞게 분리, 상태 관리 체계와 URL 동기화 로직 일원화 권장
  • 전역 상태 관리 활용 시 중복된 로컬 상태 관리를 지양하여 상태 불일치 방지
  • 이벤트 핸들러 내 DOM 조작과 이벤트 타겟 탐색을 엄격히 하여 예외 케이스 대비
  • 컴포넌트 간 이벤트 핸들러 전달 및 래핑 구조 간결화
  • URL 조합과 라우터 경로 처리 로직에서 경로 합성 방식 통일화

이 구조는 유지보수성과 확장성 기반 마련에 적합하지만, 더 큰 규모에서는 상태 관리 라이브러리 도입, 모듈화 강화, 이벤트 위임 명확화 등이 필요할 것입니다.### 1) 작업 및 PR 작성 방식 관련 피드백

학습 단계에서 여러 방향을 고민하고 다양한 시도를 해보는 것은 매우 중요한 성장 과정입니다. 최종 결과 완성도도 중요하지만, 그 과정에서 고민과 선택, 경험이 쌓이는 것이 본질적 가치입니다. 다만 실제 업무에서는 명확한 목표 설정과 효율적 방향 전환이 중요하니, 상황에 따라 집중 대상과 적절한 타협점을 찾는 연습도 필요합니다.

코치의 경험을 토대로 몇 가지 제언을 드립니다:

  • 작업 중간 중간 작게라도 자주 커밋하고 목적별, 기능별로 분리해두면 나중에 리뷰하고 읽기 좋습니다.
  • PR 작성 시 현재 구현 목적, 문제점, 고민한 대안, 의뢰하고 싶은 포인트를 명확히 정리하면 리뷰어가 구체적으로 도움을 줄 수 있습니다.
  • 중요한 변경 사항과 의도, 코드 패턴에 대해 간단한 설명을 주석이나 PR 코멘트에 포함하면 이해가 더욱 쉽습니다.
  • 코드 스타일, 네이밍, 구조 등 통일성을 신경 쓰되 너무 완벽함에 집착하지 마세요.

2) 개인 PR 작성에서 고려할 점

  • PR 제목과 본문은 분명하게 작성하기
  • 변경한 파일과 대표적인 변경 사항을 정리하기
  • 코드 리뷰에서 특히 보고 싶은 부분이나 애매한 부분 콕 집어 질문하기
  • 테스트 수행 결과나 실행 방법 간단 기술

결론적으로, 자신의 학습 스타일과 업무 스타일을 점검하며, 현재 PR에 대해 다음 단계 목표를 세워 보시면 좋겠습니다. 질문해주셔서 감사드리며, 앞으로도 꾸준한 학습 응원하겠습니다! 🙏

console.error("[HomePage] onMount error", error);
} finally {
setState("isLoading", false);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

1. 문제상황 제시

실제 상품 검색, 필터, 정렬 조건들이 URL 쿼리 파라미터에 반영되어 사용자 상태 공유와 새로고침 후 상태 유지가 요구될 때 현 구조에서는 URL 반영과 상태 동기화가 일부 중복 관리되며, 개선 없이 확장하면 관리하기 어렵습니다.

  • URL 파라미터를 직접 업데이트하는 로직이 컴포넌트 내부 효과(effects)와 핸들러에 산재함
  • 여러 상태 조작 함수 내에서 Router.updateQueryParams 호출이 분산됨
  • 무한 스크롤과 초기 필터 로드 등 복잡한 상태 변화가 섞여 있어 유지보수가 어려움

2. 근본 원인

렌더링 함수 내에서 직접 URL 업데이트, 로컬 상태관리, API 호출 로직이 함께 다뤄지면서 관심사 분리가 약해지고, 상태 관리와 사이드 이펙트가 혼재되어 있습니다.

3. 개선 구조

  • 현 구조: HomePage 컴포넌트 내 상태 관리 + 렌더링 + URL 쿼리 업데이트 + API 호출 (한 곳에서 복합적으로)
  • 개선 구조: 상태 관리(Store 또는 전역 상태)와 URL 동기화를 별도의 모듈로 분리, HomePage는 상태 변경 알림 수신 + 단순 렌더링만 담당

개선 사항:

  • URL 쿼리 상태와 내부 상태를 1:1 대응시키는 커스텀 훅/모듈 분리
  • 상태 변경 시 URL 업데이트 로직을 일원화하여 중복 호출을 방지
  • API 호출은 상태 변화에 따른 단일 Effect로 처리하여 의존성 명확화
// ❌ 현 상태 변경 핸들러 예
handleSetSort: (value) => {
  setState("listResponse", (curr) => ({
    ...curr,
    filters: { ...curr.filters, sort: value },
  }));
  Router.updateQueryParams({ sort: value });
},

// ✅ 개선안 (예)
import { syncFiltersWithUrl } from './url-sync';
function handleSetSort(value) {
  const newFilters = { ...filters, sort: value };
  setFilters(newFilters);
  syncFiltersWithUrl(newFilters); // 중앙화된 함수
}

추가적인 전역 상태 사용 및 이벤트 위임 관리 고려

현재 컴포넌트 생성방식은 효과적이나, 전역 상태 및 이벤트 위임이 가능한 구조 도입 시 확장과 유지보수가 더 용이해질 수 있습니다.

stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 3h2l.4 2M7 13h10l4-8H5.4m2.6 8L6 2H3m4 11v6a1 1 0 001 1h1a1 1 0 001-1v-6M13 13v6a1 1 0 001 1h1a1 1 0 001-1v-6"
Copy link
Contributor

Choose a reason for hiding this comment

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

1. 불필요한 중복 상태 변경 문제

handleIncreaseQuantity, handleDecreaseQuantity 등 함수 내부에서 setState와 appStore.setCart를 연속 호출하며 두 상태값을 별도로 관리하고 있는데, 상태 불일치 및 불필요한 렌더링 유발 가능성이 있습니다.

2. 근본 원인

로컬 컴포넌트 상태와 전역 상태(appStore)가 모두 동일한 데이터를 관리하며 동기화 책임이 분산되어 있음

3. 개선 구조

  • 전역 상태(appStore)를 단일 출처로 하여 로컬 state 제거하거나, 전역 상태를 구독하여 로컬 상태와 혼용하지 않도록 개선
  • setState는 appStore 변경에 따른 구독 콜백에서 처리하게 하여 data flow를 단방향으로 유지
// ❌ 현재 방식
const handleIncreaseQuantity = (productId) => {
  setState("cart", (cur) => /* ... */);
  appStore.setCart(cart.map(/* ... */));
};

// ✅ 개선 예시
const handleIncreaseQuantity = (productId) => {
  appStore.addCartItemCountByProductId(productId);
};
// 컴포넌트는 appStore 구독으로만 cart 상태를 받음

if (isRendering) return;
handler(currentProps, getState, setState, event);
},
]),
Copy link
Contributor

Choose a reason for hiding this comment

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

1. 이벤트 위임 시 일관성 및 성능 이슈

문서 전체에 이벤트 리스너를 부착할 때 모든 이벤트에서 조건 검사 후 핸들러를 실행하게 구현되어, 이벤트 빈도 높은 요소(예: input, scroll 등)에서 성능 저하 우려

2. 근본 원인

한 문서 전체의 이벤트를 capture phase 또는 bubble phase에서 전부 처리하면서 이벤트 필터링이 불분명하고, 잘못 처리될 여지가 있음

3. 개선

  • 이벤트 타입별로 보다 명확하고 구체적인 위임 방식 적용
  • 이벤트 위임 원칙에 충실히, 반드시 필요한 엘리먼트에 한정
  • 버블링 단계 활용을 명확히 하여 이벤트 중복 처리 최소화
  • Propagation 제어를 엄격히 하여 중복 호출 방지

lprice: 0,
},
eventHandlers: {
"navigate-to-detail": (props, getter, setter, event) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

1. eventHandlers에 직접 DOM 이벤트 의존

navigate-to-detail 핸들러가 이벤트 타겟 확인 로직이 불완전해, 제품 이미지 외 다른 영역 클릭 시도시 버그가 발생할 위험

2. 근본 원인

DOM 이벤트 타겟 검사 구현이 단순히 event.target에 의존해서 정확한 최상위 타겟을 확인하지 않음

3. 개선

  • 이벤트 타겟 대신 이벤트 위임 시 이벤트가 발생한 컴포넌트 루트 엘리먼트 혹은 적절한 상위 엘리먼트에서 데이터 속성을 활용해 정확한 상품 ID 획득
  • event.target.closest() 등 안전한 탐색 함수 사용
// ❌ 현재
const productId = event.target.dataset.productId;

// ✅ 개선
const productId = event.target.closest('[data-product-id]')?.dataset.productId;

<div class="flex items-center mb-3">
<div class="flex items-center">
<svg class="w-4 h-4 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
<path
Copy link
Contributor

Choose a reason for hiding this comment

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

1. 상품 수량 범위 하드코딩 문제

quantity-decreasequantity-increase 핸들러에서 수량 제한을 하드코딩(최대 107)하고 있으나, 재고(stock) 등의 외부 데이터와 동기화 되어 있지 않아 재고 변경 시 수량 제한이 일치하지 않을 위험 있음

2. 근본 원인

상품 상세 정보 내 stock 필드와 수량 input 제약 사이 연결고리가 없음

3. 개선

  • stock 값을 맥스 값으로 동적으로 활용하도록 수정
  • 동적 범위 체크 및 UI 반영으로 재고와 수량 제약 일관성 유지
const stock = productDetailResponse?.stock || 999;

// 수량 증가 제한
if (getter("count") >= stock) return;

AFTER: appState.cart.find((item) => item.id === productId)?.count,
});
appStore.setCart(
appState.cart.map((item) => (item.id === productId ? { ...item, count: Math.min(item.count + 1, 999) } : item)),
Copy link
Contributor

Choose a reason for hiding this comment

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

1. 선택 삭제 함수 문제

removeSelectedCartItems 메서드 내부에 체크된 상품을 삭제하는 것이 아니라 선택 상태를 모두 false로 리셋하는 버그 있음

2. 근본 원인

함수 내부에서 선택한 상품 제거 로직이 아닌, 선택 여부를 모두 false로 변경하는 코드가 사용됨

3. 개선

  • 선택된 상품만 필터링하여 cart에서 제거하도록 변경
// ❌ 현재
removeSelectedCartItems: () => {
  appStore.setCart(appState.cart.map(item => ({ ...item, selected: false })));
},

// ✅ 개선
removeSelectedCartItems: () => {
  appStore.setCart(appState.cart.filter(item => !item.selected));
},

const fullUrl = queryString ? `${url}?${queryString}` : url;

history.replaceState(null, "", fullUrl);

Copy link
Contributor

Choose a reason for hiding this comment

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

1. URL 조작 시 경로 생성 로직 문제

push 메서드 내 경로 생성에서 basePath와 경로 연결 시 슬래시 처리 로직이 불명확하여 URL 중복 또는 누락 가능성 있음

2. 근본 원인

basePath 변수에 따라 슬래시가 중복될 위험이나, 일관적인 경로 합성 처리 부족

3. 개선

  • URL 합성 시 URL 객체 또는 명확한 분리+조합 로직 사용 권장
// 기존
const fullUrl = queryString
  ? `${Router.basePath}${pathOnly.replace("/", "")}?${queryString}`
  : `${Router.basePath}${pathOnly.replace("/", "")}`;

// 개선
const normalizedBase = Router.basePath.endsWith('/') ? Router.basePath.slice(0, -1) : Router.basePath;
const normalizedPath = pathOnly.startsWith('/') ? pathOnly : `/${pathOnly}`;
const fullUrl = queryString
  ? `${normalizedBase}${normalizedPath}?${queryString}`
  : `${normalizedBase}${normalizedPath}`;

${(!props.selectedCategory2 && !props.selectedCategory1
? firstDepthOptions.map(
(option) => /* HTML */ `
<button
Copy link
Contributor

Choose a reason for hiding this comment

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

1. 카테고리 선택 이벤트 핸들링 개선점

핸들러에서 event.target.dataset으로 직접 접근하는데, event.target이 예상과 다를 경우 오류 여지 있음

2. 근본 원인

이벤트 전파 과정에서 event.target이 버튼 외 다른 요소인 경우 처리 누락 가능성

3. 개선

  • event.target.closest를 활용해 명확한 버튼 요소를 찾아 처리
const button = event.target.closest('button[data-category1]');
if (!button) return;
const value = button.dataset.category1;
props.handleSetSelectedCategory1(value);

이 같은 방식을 category2-filter 핸들러 등에도 적용 권장

value="${props.search}"
class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg
focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

1. prop 전달 불필요 중복 및 코드 정돈

FilterToolbox에서 핸들러를 래핑하여 다시 같은 인자를 넘기는 형태가 반복적임

2. 근본 원인

필요 이상으로 핸들러를 재정의 및 다시 전달하여 코드 양 증가

3. 개선

  • 핸들러를 직접 props.handleXXX로 사용하는 것이 가독성/간결성에 도움
// ❌ 현재
const handleSetSort = (value) => {
  props.handleSetSort(value);
};

// ✅ 개선
// templateFn 내부 바로 사용
// props.handleSetSort

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.

2 participants