💡 오늘 할 것!
- 서버 pagination
- 무한스크롤 적용
- 어드민 페이지 - api 작성
1️⃣ 서버 페이지네이션
1. server/resolvers/products.ts
- cursor="" 추가하기
const productResolver: Resolver = {
Query: {
products: (parent, { cursor = "" }, { db }) => {
const fromIndex =
db.products.findIndex((product) => product.id === cursor) + 1;
return db.products.slice(fromIndex, fromIndex + 15) || [];
},
...
},
},
};
export default productResolver;
📚 cursor가 빈 값이면
-> product.id === cursor 값이 없으므로 -1 반환 (findIndex)
📚 cursor가 15라면
-> product.id === 15인 데이터 반환 - fromIndex에는 15 저장
2. server/src/schema/product.ts
- cursor 생성하기
extend type Query {
products(cursor: ID): [Product!]
product(id: ID!): Product!
}
3. Apollographql로 확인
- cursor값이 null일 때
2️⃣ 무한스크롤 적용
- react-query의 useInfiniteQuery
- Interaction Observer
1. client/src/pages/products/index.tsx
import { useInfiniteQuery, useQuery } from "react-query";
import { QueryKeys, graphqlFetcher } from "../../queryClient";
import GET_PRODUCTS, { Products } from "../../graphql/products";
import ProductList from "../../assets/components/product/list";
import { useCallback, useEffect, useRef, useState } from "react";
import useInfiniteScroll from "../../assets/hooks/useInfiniteScroll";
const ProductsListPage = () => {
// 무한 스크롤을 위해
const fetchMoreRef = useRef<HTMLDivElement>(null);
const intersecting = useInfiniteScroll(fetchMoreRef);
// 데이터 가져오기, type 정의
const { data, isSuccess, isFetchingNextPage, fetchNextPage, hasNextPage } =
useInfiniteQuery<Products>(
QueryKeys.PRODUCTS,
({ pageParam = "" }) =>
graphqlFetcher<Products>(GET_PRODUCTS, { cursor: pageParam }),
{
getNextPageParam: (lastPage, allPages) => {
return lastPage.products.at(-1)?.id;
},
}
);
useEffect(() => {
if (!intersecting || !isSuccess || !hasNextPage || isFetchingNextPage)
return;
fetchNextPage();
}, [intersecting]);
return (
<div>
<h2>상품목록</h2>
<ProductList list={data?.pages || []} />
<div ref={fetchMoreRef} />
</div>
);
};
export default ProductsListPage;
- getNextPageParam : 함수 실행 시 return 값으로 페이지 마지막 상품 id값 전달
- pageParam = " " : return 받은 상품 id값이 cursor에 전달
📚 이렇게 하게 되면 데이터 들어오는 방식이 바뀜!
data : {
pages : [
{products : [...]},
{products : [...]}
],
pageParams : [undefined, ...]
}
방법1. map => map
방법2. data.pages.flatMap(page => page.products) => [{},{},{}]
2. 방법1을 택하여 client/src/assets/components/product/list.tsx 를 구현
import { Product } from "../../../graphql/products";
import ProductItem from "./item";
const ProductList = ({ list }: { list: { products: Product[] }[] }) => {
return (
<ul className="products">
{list.map((page) =>
page.products.map((product) => (
<ProductItem {...product} key={product.id} />
))
)}
</ul>
);
};
export default ProductList;
3. 페이지 끝에 도달했는지 확인하기 위하여 Intersection observer 사용하기
- isFetchNextPage -> true, false에 따라 앞에서 생성한 fetchNexPage 실행여부 판단
useEffect(() => {
if (!intersecting || !isSuccess || !hasNextPage || isFetchingNextPage)
return;
fetchNextPage();
}, [intersecting]);
- 페이지 끝에 도달했을 때 ref를 useInfiniteScroll Hook에 전달
<div ref={fetchMoreRef} />
4. client/ src/assets/hooks/useInfiniteScroll.ts
import { RefObject, useCallback, useEffect, useRef, useState } from "react";
const useInfiniteScroll = (targetRef: RefObject<HTMLElement>) => {
const observerRef = useRef<IntersectionObserver>();
const [intersecting, setIntersecting] = useState(false);
const getObserver = useCallback(() => {
if (!observerRef.current) {
observerRef.current = new IntersectionObserver((entries) => {
setIntersecting(entries[0]?.isIntersecting);
});
}
return observerRef.current;
}, [observerRef.current]);
useEffect(() => {
if (targetRef.current) getObserver().observe(targetRef.current);
}, [targetRef.current]);
return intersecting;
};
export default useInfiniteScroll;
5. 크롬 component 확장자로 확인
- 스크롤을 맨 밑으로 내렸을 때 state : true로 잘 뜬다!
3️⃣ 어드민 - api 작성
1. client/src/pages/admin/index.tsx 파일 생성
2. server/src/resolvers/product.ts에 Mutation 추가
Mutation: {
addProduct: (parent, { imageUrl, price, title, description }, { db }) => {
const newProduct = {
id: uuid(),
imageUrl,
price,
title,
description,
createdAt: Date.now(),
};
db.products.push(newProduct);
setJSON(db.products);
return newProduct;
},
updateProduct: (parent, { id, ...data }, { db }) => {
const existProductIndex = db.products.findIndex((item) => item.id === id);
if (existProductIndex < 0) {
throw new Error("없는 상품입니다");
}
const updatedItem = {
...db.products[existProductIndex],
...data,
};
db.products.splice(existProductIndex, 1, updatedItem);
setJSON(db.products);
return updatedItem;
},
deleteProduct: (parent, { id }, { db }) => {
// 실제 db에서 delete를 하는 대신, createdAt을 지워준다.
const existProductIndex = db.products.findIndex((item) => item.id === id);
if (existProductIndex < 0) {
throw new Error("없는 상품입니다");
}
const updatedItem = {
...db.products[existProductIndex],
};
delete updatedItem.createdAt;
db.products.splice(existProductIndex, 1, updatedItem);
setJSON(db.products);
return id;
},
},
3. server/src/schema/product.ts에서 Mutation 추가
extend type Mutation {
addProduct(
imageUrl: String
price: Int!
title: String!
description: String!
): Product!
updateProduct(
id: ID!
imageUrl: String
price: Int
title: String
description: String
): Product!
deleteProduct(id: ID!): ID!
}
4. Apollo Server 확인
오늘은 큰 이슈 없이 잘 해결되었다 ㅎㅎ
로그인 기능까지 넣으면 더할 나위 없겠지만,, 나중에 개인프로젝트를 진행할 때 넣는 걸로,, ㅎㅎ
'REACT > 쇼핑몰 프로젝트' 카테고리의 다른 글
[REACT] React + Typescript + Vite + SCSS로 만드는 쇼핑몰 개인프로젝트 (8/10) (1) | 2024.01.27 |
---|---|
[REACT] React + Typescript + Vite + SCSS로 만드는 쇼핑몰 개인프로젝트 (6/10) (0) | 2024.01.23 |
[REACT] React + Typescript + Vite + SCSS로 만드는 쇼핑몰 개인프로젝트 (5/10) (0) | 2024.01.22 |
[REACT] React + Typescript + Vite + SCSS로 만드는 쇼핑몰 개인프로젝트 (4/10) (0) | 2024.01.22 |
[REACT] React + Typescript + Vite + SCSS로 만드는 쇼핑몰 개인프로젝트 (3/10) (0) | 2024.01.18 |