Skip to content

Conversation

@devmineee
Copy link

@devmineee devmineee commented Nov 11, 2025

과제 체크포인트

배포 링크

https://devmineee.github.io/front_7th_chapter2-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로 다시 구현한다.
  • 이 과정에서 직접 가공하는 것은 최대한 지양한다.

과제 셀프회고

기술적 성장

  • 바닐라 자바스크립트를 직접 사용해봄으로써 평소 리액트나 기타 라이브러리로 대신하던 기술들을 직접 구현해보며 원리를 이해해보는 시간이 되었습니다.
  • 직접 배포를 진행해보며 배포 관련한 지식을 쌓을 수 있었습니다.

학습 효과 분석

  • 자바스크립트를 통해 직접 DOM을 선택하고, 이벤트리스너를 등록해서 원하는 기능을 구현해봄으로써 자바스크립트 기본 사용법 숙지
  • 페이지의 렌더링 과정을 이해하여 최적화 과정 이해

과제 피드백

  • 아주 재미있는 과제였습니다! 무언가 자바스크립트의 근본적인 공부를 할 수 있었던 시간이라 도움이 많이 되었던 것 같습니다.

AI 활용 경험 공유하기

  • Cursor + Claude 를 통하여 도저히 혼자 힘으론 할 수 없었던 토스트를 부분을 구현했습니다..

리뷰 받고 싶은 내용

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은 전체적으로 요구사항에 맞는 기능들을 충실히 구현하여, 상품 목록 조회, 필터링, 무한 스크롤, 장바구니 기능, 상품 상세 페이지, SPA 네비게이션 및 URL 관리 부분까지 구현되었습니다.

코드 구조는 Vanilla JS 환경에서 컴포넌트 기반으로 분리하여 가독성을 더했고, 상태 관리는 singleton store 객체를 통해 상태 변경과 구독 시스템을 갖추었습니다. 이벤트 위임 방식으로 DOM 이벤트들을 한곳에서 처리하고, 상품 목록 및 상세, 장바구니 모달 등 UI 컴포넌트마다 역할을 나누었습니다.

다만 앞으로 기능이 증가하고 유지보수가 필요한 대형 프로젝트로 성장할 경우 아래와 같은 개선이 필요합니다.

  • 현재 main.js 내 모든 상태 및 이벤트 관리가 집중되어 있어, 모듈화와 컴포넌트별 이벤트 등록 체계가 필요합니다.
  • 상태와 URL 동기화 책임을 명확히 분리하면, 쿼리 파라미터 관리 및 필터 상태 유지가 더욱 견고해집니다.
  • 장바구니 모달 컴포넌트 내 상태 및 이벤트 관리가 UI와 혼재되어 있으므로, 상태 관리 로직과 렌더링을 분리하는 구조가 확장성에 유리합니다.
  • 다단계 카테고리 지원을 위해 필터링 로직을 재귀적, 동적 구조로 개선하면 유지보수가 용이합니다.
  • 토스트 메시지와 이벤트 핸들링 등 UI 컴포넌트를 인스턴스화하거나 상태 기반으로 관리하면, 중복 및 충돌 문제를 방지할 수 있습니다.
  • 패키지 버전 고정 등 의존성 관리 측면에서 안정성을 확보하는 것이 좋습니다.

이러한 변화는 확장성과 유지보수성을 강화하여 실제 서비스 운영 환경에서 발생할 수 있는 요구사항 변화나 버그 대응에 신속히 대응할 수 있게 해 줄 것입니다.

전체적으로 훌륭한 완성도를 보여주었고, 앞으로 상태 관리 분리, 컴포넌트 책임 분할 등을 적용하면 더욱 견고한 아키텍처를 구축할 수 있습니다.질문이 없으셨기에, 향후 추가 학습이나 의문 사항이 있을 때 언제든 질문해 주세요. 10년차 프론트엔드 개발자의 관점에서 좀 더 깊이있게 말씀드리면,

현대 프론트엔드 개발에서는 상태 관리와 UI 컴포넌트의 책임 분리가 매우 중요합니다. 특히 SPA를 구현할 때 URL과 상태 동기화, 사용자 인터랙션과 상태 변경, 비동기 데이터 로딩 등을 체계적으로 관리하지 않으면 버그를 유발하기 쉽습니다.

따라서 이번 과제를 통해 배운 Vanilla JS 구조에서 한 단계 나아가, React, Vue 등 프레임워크가 내부적으로 하는 상태 관리, 랜더링, 이벤트 관리를 이해해 보시면 좋겠습니다.

또한 테스트 자동화, 타입스크립트 적용, 상태 변경 로그 관리 같은 고급 주제도 현업에서 널리 활용됩니다. 이를 차근차근 학습해 나가면 나만의 견고한 프론트엔드 아키텍처를 설계하는 데 큰 도움이 될 것입니다.

끝으로, 변화하는 요구사항에 대응하기 위해 항상 코드의 확장성, 변경 용이성, 가독성을 염두에 두고 설계하는 습관이 중요함을 기억해 주세요.

category2: "",
sort: "price_asc",
limit: 20,
},
Copy link
Contributor

Choose a reason for hiding this comment

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

1. 문제상황 제시

구체적인 문제 상황:
만약 향후 상품 목록 또는 상세 페이지가 복잡해지고, 다양한 상태(예: 로딩, 에러, 페이지 네비게이션 등) 및 이벤트 처리가 더 많아질 경우, 현재 main.js 내에서 모든 상태와 로직이 한 파일에 집중되어 관리되고 있어 확장성과 유지보수성이 떨어질 수 있습니다.

현재 코드의 한계:

  • 상태 관리와 렌더링 로직이 강하게 결합되어 있다.
  • 페이지별 상태 분리나 컴포넌트별 독립성이 부족하다.
  • 글로벌 이벤트 핸들러에 너무 많은 이벤트 처리 로직이 몰려 있어, 특정 기능 확장 시 코드 복잡도가 급격히 증가한다.

2. 근본 원인

핵심 문제:
"애플리케이션 상태 관리, 렌더링, 이벤트 처리가 명확히 분리되고 컴포넌트화 되어 있지 않아서 코드가 점차 커질 때 유지보수가 어려운 단일 책임 원칙 위반 구조"

왜 문제인가:

  • 기능이 추가되거나 수정될 때, 하나의 대형 파일에서 여러 책임을 동시에 다루어야 하므로 의도치 않은 버그 유발 가능성이 커진다.
  • 코드 가독성이 떨어지고, 새로운 개발자 진입 장벽이 높아진다.
  • 재사용성 저하와 테스트 어려움 등으로 품질 관리를 할 수 없다.

3. 개선 구조

현재 구조:

앱 상태 + 렌더링 + 이벤트처리 (main.js) -- 하나의 큰 파일에 모두 포함
  ├─ HomePage 렌더러
  ├─ DetailPage 렌더러
  ├─ 전역 이벤트 리스너 (카테고리, 장바구니, 네비게이션 등)
  └─ 상태 객체 (appState)

개선된 구조:

- 상태 관리(store.js 혹은 Zustand/Redux 등)
- UI 컴포넌트별 분리 (HomePage, DetailPage, CartModal 등)
- 각 컴포넌트 별 상태 및 이벤트 관리
- 라우터 역할 분리 (path에 따른 분기 + 상태 관리)
- 전역 이벤트는 각 컴포넌트에 위임

개선 사항:

  • [1] 상태 관리를 별도의 모듈 혹은 라이브러리로 분리하고, 데이터를 변경하는 함수와 구독자 패턴을 명확하게 설계합니다.
  • [2] UI 컴포넌트를 함수 단위로 명확히 분리하고, 각 컴포넌트가 자신과 연관된 이벤트만 수정 및 관리하도록 설계합니다.
  • [3] 전역 이벤트리스너 로직은 페이지 혹은 컴포넌트별 이벤트 위임 방식으로 리팩터링해 복잡도를 낮춥니다.

코드 비교:

// ❌ 현재 방식
// 글로벌 이벤트 리스너 내에서 너무 다양한 이벤트와 상태를 동시에 처리

document.addEventListener('click', (e) => {
  // 카테고리 선택, 장바구니 관련, 네비게이션 등 한 이벤트 핸들러에 집중
});

// ✅ 개선된 방식
// 각 컴포넌트 별 이벤트 등록 및 핸들링
const homePage = new HomePage(store);
homePage.attachEvents();

const detailPage = new DetailPage(store);
detailPage.attachEvents();

// 리스너를 해당 컴포넌트 내부에서 관리
class HomePage {
  constructor(store) {
    this.store = store;
  }

  attachEvents() {
    const searchInput = document.getElementById('search-input');
    searchInput?.addEventListener('keypress', this.onSearchKeyPress);
    // ...
  }
  onSearchKeyPress = (e) => {
    if(e.key === 'Enter') {
      // 검색 수행
    }
  }
}

void container.offsetHeight;
void container.getBoundingClientRect();
}
});
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이 동기화 문제를 일으킬 수 있습니다.

  • [한계점 1] 필터 변경 시 상태만 변경되고 URL갱신이 누락되거나 지연 발생 가능
  • [한계점 2] URL과 상태가 불일치하는 상황이 유발
  • [한계점 3] 복잡한 조건일수록 디버깅 어렵고 기능 확장 어려움

2. 근본 원인

핵심 문제:
상태 변경과 URL 반영 책임이 분리, 추상화되어 있지 않아 update 순서 관리가 어렵고,
네비게이션 함수가 렌더링 로직에 과도하게 의존해 상태-URL-렌더링 간 결합도가 높음

왜 문제인가:
이 부분은 SPA에서 중요한 상태와 URL 동기화를 명확히 별도 관리하지 않으면
버그가 발생하거나, URL 공유 기능 확장 시 유지보수가 매우 어려워집니다.

3. 개선 구조

현재 구조:

필터변경 -> 상태변경 -> navigateTo(url) 내부에서 history.pushState 후 render 호출

render 함수 내에서 상태에 맞게 화면 그리기

개선된 구조:

전체 필터값을 URL과 상태에 동기화 하는 별도 모듈 생성
필터 변경 시 
  1) URL 쿼리 변경
  2) URL 변경 이벤트나 popstate에서
  3) 쿼리를 해석해 상태를 일관되게 업데이트
  4) 상태 변경에 따른 화면 렌더링

개선 사항:

  • [1] 필터, 카테고리 등 상태와 URL 동기화를 전담하는 함수 또는 훅 별도 구현
  • [2] 모든 상태 변경은 URL 쪽에 반영되고, 변경된 URL을 읽어 상태 변환 로직 실행
  • [3] 네비게이션 및 렌더링 분리로 두 방향성 유지 (URL->상태, 상태->URL)
// ❌ 현재 방식
const onFilterChange = (filter) => {
  appState.filters = { ...appState.filters, ...filter };
  const url = buildUrl('/', appState.filters);
  navigateTo(url); // pushState + render 호출
}

// ✅ 개선된 방식
// 모든 필터는 URL 변경 함수에 의해 관리되고
// popstate 이벤트에서 상태와 렌더링 모두 호환되게 함

window.addEventListener('popstate', () => {
  const filters = parseFiltersFromURL();
  store.setFilters(filters);
  render();
});

const onFilterChange = (filter) => {
  const nextFilters = { ...store.getFilters(), ...filter };
  const url = buildUrl('/', nextFilters);
  history.pushState(null, null, url); // URL만 변경
  store.setFilters(nextFilters); // 상태 변경
  render(); // 화면 재렌더링
};

`
: ""
}
<!-- 총 금액 -->
Copy link
Contributor

Choose a reason for hiding this comment

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

1. 문제상황 제시

현 상태에서 장바구니 선택 체크박스, 전체 선택 등 UI 동작 로직이 HTML 템플릿 문자열 내에 직접 포함되어 있어,
추후 기능 확장이나 복잡한 상태 요구 발생 시 관리가 어려워질 위험이 있습니다.

  • [한계점 1] UI 렌더링과 상태 관리 로직가 결합되어 가독성 저하
  • [한계점 2] 선택 상태 관리가 로컬 변수로 이뤄져 재사용성 제한됨
  • [한계점 3] 복잡한 이벤트 처리 시 상태와 UI 동기화 오류 가능성

2. 근본 원인

핵심 문제:
UI 표현(템플릿)과 상태 관리(선택된 항목, 이벤트 핸들링)가 한 함수 내에서 혼재되어 있기 때문

왜 문제인가:
컴포넌트가 커질수록 UI와 상태가 밀접하게 섞여 변경, 디버깅이 어려워지고,
별도의 상태 관리 없이 이벤트 위임 등에만 의존하는 방식은 예외 처리 및 기능 확장에 제약이 생김.

3. 개선 구조

현재 구조:

CartModal 컴포넌트 내 HTML 생성 + 선택 여부 단순 렌더
전체 선택 체크박스와 개별 체크박스 이벤트 처리가 다른 곳에서 이루어짐

개선된 구조:

컴포넌트를 두 부분으로 분리
- UI 렌더링 전용: 상태 기반 UI 표현에 집중
- 상태 및 이벤트 관리 전용: 선택 상태, 갱신 함수 로직 집중

컴포넌트 내부에 선택 상태와 유틸 함수 내장, 렌더링 함수에서 상태만 참고하여 UI 표현
이벤트 핸들러에서 상태 변경 후 컴포넌트 다시 렌더링

개선 사항:

  • [1] 컴포넌트 내부 상태(선택된 상품들)를 명확히 유지하고 렌더링에 주입
  • [2] 이벤트 핸들러는 상태 변경 후 렌더링 함수를 호출하여 UI 갱신
  • [3] 선택 상태 로직을 별도의 훅/함수로 추출, 테스트 가능하게 설계
// 상태 유지 예시
class CartModal {
  constructor(cart) {
    this.cart = cart;
    this.selected = new Set();
  }

  toggleSelect(productId) {
    if (this.selected.has(productId)) this.selected.delete(productId);
    else this.selected.add(productId);
    this.render();
  }

  render() {
    const html = `... ${this.cart.map(item => ...)} ...`;
    // 변경된 html 삽입
  }
}

<label class="text-sm text-gray-600">카테고리:</label>
${Breadcrumb(filters)}
</div>
<!-- 1depth 카테고리 -->
Copy link
Contributor

Choose a reason for hiding this comment

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

1. 문제상황 제시

카테고리 버튼 렌더링 부분에서 조건문이 HTML 문자열 내에 직접 중첩되어 가독성이 매우 떨어집니다.

  • [한계점 1] JSX나 템플릿 엔진 미사용으로 중첩 컴포넌트 렌더링 코드가 복잡하고 오류 가능성 증가
  • [한계점 2] HTML 문자열을 만드는 과정에서 조건부 로직이 많아 유지보수성 저하
  • [한계점 3] UI 상태(선택 여부) 변경 시 렌더링 함수 재사용 어려움

2. 근본 원인

핵심 문제:
템플릿 빌드 시 문자열 내 조건부 로직이 수직적 중첩으로 작성되어 가독성 및 재사용성 저하

왜 문제인가:
복잡한 UI 상태가 늘어날수록 가독성 떨어지고, 작은 수정에도 의도치 않은 부작용 발생 가능

3. 개선 구조

현재 구조:

SearchForm 함수 내에서 if/else 조건을 문자열 내 포함하여 복잡한 JSX 스타일 렌더링 구현

개선된 구조:

카테고리1, 카테고리2 렌더러를 별도 함수로 분리
각 함수는 선택 상태에 따라 클래스 네임이나 스타일 결정
주 컴포넌트는 컴포넌트 단위 함수 호출만 담당

개선 사항:

  • [1] 카테고리 버튼 렌더링용 유틸 함수 분리
  • [2] 선택 상태를 파라미터로 받아 스타일만 조절
  • [3] UI 상태 업데이트 로직은 컴포넌트 외부에서 처리
const CategoryButton = ({category, selected}) => `
  <button class="${selected ? 'selected-class' : ''}">${category}</button>
`;

const renderCategoryButtons = (categories, selectedCat) => {
  return categories.map(cat => CategoryButton({category: cat, selected: cat === selectedCat})).join('');
};

const SearchForm = (...) => {
  const categoryButtons = renderCategoryButtons(categories, selectedCategory1);
  return `<div>${categoryButtons}</div>`;
};

// 뒤로 가기 버튼 (상세 페이지 헤더에 있으므로 main 조건 없이 체크)
if (relativePath.startsWith("/product/")) {
const backBtn = target.closest("#back-btn");
if (backBtn) {
Copy link
Contributor

Choose a reason for hiding this comment

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

1. 문제상황 제시

현재 스크롤 이벤트 핸들러는 무조건 window에 등록되고, 이전 핸들러를 제거하는 로직이 직접 관리되고 있습니다.
만약 여러 페이지에서 스크롤 이벤트 처리 요구가 다양해지면 이벤트 핸들링 관리가 어려워지고,
특정 페이지에서 스크롤 이벤트를 강제로 제거 필요 시 적용이 까다로워질 수 있습니다.

  • [한계점 1] 스크롤 이벤트가 전역으로 등록되어 특정 페이지에서만 활성화하거나 비활성화하기 어렵다
  • [한계점 2] 중복 등록 방지를 수동으로 관리해 오류 가능성 존재
  • [한계점 3] 이벤트 헬퍼 함수 없이 직접 구현하여 확장성 제한

2. 근본 원인

핵심 문제:
이벤트 리스너 등록 및 해제를 함수로 추상화하지 않고 직접 관리하며,
페이지별 UI 상태에 따른 이벤트를 분리하지 않음

왜 문제인가:
확장 시 스크롤 외 이벤트 추가 때마다 중복 코드 발생 및 관리 복잡도 증가,
유지보수 어려움 및 확장성 저하

3. 개선 구조

현재 구조:

window.removeEventListener('scroll', scrollEventListener);
window.addEventListener('scroll', handleScroll);

// 전역 변수 scrollEventListener 수동 관리

개선된 구조:

전용 EventManager 클래스 또는 훅으로 이벤트 등록/해제 관리
페이지별 스크롤 이벤트 활성화 여부 및 콜백 관리
이벤트 중복 등록 방지를 내부에서 처리

개선 사항:

  • [1] 스크롤 이벤트를 등록/해제 관리하는 헬퍼 함수 도입
  • [2] 이벤트 매니저를 통해 페이지 상태 변화에 따른 이벤트 자동 바인딩
  • [3] 기능 테스트 및 예외 처리 체계 구성
class EventManager {
  constructor() {
    this.handlers = new Map();
  }
  setScrollHandler(handler) {
    if (this.handlers.has('scroll')) {
      window.removeEventListener('scroll', this.handlers.get('scroll'));
    }
    if (handler) {
      window.addEventListener('scroll', handler);
      this.handlers.set('scroll', handler);
    }
  }
  clearAll() {
    this.handlers.forEach((fn, type) => {
      window.removeEventListener(type, fn);
    });
    this.handlers.clear();
  }
}

const eventManager = new EventManager();
// 페이지 전환 시 eventManager.setScrollHandler(newScrollHandler);
// 필요 시 eventManager.setScrollHandler(null);


const Loading = `
<div class="text-center py-4">
<div class="inline-flex items-center">
Copy link
Contributor

Choose a reason for hiding this comment

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

1. 문제상황 제시

컴포넌트 내에서 '로딩'과 '로딩중 더보기' UI가 명확히 분리되어 있으나,
담당하는 상태 플래그가 많아 복잡도가 증가하여 버그 및 상태 불일치가 발생할 수 있습니다.

  • [한계점 1] loading, loadingMore, hasMore 등의 상태가 서로 간섭 가능
  • [한계점 2] UI 상태 조합에 따라 렌더링 복잡성 증가
  • [한계점 3] 이 조건 로직이 여러 컴포넌트에서 중복될 위험

2. 근본 원인

핵심 문제:
여러 상태 플래그를 UI 조건 처리에 직접 사용하므로,
상태 머신 형태의 명확한 상태 전환이 없는 점

왜 문제인가:
명확한 상태 정의 및 전환이 없으면 각 렌더링 조건이 누락되거나 중복 실행되어,
예측 불가한 UI 상태가 나타날 수 있음

3. 개선 구조

현재 구조:

{loading ? ... : loadingMore ? ... : hasMore ? ... : ...}

개선된 구조:

loading: 'idle' | 'loading' | 'loadingMore' | 'loaded' | 'error'
상태 머신 기반 렌더링 =>
loading === 'loading' ? 로딩 UI : loading === 'loadingMore' ? 스켈레톤 UI : done UI 등

개선 사항:

  • [1] loading 상태를 enum이나 문자열 상태로 통합 관리
  • [2] 각 상태 전환 로직 명확히 작성
  • [3] 공통 UI는 분리하여 재사용
// enum 예시
const LoadingStates = {
  IDLE: 'idle',
  LOADING: 'loading',
  LOADING_MORE: 'loadingMore',
  LOADED: 'loaded',
  ERROR: 'error'
};

// 렌더
if(state.loading === LoadingStates.LOADING) {
  // 스켈레톤
} else if(state.loading === LoadingStates.LOADING_MORE) {
  // 로딩 인디케이터
} else if(state.loading === LoadingStates.ERROR) {
  // 에러 UI
} else {
  // 상품 리스트
}

"build": "vite build",
"lint:fix": "eslint --fix",
"prettier:write": "prettier --write ./src",
"preview": "vite preview",
Copy link
Contributor

Choose a reason for hiding this comment

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

1. 문제상황 제시

package.jsonpnpm-lock.yaml 파일에서 여러 패키지의 버전을 latest로 설정하고 있으나,
이는 프로젝트의 빌드 안정성과 재현성을 저해할 수 있습니다.

  • [한계점 1] 패키지 버전 자동 업데이트로 인한 예상치 못한 동작 변경 위험
  • [한계점 2] CI/CD 빌드 실패 또는 환경별 차이 발생 가능성
  • [한계점 3] 특정 이슈 패치 후에도 버전 고정하지 않아 디버깅 어려움

2. 근본 원인

핵심 문제:
개별 의존성 버전을 구체적으로 고정하지 않고, 항상 최신으로 가져오도록 설정되어 있기 때문

왜 문제인가:
일관된 빌드환경 유지와 버전 관리가 필수적인 협업 및 배포 상황에서 불안정성을 초래

3. 개선 구조

현재 구조:

"@playwright/test": "latest",
"vitest": "latest",

개선된 구조:

"@playwright/test": "^1.53.2",
"vitest": "^3.2.4",

개선 사항:

  • [1] 프로젝트에 맞는 안정적인 패키지 버전을 명시적으로 지정
  • [2] lockfile 유지 및 관리로 재현 가능한 빌드 환경 조성
  • [3] 버전 업데이트는 별도 PR 및 테스트 절차 통해 진행

(item) =>
item.title.toLowerCase().includes(searchTerm) ||
item.brand.toLowerCase().includes(searchTerm),
(item) => item.title.toLowerCase().includes(searchTerm) || item.brand.toLowerCase().includes(searchTerm),
Copy link
Contributor

Choose a reason for hiding this comment

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

1. 문제상황 제시

필터링 함수 내에서 다중 카테고리 레벨(category3, category4)에 대해 주석처리 및 제거되어 있는데,
향후 3단계 이상의 카테고리 구조가 요구되면 확장에 대비하지 못할 우려가 있습니다.

  • [한계점 1] 현재 2단계까지만 지원하고 있음
  • [한계점 2] 코드가 나중에 3단계 카테고리 지원 시 아예 저수준부터 수정 필요
  • [한계점 3] 카테고리 데이터 구조 및 필터링 함수가 하드코딩 형태로 작성되어 유연성 부족

2. 근본 원인

핵심 문제:
카테고리 데이터 구조와 필터링 로직이 2단계 구조에 의존적이고, 계층적 필터링 함수가 확장가능하도록 설계되지 않음

왜 문제인가:
복잡한 카테고리를 지원해야 하는 대규모 쇼핑몰 기능에서 확장성이 매우 중요하기 때문에,
사전에 구조를 명확히 잡지 않으면 유지보수가 힘듦

3. 개선 구조

현재 구조:

if (query.category1) filter category1
if (query.category2) filter category2
// category3, category4 제거

개선된 구조:

카테고리 데이터는 객체 트리로, 필터는 재귀적으로 트리를 탐색하는 방식 구현
카테고리 필터 키를 배열로 받고 순차 필터링 적용

개선 사항:

  • [1] 카테고리 경로를 배열로 관리(ex: ['생활/건강', '주방용품', '냄비'])
  • [2] 재귀적으로 상품의 카테고리 속성과 비교해 필터링
  • [3] 카테고리 데이터도 트리형 자료구조로 관리하여 UI에도 재활용
function filterByCategoryPath(products, categoryPath) {
  return products.filter(product => {
    return categoryPath.every((cat, idx) => product[`category${idx + 1}`] === cat);
  });
}

<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4"></path>
</svg>
</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. 문제상황 제시

상품 상세 페이지 내 '관련 상품' 섹션 렌더링 시, 데이터 필터링 및 UI 표현이 컴포넌트 내부에 하드코딩되어 있어,
추후 관련 상품 기준을 변경하거나 다변화할 때 힘들 수 있습니다.

  • [한계점 1] 관련 상품 기준(category2)에 고정돼 있음
  • [한계점 2] UI 렌더링에서 필터링 로직이 분리 안됨
  • [한계점 3] 관련 상품의 수를 직접 2개로 제한하고 있어 확장 어려움

2. 근본 원인

핵심 문제:
비즈니스 로직(관련 상품 기준 및 수량 제한)이 UI 컴포넌트와 섞여 있어 변경 시 영향이 큼

왜 문제인가:
복잡한 요구사항 변경이나 비즈니스 로직이 바뀔 경우 컴포넌트 재작성 발생, 코드 유지보수가 힘듦

3. 개선 구조

현재 구조:

컴포넌트 내부에서 관련 상품을 서버에서 미리 받아 필터링 후 직접 렌더
관련 상품 제외 및 최대 2개 제한 하드코딩

개선된 구조:

관련 상품 데이터는 별도 API 레이어에서 처리
컴포넌트에는 완성된 데이터만 주입
관련 상품 표시 수 및 필터 조건은 상위 로직 또는 설정에서 변경 가능

개선 사항:

  • [1] 서버 또는 상위 상태 로직에서 관련 상품 추출
  • [2] ProductDetail 컴포넌트는 단순히 관련 상품 배열 렌더링 역할만 수행
  • [3] 관련 상품 수, 필터 조건 등은 파라미터로 전달받도록 설계
// 상위 fetch
const relatedProducts = await getRelatedProducts(productId, { maxItems: 3 });

// 컴포넌트 호출
<ProductDetail product={product} relatedProducts={relatedProducts} />

@@ -0,0 +1,1114 @@
/* eslint-disable */

const 상품목록_레이아웃_로딩 = `
Copy link
Contributor

Choose a reason for hiding this comment

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

1. 문제상황 제시

template.js 파일 내 HTML 템플릿을 문자열로 장황하게 작성했는데, 이는 유지보수와 가독성에 부담을 줍니다.

  • [한계점 1] 동일한 구조가 여러 곳에 중복되고 있어 수정 시 일관된 변경 어려움
  • [한계점 2] UI 변경, 스타일 변경 시 코드량이 많아 실수가 나기 쉬움
  • [한계점 3] 별도의 컴포넌트 분리 및 재사용이 힘든 구조

2. 근본 원인

핵심 문제:
템플릿 문자열만으로 UI를 관리하면서 컴포넌트 기반 설계 원칙을 적용하지 않아 코드 중복과 복잡도를 증가

왜 문제인가:
컴포넌트 기반 설계는 재사용성과 유지보수성 향상에 필수이며,
단일 문자열 템플릿 관리는 변경에 따른 디버깅 비용 증가와 UI 품질 저하 초래

3. 개선 구조

현재 구조:

대용량의 하드코딩 된 HTML 문자열을 직접 main.js나 개별 컴포넌트에 구현

개선된 구조:

재사용 가능한 UI 컴포넌트 단위로 분리 후 함수 또는 모듈로 관리
Tailwind CSS 등 스타일링을 적용하는 것도 개별 컴포넌트 수준에서 관리

개선 사항:

  • [1] 공통 UI 요소(헤더, 푸터, 모달 등)를 컴포넌트화
  • [2] 반복되는 상품 카드, 버튼 등을 별도 함수 혹은 클래스로 분리
  • [3] 상태 기반 렌더링과 이벤트 연결을 명확히 구현
export const ProductCard = ({ product }) => {
  return `
    <div class="product-card"> ... </div>
  `;
};

const ProductList = ({ products }) => products.map(ProductCard).join('');

// main.js에서 import 후 사용

Copy link

@eveneul eveneul left a comment

Choose a reason for hiding this comment

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

성민 님~ 이번 주차도 너무 고생 많으셨습니다! 재사용이 가능한 컴포넌트는 분리하고, main.js에 작성된 역할이 다른 로직들은 /js나 컴포넌트 같은 경우 /components 폴더 같은 데에 정리해 놓으면 좋을 것 같아요.

다음 주차도 파이팅입니다!

@@ -0,0 +1,152 @@
const Loading = `
Copy link

Choose a reason for hiding this comment

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

로딩ui는 상품 상세에만 쓰이는 UI가 아닌 걸로 알고 있는데 ProductDetail.js에 넣으신 이유가 있을까요? components/ui/Loading.js로 공통 ui로 넣으심이 어떨까요?

</div>
`;

const RelatedProductItem = ({ productId, title, image, lprice }) => `
Copy link

Choose a reason for hiding this comment

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

요것도 components/product/RelatedProductItem.js로 컴포넌트 분리할 수 있을 것 같아요!

}">
${categoryName}
</button>
`;
Copy link

Choose a reason for hiding this comment

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

Category 버튼을 1뎁스, 2뎁스를 따로따로 만드는 것보다는 하나의 컴포넌트로 합치는 건 어떨까요? 저도 이번 챕터 문제를 풀 때 똑같은 고민을 했다가 성민 님처럼 두 개로 나눈 경험이 있는데요, 지금 생각해 보면 카테고리 뎁스가 더 많아질 때는 어떻게 하지? 생각이 들어서요.

지금 성민 님께서는 Category1Button에서는 두 개의 인자를 받고, Category2Button은 Category1Button의 정보를 포함해 총 3개의 인자를 받는데요,
이렇게 했을 때 두 개의 차이점은 그저 category1에 대한 정보를 받고 그걸 data-set으로 넣는 것만 차이가 있는 것 같아서,

제가 생각한 코드는..

const Category = (attrs, label, isSelected) => /* HTML */ `
  <button 
    ${Object.entries(attrs)
      .map((k, v) => `data-${k}=${v}`)
      .join(" ")}
    class="px-3 py-2 text-sm rounded-md border transition-color 
    ${isSelected ? "bg-blue-100 border-blue-300 text-blue-800" : "bg-white border-gray-300 text-gray-700 hover:bg-gray-50"}
  >${label}</button>
`;

이렇게 만들고, Category 컴포넌트를 사용할 때는,

.map(product => Category({ category1: product }, product, false)
.map(product => Category({ category1: selectedCategory1, category2: product },
product,
product === selectedCategory2 
)
)

이런 식으로 해도 좋을 것 같아용

const body = document.body;
if (!body) return;

const toastHTML = `
Copy link

Choose a reason for hiding this comment

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

토스트 컴포넌트 UI가 있는데 따로 만든 이유가 궁금합니다..!

void container.offsetHeight;
void container.getBoundingClientRect();
}
});
Copy link

Choose a reason for hiding this comment

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

음.. 단순히 토스트 컴포넌트를 노출/비노출 시키는 데에 requestAnimationFrame을 쓸 필요는 없을 것 같아요. requestAnimationFrame은 웹이 화면에 1초에 60번 화면을 그려야 하는데 (그게 사람 눈에 제일 보기 좋대요), 그 1번 (1프레임) 그릴 때 맞춰서 실행되는 함수예요.

보통은 애니메이션 기능 작업할 때 사용되고는 합니다.

const tick = () => {
  requestAnimationFrame(tick);

  box.render();
  /* 등등.. 애니메이션 코드들 */
}

tick();

그런데 토스트는 애니메이션으로 큰 요구 사항이 없으니, mount, unmount (remove)만 시켜 줘도 될 듯 합니다!

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.

3 participants