Skip to content

Commit 772dffd

Browse files
committed
fix: 무한 스크롤 수정
1 parent e123280 commit 772dffd

File tree

4 files changed

+153
-36
lines changed

4 files changed

+153
-36
lines changed

src/core/useRender.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import Layout from "../components/Layout";
22
import routes from "../routes";
33
import { getAppPath } from "../main";
44

5+
let currentPage = null;
6+
57
const useRender = () => {
68
const init = () => {
79
document.querySelector("#root").innerHTML = Layout();
@@ -12,10 +14,16 @@ const useRender = () => {
1214
};
1315

1416
const view = async () => {
17+
// 이전 페이지의 unmount 호출
18+
if (currentPage && currentPage.unmount) {
19+
currentPage.unmount();
20+
}
21+
1522
for (const route of routes) {
1623
const match = getAppPath().match(route.path);
1724
if (!match) continue;
1825
const Page = route.component;
26+
currentPage = Page; // 현재 페이지 저장
1927
Page.init?.(match?.[1]);
2028
draw("main", Page({}));
2129
await Page.mount?.();

src/pages/Home.js

Lines changed: 93 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const state = {
1212
products: [],
1313
pagination: {},
1414
categories: {},
15+
watchRegistered: false,
1516
};
1617

1718
const fetchProducts = async (params = {}) => {
@@ -23,14 +24,17 @@ const fetchProducts = async (params = {}) => {
2324
}
2425

2526
const productData = await getProducts(params);
26-
state.isLoading = false;
2727

2828
// 무한 스크롤 방식 구현으로 누적된 product 값
2929
if (params.page && params.page > 1) {
3030
state.products = [...state.products, ...productData.products];
31-
state.isLoadingMore = false;
31+
// 로딩을 조금 늦게 false로 설정
32+
setTimeout(() => {
33+
state.isLoadingMore = false;
34+
}, 500);
3235
} else {
3336
state.products = productData.products;
37+
state.isLoading = false;
3438
}
3539

3640
state.pagination = productData.pagination;
@@ -42,32 +46,48 @@ const fetchCategories = async () => {
4246
state.isLoading = false;
4347
};
4448

49+
let scrollHandler = null;
50+
let scrollTimeout = null;
51+
4552
const fetchMoreProductsScroll = () => {
4653
const location = getAppPath();
4754
if (location !== "/") return;
4855
const triggerHeight = 100;
49-
let scrollHandler = null;
5056

5157
const handleScroll = () => {
5258
const location = getAppPath();
5359
// 메인페이지에서만 가능하도록 처리
5460
if (location !== "/") return;
55-
if (state.isLoadingMore || !state.pagination?.hasNext) return;
61+
// 로딩 중이거나 다음 페이지가 없으면 스크롤 감지 안함
62+
if (state.isLoadingMore || state.isLoading || !state.pagination?.hasNext) return;
63+
5664
const currentScroll = window.scrollY;
5765
const viewHeight = document.documentElement.clientHeight;
5866
const bodyHeight = document.body.scrollHeight;
67+
5968
if (currentScroll + viewHeight > bodyHeight - triggerHeight) {
60-
state.isLoadingMore = true;
61-
const currentPage = store.get("params")["page"];
62-
store.set("params", {
63-
...store.get("params"),
64-
page: currentPage + 1,
65-
});
69+
if (state.isLoadingMore) return;
70+
71+
if (scrollTimeout) {
72+
clearTimeout(scrollTimeout);
73+
}
74+
75+
scrollTimeout = setTimeout(() => {
76+
if (state.isLoadingMore || state.isLoading) return;
77+
78+
state.isLoadingMore = true;
79+
const currentPage = store.get("params")["page"] || 1;
80+
store.set("params", {
81+
...store.get("params"),
82+
page: currentPage + 1,
83+
});
84+
}, 300);
6685
}
6786
};
6887

88+
// 기존 스크롤 핸들러 제거
6989
if (scrollHandler) {
70-
window.removeEventListener("scroll", handleScroll);
90+
window.removeEventListener("scroll", scrollHandler);
7191
}
7292

7393
scrollHandler = handleScroll;
@@ -91,6 +111,7 @@ const renderHome = () => {
91111

92112
Home.init = () => {
93113
state.isLoading = true;
114+
state.watchRegistered = false;
94115
};
95116

96117
Home.mount = async () => {
@@ -112,38 +133,74 @@ Home.mount = async () => {
112133

113134
fetchMoreProductsScroll();
114135

115-
store.watch(async (newValue) => {
116-
const url = new URL(window.location);
117-
Object.entries(newValue).forEach(([key, value]) => {
118-
if (value !== "" && value) {
119-
url.searchParams.set(key, value);
120-
} else {
121-
url.searchParams.delete(key);
136+
// store.watch 중복 등록 방지
137+
if (!state.watchRegistered) {
138+
state.watchRegistered = true;
139+
store.watch(async (newValue) => {
140+
console.log("test");
141+
// 무한 스크롤로 인한 page 변경은 별도 처리
142+
if (state.isLoadingMore && newValue.page) {
143+
await fetchProducts(newValue);
144+
state.isLoadingMore = false;
145+
146+
render.draw(
147+
"#product-list",
148+
ProductList({
149+
products: state.products,
150+
pagination: state.pagination,
151+
}),
152+
);
153+
ProductList.mount(state.products);
154+
ProductCard.mount();
155+
return;
122156
}
123-
});
124-
window.history.pushState({}, "", url.toString());
125157

126-
await fetchProducts(newValue);
127-
state.isLoadingMore = false;
158+
const url = new URL(window.location);
159+
Object.entries(newValue).forEach(([key, value]) => {
160+
if (value !== "" && value) {
161+
url.searchParams.set(key, value);
162+
} else {
163+
url.searchParams.delete(key);
164+
}
165+
});
166+
window.history.pushState({}, "", url.toString());
167+
168+
await fetchProducts(newValue);
169+
state.isLoadingMore = false;
170+
171+
render.draw("#search-container", Search(store.get("categories"), false));
172+
Search.mount();
128173

129-
render.draw("#search-container", Search(store.get("categories"), false));
130-
Search.mount();
174+
render.draw("#breadcrumb-container", Breadcrumb());
175+
Breadcrumb.mount();
131176

132-
render.draw("#breadcrumb-container", Breadcrumb());
133-
Breadcrumb.mount();
177+
render.draw(
178+
"#product-list",
179+
ProductList({
180+
products: state.products,
181+
pagination: state.pagination,
182+
}),
183+
);
134184

135-
render.draw(
136-
"#product-list",
137-
ProductList({
138-
products: state.products,
139-
pagination: state.pagination,
140-
}),
141-
);
185+
ProductList.mount(state.products);
142186

143-
ProductList.mount(state.products);
187+
ProductCard.mount();
188+
}, "params");
189+
}
190+
};
144191

145-
ProductCard.mount();
146-
}, "params");
192+
Home.unmount = () => {
193+
// 스크롤 이벤트 리스너 정리
194+
if (scrollHandler) {
195+
window.removeEventListener("scroll", scrollHandler);
196+
scrollHandler = null;
197+
}
198+
199+
// 타이머 정리
200+
if (scrollTimeout) {
201+
clearTimeout(scrollTimeout);
202+
scrollTimeout = null;
203+
}
147204
};
148205

149206
export default function Home({ products, pagination, isLoading, categories, isLoadingMore }) {

src/pages/NotFound.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ NotFound.mount = () => {
88
});
99
};
1010

11+
NotFound.unmount = () => {
12+
const homeLink = document.querySelector("#notFound [data-link]");
13+
if (homeLink) {
14+
homeLink.removeEventListener("click", (event) => {
15+
event.preventDefault();
16+
navigate.push({}, "/");
17+
});
18+
}
19+
};
20+
1121
export default function NotFound() {
1222
return /* html */ `
1323
<div class="text-center my-4 py-20 shadow-md p-6 bg-white rounded-lg" id="notFound">

src/pages/Product.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,48 @@ Product.mount = async () => {
112112
// });
113113
};
114114

115+
Product.unmount = () => {
116+
// 이벤트 리스너들 정리
117+
const goToProductListBtn = document.querySelector(".go-to-product-list");
118+
const homeLink = document.querySelector("a");
119+
const quantityInput = document.querySelector("#quantity-input");
120+
const quantityIncrease = document.querySelector("#quantity-increase");
121+
const quantityDecrease = document.querySelector("#quantity-decrease");
122+
const cartBtn = document.getElementById("add-to-cart-btn");
123+
const relatedProductList = document.querySelectorAll(".related-product-card");
124+
125+
if (goToProductListBtn) {
126+
goToProductListBtn.removeEventListener("click", methods.goToProductList);
127+
}
128+
if (homeLink) {
129+
homeLink.removeEventListener("click", methods.goToProductList);
130+
}
131+
if (quantityInput) {
132+
quantityInput.removeEventListener("change", (event) => {
133+
if (event.target.value < 1) event.target.value = 1;
134+
});
135+
}
136+
if (quantityIncrease) {
137+
quantityIncrease.removeEventListener("click", methods.handleUpQuantity);
138+
}
139+
if (quantityDecrease) {
140+
quantityDecrease.removeEventListener("click", methods.handleDownQuantity);
141+
}
142+
if (cartBtn) {
143+
cartBtn.removeEventListener("click", () => {
144+
methods.handleAddCard(quantityInput.value);
145+
});
146+
}
147+
if (relatedProductList.length > 0) {
148+
relatedProductList.forEach((product) => {
149+
const productId = product.getAttribute("data-product-id");
150+
product.removeEventListener("click", () => {
151+
methods.goToRelatedProducts(productId);
152+
});
153+
});
154+
}
155+
};
156+
115157
export default function Product() {
116158
return /* html */ `
117159
${

0 commit comments

Comments
 (0)