Skip to content

[5팀 손승현] Chapter2-1. 프레임워크 없이 SPA 만들기 - #28

Open
sonsonsh1125 wants to merge 40 commits into
hanghae-plus:mainfrom
sonsonsh1125:main
Open

[5팀 손승현] Chapter2-1. 프레임워크 없이 SPA 만들기#28
sonsonsh1125 wants to merge 40 commits into
hanghae-plus:mainfrom
sonsonsh1125:main

Conversation

@sonsonsh1125

@sonsonsh1125 sonsonsh1125 commented Nov 9, 2025

Copy link
Copy Markdown

과제 체크포인트

배포 링크

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

과제 셀프회고

배포문제(25.11.10)

작업을 상세페이지까지 작업하던 도중 왠지 추후에 배포에서 에러가 나서 시간이 많이 소요될 것을 감지해 배포 작업을 진행하게 되었다.
당연히 스무스 하게 진행될 것이라 생각했는데 아뿔싸.. 정말 꼬박 3시간은 걸린것 같다. 남들이면 이정도는 소요 안될 것 같은데
내 적은 뇌용량 및 체력 때문이다.

배포 세팅 관련

  1. gh-pages 를 npm을 통해 설치를 안했다. 근데 이거는 deploy.yml 설정해 놓으면 배포상 문제는 없음
  2. deploy.yml 을 peaceiris/actions-gh-pages 사용해놓고 환경설정을 Github Page 액션사용 하는 방법으로 해놓음.
    • peaceiris/actions-gh-pages는 gh-pages 브랜치에 빌드 결과물을 푸시하는 방식
    • Github Page 아티팩트를 직접 업로드 하고 환경 설정으로 배포 URL 자동 출력
    • 두번째 방법은 레포지토리 설정 Settings → Pages → Build and deployment Source를 **"GitHub Actions"**로 변경해야 함
    • 내가 최후로 설정한 방법은 두번재 방법이고 이 방법의 흐름은 다음과 같다
      코드 수정 → git push → GitHub Actions 자동 실행 → deploy.yml 워크플로우 실행 → 배포 완료
  3. 상세페이지 작업을 하고 라우팅 설정을 안한 상태로 배포를 시도
  4. vite.config.js에서 빌드 설정을 했어야 했음
      import { fileURLToPath, URL } from "url";
      import { defineConfig } from "vitest/config";
      
      export default defineConfig(({ mode }) => ({
      base: mode === "production" ? "/front_7th_chapter2-1/" : "/",
      resolve: {
        alias: {
          "@": fileURLToPath(new URL("./src", import.meta.url)),
        },
      },
      }));
    개발환경(pnpm dev) 에서는 baseL: "/" 프로덕션에서는 base "base: "/front_7th_chapter2-1/" 를 사용하고 alias 설정을 통해서 현재 설정 파일 위치를 기준으로 절대 경로를 안전한게 만들었다. -> jsconfig.js 추가 작성해야한다.
  5. msw 오류를 수정하기 위해 serviceWorker 파일을 base URL을 고려한 경로에서 찾게 작성
  6. getNormalizedPathname 함수를 추가해서 URL에서 basePath를 제거하여 정규화된 경로를 추출 -> 근데 너무 복잡해 진듯 하여 vite의 base를 /로 유지하고 404.html을 추가하는 방법을 고려중(단점은 해쉬가 꼴뵈기 싫음)

DOM 직접 조작을 하지 않고 처리하기(25.11.11)

직접 조작시 문제점

  1. 상태와 UI 불일치
  2. 예측 불가능한 동작
  3. 메모리 누수
  4. 재사용성 저하

바닐라 JS에서 DOM 직접 조작 대안

  1. 상태 기반 레더링 패턴 : 상태와 UI가 항상 동기화 됨, 디버깅에 용이하다
  2. 이벤트 위임 : 각 요소에 개별 리스너를 하는게 아니라 부모에 하나의 리스너로 조작
  3. 템플릿 함수 사용
  4. Observer 패턴으로 상태 변경 감지

단방향 데이터 흐름에 주의 하자 : 상태변경 -> 렌더링

현재 코드의 문제점 진단 및 store로 상태관리 처리하기(25.11.12)

현재 코드의 문제점

let currentLimit = 20;
let currentSort = "price_asc";
let currentSearch = "";
let currentCategory1 = "";
let currentCategory2 = "";
let categoriesCache = null;
let categoriesInFlight = null;
  1. 상태가 여러 변수로 분산되어 있다
  2. 상태 변경을 추적하기 어렵다
  3. 컴포넌트 간 데이터 공유가 복잡하다
  4. 테스트하기 어렵다
onSortChange: (nextSort) => {
  actions.setFilter('sort', nextSort);
  render();  // 일일이 호출해야 함
},

onSearchSubmit: (nextSearch) => {
  actions.setFilter('search', nextSearch);
  render();  // 또 호출...
},

function render() {
  $root.innerHTML = HomePage({...});
  bindFilters(); // ← 매번 호출해야 함
}
  1. 상태 변경할 때마다 render()를 호출해야 한다
  2. bindFilters()도 매번 다시 연결해야 한다
  3. 실수로 render() 안 부르면 화면 안바뀜
  4. innerHTML로 DOM을 교체하면 이벤트 리스너가 사라진다

store로 관리해야할 상태들

  1. 필터 상태
  • 여러 컴포넌트에서 공유 (SearchForm, ProductList, Pagination)
  • 변경이 잦음
  • URL과 동기화 필요
  1. 캐시 데이터
  • 앱 전체에서 공유
  • 중복 요청 방지
  • 만료 시간 관리 가능
  1. UI 상태

새로 알게 된 함수(25.11.12)

history.pushState

페이지 새로고침 없이 브라우저 URL을 변경하는 API

E2E 테스트 작업 중 수정 사항(25.11.13)

  • 로딩 상태 미표시 문제
    CI에서는 Mock API 응답이 너무 빨라 "카테고리 로딩 중..." 문구가 렌더되기 전에 데이터가 도착. 로컬에서는 보였지만, CI 테스트가 실패하면서 로딩 단계가 비동기 속도에 의존한다는 사실이 드러남
    → Mock API에 지연을 추가해 “로딩 중” 상태가 재현 되도록 처리

  • 장바구니 모달의 선택자 불일치
    .cart-modal-overlay, .cart-modal 등 테스트가 찾는 클래스가 실제 DOM에 없거나, 모달이 #root 밖에 렌더링되어 테스트가 접근하지 못함(내가 처음부터 구조를 이상하게 했다)
    → 클래스명을 테스트 기대대로 추가하고, 모달을 #root 안에 붙여 구조를 일치

  • 토스트 닫기 버튼 선택 불가
    테스트는 #toast-close-btn을 클릭하려 했지만 해당 ID가 없었다.
    → 닫기 버튼에 ID를 부여해 테스트가 안정적으로 DOM 요소를 찾도록 함.

  • 필터 상태·URL 싱크 불일치
    페이지당 개수, 정렬, 검색어가 변경돼도 URL 쿼리에 반영되지 않아 새로고침/직접 접근 시 상태가 복원되지 않았고, 테스트는 URL에 특정 쿼리를 기대했기 때문에 실패.
    → 스토어-URL 간 양방향 동기화를 구축해 필터 변경 시 쿼리를 갱신하고, 쿼리로 진입 시 필터를 복원하도록 수정.
    - URL 쿼리 → 스토어를 초기화 (parseFiltersFromUrl()),
    - 스토어 변경 → URL 갱신 (appStore.subscribe),
    - 필터 UI 이벤트 → 스토어 & URL 동시에 갱신 (updateFiltersWithNavigation()),
    - 뒤로가기/앞으로가기에서도 render()가 호출되면서 URL 기반 필터가 다시 복원

  • 상세 페이지에서 뒤로가기 시 리스트 사라짐
    상세 페이지 진입 시 appStore.clearProducts()로 기존 목록을 비워버려 뒤로가기 직후 홈 화면에 데이터가 없어 테스트가 간헐적으로 실패.
    → 상세 진입에서도 캐시를 유지하고, 홈으로 돌아올 때 기존 데이터를 즉시 재사용하도록 리팩터링.

  • 404 페이지에서 <main> 중복
    404 컴포넌트가 <main>을 중첩해서 렌더링해 ARIA strict 모드에서 두 개의 main이 감지되었습니다.
    NotFound 컴포넌트 내부의 <main>을 제거하여 PageLayout이 제공하는 단일 <main>만 사용하도록 수정

기술적 성장

  1. SPA 흐름에 대한 이해 심화
  • 단일 페이지 애플리케이션에서 페이지 이동 없이 상태와 URL만으로 화면을 전환시키는 구조를 직접 다루면서, 히스토리 API·라우팅·URL 쿼리 관리가 전체 사용자 경험에 미치는 영향을 깊이 이해하게 되었습니다.
  1. 상태와 URL 연동 경험 확대
  • 필터 상태를 URL 쿼리에 반영하고 역으로 복원하는 과정을 통해 URLSearchParams, history.pushState/replaceState 등 SPA 환경에서 자주 쓰이는 기능의 활용법을 체득했습니다.
  1. 비동기 시나리오 안정화
  • Mock API 지연, 뒤로가기 후 데이터 복원 등 Playwright 기반 E2E 환경에서 발생하는 타이밍 이슈를 직접 해결하면서, 테스트 안정화를 위한 코드 구조/로직(캐시 재사용, 명시적 대기 처리)의 중요성을 체감했습니다.

자랑하고 싶은 코드

updateFiltersWithNavigation() 도입

  • 필터 변경 → 스토어 갱신 → URL 반영을 한 곳에서 처리해, 필터 이벤트 핸들러가 매우 단순해졌습니다. 하나의 헬퍼로 URL 동기화와 상태 관리를 연결한 구조라 재사용성이 뛰어납니다.

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

  1. main.js의 비대화
    필터 동기화, 라우팅, 제품 캐시 관리가 모두 한 파일에 몰려 있어 가독성이 떨어집니다. 필터-URL 동기화, 라우터, 데이터 페치 등을 모듈화하면 이해와 유지보수가 쉬울 것 같습니다.
  2. 캐시 재사용 조건
    상세 페이지에서 홈으로 돌아올 때 캐시를 재사용하도록 했지만, 필터가 바뀌었는지 여부에 따라 캐시를 초기화해야 하는 상황을 더 정교하게 다듬을 필요가 있습니다.

학습 효과 분석

  1. playwright가 DOM 구조와 타이밍에 매우 민감하다는 사실을 체감했습니다. DOM 구조(클래스명, 위치), 상태 동기화가 조금만 틀어져도 테스트가 실패하므로, 테스트 주도 시나리오를 고려한 설계가 필요함을 확인했습니다.
  2. 히스토리 API, history.pushState/replaceState 활용 경험을 쌓으며 SPA에서 브라우저 네비게이션을 자연스럽게 처리하는 방식을 익혔습니다.

과제 피드백

  1. 옵저버 패턴을 이용해서 상태관리를 할 수 있었던 점이 좋았습니다. 항상 정보처리기사 시험을 보면서 단골문제였던 디자인 패턴을 아무생각없이 암기 했었는데 이번 기회를 통해 옵저버 패턴은 정말 확실히 익힐수 있었던 것 같습니다.
- listeners라는 Set에 구독자(콜백)를 등록하고(subscribe()),
- 상태가 바뀔 때마다(updateFilters, setProducts, …) notify()를 호출하여 모든 구독자에게 최신 스냅샷을 전달
- subscribe()가 반환하는 함수로 구독 해제를 지원

AI 활용 경험 공유하기

cusor를 사용하여 기본적인 컴포넌트 분리 및 작성된 코드와 기존 과제 탬플릿의 차이점 관련하여 구별할 수 있게 처리하였습니다.
또한 E2E 테스트 중 DOM 구조 잡기 같은 기본적인 부분들을 위임해서 작업했습니다.

리뷰 받고 싶은 내용


@sonsonsh1125 sonsonsh1125 changed the title [5팀 손승현] [5팀 손승현] Chapter2-1. 프레임워크 없이 SPA 만들기 Nov 9, 2025
Comment thread src/main.js
Comment on lines +296 to +304
const existingProducts = Array.isArray(state.products) ? state.products : [];
const shouldReuseProducts = existingProducts.length > 0;

if (!shouldReuseProducts) {
appStore.resetPagination();
}
appStore.setFetchingNextPage(false);

const { filters } = appStore.getState();

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

필터 캐시 재사용 조건 모호하다. 홈으로 돌아올 때 existingProducts가 있으면 재사용하는데, 필터가 바뀐 경우에도 동일한 데이터를 보여줄 가능성이 있다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@sonsonsh1125

appStore에 마지막 사용한 필터에 필터 상품을 저장하고 비교하는 방식???? 을 사용하면 어떨까..

Comment thread src/main.js
Comment on lines +145 to +151
const forceInclude = {};
if (Object.prototype.hasOwnProperty.call(partialFilters ?? {}, "limit")) {
forceInclude.limit = true;
}
if (Object.prototype.hasOwnProperty.call(partialFilters ?? {}, "sort")) {
forceInclude.sort = true;
}

@sonsonsh1125 sonsonsh1125 Nov 13, 2025

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

기본값(limit=20)도 쿼리에 강제로 남기는 부분은 나중에 정책이 바뀌면 수정하기 번거로울 수 있다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@sonsonsh1125 걍 상수들 모아놓고 거기서 컨트롤 하는 방법밖에 없지않나ㅠ모르겠어요 정책바꾸면 혼내줘야죠

Comment thread src/app/cart/modal.js

const root = document.getElementById("root") ?? document.body;

overlay = document.createElement("div");

@sonsonsh1125 sonsonsh1125 Nov 13, 2025

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

이정도 DOM 요소의 직접접근..! 방법이 없다. 이부분 뭔가 더 좋은 방법이 생각이 안남

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

네 좋은 아이디어 감사합니다 재윤님

@jy0813 jy0813 Nov 15, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@sonsonsh1125

<div id="overlay"></div>

말씀하신거랑은 좀 다를수잇는데 직접접근이 아니고 직접생성이 문제라면 정적 DOM을 미리 준비하는거도 방법일거같아요

Comment thread src/main.js
products: shouldReuseProducts ? existingProducts : [],
filters,
categories: categoriesCache ?? {},
pagination: state.pagination,

@sonsonsh1125 sonsonsh1125 Nov 13, 2025

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

이 부분으로 인해서 장바구니 버튼이 깜빡이는것 같은데 pageLayout에서 매번 새헤더를 불러내서..? 장바구니 깜빡이는거 너무 거슬리네!!!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@sonsonsh1125 장바구니에 담긴 상품 개수 불러오는걸 Header.js 컴포넌트에서 호출하면 해당 이슈는 해결될거같아요!

@JunilHwang JunilHwang left a comment

Copy link
Copy Markdown
Contributor

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 동기화를 적절히 분리하여 SPA 경험과 상태 복원이 원활한 점이 긍정적입니다.

그러나 전반적으로 DOM 조작을 직접 수행하며 상태와 UI가 다소 결합된 부분이 있고, 이벤트 핸들링과 상태 업데이트가 분리되어 관리되지 않은 곳이 있어 확장성과 유지보수성 측면에서 개선이 필요해 보입니다.

주요 설계 피드백

  • 상태 관리 및 의존성 주입: 장바구니나 상세 수량 제어 같은 상태 관리 객체를 전역(window)에서 독립된 모듈로 분리하고, 필요한 곳에 명시적으로 주입하는 방향을 권장합니다.
  • 이벤트 위임과 부분 렌더링: 장바구니 모달 등에서는 innerHTML 재할당 방식보다 이벤트 위임 기법과 최소한의 DOM 업데이트 전략을 사용해 성능과 유지보수를 개선할 수 있습니다.
  • UI 컴포넌트 분리와 재사용성: SearchForm의 카테고리 렌더링 로직 다듬기, 통화 포맷팅 유틸 분리, 공통 컴포넌트 import 관리 등 모듈화와 재사용 향상을 고려하는 것이 좋습니다.
  • 성능 최적화: 무한 스크롤 이벤트에 throttle/debounce 적용과 애니메이션을 사용하는 토스트 메시지 관리에서 과도한 DOM 조작을 제어할 필요가 있습니다.
  • 템플릿 문법 오류 수정: PageLayout HTML 템플릿 닫는 태그 누락 등은 작은 실수지만 큰 문제를 일으키므로 집중 점검 바랍니다.

현실적인 확장 요구사항 예시

상품 상세 페이지에 여러 상품을 동시에 표시하거나, 장바구니가 복수 뷰를 갖는 복잡한 UI 구성이 추가된다면, 현재의 DOM 직접 조작/innerHTML 교체 방식은 심각한 성능 저하와 이벤트 관리 오류를 유발할 수 있습니다.

이를 대비해 상태와 UI를 명확히 분리한 컴포넌트 기반 구조, 이벤트 위임, 상태 변화만 최소한으로 DOM 조작하는 방식을 도입하면 확장성, 유지보수성, 성능이 크게 향상될 것입니다.추가 질문이 없으므로, 제출하신 코드에 대한 상세 리뷰와 설계 개선 방향을 드렸습니다. 앞으로도 상태관리와 UI 분리, 이벤트 처리 최적화에 좀 더 신경 쓰시면서 리팩토링해 보시면 좋겠습니다. 질문이 생기시면 언제든 도움을 요청해 주세요!

Comment thread src/main.js
}
updateFiltersWithNavigation({ sort: nextSort });
render();
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1. 문제상황 제시

구체적인 문제 상황:
현재 src/main.js에서는 상품 상세 페이지 내에서 수량 변경 UI를 직접 DOM 이벤트 핸들러로 바인딩하고 있습니다. 만약 향후 기능이 확장되어 여러 상품 상세 컴포넌트가 동시에 존재하거나, SPA에서 복잡한 상태 관리가 요구될 경우 이러한 DOM 직접 조작 방식은 유지보수가 어려워질 수 있습니다.

현재 코드의 한계:

  • DOM 요소와 로직 결합이 강해 테스트 및 유지보수가 어렵습니다.
  • 수량 변경 로직이 수량 입력 박스에 종속적이며, 상태가 분산되어 관리됩니다.
  • 같은 로직을 상세 페이지 외 컴포넌트에서 재사용하기 어렵습니다.

2. 근본 원인

핵심 문제:
UI 이벤트 핸들링 로직이 DOM에 직접 바인딩되어 있고, 상태와 비즈니스 로직이 분리되지 않았습니다.

왜 문제인가:
이 구조는 코드 확장과 유지보수 시 비효율적이며, 향후 상태관리 패턴 (예: 상태머신, 리액티브 패턴) 도입을 어렵게 만듭니다.

3. 개선 구조

현재 구조:

  • DOM 이벤트 핸들러가 상세페이지 내에서 직접 수량 조절과 관련된 로직 처리

개선된 구조:

  • 수량 조절 로직을 별도의 상태관리 함수로 분리
  • 이벤트 핸들러는 상태관리자를 호출하는 형태로 변경
  • 상태 관리를 중앙화하여 다른 기능과 재사용 가능하게 구조화

개선 사항:

  • 수량 관련 로직을 한 곳에 모아 재사용성 증가
  • 검증 로직도 상태 관리자에서 처리해 입력 밸리데이션 명확화
  • 코드 가독성 및 유지보수성 향상

코드 비교:

// ❌ 현재 방식
const quantityInput = document.getElementById("quantity-input");
const decreaseBtn = document.getElementById("quantity-decrease");
decreaseBtn.addEventListener("click", () => {
  adjustDetailQuantity(quantityInput, -1);
});

// ✅ 개선된 방식
// 상태관리 함수
function changeQuantity(productId, delta) {
  const item = getProductById(productId);
  if (!item) return;
  const newQuantity = Math.max(1, item.quantity + delta);
  updateProductQuantity(productId, newQuantity);
  renderQuantity(newQuantity);
}

// 이벤트 바인딩
const decreaseBtn = document.getElementById("quantity-decrease");
decreaseBtn.addEventListener("click", () => changeQuantity(productId, -1));

Comment thread src/main.js
search: state.filters.search,
category1: state.filters.category1,
category2: state.filters.category2,
page: nextPage,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

2. 문제상황 제시

구체적인 문제 상황:
장바구니 기능이 window.cartManager 전역 객체로 관리되는 구조로 되어 있습니다. 이로 인해 다른 모듈이 장바구니 상태에 직접 접근하거나 수정할 수 있는 위험이 있고, 협업 시 충돌 가능성이 있습니다.

현재 코드의 한계:

  • 전역 상태 관리로 인한 의존성 및 사이드 이펙트 위험
  • 모듈 간 결합도가 높아 유지보수성 저하
  • 테스트 작성이 어렵고, 상태 변경 추적이 비효율적

2. 근본 원인

핵심 문제:
중앙 집중식 상태 관리 없이 전역 객체에 상태와 메서드를 노출하는 방식.

왜 문제인가:
이러한 설계는 앱이 확장될수록 상태동기화 문제를 야기하며, 모듈 단위 테스트 및 코드 분리가 어려워집니다.

3. 개선 구조

현재 구조:

  • 전역 객체에 상태 및 메서드 직접 노출

개선된 구조:

  • 상태 관리 라이브러리 혹은 자체 스토어(예: createCartStore) 도입
  • 구독/발행 패턴 적용하여 상태 변경 감지
  • API만 노출하여 외부에서 직접 상태 변경 불가하게 제한

개선 사항:

  • 장바구니 상태 관리 모듈화
  • 외부 접근은 공개 API만 가능하게 제한
  • 상태 변경은 정해진 액션을 통해서만 가능하게 설계

코드 비교:

// ❌ 현재 방식
window.cartManager = {
  getState: getCartSnapshot,
  subscribe: subscribeCart,
  addItem: addProductToCart,
  ...
};

// ✅ 개선된 방식
function createCartStore() {
  let state = { items: [] };
  const listeners = new Set();

  function subscribe(listener) {
    listeners.add(listener);
    listener(state);
    return () => listeners.delete(listener);
  }

  function addItem(productId, quantity) {
    // ... update state immutably
    // ... notify listeners
  }

  return { subscribe, addItem, ... };
}

const cartStore = createCartStore();
export default cartStore;

}

const categorySummaryWhenNone = category1Keys.map((category1) => escapeHtml(category1)).join(" ");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

3. 문제상황 제시

구체적인 문제 상황:
카테고리 선택 UI를 렌더링할 때 여러 중첩된 조건과 문자열 조작이 섞여 가독성이 떨어집니다. 카테고리 상태가 복잡해질수록 유지보수가 어려워질 수 있습니다.

현재 코드의 한계:

  • 가독성이 낮아 수정하기 어려움
  • 카테고리 버튼 생성 로직이 분리되지 않아 재사용성 부족
  • 상태 변경 시 렌더링 로직 수정이 어려움

2. 근본 원인

핵심 문제:
UI 생성 로직과 상태 처리 로직이 분리되지 않고, HTML 문자열 템플릿 안에 조건부 처리들이 섞여 있음

왜 문제인가:
코드가 길어지고 중첩될수록 의도 파악과 디버깅이 어려워지고, 작은 변경에도 버그 발생 가능성이 높아집니다.

3. 개선 구조

현재 구조:

  • 카테고리 렌더링 로직이 단일 함수 내 중첩 HTML 문자열 템플릿과 조건부 처리 혼합

개선된 구조:

  • 카테고리 렌더링을 작은 컴포넌트 함수로 분리
  • 상태와 UI 분리를 명확히 하여 구조화
  • 재사용 가능한 컴포넌트/함수로 제작

개선 사항:

  • 카테고리 버튼 및 선택 영역을 별도 함수로 추출
  • 명확한 상위/하위 카테고리 컴포넌트 분리
  • 상태 변경 시 UI 상태만 교체

코드 예시:

function renderCategoryButton(name, isActive) {
  return `<button class="${isActive ? activeClass : baseClass}">${escapeHtml(name)}</button>`;
}

function renderCategoryList(categories, selected) {
  return categories.map(name => renderCategoryButton(name, selected === name)).join("");
}

Comment thread src/app/cart/modal.js
keydownHandler = (event) => {
if (event.key === "Escape") {
event.preventDefault();
closeCartModal();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

4. 문제상황 제시

구체적인 문제 상황:
장바구니 모달에서 UI 상태 업데이트 시 전체 HTML을 innerHTML로 덮어쓰기 때문에 이벤트 핸들러가 초기화되고 다시 붙여야 하는 구조입니다. 만약 상품 수가 많아질 경우 DOM 재생성 비용이 상승하고, 이벤트 중복 바인딩 관리가 어려울 수 있습니다.

현재 코드의 한계:

  • innerHTML 재할당으로 기존 이벤트 핸들러 소멸
  • 상태 변화마다 DOM 전체 재렌더링으로 퍼포먼스 저하
  • 이벤트 위임 패턴 미적용으로 코드 복잡도 상승

2. 근본 원인

핵심 문제:
DOM에 직접 innerHTML로 전체 내용을 교체하는 방식으로 상태 및 이벤트 바인딩 관리 미흡

왜 문제인가:
이 구조는 복잡도가 증가할수록 코드 유지보수가 어려워지고, UI 반응 속도가 저하됩니다.

3. 개선 구조

현재 구조:

  • 전체 컨테이너 innerHTML 재할당 + 이벤트 리바인딩

개선된 구조:

  • 이벤트 위임 방식 적용: 상위 컨테이너에 단일 이벤트 리스너 등록
  • 변경된 부분만 DOM 업데이트 (예: 수량, 선택 상태)
  • 상태 관리 분리 및 최소한의 DOM 조작 유도

개선 사항:

  • 이벤트 위임으로 이벤트 핸들러 일원화
  • 데이터 속성과 상태를 DOM에 명확히 연결
  • 필요한 부분만 최소한으로 업데이트

코드 예시:

// 이벤트 위임 예시
container.addEventListener('click', (event) => {
  const target = event.target;
  if(target.matches('.quantity-increase-btn')) {
    const productId = target.dataset.productId;
    cartManager.updateQuantity(productId, /* newQuantity */);
  }
  // 기타 이벤트 처리
});

// 상태 변화 시 필요한 부분만 업데이트
function updateQuantityDisplay(productId, quantity) {
  const input = container.querySelector(`input[data-product-id="${productId}"]`);
  if (input) input.value = quantity;
}

Comment thread src/app/cart/modal.js
const snapshot = manager.getState();

if (overlay) {
updateModalContent(snapshot, manager);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

5. 문제상황 제시

구체적인 문제 상황:
전역 window.cartManager 객체에 의존하여 장바구니 상태를 가져오는데, 모듈 간 의존성이 높아 추후 모듈화나 테스트가 어렵습니다.

현재 코드의 한계:

  • 전역 변수 의존성으로 인한 테스트 어려움
  • 모듈간 결합도가 높아서 재사용성과 유지보수성 저하

2. 근본 원인

핵심 문제:
장바구니 상태 및 동작을 전역 객체에 밀접하게 의존함

왜 문제인가:
전역 변수 사용은 명확한 의존성 주입 패턴이 없고, 상태 추적과 디버깅이 어렵습니다.

3. 개선 구조

현재 구조:

  • 전역 객체 직접 접근 및 상태 사용

개선된 구조:

  • 함수나 클래스의 생성 시점에 상태 관리자를 주입받는 방식 채택
  • DI(의존성 주입) 패턴 사용으로 테스트 및 재사용성 향상

개선 사항:

  • openCartModal 등 함수에 상태 관리자를 파라미터로 받거나 컨텍스트 모듈에서 주입
  • 테스트용 목 상태 관리자 교체 용이

코드 예시:

export function openCartModal(manager) {
  const snapshot = manager.getState();
  // ...
}

// 모듈 외부에서
import { cartManager } from './cartStore';
openCartModal(cartManager);

Comment thread src/app/toast/toast.js
type="button"
id="toast-close-btn"
class="flex-shrink-0 ml-3 text-white hover:text-gray-200 transition-colors"
aria-label="토스트 닫기"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

6. 문제상황 제시

구체적인 문제 상황:
showToast 함수가 호출될 때 매번 새 DOM 요소를 생성하고, 애니메이션 끝나면 제거하는 방식인데, 많은 알림이 빠르게 호출되면 애니메이션 충돌이나 메모리 누수가 발생할 수 있습니다.

현재 코드의 한계:

  • 중복 알림 생성 가능성이 높음
  • 애니메이션 상태 관리가 이벤트 기반이며 중첩 호출에 취약

2. 근본 원인

핵심 문제:
토스트 알림이 개별 DOM 및 애니메이션 타이머에 의존하여 종합적인 관리가 어렵다.

왜 문제인가:
빠른 연속 호출 시 과도한 DOM 조작과 애니메이션 충돌이 발생할 수 있어 자원 낭비 우려

3. 개선 구조

현재 구조:

  • 매 호출마다 독립적인 DOM 토스트 생성 및 제거

개선된 구조:

  • 토스트 메시지를 큐로 관리해 일정 갯수 이상 생성하지 않음
  • 애니메이션 완료 후 콜백으로 정리
  • 상태 관리 변수를 이용해 중복 호출 방지

개선 사항:

  • 내부 큐 구현 및 중복 메시지 필터링
  • 동시 알림 갯수 제한

코드 예시:

const toastQueue = [];
function showToast(message, type) {
  if (toastQueue.length > MAX_TOASTS) return;
  // ...
  toastQueue.push(toast);
  // 애니메이션 후 삭제 및 큐에서 제거
}

Comment thread src/pages/PageLayout.js
${children}
</main>
${Footer()}
</div

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

7. 문제상황 제시

구체적인 문제 상황:
PageLayout 컴포넌트의 리턴 문자열에 문법 오류가 있어 실제 적용 시 의도한 레이아웃이 깨질 위험이 있습니다.

현재 코드의 한계:

  • 닫는 div 태그에 닫는 꺽쇠(>)가 빠져 있음

2. 근본 원인

핵심 문제:
템플릿 문자열 내 HTML 태그 작성 오류

왜 문제인가:
이로 인해 HTML 파싱 오류가 발생하며, 레이아웃이 왜곡되거나 자바스크립트가 정상 작동하지 않을 수 있습니다.

3. 개선 구조

개선된 구조:

  • 문자열 마지막 부분 닫는 태그에 > 추가

코드 비교:

// ❌ 현재
return `
 <div class="min-h-screen bg-gray-50">
  ${Header()}
  <main class="max-w-md mx-auto px-4 py-4">
    ${children}
  </main>
  ${Footer()}
 </div
`;

// ✅ 개선
return `
 <div class="min-h-screen bg-gray-50">
  ${Header()}
  <main class="max-w-md mx-auto px-4 py-4">
    ${children}
  </main>
  ${Footer()}
 </div>
`;

Comment thread src/components/Cart.js
<p class="text-sm font-medium text-gray-900">${total ?? price}</p>
${
controls
? `<button class="cart-item-remove-btn mt-1 text-xs text-red-600 hover:text-red-800" data-product-id="${productId}">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

8. 문제상황 제시

구체적인 문제 상황:
Cart 컴포넌트 템플릿 내에서 가격, 총합 등의 통화 단위를 문자열로 표시하는 로직이 여러 곳에 분산되어 중복되고 있습니다.

현재 코드의 한계:

  • 통화 단위 포매팅 로직이 각 컴포넌트에서 중복
  • 변경 시 일괄 수정이 어려움

2. 근본 원인

핵심 문제:
뷰 컴포넌트에서 통화 포매팅 로직이 별도 함수나 유틸을 통해서가 아닌 직접 구현됨

왜 문제인가:
중복 코드로 인해 유지보수 비용 증가 및 일관성 없는 표시 가능성 존재

3. 개선 구조

개선된 구조:

  • 통화 포맷팅 유틸 함수를 별도로 작성해 재사용
  • 컴포넌트에서는 유틸 호출 형태로 간결화

코드 비교:

function formatPrice(value) {
  return `${Number(value).toLocaleString()}원`;
}

// 사용 예시
priceLabel: formatPrice(item.price),

Comment thread src/main.js
continue;
}
const normalized = parsed.map((item) => ({
productId: item.productId,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

9. 문제상황 제시

구체적인 문제 상황:
무한 스크롤 로직이 스크롤 이벤트에서 직접 페이지네이션 로딩 여부를 체크 후 데이터를 불러오는 방식인데, 스크롤 이벤트 빈번 호출로 성능 문제 가능성이 있습니다.

현재 코드의 한계:

  • 스크롤 이벤트에 디바운스 또는 스로틀 처리 없음
  • 불필요한 함수 반복 호출 가능성

2. 근본 원인

핵심 문제:
이벤트 핸들러 최적화 미흡으로 과도한 호출 발생

왜 문제인가:
불필요한 렌더링, API 호출 부담 증가

3. 개선 구조

개선된 구조:

  • 스로틀링 또는 디바운싱 적용하여 호출 빈도 제한

개선 사항:

  • lodash 같은 외부 라이브러리 사용 혹은 직접 스로틀 함수 작성
  • 이벤트 청취 시 옵션에 passive: true 유지

코드 예시:

window.addEventListener('scroll', throttle(() => {
  if (shouldLoadMore()) {
    loadMoreProducts();
  }
}, 200), { passive: true });

Comment thread src/components/index.js
export { SearchForm } from "./SearchForm.js";
export { Skeleton } from "./ProductList.js";
export { Header } from "./Header.js";
export { Footer } from "./Footer.js";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

10. 문제상황 제시

구체적인 문제 상황:
src/components/index.js에서 Loading 컴포넌트를 ProductList.js로부터 별도 export 하도록 했는데, 이는 컴포넌트가 ProductList와 결합되는 의존성이 높아질 수 있는 구조입니다.

현재 코드의 한계:

  • 다수 컴포넌트를 한 파일에서 export하면 재사용성 및 변경 관리 어려움

2. 근본 원인

핵심 문제:
컴포넌트 경로나 파일이 자주 변경될 경우 import 경로 관리가 복잡해짐

왜 문제인가:
모듈 별 분리가 어렵고, 중복 import 발생 가능성

3. 개선 구조

개선된 구조:

  • 컴포넌트별로 별도의 독립파일 구성 유지
  • index.js는 주요 컴포넌트만 export하여 API 명확화

권장사항:

  • Loading을 ProductList 내부 컴포넌트로 취급하거나 별도 내포 컴포넌트로 관리
  • 불필요한 export 최소화

@eveneul eveneul left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

승현 님 이번 주차도 고생 많으셨습니다! 많이 기대하시고기다리던 자바스크립트 / 리액트 챕터인데 어떠셨나요! 많은 도움이 되셨을까여..

관련이 있는 값들은 하나의 객체로 묶어 주고, 오탈자 늘 신경 써 주세요!

이번 주도 넘 고생 많으셨고 다음 주부터 다시 파이팅해봅시다!

Comment thread src/app/cart/modal.js

const clearBtn = container.querySelector("#cart-modal-clear-cart-btn");
if (clearBtn) {
clearBtn.addEventListener("click", () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

클릭 이벤트가 여러군데 쓰이는데 차라리 이걸 함수로 하면 어떨까요?

const addEvent = (eventType, element, callback) => { ... }

Comment thread src/app/toast/toast.js
</svg>
`,
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

여러 상태를 가지고 있는 컴포넌트는 switch문으로 관리하신 것 좋습니다!

</div>
`;

const ProductItem = ({ title, image, productId, lprice, brand }) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Item은 개별적인 컴포넌트로 봐도 되지 않을까..! 라는 제 생각
왜냐하면 상품 상세에서도 관련 상품으로 ProductItem이 쓰이거든요!


const categorySummaryWhenNone = category1Keys.map((category1) => escapeHtml(category1)).join(" ");

const category1Buttons =

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

제가 생각한 코드는..

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 
)
)

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

Comment thread src/pages/Detailpage.js
</div>
`;

const STAR_PATH =

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

rating component도 따로 분리하면 좋을 듯 해요!
/components/product/Rating.js

Comment thread src/pages/Detailpage.js
const ratingValue = Number(product.rating ?? 0);
const normalizedRating = Number.isFinite(ratingValue) ? ratingValue : 0;
const reviewValue = Number(product.reviewCount ?? 0);
const normalizedReviewCount = Number.isFinite(reviewValue) ? reviewValue : 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

const productInfo = {
  pridct: Number(product.lprice ?? product.price ?? 0).toLocaleString(),
  stock: Number ..... 후략
}

이렇게 했을 때 객체로 접근하면 가독성이 쪼금 더 좋지 않을까ㅏ...! 라는 제 생각

Comment thread src/pages/PageLayout.js
${children}
</main>
${Footer()}
</div

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

오타다! 오타!

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.

4 participants