Skip to content

[3팀 주민수] Chapter2-1. 프레임워크 없이 SPA 만들기#48

Open
Thomas97-J wants to merge 43 commits intohanghae-plus:mainfrom
Thomas97-J:main
Open

[3팀 주민수] Chapter2-1. 프레임워크 없이 SPA 만들기#48
Thomas97-J wants to merge 43 commits intohanghae-plus:mainfrom
Thomas97-J:main

Conversation

@Thomas97-J
Copy link

@Thomas97-J Thomas97-J commented Nov 12, 2025

과제 체크포인트

배포 링크

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

과제 셀프회고

처음에 걱정했던 것 만큼 어마어마한 양의 과제는 아니었다. UI 구현이 이미 다 되어 있는 상태로 관련 로직만을 작성하도록 되어 있어 생각보다 빨리 진도를 나갈 수 있었다. 낯선 개념을 처음부터 배워가며 과제를 해야 했던 지난 주차들과 달리, 이번 주차의 과제는 알고 있던 내용들을 깊게 생각하며 진행할 수 있어 심리적인 부담이 덜했다.

작업 막바지에 깃헙 페이지 배포 관련해서 조금 이슈가 있었다. 예전에 깃헙 페이지용으로 레포지토리를 하나 만들어서 해당 페이지 내부에 dist 파일들만 이동시켜놓고 배포를 하고는 했는데, 이번 작업에서도 빌드 후 빌드 산출물들을 해당 레포지토리로 올리고 나서 배포를 마무리했다.
별다른 기능의 문제점은 없었지만, 배포 과정에서의 이슈를 극복해보라는 코치님의 이야기와는 다르게 아무런 문제도, 수정할 사항도 없었기에 이런 이야기를 팀원분과 나누다 다른 분들은 모두 해당 과제 번호 이름을 배포 링크에 추가했다는 이야기를 들었다. 나 역시도 예시 주소를 보았지만 주소는 주소일 뿐이지 그다지 중요하다고 생각하지 않았기에 넘어간 부분이었다.

때문에 간단하게 base 주소를 세팅한 다음 배포를 수정하려 했는데, 여기서부터 문제가 발생했다. 우선 루트 경로가 기본 경로가 아니개 되었으므로 404 페이지에 대한 처리가 추가되어야 했다. 그러나 이런저런 처리를 한 다음에도 페이지가 로드되지 않았다. 네트워크 탭에서 넘어오는 html에는 문제가 없었고, 다만 가져와야 할 에셋들이 찾아지지 않는 상황이었다.

한참을 이런저런 시도를 한 끝에 html에서 요구하는 과제 레포지토리의 asset 경로에 해당 브랜치에서는 당연히 접근이 불가능하다는 것을 알 수 있었다. 애초에 다른 레포지토리에서 깃헙 페이지 배포를 진행한 것이 문제가 되고 있었다. 항해에서 제공한 페이지 역시 레포지토리를 뒤져보니 같은 레포지토리에서 다른 브랜치를 파 배포되고 있다는 것을 알 수 있었다. 나 역시 해당 방식으로 배포를 진행한 뒤에야 정상적으로 로드되는 화면을 볼 수 있었다.

그 외에는 크게 오래 헤메이는 이슈는 없었다. 다만 useState와 useEffect를 모사한 함수를 추가하며 대략적인 동작 원리를 다시 생각해 보는 것이 무척 인상깊은 경험이었다. 프론트앤드에 막 입문해 자바스크립트도 제대로 떼지 못한 채 리액트를 다루던 시절에, 왜 리액트가 프레임워크가 아니라 라이브러리인지 이해할 수 없다는 생각을 했었다. 점차 시간이 지나며 리액트가 UI 라이브러리일 뿐임을 받아들이게 되었지만 명확히 이해하고 있는 것은 아니었다. 이번 경험을 통해 리액트의 핵심 로직은 화면을 새로 그리는데에 있다는 것을 새삼 느낄 수 있었다.

기술적 성장

SPA가 어떻게 동작하는지 이해할 수 있게 되었다.
리액트가 왜 필요한지, 리액트가 나 대신 해주던 것들이 무엇인지 명확히 알 수 있었다.

자랑하고 싶은 코드

DetailPage, HomePage 등에서 직접 제작한 훅을 사용하여 무척 리액트 스럽게 api를 호출하고 내부 상태를 관리했습니다. 라우터 단에서 상태를 관리하고 api를 내려주던 초기 모델보다 많이 나아진 모습이라 보기 좋습니다.

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

main.js 내부에 ui에 부착되는 이벤트들이 모두 모여있는것이 무척 보기 괴롭습니다.

학습 효과 분석

프론트엔드의 기초를 다진 것 같아 든든한 기분이 듭니다.

과제 피드백

UI가 모두 제작된 채로 프로젝트를 시작할 수 있어 부담을 덜 수 있었습니다.
전반적으로 과제의 스트레스가 심하게 튀는 부분이 없이 꾸준히 진행할 수 있었습니다.
간만에 제가 개발을 하는 이유에 대해 생각해보게 되었습니다. 학창시절에는 개발이 재미있어서 개발자 말고 다른 직업은 생각도 안하며 살았는데 요즘은 그런마음을 잊고 있었습니다. 무엇에 대해 깊게 생각해보고 머리를 싸매며 문제를 해결한다는 것의 즐거움을 다시 느낄 수 있었습니다.

AI 활용 경험 공유하기

리액트의 핵심 훅의 로직을 참고하여 간단한 기능만을 남긴 훅을 제작하는데 유용하게 사용했습니다.
리액트, 뷰 등등의 SPA의 상태 관리 방법을 비교하여 마음에 들고 직관적인 구현 방식이 무엇인지 한눈에 볼 수 있어 편하게 추가할 수 있었습니다.

리뷰 받고 싶은 내용

현재 클릭 이벤트 등 ui에 부착되는 이벤트들을 main.js 안에 모두 모아 둔 상황입니다. 굳이 이걸 분리시키지 않은 이유는 첫째로 모두 root에 접근해서 이벤트를 붙이기 때문에, 각 컴포넌트 내부로 가져가는 것 보다 한자리에 모아두는 것이 이벤트의 중복이나 잘못된 부착 등을 막는데 더 효과적이지 않을까? 싶은 생각이 들었기 때문이었습니다.

그러나 코드의 상태가 심히 보기 괴롭습니다. 지금처럼 이벤트를 $root.addEventListener 로 붙이는 상황에서는 이런 방식이 어느정도 강제되는 것인지, 아니라면 어떤것을 기준으로 이벤트를 나누어 관리할 수 있을지 궁금합니다.

JunilHwang and others added 30 commits November 9, 2025 14:34
# Conflicts:
#	.github/pull_request_template.md
#	.prettierrc
#	requirement.md
#	src/main.js
#	src/setupTests.js
#	vite.config.js
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 쇼핑몰의 주요 기능들을 모듈화하고 리팩토링하여 유지보수성과 확장성을 개선하는 작업입니다. 구체적으로는

  • main.js에서 모든 이벤트를 전역에 위임하는 구조를 컴포넌트별 이벤트 모듈로 분리하여 책임 분산 및 코드 가독성 향상
  • CartModal 및 기타 UI 컴포넌트에서 HTML 문자열 조합 방식을 보완하여 컴포넌트별 구조 명확화
  • HomePage에서 IntersectionObserver를 안정적으로 관리하는 개선
  • ProductDetail 컴포넌트에서 별점 렌더링 함수 확장성 개선
  • cartStore 내부 상태 변경 함수 중복 개선 제안
  • 커스텀 훅 상태 관리 배치 렌더링 최적화 방향 제시
  • Router 모듈에서 정규식 기반 경로 매칭 구조화 및 확장성 개선 제안

아래는 주요 코드 흐름 구조 요약입니다.

App (main.js)
 ├─ Router (router/index.js, router/router.js, router/routes.js)
 │   ├─ HomePage (pages/HomePage.js)
 │   │   ├─ SearchForm (components/SearchForm.js)
 │   │   └─ ProductList (components/ProductList.js)
 │   └─ DetailPage (pages/DetailPage.js)
 │       └─ ProductDetail (components/ProductDetail.js)
 ├─ PageLayout (pages/PageLayout.js)
 │   ├─ Header (components/Header.js)
 │   ├─ Footer (components/Footer.js)
 │   └─ CartModal (components/CartModal.js)
 ├─ CartStore (utils/cartStore.js)
 ├─ ModalStore (utils/modalStore.js)
 ├─ Hooks (utils/hooks.js)
 └─ Toast (components/Toast.js)

이 구조는 각 영역별 관심사 분리가 어느 정도 되면서도, 이벤트 처리와 상태 관리 분리에 보다 집중하면 유지보수성과 재사용성, 확장성이 좋아질 것입니다.현재 main.js에서 전역 root에 이벤트 핸들러를 위임하는 설계는 소규모 또는 중소형 프로젝트에서는 관리가 용이하고 충돌 예방 측면에서 유용할 수 있습니다. 하지만 서비스가 커지고 복잡도가 증가하면 이런 일원화 이벤트 관리가 오히려 코드 가독성을 떨어뜨리고, 특정 기능 버그 수정 시 큰 영역에 영향을 미치는 위험성을 가집니다.

  1. 컴포넌트 단위 이벤트 모듈화를 추천드립니다. 각 컴포넌트별로 자신이 필요한 이벤트만 별도 파일 혹은 함수로 분리하여 초기화하는 방식입니다. 메인에서는 이런 모듈의 init 함수를 호출해 이벤트만 등록합니다.

  2. 역할 분리와 관심사의 분리를 명확히 하세요. UI 이벤트, 상태 관리, 라우팅, API 호출 등의 책임이 뒤섞이지 않게 구조를 만들어야 유지보수와 확장이 쉬워집니다.

  3. 위임 이벤트는 그대로 유지하되 이벤트 조건 검사와 핸들러 함수는 분리하세요. 메인 이벤트 핸들러 내부에서 각 컴포넌트 기능별 핸들러를 호출하는 중간 역할 구조도 나쁘지 않습니다.

  4. 이벤트 핸들러가 너무 크면 기능별로 분할하고, 가능한 event delegation은 유지하되, 핸들러 함수 내 역할을 단순화시키는 것이 좋습니다.

  5. 장기적으로는 리액트, 뷰 등 프레임워크 기반 UI 개발로 전환을 고려한다면 이벤트 관리, 상태 관리가 기본적으로 분리되어 있기 때문에 자연스럽게 이벤트 복잡도가 경감됩니다.

결론적으로, 지금 상태에서 이벤트를 한 곳에 두는 명확한 기술적 문제는 없지만, 실무에서 확장성과 가독성, 협업의 편의성을 고려하면 점진적으로 여러 모듈로 분리하는 것이 권장됩니다.

@@ -1,1148 +1,348 @@
import { router } from "./router/index.js";
import { showToast } from "./components/Toast.js";
import {
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 에서 화면 전역(root) 대상으로 모든 UI 이벤트를 한 군데에서 위임 처리하고 있습니다. 이는 이벤트 관리 일원화라는 장점이 있지만, 파일이 커지고 다양한 이벤트 조건문이 중첩되면서 코드 가독성과 유지보수성이 떨어지는 문제로 이어질 수 있습니다.

2. 근본 원인

핵심 문제: UI 이벤트 핸들러들이 컴포넌트나 기능별로 분리되지 않고 한 파일에 몰려 있어서 코드 복잡도가 증가하고 있음

왜 문제인가: 특정 기능이나 컴포넌트와 관련된 이벤트 로직이 분리되어 있지 않아 기능 확장 시 코드 이해와 수정이 어려워집니다. 만약 특정 컴포넌트 리팩토링이나 독립적인 테스트가 필요해질 경우 많은 의존성과 변경점이 같이 따라오게 됩니다.


3. 개선 구조

현재 구조:

  • main.js 에서 전역 root에 이벤트 위임
  • 이벤트 조건문이 여러 기능이 한데 혼재

개선 구조:

  • 컴포넌트 별 또는 기능 별 이벤트 처리 로직 분리
  • main.js에서는 각 컴포넌트별 이벤트 관리 모듈을 import 하여 초기화
  • 각 이벤트 모듈은 자신이 담당하는 UI 엘리먼트에 위임 이벤트를 붙이고 관리

개선사항:

  • [모듈화] 이벤트를 기능 단위 JS 파일 혹은 컴포넌트 기반으로 분리
  • [컴포넌트 포함 관리] 각 컴포넌트는 자기 관련 이벤트 핸들러를 관리하는 init 함수 노출
  • [전역과 지역이벤트 구분] 전역 이벤트와 컴포넌트별 이벤트를 명확히 구분

예시 코드 비교:

//❌ 현재 main.js 내 이벤트 핸들러 집중
$root.addEventListener('click', (e) => {
  if(e.target.closest('.add-to-cart-btn')) { /* 장바구니 추가 이벤트 */ }
  else if(e.target.closest('#cart-modal-close-btn')) { /* 모달 닫기 이벤트 */ }
  // ... 이벤트가 많아서 복잡
});

//✅ 개선된 구조
// cartModalEvents.js
export function initCartModalEvents($root) {
  $root.addEventListener('click', (e) => {
    if(e.target.closest('#cart-modal-close-btn')) { /* 모달 닫기 */ }
    // ... 모달 관련 이벤트만 처리
  });
}

// productListEvents.js
export function initProductListEvents($root) {
  $root.addEventListener('click', (e) => {
    if(e.target.closest('.add-to-cart-btn')) { /* 장바구니 추가 */ }
    // ... 상품 목록 관련 이벤트 처리
  });
}

// main.js
import { initCartModalEvents } from './events/cartModalEvents.js';
import { initProductListEvents } from './events/productListEvents.js';

const $root = document.querySelector('#root');
initCartModalEvents($root);
initProductListEvents($root);

이런 구조는 코드 분리가 가능하고 각 모듈 책임이 명확해지는 장점이 있습니다.


/**
* 장바구니 모달 메인 컴포넌트
* @param {Array} cartItems - 장바구니 아이템 배열
Copy link
Contributor

Choose a reason for hiding this comment

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

1. 문제 상황

CartModal 컴포넌트가 HTML 문자열 생성과 로직이 같이 존재해서 가독성이 약간 떨어집니다. 특히 여러 HTML chunk들을 함수로 나누어 구성했지만, 모두 문자열을 반환하는 방식이라 실제 UI 구조 파악이 어렵고 유지 보수가 번거로워질 수 있습니다.

2. 근본 원인

핵심 문제: UI를 HTML 문자열로 작성하며 로직과 UI 표현이 혼재되어 있음

왜 문제인가: UI 변경 시 문자열 조작이 번거롭고 조건별 UI 변경 표현에 일관성이 떨어지며 JSX나 템플릿 라이브러리 대비 코드 재사용과 유지보수가 어렵습니다.


3. 개선 구조

개선사항:

  • 컴포넌트별로 상태와 UI를 명확히 분리
  • DOM 템플릿 함수에 더 명확한 역할과 props 전달
  • 추후 리액트/뷰 등 프레임워크로 전환을 고려한 구조로 설계

예) 상위 컴포넌트에서 상태를 계산해 하위 컴포넌트에 props로 넘기기

// 현재
const CartModalItem = (item, isSelected = false) => `<div ...>${item.title}</div>`;

// 개선
const CartModalItem = ({ item, isSelected }) => {
  return `<div ...>${item.title}</div>`;
};
  • 또한 실제로는 작은 템플릿 유틸리티를 만들어 HTML 문자열 관련 중복 제거 및 상태 표현을 관리하면 가독성 향상에 도움이 됩니다.

}
}
};

Copy link
Contributor

Choose a reason for hiding this comment

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

1. 문제 상황

무한 스크롤 구현 부분에서 observer를 setTimeout 내에서 세팅하고 있고, cleanup 함수가 제대로 동작하지 않을 수 있습니다. 특히 setTimeout의 리턴값을 cleanup에서 사용하지 않아 observer 인스턴스가 누수될 위험이 있습니다.

2. 근본 원인

핵심 문제: useEffect 내부에서 비동기로 observer 생성 및 unobserve 처리 누락

왜 문제인가: 렌더링 라이프사이클과 비동기 observer 관리가 맞지 않아 메모리 누수 및 이벤트 중복 실행 우려가 존재함


3. 개선 구조

개선사항:

  • useEffect 안에서 observer를 즉시 생성하고 cleanup 시 unobserve와 disconnect를 실행
  • setTimeout 대신 requestAnimationFrame 혹은 refs 사용 고려

코드 예시:

useEffect(() => {
  if (loading || isLoadingMore || !pagination.hasNext) return;

  const sentinel = document.querySelector('#infinite-scroll-trigger');
  if (!sentinel) return;

  const observer = new IntersectionObserver(async (entries) => {
    const [entry] = entries;
    if (entry.isIntersecting && !isLoadingMore) {
      // 데이터 로드
    }
  }, { rootMargin: '100px', threshold: 0.1 });

  observer.observe(sentinel);

  return () => {
    observer.unobserve(sentinel);
    observer.disconnect();
  };
}, [...deps]);
  • 이렇게 하면 observer를 확실히 관리하여 안정적인 리소스 해제를 보장합니다.

// ]
// }

if (loading) {
Copy link
Contributor

Choose a reason for hiding this comment

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

1. 문제 상황

별점 평점 렌더링 함수인 renderStars 안에서 반복문으로 SVG 별 아이콘을 직접 하드코딩하는 방식입니다. 이 구조가 길고 가독성이 떨어지며, 추가 요구사항으로 별점이 0.5 단위 지원 혹은 다른 평점 표시 추가 시 확장할 때 어려움이 있습니다.

2. 근본 원인

핵심 문제: 평점 렌더링 로직이 하드코딩 및 다중 반복으로 인해 확장성 부족

왜 문제인가: 만약 반올림, 반 별점, 애니메이션 등이 요구될 때 코드 수정이 복잡하며 재사용성이 낮습니다.


3. 개선 구조

개선사항:

  • 별점 아이콘을 컴포넌트 혹은 함수로 분리
  • 평점(소수점)을 계산해 정밀한 별 렌더링 가능하게 구현
  • 화면 표시용 별점 로직 모듈화

예시:

const Star = ({ filled }) => filled ? yellowStarSVG : grayStarSVG;

const renderStars = (rating) => {
  const stars = [];
  for(let i=1; i<=5; i++) {
    if(rating >= i) stars.push(Star({filled:true}))
    else if(rating >= i - 0.5) stars.push(Star({filled:'half'}))
    else stars.push(Star({filled:false}))
  }
  return stars.join('');
};
  • 이런 구조는 나중에 별점 종류 추가 및 디자인 변경에 매우 유용합니다.

// 수량 증가
export function increaseQuantity(productId) {
const item = cartItems.find((item) => item.id === productId);
if (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. 문제 상황

장바구니 수량 조절 함수 내부에 중복 코드(saveCart, notifyListeners)가 여러 곳에 반복됩니다. 확장 시 누락 가능성이 있고, 관련 상태 변경 로직도 분산되어 있습니다.

2. 근본 원인

핵심 문제: 상태 변경 후 저장과 구독자 알림이 개별 함수 안에 반복적으로 직접 호출되어 코드 중복 발생

왜 문제인가:
이런 구조는 상태 변경 방식 일관성이 떨어져 실수로 일부 함수에서 저장이나 알림을 생략할 수 있습니다.


3. 개선 구조

개선사항:

  • 상태 변경 함수들은 순수 로직만 수행하고, 외부의 상태 변경 관리 함수가 save와 notify를 담당하도록 분리
  • 상태 변경 시 액션을 dispatch하는 패턴 연상

예)

function updateCartItems(newItems) {
  cartItems = newItems;
  saveCart();
  notifyListeners();
}

export function increaseQuantity(productId) {
  const updated = cartItems.map(item => item.id === productId ? {...item, quantity: item.quantity + 1} : item);
  updateCartItems(updated);
}
  • 이 방법으로 상태 변경 후 동기화 작업을 중앙 집중화할 수 있습니다.

const setState = (newValue) => {
const value = typeof newValue === "function" ? newValue(states.get(key)) : newValue;

if (states.get(key) !== value) {
Copy link
Contributor

Choose a reason for hiding this comment

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

1. 문제 상황

커스텀 Hooks 유틸리티 내부에서 상태 업데이트 시 Promise.resolve().then()으로 비동기를 이용해 상태 변화를 트리거하고 있습니다. 이를 통해 배치 렌더링 효과를 내지만, 여러 연속 상태 변경에 대해 명확하거나 최적화된 동작 보장이 조금 불명확합니다.

2. 근본 원인

핵심 문제: 수동으로 배치 렌더링을 구현하고 있어, 상태 변경이 상위 컴포넌트 혹은 여러 훅에서 연속적으로 발생할 때 업데이트 시점이 예상 밖으로 중복 실행될 우려가 있음

왜 문제인가: React 등 프레임워크는 내부적으로 더 정교한 스케줄링과 렌더링 최적화를 제공하므로, 수동 구현 시 동기화 이슈 및 렌더링 오버헤드 발생 가능성 존재


3. 개선 구조

개선사항:

  • 상태 변경 요청을 큐에 저장하고 animation frame 또는 microtask 내에서 한 번만 실행하도록 하는 개선
  • 필요 시 debounce 또는 throttle 기법을 도입해 렌더링 빈도를 최적화

예시:

let renderScheduled = false;
let renderQueue = [];
function scheduleRender() {
  if(!renderScheduled) {
    renderScheduled = true;
    Promise.resolve().then(() => {
      renderScheduled = false;
      router.rerender();
    });
  }
}
  • 이미 구현된 부분이지만, 렌더링 충돌과 중복 호출 여부를 지속 점검 필요함

const data = await getCategories();
setCategories(data);
setCategoriesLoading(false);
setCategoriesFetched(true);
Copy link
Contributor

Choose a reason for hiding this comment

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

1. 문제 상황

쿼리 파라미터에서 current 페이지를 제거하는 로직이 useEffect내에 직접 window.history.replaceState로 처리되어 있습니다. 이 부분이 필터 변경마다 불필요하게 호출되고 복잡한 URL 조작을 합니다.

2. 근본 원인

핵심 문제: 컴포넌트 내부에 URL 조작 로직이 직접 구현되어 있어, router 등의 라우팅 책임과 겹침

왜 문제인가: URL 상태 관리가 컴포넌트 내부에 분산되어 유지보수가 어렵고 상태 일관성 파악에 혼란을 유발함


3. 개선 구조

개선사항:

  • 쿼리 파라미터 조작과 URL 제어는 router 모듈로 위임
  • 필터 상태 변경 시 router.pushWithQuery 같은 중앙화된 API로 관리
  • 중복 URL 변경 방지 및 코드 가독성 향상

예)

router.pushWithQuery('/', { ...filters, current: 1 });
  • 이 방식은 상태와 URL의 싱크를 명확히 하고 URL 조작 분리를 용이하게 합니다.

${renderBreadcrumb()}
</div>

${
Copy link
Contributor

Choose a reason for hiding this comment

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

1. 문제 상황

SearchForm 컴포넌트가 카테고리 및 필터 UI 렌더링을 HTML 문자열 조합 방식으로 처리하고 있는데, JSX, 템플릿 리터럴에 의존해서 작성된 편집 방식이라 실수로 HTML 태그가 중첩되거나 조건부 렌더링 로직이 복잡해지기 쉽습니다.

2. 근본 원인

핵심 문제: UI가 문자열로 작성돼서 렌더링 가독성과 확장성 저하

왜 문제인가: 컴포넌트별 UI 분리가 어려우며, 복잡한 조건부 UI 처리나 확장성이 떨어짐


3. 개선 구조

개선사항:

  • UI 아이템 별 작은 함수 컴포넌트로 분리
  • 특히 카테고리, 브레드크럼, 각각의 필터 UI를 별도 함수로 분할
  • props로 상태와 이벤트 핸들러를 명확하게 전달

예)

const Breadcrumb = ({ category1, category2 }) => `...`;
const CategoryFilter = ({ categories, selectedCategory }) => `...`;

export const SearchForm = (props) => {
  return `
    ${Breadcrumb(props.filters)}
    ${CategoryFilter(props.categories, props.filters.category1)}
    ...
  `;
};
  • 이런 기법은 유지보수성을 높이고, UI 변경 시 재사용 및 변경이 용이해집니다.

return router;
};

// 경로 이동
Copy link
Contributor

Choose a reason for hiding this comment

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

1. 문제 상황

라우터 구현 시 동적 라우트 파라미터 추출하는 로직과 경로 매칭이 자체 RegExp로 직접 구현되어 있습니다. 이 방식은 기능이 복잡해질 경우 매칭 실패, 확장성 문제, 디버깅 어려움이 생길 수 있습니다.

2. 근본 원인

핵심 문제: 직접 정규식 기반 경로 매칭 로직을 구현하여 프레임워크 혜택 및 표준화된 라우팅 방식 활용 부족

왜 문제인가: 라우트 확장, 예외 처리, 경로 규칙 변경 시 유지보수 부담 증가, URL 쿼리 및 파라미터 처리의 일관성 문제


3. 개선 구조

개선사항:

  • 오픈소스 라우팅 라이브러리(예: path-to-regexp) 혹은 자체 유틸 함수로 경로 매칭 분리
  • 정규식 생성과 파라미터 추출 로직 캡슐화
  • 라우트 등록 시 디버깅 용이한 구조 제공

예)

import { match } from 'path-to-regexp';

const matcher = match('/product/:id');
const matched = matcher('/product/123');
if(matched) {
  const params = matched.params; // { id: '123' }
}
  • 이렇게 하면 코드 완성도가 높아지고 여러 라우팅 패턴 지원 가능

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