-
Notifications
You must be signed in to change notification settings - Fork 50
[1팀 강승훈] Chapter2-1. 프레임워크 없이 SPA 만들기 #42
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)를 활용하여 자동으로 생성된 내용입니다.
전체 요약
- 충돌 마커가
.github/pull_request_template.md와requirement.md에 남아 있어 체크리스트가 혼란스럽고 오토메이션에서 해석할 수 없습니다. 이 부분을 정리해야 다른 정렬 옵션을 추가할 때마다 템플릿이 깨지지 않습니다. main.js에서는 렌더링이 반복될 때마다 글로벌 이벤트(click,popstate)를 재등록하고 있으므로 필터 변경이나 향후 무한 스크롤처럼 re-render가 잦은 상황에서 이벤트가 누적됩니다. 전역 핸들러는 앱 초기화 단계에서 한 번만 등록하고,render()는 UI 갱신만 책임지도록 구조를 리팩터링하는 것이 좋습니다.- API 호출에 대한 에러 핸들링, 무한 스크롤을 위한 페이징, URL 쿼리로 상태를 유지하는 로직이 빠져 있어서 요구사항을 완전히 만족하지 못하고 있습니다. 에러 UI/재시도 버튼, 다음 페이지 데이터를 쿼리하는 로직,
URLSearchParams를 이용한 상태 동기화 등을 추가하면 확장성도 확보할 수 있습니다. Cart컴포넌트는 총액 계산이 하드코딩되어 있고 수량 버튼/ESC 키 처리가 빠져 있어 "장바구니 기능" 요구사항을 일부 충족하지 않습니다. 각 버튼을 실제 로직에 연결하고, 총액을 계산해서 보여주고, ESC 키로 닫는 핸들러를 추가해야 합니다.
설계 피드백
- 글로벌 상태와 이벤트를 분리:
filtersstore가 잘 정리되어 있지만 이벤트 등록이 렌더링 로직 안에 있으니init()단계에서 한 번만 등록하도록 분리하면 메모리 누수를 막을 수 있습니다. - 로딩/에러/페이지네이션을 명확하게 분리:
HomePage에isLoading,error,pagination같은 상태를 명시적으로 넘기고, API 호출을 래핑하여 재시도 가능한 에러 UI를 보여주면 신뢰성 있는 사용자 경험이 됩니다. - URL ↔ 상태 동기화: SPA 네비게이션이나 공유 링크를 고려할 때,
filters는URLSearchParams와historyAPI를 통해 브라우저 주소창과 양방향으로 동기화하는 구조로 바꾸는 것이 좋습니다. 이 작업은 향후 상세 페이지가 늘어나거나 여러 필터가 서브 앱에서 공유될 때 복잡도를 줄여줍니다.
질문에대한 답변
장바구니 옵저버 패턴 질문에 대한 답변
옵저버 패턴을 명시적으로 생각하는 것도 좋지만, 실제로는 다음과 같은 두 가지 방식으로 충분히 대응 가능합니다.
-
간단한 pub/sub 모듈을 만들어 cart 상태를 감지하는 방식
- cart 상태를
localStorage로 관리한다면, 그 상태를 읽고cartItems가 바뀔 때마다CustomEvent를 디스패치해서 다른 컴포넌트가 듣도록 할 수 있습니다. - 예를 들면
const emitCartChange = () => window.dispatchEvent(new CustomEvent("cart.changed"));를 로컬 스토리지에 쓰는 유틸에서 호출하고,renderCartBadge()등은window.addEventListener("cart.changed", renderCartBadge)처럼 구독하면 됩니다.
- cart 상태를
-
단순한
EventEmitter를 직접 만들어 사용하는 방식const cartSubscribers = new Set(); const subscribeCart = (fn) => cartSubscribers.add(fn); const publishCart = () => cartSubscribers.forEach((fn) => fn());과 같은 구조로 만들고, 장바구니 관련 유틸 함수에서 상태가 바뀔 때publishCart()를 호출합니다.- 구독자는 이벤트 이름 없이 상태 자체만 듣기 때문에 구현이 간결하고, 프레임워크 없이도 재사용 가능합니다.
개인적 아이디어: 옵저버 패턴을 쓰기보다 저는
cartStore와filtersstore를 동일한 방식으로 만드는 편입니다. 즉, 작은 상태 저장소(createStore)를 만들어cartStore.subscribe(renderCart)처럼 쓸 수 있으면 훨씬 직관적입니다. 중요한 것은 "누군가 장바구니를 바꿨을 때 render를 다시 트리거할 수 있는 지점"을 명확하게 두는 것이고, 단일 이벤트 발행/구독 구조면 충분합니다.
필요하다면 cartStore를 다음과 같이 만들어도 좋습니다:
const cartStore = createStore(getCartItemsFromLocalStorage());
cartStore.subscribe(() => {
renderCart();
renderCartBadge();
});
const updateCart = (updater) => {
cartStore.setState(updater(cartStore.getState()));
saveToLocalStorage(cartStore.getState());
};이처럼 상태를 한 곳으로 모으면 옵저버(구독자)도 한 번만 정의하면 되고, 다른 컴포넌트도 쉽게 구독할 수 있어서 확장성이 훨씬 좋아집니다.
|
|
||
| - [ ] 드롭다운에서 10, 20, 50, 100개 중 선택할 수 있으며 기본 값은 20개 이다. | ||
| - [ ] 선택 변경 시 즉시 목록에 반영된다 | ||
|
|
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.
문제 상황
상품 정렬 기능 항목에 <<<<<<< HEAD/=======/>>>>>>> 충돌 마커가 그대로 남아 있어서 GitHub에서 자동으로 PR 템플릿을 렌더링할 때 두 가지 중복된 체크리스트 항목이 동시에 노출됩니다. 이 상태로는 어떤 조건으로 체크해야 하는지 명확하지 않습니다.
현재 코드의 한계
- [한계점 1] PR 템플릿을 복사해서 쓰는 개발자는
HEAD영역과caecb93영역을 비교해서 어떤 체크박스를 써야 하는지 매번 판단해야 합니다. - [한계점 2] 템플릿을 파싱해서 자동화를 돌리는 워크플로우(예: 특정 체크리스트를 만족해야 병합 가능)에서 충돌 마커가 있는 문자열을 그대로 읽어버리면 오류를 발생시킬 수 있습니다.
- [한계점 3] 비교적 잦은 ‘정렬 기준 변경’ 요구사항이 생길 때마다 충돌 마커를 만지는 과정이 반복되면 유지보수가 어려워집니다.
근본 원인
PR 템플릿을 수정하면서 충돌을 완전히 해소하지 않고 커밋해서 충돌 마커가 리포지토리에 남아 있었습니다.
개선 구조
- [개선 사항 1] 충돌 마커를 제거하고 하나의 체크리스트(예: 가격순/인기순)만 남겨주세요.
- [개선 사항 2] 정렬 옵션이 변경될 때는 템플릿을 다시 수정해서 새로운 옵션을 단일한 리스트로 반영합니다.
- [개선 사항 3] GitHub 템플릿은 머지되기 전에
git diff --check로 확인해서 충돌 마커가 없는지를 검증하는 훅을 둘 수 있습니다.
-<<<<<<< HEAD
- - [ ] 상품을 가격순/이름순으로 오름차순/내림차순 정렬을 할 수 있다.
-=======
-- [ ] 상품을 가격순/인기순으로 오름차순/내림차순 정렬을 할 수 있다.
->>>>>>> caecb93 (feat: 기본 코드 추가)
+- [ ] 상품을 가격순/인기순으로 오름차순/내림차순 정렬을 할 수 있다.추가 요구사항: 앞으로
인기순외에리뷰순같은 새 정렬을 도입하면 템플릿을 다시 열어 다수의 항목이 한 번에 보일 것이고, 현재처럼 충돌 마커가 섞여 있으면 합의된 상태로 유지하기 어렵습니다. 이번 기회에 템플릿을 정리해 두면 나중에 커스텀 옵션이 들어와도 깔끔하게 유지할 수 있습니다.
|
|
||
| - 드롭다운에서 10, 20, 50, 100개 중 선택할 수 있으며 기본 값은 20개 이다. | ||
| - 선택 변경 시 즉시 목록에 반영된다 | ||
|
|
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.
문제 상황
요구사항 문서에도 <<<<<<< HEAD 등 충돌 마커가 남아 있어서 상품 정렬 기능 요구사항이 둘 다 존재하는 것처럼 보입니다. 문서를 보는 사람마다 정렬 기준을 다르게 해석할 수 있습니다.
현재 코드의 한계
- [한계점 1] 문서를 참고해서 구현하는 사람들이 어떤 정렬을 우선으로 해야 할지 헷갈립니다.
- [한계점 2] 충돌 마커가 포함된 상태로 배포하면 린트/검수팀이 "문서가 불안정하다"고 판단할 수 있습니다.
- [한계점 3] 새로운 정렬 옵션을 추가하려면 기존 내용을 정리해야 하는데, 충돌 상태면 어느 줄을 편집해야 하는지 판단하기 어렵습니다.
근본 원인
마찬가지로 문서 편집 도중 merge conflict를 해결하지 않고 커밋해서 충돌 마커가 남아 있습니다.
개선 구조
- [개선 사항 1] 중복된 정렬 항목을 하나로 정리하고 충돌 마커를 제거합니다.
- [개선 사항 2]
가격순/인기순을 기본으로 유지하면서, 이후리뷰순등 다른 옵션을 추가할 때에는 별도의 항목을 추가합니다. - [개선 사항 3] 요구사항 문서는 CI에서 문법 검사(머지 마커가 없는지)하도록 체크하는 것이 좋습니다.
-<<<<<<< HEAD
- - 상품을 가격순/이름순으로 오름차순/내림차순 정렬을 할 수 있다.
-=======
-- 상품을 가격순/인기순으로 오름차순/내림차순 정렬을 할 수 있다.
->>>>>>> caecb93 (feat: 기본 코드 추가)
+- 상품을 가격순/인기순으로 오름차순/내림차순 정렬을 할 수 있다.추가 요구사항: 앞으로
리뷰 순,신상품 순같이 정렬 기준이 늘어날 가능성이 있는데, 충돌 마커가 남아 있으면 누가 어떤 기능을 구현했는지 알기 어렵습니다. 각 항목을 명확하게 구분해두어야 확장성도 확보됩니다.
| } | ||
| cartModal.classList.remove("hidden"); | ||
| } | ||
|
|
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.
문제 상황
eventHandlers() 안에서 document.addEventListener("click", …)을 등록하고 있는데, 이 함수는 init()과 render()에서 모두 호출됩니다. 필터가 바뀔 때마다 render()가 다시 실행되면 동일한 클릭 핸들러가 여러 번 등록됩니다.
현재 코드의 한계
- [한계점 1] 필터를 여러 번 변경하면 클릭 이벤트가 누적되면서 버튼을 한 번 눌러도 여러 번 처리됩니다.
- [한계점 2] 메모리 사용량도 늘어나므로 모바일처럼 리소스가 제한된 환경에서 성능이 저하될 수 있습니다.
- [한계점 3] 앞으로
무한 스크롤이나실시간 필터링을 추가해서render()가 더 자주 호출되면 문제는 더욱 심각해집니다.
근본 원인
글로벌 이벤트 리스너 등록을 렌더링 로직 내부에서 반복하고 있기 때문입니다.
개선 구조
- [개선 사항 1] 글로벌 핸들러들을
init()에서 한 번만 등록하고, 내부에서는event.target으로 구분해서 필요한 로직을 분기합니다. - [개선 사항 2] 핸들러는 사전에
const handleDocumentClick = async (event) => { … }처럼 선언해두고document.addEventListener("click", handleDocumentClick)/removeEventListener형태로 관리하면 다음 리렌더링 시에도 중복 등록을 방지할 수 있습니다. - [개선 사항 3] 여러 핸들러가 필요할 경우
eventHandlers()를 여러 번 호출하지 않고init()에서setupGlobalHandlers()를 한 번 호출하도록 리팩터링합니다.
// ❌ 현재 방식
const render = async () => {
…
eventHandlers(); // 매번 호출 → 같은 click 핸들러가 누적
};
// ✅ 개선된 방식
const handleDocumentClick = async (event) => {
…
};
const init = async () => {
setupGlobalHandlers(); // 한 번만 등록
filters.subscribe(render);
render();
};추가 요구사항 예시: 스크롤 위치에 따라
render()를 자동으로 다시 호출하는 무한 스크롤을 도입하면 클릭 핸들러도 같이 매번 누적되어 이벤트가 2배, 3배씩 처리됩니다. 현재 구조로는 간단한 기능 확장만 해도 버그가 폭발하므로, 글로벌 핸들러는 렌더링 바깥에서 한 번만 설정하는 방식으로 바꾸는 것이 안정적입니다.
| } | ||
|
|
||
| eventHandlers(); | ||
| renderCartBadge(); |
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.
문제 상황
render() 함수 마지막에 window.addEventListener("popstate", render)를 등록하고 있습니다. 이 함수는 filters.setState()가 호출될 때마다 재실행되므로 같은 popstate 리스너가 계속 누적됩니다.
현재 코드의 한계
- [한계점 1] 브라우저 히스토리 버튼을 누르면 같은
render가 여러 번 중첩되어 호출될 수 있습니다. - [한계점 2]
popstate가 여러 번 실행되면 내부 캐시/상태가 꼬일 수 있고, 성능도 저하됩니다. - [한계점 3] 향후
filters가 많아져 렌더링이 잦아지면 이 문제는 더욱 심각해집니다.
근본 원인
render() 내부에서 popstate 핸들러를 등록해 매 렌더링마다 새로운 리스너를 추가하고 있기 때문입니다.
개선 구조
- [개선 사항 1]
popstate는 앱 전체에서 한 번만 등록하고,render()는 단순히 UI를 갱신하는 역할만 하도록 분리합니다. - [개선 사항 2]
const handlePopstate = () => render();를init()에서 한 번 등록하고, 이후에는 다시 등록하지 않습니다. - [개선 사항 3]
filters.subscribe(render)과 같이 앱 전체에서 하나의 렌더 함수를 공유하면서 이벤트는 모든 렌더를 통틀어 한 번만 등록합니다.
// ❌ 기존
const render = async () => {
…
window.addEventListener("popstate", render);
};
// ✅ 개선
const init = async () => {
window.addEventListener("popstate", render);
filters.subscribe(render);
render();
};추가 요구사항 상황: 사용자 필터 적용이나 스크롤에 따라 렌더링을 자주 트리거하는 SPA에서 위와 같은 반복 등록은 디버깅을 매우 어렵게 만듭니다.
popstate와 같은 브라우저 전역 이벤트는 앱 초기화 단계에서 단 한 번만 등록하는 방식으로 설계해 두는 것이 이후 기능을 붙일 때 도움이 됩니다.
| }, | ||
| onUnhandledRequest: "bypass", | ||
| }), | ||
| ); |
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.
문제 상황
init()와 render()에서 await getProducts()를 호출하고 있으나 try/catch가 없어서 API 호출이 실패할 경우 아무 피드백도 제공되지 않습니다. 동시에 사용자는 다시 시도할 방법이 없습니다.
현재 코드의 한계
- [한계점 1] 네트워크 오류나 API 응답 실패가 발생하면 UI는 빈 화면 상태로 남고, 사용자는 무슨 일이 일어났는지 모릅니다.
- [한계점 2] 요구사항에 명시된 "로딩 실패 시 에러 상태 표시 및 재시도 버튼 제공"을 충족하지 못합니다.
- [한계점 3] 최신 제품 데이터가 자주 바뀌는 환경에서 일시적인 오류가 발생하면 포기하게 됩니다.
근본 원인
API 호출을 감싸는 에러 핸들링 로직이 없어서 실패 시 아무 처리도 하지 않습니다.
개선 구조
- [개선 사항 1]
getProducts()앞뒤에try/catch를 추가해 에러 상태를 UI로 표현하고,HomePage를 다시 렌더링할 수 있는retry버튼을 표시합니다. - [개선 사항 2] 에러 상태를 나타낼 수 있도록
HomePage에errorprop을 추가하고, 버튼 클릭 시 다시 데이터를 요청합니다. - [개선 사항 3] 성공/실패에 따라
isLoading/error상태를 적절히 조절합니다.
// ✅ 개선된 방식
const fetchProducts = async () => {
try {
return await getProducts(filters.getState());
} catch (error) {
return { error: "상품을 불러오는 중 오류가 발생했습니다." };
}
};
// 에러 UI 예시
${error ? `<div class="text-red-600">${error}</div><button id="retry-btn">다시 시도</button>` : ""}추가 요구사항: 예를 들어 API가 일시적으로 500을 반환하고 일정 시간이 지나야 다시 정상화될 수 있는 상황에서는, 사용자에게 에러 메시지와 함께
다시 시도버튼을 제공하면 서비스 신뢰도가 올라갑니다. 현재 구조라면 사용자가 F5를 누르거나 브라우저를 잠깐 닫지 않는 이상 에러를 넘기지 못합니다.
| // 카테고리 담기 | ||
| const newCategories = await getCategories(); | ||
| categories = newCategories; | ||
|
|
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.
문제 상황
getProducts()는 최초 로딩 한 번만 호출되고, 현재 페이지네이션/무한 스크롤 로직이 전혀 없습니다. 요구사항대로 "페이지 하단 근처 도달 시 다음 페이지 자동 로드" 기능이 빠져 있습니다.
현재 코드의 한계
- [한계점 1] 상품이 수백 개 이상일 때 한 번에 전부 가져오면 최초 로딩이 매우 느려집니다.
- [한계점 2] 스크롤을 내릴 때 다음 데이터를 추가로 가져오는 로직이 없어 사용자 경험이 끊깁니다.
- [한계점 3] 미래에
상품 목록 무한 스크롤을 추가하려 할 때render()가 기존 데이터를 덮어쓰기 때문에 쉽게 확장하기 어렵습니다.
근본 원인
getProducts()를 한 번만 호출하고, 다음 페이지를 로딩할 수 있는 상태 값을 유지하지 않기 때문입니다.
개선 구조
- [개선 사항 1]
filters나 별도의paginationstore에page와hasMore상태를 추가합니다. - [개선 사항 2] 스크롤 이벤트(
IntersectionObserver등)를 감지하여page + 1의 데이터를 API에서 가져온 뒤 기존products배열에 병합합니다. - [개선 사항 3] 로딩 중에는
isLoadingMore플래그를 켜서 추가 로딩 스켈레톤/인디케이터를 보여줍니다.
const pagination = { page: 1, hasMore: true };
const loadNextPage = async () => {
if (!pagination.hasMore || isLoadingMore) return;
isLoadingMore = true;
const next = await getProducts({ ...filters.getState(), page: pagination.page + 1 });
pagination.hasMore = next.products.length > 0;
pagination.page += 1;
products = [...products, ...next.products];
isLoadingMore = false;
};추가 요구사항: 향후 인기 상품을 순차적으로 노출하거나
관련 상품 더보기에서 무한 스크롤을 추가하려면 기본 목록도 페이지네이션 기반으로 만들어두는 것이 좋습니다. 지금처럼 한 번의 API 호출로 끝내면 새로운 스크롤 요구사항이 생길 때마다 구조를 대대적으로 바꿔야 합니다.
| </div> | ||
| <!-- 가격 및 삭제 --> | ||
| <div class="text-right ml-3"> | ||
| <p class="text-sm font-medium text-gray-900"> |
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.
문제 상황
장바구니 하단에 총 금액 670원과 같이 고정된 값을 보여주고 있습니다. 하지만 cartItems의 내용이 바뀌어도 값은 갱신되지 않으므로 실제 총합과 불일치합니다.
현재 코드의 한계
- [한계점 1] 상품 가격이나 수량이 변해도 총액이 고정되어 있어서 사용자가 잘못된 결제 정보를 받게 됩니다.
- [한계점 2] 향후 할인/세금/적립금 등과 같이 총액 계산이 복잡해질 경우 현재 구조로는 확장하기 어렵습니다.
- [한계점 3] 데이터 기반이 아닌 하드코딩이라서 체크리스트의 "총 금액 실시간 업데이트" 항목을 충족하지 못합니다.
근본 원인
총액을 계산하는 로직이 없고, 단순히 정적인 문자열을 출력하고 있기 때문입니다.
개선 구조
- [개선 사항 1]
cartItems.reduce(...)로 총액을 계산하고,toLocaleString으로 포맷해서 출력합니다. - [개선 사항 2] Cart 컴포넌트의
const totalPrice = ...;를 화면에 바인딩하여 아이템이 바뀔 때마다 재생성될 수 있게 합니다. - [개선 사항 3] 향후 세금, 배송비 등 추가 요소가 생길 수 있으니
getCartSummary()같은 유틸로 분리하면 재사용성이 올라갑니다.
const totalPrice = cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0);
…
<span class="text-xl font-bold text-blue-600">${totalPrice.toLocaleString()}원</span>추가 요구사항: 장바구니에 할인/쿠폰이 적용되는 시나리오가 온다면 총액 계산이 자주 바뀌기 때문에 지금처럼 하드코딩하면 수정 포인트가 여러 곳에 흩어져 유지보수가 어려워집니다. 계산 로직을 중앙에 두면 이런 확장도 자연스러워집니다.
| <!-- 상품 정보 --> | ||
| <div class="flex-1 min-w-0"> | ||
| <h4 | ||
| class="text-sm font-medium text-gray-900 truncate cursor-pointer cart-item-title" |
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.
문제 상황
수량 증가/감소 버튼은 DOM 상에만 있고 클릭 핸들러가 전혀 연결되어 있지 않습니다. 수량 변경 버튼을 눌러도 아무런 동작이 없습니다.
현재 코드의 한계
- [한계점 1] 요구사항 "장바구니 수량 조절"을 충족하지 못합니다.
- [한계점 2] 수량이 바뀌어도
localStorage에 저장된 상태가 일치하지 않아, 총액, 선택 상태에도 반영되지 않습니다. - [한계점 3] UI와 Business 로직이 분리되어 있지 않아, 이벤트를 어디서 처리해야 할지 코드 곳곳을 뒤져야 합니다.
근본 원인
수량 버튼 각각에 event.target.closest(...)로 로직을 연결할 핸들러가 없기 때문입니다.
개선 구조
- [개선 사항 1]
.quantity-decrease-btn/.quantity-increase-btn클릭을 감지하는 로직을eventHandlers()같은 전역 이벤트에서 처리합니다. - [개선 사항 2] 버튼 클릭 시
updateCartItemQuantity(productId, delta)유틸을 만들어localStorage를 갱신합니다. - [개선 사항 3] 재렌더링 시
renderCart()를 호출하여 새 수량과 총액을 반영합니다.
// ✅ 예시
if (event.target.closest(".quantity-increase-btn")) {
const id = event.target.closest(".quantity-increase-btn").dataset.productId;
updateCartItemQuantity(id, 1);
renderCart();
}추가 요구사항 예시: 차후에 수량 조절 시
max나pre-order제약을 추가해야 한다면, 지금처럼 이벤트가 붙어 있지 않으면 해당 로직을 붙이기가 매우 번거롭습니다. 처음부터 이벤트 처리를 한 곳에서 관리하는 구조를 갖춰두면 확장성도 확보됩니다.
| </div> | ||
| </div> | ||
| `; | ||
| const cartItems = getCartItemsFromLocalStorage(); |
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.
문제 상황
장바구니 모달을 닫는 유일한 방법이 버튼과 배경 클릭뿐인데, ESC 키 누름으로 닫힌다는 요구사항을 만족하지 못하고 있습니다.
현재 코드의 한계
- [한계점 1] 키보드 접근성이 떨어지고, 데스크톱에서
ESC키를 기대한 사용자 경험이 무시됩니다. - [한계점 2] 크롬 DevTools 등에서 ESC 키를 눌러도 모달이 계속 떠 있어 유저가 멈춘 듯한 느낌을 받습니다.
- [한계점 3] SPA 확장(예: 다중 모달) 시 ESC 처리를 전역에서 한 번만 하도록 구조화해 두는 것이 안전합니다.
근본 원인
키보드 이벤트 리스닝 부분이 없고, ESC 키 시퀀스를 처리하는 로직이 전혀 존재하지 않습니다.
개선 구조
- [개선 사항 1]
document.addEventListener("keydown", (event) => { if (event.key === "Escape") closeAllModals(); });형태로 전역 등록합니다. - [개선 사항 2]
closeAllModals()는 현재 열려 있는.cart-modal에hidden클래스를 붙이는 함수여야 합니다. - [개선 사항 3] 이러한 키보드 액세스는
init()에서 한 번만 등록하고, 복수 모달에도 동일한 함수로 재사용할 수 있게 합니다.
// ✅ 개선
const handleKeydown = (event) => {
if (event.key === "Escape") {
const modal = document.querySelector(".cart-modal");
modal?.classList.add("hidden");
}
};
document.addEventListener("keydown", handleKeydown);추가 요구사항 예시: 향후 알림 모달/설정 모달 등 여러 개가 생기면 ESC 키로 종합적으로 닫을 수 있는 구조가 필요합니다. 지금처럼 DOM 요소만 보고 닫는 구조면 키보드 이벤트를 추가할 때마다 여러 곳에 붙여야 해서 실수가 생기기 쉽습니다.
| const render = async () => { | ||
| const basePath = import.meta.env.BASE_URL; // vite 제공 | ||
| const pathName = window.location.pathname; | ||
| const relativePath = pathName.replace(basePath, "/").replace(/\/$/, "") || "/"; |
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.
문제 상황
filters 상태는 내부에서만 관리하고 URL에는 반영되지 않습니다. 고급 요구사항 중 "URL 쿼리로 검색/필터 상태를 유지"하는 항목을 만족시키지 못하고 있습니다.
현재 코드의 한계
- [한계점 1] 필터를 바꾼 후 페이지를 새로고침하면 아무 조건도 적용되지 않습니다.
- [한계점 2] 다른 사람에게 공유할 수 있는 링크가 없어 사용성이 떨어집니다.
- [한계점 3] 브라우저 히스토리에도 필터 상태가 남지 않아서 뒤로 가기/앞으로 가기 시 상태 복원이 어렵습니다.
근본 원인
filters 변화를 URL에 반영하는 history.pushState나 URLSearchParams 로직이 없습니다.
개선 구조
- [개선 사항 1]
filters.setState()가 호출될 때마다const params = new URLSearchParams(filters.getState()); history.replaceState(null, "",/?${params});처럼 URL을 업데이트합니다. - [개선 사항 2] 앱 초기화 시
new URLSearchParams(location.search)에서 값을 읽어와filters.setState(parsed)로 복원합니다. - [개선 사항 3]
render()시작 시에도 URL을 기반으로 한 데이터를 요청하도록 수정하여 새로고침 후에도 같은 결과가 나오게 합니다.
// ✅ 개선 예시
const syncFiltersWithUrl = () => {
const params = new URLSearchParams(filters.getState());
history.replaceState(null, "", `/?${params.toString()}`);
};
filters.subscribe((state) => {
syncFiltersWithUrl();
render();
});추가 요구사항: SPA 네비게이션을 자연스럽게 만들기 위해선 URL에 상태를 담는 것이 중요합니다. 예를 들어
?category=생활용품&sort=price_desc링크를 보내면 다른 사람이 똑같은 상태로 들어올 수 있도록 만들면 공유와 테스트가 훨씬 쉬워집니다.
과제 체크포인트
배포 링크
https://seunghoonkang.github.io/front_7th_chapter2-1/
기본과제
상품목록
상품 목록 로딩
상품 목록 조회
한 페이지에 보여질 상품 수 선택
상품 정렬 기능
무한 스크롤 페이지네이션
상품을 장바구니에 담기
상품 검색
카테고리 선택
카테고리 네비게이션
현재 상품 수 표시
장바구니
장바구니 모달
장바구니 수량 조절
장바구니 삭제
장바구니 선택 삭제
장바구니 전체 선택
장바구니 비우기
상품 상세
상품 클릭시 상세 페이지 이동
/product/{productId}형태로 변경된다상품 상세 페이지 기능
상품 상세 - 장바구니 담기
관련 상품 기능
상품 상세 페이지 내 네비게이션
사용자 피드백 시스템
토스트 메시지
심화과제
SPA 네비게이션 및 URL 관리
페이지 이동
상품 목록 - URL 쿼리 반영
상품 목록 - 새로고침 시 상태 유지
장바구니 - 새로고침 시 데이터 유지
상품 상세 - URL에 ID 반영
/product/{productId})상품 상세 - 새로고침시 유지
404 페이지
AI로 한 번 더 구현하기
과제 셀프회고
기술적 성장
자랑하고 싶은 코드
개선이 필요하다고 생각하는 코드
위 함수를 init과 render에 전달해서 그려주게 되는데, 이 방식이 앞서 필터를 구현하는 방식과는 다른점이 마음에 걸렸습니다.
필터는 옵저버로 변경을 인지하면 렌더를 알리고, 렌더가 동작하게 하는데, 이 때 렌더안에서 필터에 따른 API를 호출하고 다시 그려지는 형태가 조금 더 SPA를 구현하는 느낌이라고 생각했습니다.
학습 효과 분석
준일 코치님이 이번 과제에서 키 라고 말해준 부분이 3가지로 기억하고 있습니다.
였는데, 사실 상태 관리 말고는 (이것도 온전치는 않은것같고) 나머지 2개는 제대로 신경쓰지 못했습니다.
요 3 가지 키워드를 인지하고 추후 혼자서라도 과젤 디벨롭 해 볼 생각입니다 ! 🔥🔥🔥
과제 피드백
AI 활용 경험 공유하기
리뷰 받고 싶은 내용