-
Notifications
You must be signed in to change notification settings - Fork 50
[7팀 박희정] Chapter2-1. 프레임워크 없이 SPA 만들기 #46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
JunilHwang
left a comment
There was a problem hiding this 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)를 활용하여 자동으로 생성된 내용입니다.
- 데이터와 UI를 연결하는 부분을 보강하면 좋겠습니다.
ProductList내부에 렌더링된 검색/한 페이지 개수/정렬/카테고리/무한스크롤 UI에는 상태와 이벤트 연결이 부족해서 조건을 바꿔도 화면이 갱신되지 않습니다. - 에러 처리와 사용자 피드백이 아직 도입되지 않았습니다.
main.js의catch블록은 단순 로그에 그치고, 토스트는 렌더링조차 되지 않아 실패나 장바구니 추가/삭제를 신호로 알려줄 수 없습니다. - 그 외에도 장바구니 수량 조절, 상품 상세의 SPA 내비게이션 같은 인터랙션은 현재 템플릿 그대로로는 요구사항을 충족하기 어려우므로 상태/이벤트 중심으로 리팩토링하는 것이 좋겠습니다.
질문에대한 답변
추가 질문이 없어 현재로서는 별도의 답변이 없습니다. 궁금한 점 생기면 언제든 말씀 주세요.
| isLoading: false, | ||
| products: products, | ||
| totalCount: productsData.pagination.total, | ||
| categories: categoriesData, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
문제상황 제시 — API 호출 실패 시 사용자 경험
추가 요구사항에서도 강조되었듯, getProducts/getCategories가 실패하면 사용자에게 재시도가 가능한 에러 UI를 보여줘야 합니다. 그런데 catch 블록은 단순히 console.error만 호출하고 있어 사용자는 로딩 상태 그대로 멈춘 화면만 보게 됩니다.
현재 코드의 한계
- [한계점 1] 네트워크가 끊기거나 API가 500을 내려도 화면에는 아무 변화가 없고, 사용자에게 실패 메시지가 표시되지 않습니다.
- [한계점 2]
재시도버튼이 없어서 사용자가 스스로 새로고침하거나 브라우저를 다시 열기 전에는 서비스를 재시도할 방법이 없습니다. - [한계점 3] 화면 상태를 관리하는 로직이
try/catch의 다른 지점과 분리되어 있어, 상태 전이(로딩 → 에러 → 재시도)를 표현하기 어렵습니다.
근본 원인
main함수 안에서try/catch가 UI 상태와 단일 템플릿 사이에서 직접 제어하는 구조라서, 에러 시 DOM을 다시 그릴 수 있는 추상화가 부족합니다.
개선 구조
현재 구조:
Loading view (ProductList – isLoading=true)
└── fetch products/categories
├─ success → render ProductList with data
└─ failure → console.error (no UI change)
개선 후 구조:
Loading view
└── fetch ...
├─ success → render ProductList
└─ failure → render ErrorView + Retry button
개선 사항
catch블록에서 에러 메시지를 담은 상태를 만들어layout에 다시 넣어서 사용자에게 알려줍니다.- 재시도 버튼에 클릭 핸들러를 달아
main을 다시 호출하거나 fetch 로직만 재실행하도록 처리합니다. - 에러 상태에서만 보여줄 수 있는 작은 컴포넌트 (예:
<ErrorEmptyState onRetry={...} />)를 만들어 구조를 분리합니다.
코드 비교
// ❌ 현재 방식
} catch (error) {
console.error('상품 목록을 불러오는데 실패했습니다:', error);
// 에러 처리 로직 추가 가능
}
// ✅ 개선된 방식
} catch (error) {
const errorHTML = layout({
children: () => ErrorView({ message: '상품을 불러오는 데 실패했습니다', onRetry: main }),
});
document.querySelector('#root').innerHTML = errorHTML;
}| .join(""); | ||
|
|
||
| return ` | ||
| <!-- 검색 및 필터 --> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
검색 입력 필드(75~88)를 렌더링하고 있지만 Enter 키나 입력값을 처리하는 로직이 없습니다. 요구사항 중 'Enter 키로 검색'이 명시되어 있으므로 이 input에서 keydown 또는 submit을 감지해서 새로 fetchProducts를 호출해야 합니다. 현재 아무 이벤트가 붙어 있지 않아 검색 결과가 갱신되지 않습니다.
개선 방법:
layout이 렌더링된 직후document.querySelector('#search-input')에keydown핸들러를 등록하고, Enter 키가 눌리면getProducts({ keyword: input.value })를 재호출합니다.ProductList에searchTextprop을 추가해 렌더링 시 값이 유지되도록 하면 새로고침/공유 시에도 검색어가 복원됩니다.
| </div> | ||
| <!-- 기존 필터들 --> | ||
| <div class="flex gap-2 items-center justify-between"> | ||
| <!-- 페이지당 상품 수 --> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#limit-select(106~123)에는 10/20/50/100 옵션만 있고 change 리스너가 없습니다. 따라서 사용자가 페이지당 아이템 수를 바꿔도 화면에는 아무 변화가 없고, 요구사항인 '선택 변경 시 즉시 목록에 반영'이 충족되지 않습니다. 선택값을 감지하여 getProducts({ limit: Number(value) })로 재요청하고 UI를 다시 그려야 합니다.
| </option> | ||
| </select> | ||
| </div> | ||
| <!-- 정렬 --> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#sort-select(125~134)은 정렬 옵션을 렌더링하지만 change 핸들러가 없기 때문에 정렬 기준을 바꿔도 상품 정렬 순서가 반영되지 않습니다. change 이벤트로 정렬 조건을 상태에 저장한 뒤 getProducts({ sort: 'price_asc' })처럼 다시 호출하고, 받은 데이터를 ProductList에 넘겨주면 요구사항을 만족시킬 수 있습니다.
| <!-- 필터 옵션 --> | ||
| <div class="space-y-3"> | ||
| <!-- 카테고리 필터 --> | ||
| <div class="space-y-2"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
카테고리 영역(93~103)은 1단계 버튼만 반복하면서 아무 상태도 관리하지 않습니다. 현재 선택된 카테고리를 알고 있지 않기 때문에 브레드크럼이 갱신되지 않고, 2단계 카테고리도 렌더링되지 않습니다. 요구사항대로 '전체 > 1depth > 2depth' 브레드크럼을 그리고 선택 상태별로 상품을 다시 불러오려면 selectedCategory1/selectedCategory2 상태를 main.js에서 관리하고, 클릭 시 getProducts를 다시 호출하여 ProductList에 전달해야 합니다.
| </div> | ||
| </div> | ||
| <!-- 상품 목록 --> | ||
| <div class="mb-6"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
상품 그리드 영역(140~166)에서는 데이터를 한 번만 렌더링하고 끝나므로 스크롤이 끝까지 내려가도 다음 페이지를 가져오지 않습니다. '홈 페이지에서만 무한 스크롤' 요구사항을 충족하려면 window 또는 #products-grid의 스크롤 이벤트를 감지해 바닥 근처일 때 page 증가시키고 추가 데이터를 products 배열에 push. 로딩 직전에는 Loading()/Skeleton()을 표시하고, isLoading을 상태화하여 여러 번 호출이 겹치지 않도록 제어해야 합니다.
| <div class="flex items-center mt-2"> | ||
| <button class="quantity-decrease-btn w-7 h-7 flex items-center justify-center | ||
| border border-gray-300 rounded-l-md bg-gray-50 hover:bg-gray-100" data-product-id="${product.id}"> | ||
| <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
장바구니 아이템에서 수량 입력(line 25)이 disabled로 막혀 있고 수량 버튼에도 이벤트 연결이 없어, 요구사항인 '수량 증가/감소' 기능이 아예 동작하지 않습니다. 수량 변경 후 전체 금액도 갱신되어야 하므로, input을 활성화하거나 버튼 클릭 시 수량 상태를 변경하고 총합을 다시 계산하는 로직을 추가해 주세요.
| <div class="max-w-md mx-auto px-4 py-4"> | ||
| <div class="flex items-center justify-between"> | ||
| <div class="flex items-center space-x-3"> | ||
| <button onclick="window.history.back()" class="p-2 text-gray-700 hover:text-gray-900 transition-colors"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ProductDetailHeader의 뒤로가기 버튼이 inline onclick 핸들러 (window.history.back(), 59~63)으로 작성되어 있는데, SPA 내비게이션과 일관된 라우터 추상화를 이용하지 않으면 앞으로/뒤로 기능이 분산됩니다. data-action=go-back 같은 식별자를 달고 main.js에서 한 번만 이벤트를 붙여 router.goBack()을 호출하면 테스트하기도 쉽고, 새로 만든 SPA 라우터로도 손쉽게 교체할 수 있습니다.
| @@ -0,0 +1,46 @@ | |||
| const Toast = ({ type = "success", message }) => { | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Toast 컴포넌트(src/components/Toast.js, 1~24)에서는 토스트 메시지의 스타일만 정의하고 실제로 3초 뒤 자동으로 사라지게 하거나 닫기 버튼은 동작하게 하는 스크립트가 없습니다. 요구사항에 '토스트는 3초 후 자동으로 사라지며 닫기 버튼이 제공된다'고 되어 있으므로, setTimeout을 이용해 DOM에서 제거하거나 toastService를 통해 duration을 처리하는 로직을 추가해야 합니다.
| // 1. 먼저 로딩 상태 표시 | ||
| const loadingHTML = layout({ | ||
| children: ProductList, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현재 main.js(16~39)에서는 ProductList만 렌더링하고 있으며, Toast 컴포넌트를 DOM에 삽입하거나 버튼 클릭 시 토스트를 보여주는 코드가 전혀 없습니다. 따라서 장바구니 추가/삭제 작업을 해도 사용자는 어떤 피드백도 받지 못합니다. Toast를 렌더링할 <div id='toast-root'>를 만들고, 각각의 버튼 클릭 핸들러에서 Toast를 쌓거나 toastService.show({ type: 'success', message: '...' })를 호출하도록 연결해 주세요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
test
devchaeyoung
left a comment
There was a problem hiding this 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에서는 API와 UI를 연결하는 구조가 명확하며, Layout과 컴포넌트를 효과적으로 분리하여 기본 SPA 골격을 잘 조성하셨습니다. 특히 ProductList와 Cart 컴포넌트의 책임 분리가 깔끔한 편입니다. 👍
추가 질문에서 보인 자기 회고 내용은 전반적으로 구현에 대한 깊은 고민이 느껴져서 좋았습니다. 핵심 질문에 대한 답변으로는, 현재 코드 구조가 기능별 컴포넌트 분리는 잘 되어 있지만 상태 관리와 이벤트 로직의 관심사 분리가 다소 미흡해 확장성과 유지보수에 부담이 될 수 있다는 점을 말씀드리고 싶습니다.
전체적으로 컴포넌트 단위의 UI 출력과 API 호출 분리는 잘 되어 있으나, 상태 관리가 충분히 추상화되어 있지 않아 추가 기능 (예: URL 쿼리와 상태 동기화, 무한 스크롤, 장바구니 저장)등 적용 시 복잡도가 증가할 수 있습니다.
질문에대한 답변
1. 질문 요약
희정님은 이번 과제를 통해 자신의 기술적 성장과 구현한 코드에 대해 객관적인 자기 평가를 해주셨고, 코드 설계 및 리팩토링, 그리고 AI 도구 활용 경험까지 상세히 정리해주셨습니다. 리뷰에서는 특히 상태 관리와 모듈화, 이벤트 처리 부분에서 개선 방안을 요청하셨습니다.
2. 현재 선택의 장단점
현재 구조는 UI 컴포넌트가 명확히 분리되어 있어 가독성과 기본적인 책임 분리는 잘 되어 있습니다. 하지만 상태 관리와 DOM 업데이트 로직이 밀접하게 섞여 있어, 상태 변화에 따른 UI 동기화가 수작업으로 이루어지는 점이 확장성을 낮추는 한계입니다. 예를 들어, 장바구니 상태가 중앙에서 통합 관리되지 않으면 모달과 아이콘 표시, 버튼 상태를 일관되게 관리하기 어려워질 수 있습니다.
3. 실무에서라면 이렇게 설계할 것 같아요
- 상태 관리 분리: 전역 상태 관리 객체(예: store)를 만들어 장바구니, 필터, 검색어 등 상태를 중앙에서 관리합니다.
- 컴포넌트 분리와 이벤트 위임: UI 구성은 컴포넌트 별로 분리하되, 실제 데이터 변경 로직과 이벤트 처리는 별도의 컨트롤러나 상태 관리 모듈에 위임합니다.
- URL 상태 동기화: 쿼리스트링과 상태를 연동해서 URL 직접 접근, 새로고침 시 상태 복원이 자연스럽게 이루어지도록 구현합니다.
- 비동기 처리와 에러 UI 분리: API 호출과 UI 표시를 철저히 분리하며, 에러 상태 표시와 재시도 UI를 명확히 구현합니다.
4. 앞으로 구조를 잡을 때 참고하면 좋은 포인트
- 관심사 분리를 명확히 하여 UI 템플릿 렌더링을 상태 변경 감지와 분리
- 상태 관리 및 이벤트 핸들링을 별도의 영역에서 담당하도록 구조 설계
- 재사용 가능한 유틸리티 함수와 컴포넌트 분리를 통해 코드 중복 최소화
- 사용자 상호작용에서 발생하는 복잡한 상태 변화를 점진적으로 모듈화하여 유지보수성 증대
희정님 앞으로도 직접 구현하며 이러한 구조적 고민을 이어간다면, SPA 아키텍처 전반에서 한층 더 견고하고 확장성있는 설계를 할 수 있을 것입니다. 응원합니다! 👍
| // 아래는 컴포넌트 데모 코드 (개발 참고용) | ||
| // ============================================ | ||
| /* | ||
| const 상품목록_레이아웃_카테고리_1Depth = CategoryFilter1Depth({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
희정님, API에서 받은 원시 데이터를 ProductList 컴포넌트가 기대하는 형태로 변환하는 부분이 깔끔하게 처리되어 있습니다 👍 이 부분은 데이터 포맷이 변경되어도 쉽게 대응할 수 있어 확장성에 유리해요.
다만, 이 변환 로직은 여러 곳에서 재사용될 가능성이 크기 때문에 별도의 유틸 함수로 분리해 관리해보면 좋을 것 같아요. 그렇게 하면 데이터 포맷의 변경에 한 곳만 수정하면 되어 유지보수가 편리해집니다.
| id: "85067212996", | ||
| name: "PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장", | ||
| price: 220, | ||
| imageUrl: "https://shopping-phinf.pstatic.net/main_8506721/85067212996.1.jpg", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
로딩 시 UI 표시를 layout과 ProductList 컴포넌트를 조합해서 표현하는 구조가 명확하네요. 👍
다만, 에러 발생 시 보여줄 UI와 재시도 버튼 기능은 확인이 어려워 보입니다. 요구사항에 따라 에러 처리를 View에 명확히 반영하고, 재시도 버튼 이벤트를 구현하는 부분을 추가해보면 더욱 완성도가 높아질 것 같아요.
| @@ -0,0 +1,162 @@ | |||
| const CartItem = ({ product, isSelected = false }) => { | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
장바구니 모달 컴포넌트가 기능별로 CartItem과 CartModal로 나뉘어져 있어 책임이 잘 분리되어 있습니다. 👍 UI 요소들을 템플릿 리터럴로 깔끔하게 관리한 점도 가독성에 도움이 됩니다.
다만 내부 상태 관리와 이벤트 핸들링이 의존하는 부분이 따로 보여야 유지보수에 유리합니다. 현재의 HTML 생성 코드와 상태나 이벤트 관리를 분리해서 구현할 수 있다면 테스트 및 확장성이 더 좋아질 수 있어요.
| const category1Buttons = Object.keys(categories) | ||
| .map( | ||
| (cat1) => ` | ||
| <button data-category1="${cat1}" class="category1-filter-btn text-left px-3 py-2 text-sm rounded-md border transition-colors bg-white border-gray-300 text-gray-700 hover:bg-gray-50"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ProductList 내에서 반복되는 카테고리 버튼 생성 등이 템플릿 리터럴 내에서 처리되고 있는데, 이런 UI 생성 로직을 배열 메서드(map, filter 등)를 활용해 직관적으로 작성한 점이 좋습니다. 👍
하지만 조건에 따라 보여줄 UI가 HTML 문자열 중첩으로 길어지는 부분이 있습니다. 이를 컴포넌트 별 함수로 분리하거나, 간단한 상태 기반 렌더링 함수로 나누어 관리해보는 것도 추천드립니다.
| price: 230, | ||
| imageUrl: "https://shopping-phinf.pstatic.net/main_8694085/86940857379.1.jpg", | ||
| quantity: 1, | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
전체적으로 SPA 구조를 손수 구현하시면서 Layout, Component, API 연동을 명확히 분리해두셨네요. 👍 하나의 main.js에서 데이터를 fetch하고, 레이아웃에 따라 UI를 업데이트하는 구조가 잘 구현되어 있습니다.
다만, 상태 관리 (검색어, 카테고리 필터 상태, 장바구니 상태 등)를 전역적으로 분리하는 작업이 필요해 보입니다. 상태가 분산되어 있으면 사용자 상호작용에 대한 이벤트 처리와 상태 동기화가 복잡해질 수 있기 때문입니다.
과제 체크포인트
배포 링크
https://Pheejung.github.io/front-7th-chapter2-1/
기본과제
상품목록
상품 목록 로딩
상품 목록 조회
한 페이지에 보여질 상품 수 선택
상품 정렬 기능
무한 스크롤 페이지네이션
상품을 장바구니에 담기
상품 검색
카테고리 선택
카테고리 네비게이션
현재 상품 수 표시
장바구니
장바구니 모달
장바구니 수량 조절
장바구니 삭제
장바구니 선택 삭제
장바구니 전체 선택
장바구니 비우기
상품 상세
상품 클릭시 상세 페이지 이동
/product/{productId}형태로 변경된다상품 상세 페이지 기능
상품 상세 - 장바구니 담기
관련 상품 기능
상품 상세 페이지 내 네비게이션
사용자 피드백 시스템
토스트 메시지
심화과제
SPA 네비게이션 및 URL 관리
페이지 이동
상품 목록 - URL 쿼리 반영
상품 목록 - 새로고침 시 상태 유지
장바구니 - 새로고침 시 데이터 유지
상품 상세 - URL에 ID 반영
/product/{productId})상품 상세 - 새로고침시 유지
404 페이지
AI로 한 번 더 구현하기
과제 셀프회고
기술적 성장
자랑하고 싶은 코드
개선이 필요하다고 생각하는 코드
학습 효과 분석
과제 피드백
AI 활용 경험 공유하기
리뷰 받고 싶은 내용