๐Ÿ’šํ˜ธ๋‘ ์˜คํ”ˆ๋งˆ์ผ“

๐Ÿ“Œ 2022.09 - 2022.10
๐Ÿ“Œ ํ˜ธ๋‘๋งˆ์ผ“ ๋ฐฐํฌ URL : https://hodumarket.netlify.app/

๐Ÿ“„๊ฐœ์š”

ํ˜ธ๋‘๋งˆ์ผ“์€ ๋ˆ„๊ตฌ๋‚˜ ์ž์œ ๋กญ๊ฒŒ ์ƒํ’ˆ์„ ๊ฒŒ์‹œํ•˜์—ฌ ํŒ๋งคํ•˜๊ณ  ๊ตฌ๋งคํ•  ์ˆ˜ ์žˆ๋Š” ์˜คํ”ˆ๋งˆ์ผ“ ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค.

ํšŒ์›์€ ํŒ๋งค์ž/๊ตฌ๋งค์ž ์œ ํ˜•์œผ๋กœ ๋‚˜๋‰˜๋ฉฐ ํŒ๋งค์ž๋Š” ์ƒํ’ˆ ์ •๋ณด๋ฅผ ๊ฒŒ์‹œ,์ˆ˜์ •,์‚ญ์ œ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ตฌ๋งค์ž๋Š” ์›ํ•˜๋Š” ์ƒํ’ˆ์„ ์›ํ•˜๋Š” ์ˆ˜๋Ÿ‰๋งŒํผ ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ๋‹ด๊ฑฐ๋‚˜ ๋ฐ”๋กœ ๊ตฌ๋งค๋ฅผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


โš™๊ธฐ์ˆ  ๋ฐ ๊ฐœ๋ฐœํ™˜๊ฒฝ

[๊ธฐ์ˆ ]


๐Ÿ“Œ BackEnd : ์ œ๊ณต๋œ API ์‚ฌ์šฉ
๐Ÿ“Œ Daum Postcode Service API ์‚ฌ์šฉ
๐Ÿ“Œ Version :
react : "18.2.0"
react-router-dom : "6.3.0"
axios: "0.27.2",
react-daum-postcode: "3.1.1"
react-intersection-observer: "9.4.0"
react-query: "3.39.2"
tailwindcss: "3.1.8"
vite: "3.0.7"

[๊ฐœ๋ฐœํ™˜๊ฒฝ]



๐ŸŽจ๊ตฌํ˜„ ๊ธฐ๋Šฅ

  • ๐Ÿ” ๊ณ„์ •

    • ๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ
    • ๊ตฌ๋งค์ž / ํŒ๋งค์ž ํšŒ์›๊ฐ€์ž…
    • ์œ ํšจ์„ฑ ๊ฒ€์ฆ
    • ํ† ํฐ ๊ฒ€์ฆ
  • ๐Ÿ  ํ™ˆ

    • ์ƒํ’ˆ ๊ฒ€์ƒ‰
    • ์ƒํ’ˆ ๋ชฉ๋ก
    • ๋ฌดํ•œ ์Šคํฌ๋กค
  • ๐ŸŽ ์ƒํ’ˆ

    • ์ƒํ’ˆ ์ƒ์„ธ ํŽ˜์ด์ง€
    • ์ƒํ’ˆ ์ˆ˜๋Ÿ‰ ์„ ํƒ
    • ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ƒํ’ˆ ๋‹ด๊ธฐ
    • ์ƒํ’ˆ ์ฃผ๋ฌธ ๋ฐ ๊ฒฐ์ œ
    • ์ƒํ’ˆ ์žฌ๊ณ  ์œ ํšจ ๊ฒ€์‚ฌ
  • ๐Ÿ›’ ์žฅ๋ฐ”๊ตฌ๋‹ˆ

    • ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ๋‹ด๊ธด ์ƒํ’ˆ ๋ชฉ๋ก ํ™•์ธ
    • ์ƒํ’ˆ ์ˆ˜๋Ÿ‰ ์ˆ˜์ • ๋ฐ ์ƒํ’ˆ ์‚ญ์ œ
    • ์ƒํ’ˆ ๊ฐœ๋ณ„ / ์ „์ฒด ์„ ํƒ
    • ์„ ํƒ๋œ ์ƒํ’ˆ์˜ ์ด ํ• ์ธ / ๋ฐฐ์†ก๋น„ / ์ƒํ’ˆ๊ฐ€๊ฒฉ ํ™•์ธ
    • ์„ ํƒ๋œ ์ƒํ’ˆ ํ˜น์€ ๊ฐœ๋ณ„ ์ฃผ๋ฌธ
  • ๐Ÿ‘จโ€๐ŸŒพ ํŒ๋งค์ž ์„ผํ„ฐ

    • ์ƒํ’ˆ ๋“ฑ๋ก ๋ฐ ์‚ญ์ œ
    • ๋“ฑ๋ก๋œ ์ƒํ’ˆ ์ˆ˜์ •
  • ETC

    • ๋ชจ๋ฐ”์ผ ์œ ์ €๋ฅผ ์œ„ํ•œ ๋ฐ˜์‘ํ˜• ๋””์ž์ธ ๊ตฌํ˜„
    • api ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์ค‘ ๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ ํ™”๋ฉด ๊ตฌํ˜„


โœจ์ฝ”๋“œ ํฌ์ธํŠธ

โœ” react-query useQueries๋กœ promise all ๊ตฌํ˜„

const { data, status } = useQuery(["cart-list", token], getCartList, {
cacheTime: 1000000,
onSuccess: (data) => {
setIsAllChecked(true);
let checkObj = data.reduce((newObj, idx) => {
newObj["check" + data.indexOf(idx)] = true;
return newObj;
}, {});
setCheckList(checkObj);
},
});
const listDetails = useQueries(
!!data
? data.map((item) => {
return {
queryKey: ["info", item.product_id],
queryFn: () => getDetails(item.product_id),
cacheTime: 1000000,
};
})
: []
);
/**์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ */
async function getCartList() {
const res = await axios.get(url + "cart/", {
headers: { Authorization: `JWT ${token}` },
});
return res.data.results;
}
/**์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ฆฌ์ŠคํŠธ์—์„œ ๋ฝ‘์€ product_id๋กœ ์ƒํ’ˆ ์ƒ์„ธ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ */
async function getDetails(id) {
const res = await axios.get(url + "products/" + id + "/");
return res.data;
}
const loadingFinishAll = listDetails.every((item) => item.isSuccess);

๊ตฌ๋งค์ž๊ฐ€ ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ๋‹ด์€ ์ƒํ’ˆ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜จ ํ›„,
์ƒํ’ˆ ์ด๋ฆ„, ์ƒํ’ˆ ๊ฐ€๊ฒฉ, ๋ฐฐ์†ก๋น„, ํŒ๋งค์ž ์ด๋ฆ„, ์ด๋ฏธ์ง€ ์ •๋ณด๋ฅผ ๋ฐ›์•„์˜ค๊ธฐ ์œ„ํ•ด ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ƒํ’ˆ๋ชฉ๋ก์˜ ์ƒํ’ˆ id๋กœ ๋‹ค์‹œ ํ•œ๋ฒˆ ์ƒํ’ˆ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์™”์Šต๋‹ˆ๋‹ค.
์ด ๋•Œ ์•„๋ž˜์™€ ๊ฐ™์ด promise all์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  async function getCartList() {
    try {
      const res = await axios.get(url + "cart/", {
        headers: { Authorization: `JWT ${token}` }
      });
      const items = res.data.results.map((item, idx) =>
        axios.get(url + "products/" + item.product_id + "/")
      );
      const itemsArr = await Promise.all(items);
    } catch (err) {
      console.errir(err);
    }
  }

์ด์ฒ˜๋Ÿผ Promise.all์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด react-query์˜ useQueries ํ›…์„ ๋™์ ์œผ๋กœ ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.


โœ” prefetch๋กœ cached๋œ data ์‚ฌ์šฉ์œผ๋กœ ์„ฑ๋Šฅ ํ–ฅ์ƒ

async function getProduct(pageParam) {
const res = await axios.get(url + "products/?page=" + pageParam);
const result = res.data;
setDataLength(Math.ceil(result.count / 15));
return {
result: result.results,
nextPage: pageParam + 1,
isLast: !res.data.next,
};
}
useEffect(() => {
if (inView && hasNextPage) {
fetchNextPage();
}
}, [inView, hasNextPage]);
/**๋ชจ๋“  ์ƒํ’ˆ ๋ชฉ๋ก prefetch */
useEffect(() => {
for (let i = 1; i < dataLength + 1; i++) {
queryClient.prefetchQuery(["allItems", `Arr${i}`], () => getAllItems(i), {
staleTime: Infinity,
cacheTime: 86000000,
});
}
}, [dataLength]);
async function getAllItems(i) {
const res = await axios.get(url + "products/?page=" + i);
return res.data.results;
}

ํ™ˆํŽ˜์ด์ง€์—์„œ first page ์ƒํ’ˆ api ๋ฐ์ดํ„ฐ์—์„œ ์ƒํ’ˆ์˜ ์ด ๊ฐœ์ˆ˜๋ฅผ ํ•œ๋ฒˆ์— ๋ถˆ๋Ÿฌ์™€์ง€๋Š” ์ƒํ’ˆ๋ชฉ๋ก ๊ฐœ์ˆ˜(15)๋กœ ๋‚˜๋ˆ„๊ณ  ์˜ฌ๋ฆผํ•ด์ค๋‹ˆ๋‹ค. ์ด๋ฅผ dataLength์— ํ• ๋‹นํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  dataLength ๋งŒํผ for ๋ฐ˜๋ณต๋ฌธ์œผ๋กœ queryClient.prefetchQueryํ›…์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. prefetch๋œ ๋ชจ๋“  ์ƒํ’ˆ์˜ ๋ฐ์ดํ„ฐ๋Š” cached๋˜์–ด ์ƒํ’ˆ ๊ฒ€์ƒ‰์‹œ์— ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.


โœ” ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์ƒํ’ˆ ์ถ”๊ฐ€ ์‹œ cache๋œ data์‚ฌ์šฉ ํ•˜์—ฌ ์„ฑ๋Šฅํ–ฅ์ƒ

const cartData = queryClient.getQueryData(["cart-list", token]);
const addToCart = useMutation(clickAddToCart, {
onSuccess: (res) => {
setIsAddCartModalOpen(true);
setIsItemExist(
data.some((item) => item.cart_item_id === res.data.cart_item_id)
);
queryClient.invalidateQueries("cart-list", "info");
},
onError: (error) => {
console.error(error);
},
});
/**์žฅ๋ฐ”๊ตฌ๋‹ˆ ํŽ˜์ด์ง€๋ฅผ ๋ฐฉ๋ฌธํ•œ ์ ์ด ์žˆ์œผ๋ฉด data fetch X */
const { data, status } = useQuery(["cart-list", token], getCartList, {
enabled: !cartData,
});
async function getCartList() {
if (userType === "BUYER") {
const res = await axios.get(url + "cart/", {
headers: { Authorization: `JWT ${token}` },
});
return res.data.results;
}
}

๊ตฌ๋งค์ž๊ฐ€ ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์ƒํ’ˆ ์ถ”๊ฐ€ ์‹œ, ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ํ•ด๋‹น ์ƒํ’ˆ์ด ๊ธฐ์กด์— ์กด์žฌํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ์ž ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ต๋‹ˆ๋‹ค. ์ด ๋•Œ, ์‚ฌ์šฉ์ž๊ฐ€ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ํŽ˜์ด์ง€๋ฅผ ์ด๋ฏธ ๋ฐฉ๋ฌธํ•œ ์ ์ด ์žˆ์œผ๋ฉด data๋ฅผ fetch ํ•˜์ง€ ์•Š๊ณ , useQueryClientํ›…์„ ์‚ฌ์šฉํ•˜์—ฌ getQueryData๋กœ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ํŽ˜์ด์ง€์—์„œ cache๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ต๋‹ˆ๋‹ค.


โœ” ๋ฌดํ•œ ์Šคํฌ๋กค ๊ตฌํ˜„

const [ref, inView] = useInView();
const { data, status, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery("products", ({ pageParam = 1 }) => getProduct(pageParam), {
getNextPageParam: (lastPage, allPages) => {
if (!lastPage.isLast) {
return lastPage.nextPage;
} else {
return undefined;
}
},
});
async function getProduct(pageParam) {
const res = await axios.get(url + "products/?page=" + pageParam);
const result = res.data;
setDataLength(Math.ceil(result.count / 15));
return {
result: result.results,
nextPage: pageParam + 1,
isLast: !res.data.next,
};
}
useEffect(() => {
if (inView && hasNextPage) {
fetchNextPage();
}
}, [inView, hasNextPage]);

react-query์˜ useInfiniteQuery์™€ react-intersection-observer api๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฌดํ•œ ์Šคํฌ๋กค ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
useInView์˜ 'ref'๋Š” ๊ฐ ํŽ˜์ด์ง€ ๋งˆ๋‹ค ๋งˆ์ง€๋ง‰ ์ƒํ’ˆ(15๋ฒˆ์งธ)์— ์ง€์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค.


โœ” ๋‹ค์Œ ์šฐํŽธ๋ฒˆํ˜ธ API ์„œ๋น„์Šค๋กœ ์šฐํŽธ๋ฒˆํ˜ธ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ๊ตฌํ˜„

import ModalPortal from "./ModalPortal";
import DaumPostCode from "react-daum-postcode";
function PostCodeModal({ open, close, onComplete }) {
const themeObj = {
bgColor: "#F9F9F9", //๋ฐ”ํƒ• ๋ฐฐ๊ฒฝ์ƒ‰
pageBgColor: "#FFFFFF", //ํŽ˜์ด์ง€ ๋ฐฐ๊ฒฝ์ƒ‰
postcodeTextColor: "#21BF48", //์šฐํŽธ๋ฒˆํ˜ธ ๊ธ€์ž์ƒ‰
emphTextColor: "#EB5757", //๊ฐ•์กฐ ๊ธ€์ž์ƒ‰
outlineColor: "#21BF48", //ํ…Œ๋‘๋ฆฌ
};
const postCodeStyle = {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "450px",
height: "470px",
};
return (
<>
{open ? (
<>
<ModalPortal close={close}>
<aside className="flex w-[200px] h-[300px] borer-[1px] border-pink-500">
<DaumPostCode
onComplete={onComplete}
theme={themeObj}
style={postCodeStyle}
/>
</aside>
</ModalPortal>
</>
) : null}
</>
);
}
export default PostCodeModal;

/**์šฐํŽธ๋ฒˆํ˜ธ ์กฐํšŒ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ๋ชจ๋‹ฌ ์ฐฝ ๋„์šฐ๊ธฐ */
function openPostCodeModal() {
setIsModalOpen(true);
}
/**์šฐํŽธ๋ฒˆํ˜ธ ๊ฒ€์ƒ‰ ํ›„ ์„ ํƒ ์™„๋ฃŒ*/
function setPostCode(data) {
setZipCode(data.zonecode);
setMainAddress(data.address);
setIsModalOpen(false);
}

๋‹ค์Œ์นด์นด์˜ค์—์„œ ์ œ๊ณตํ•˜๋Š” ์šฐํŽธ๋ฒˆํ˜ธ ์กฐํšŒ API ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•˜์—ฌ ์šฐํŽธ ๋ฒˆํ˜ธ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
๊ตฌ๋งค์ž๊ฐ€ ์ƒํ’ˆ ์ฃผ๋ฌธ ์‹œ, ๋ฐฐ์†ก ์ฃผ์†Œ ์ž…๋ ฅ์—์„œ ์šฐํŽธ๋ฒˆํ˜ธ์ฐพ๊ธฐ๋ฅผ ํด๋ฆญํ•˜๋ฉด ์šฐํŽธ๋ฒˆํ˜ธ ๊ฒ€์ƒ‰ ๋ชจ๋‹ฌ์ฐฝ์ด ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.
์‚ฌ์ดํŠธ์˜ ๊ธฐ๋ณธ ์ปฌ๋ŸฌํŒ”๋ ˆํŠธ์™€ ์–ด์šธ๋ฆฌ๋„๋ก ์ฃผ์†Œ ๊ฒ€์ƒ‰์ฐฝ ์ƒ‰์ƒ์„ ์ˆ˜์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค.



๐Ÿ’ฃ์ด์Šˆ

- tailwind๋กœ ๋™์ ์œผ๋กœ background Image url์„ค์ •์‹œ ๋ณด์ด์ง€ ์•Š๋Š” ์ด์Šˆ.

function ProductList({ listdata, lastItemRef }) {
const navigate = useNavigate();
return (
<section className={`${styles.flexCenter} ${styles.sectionLayout}`}>
<ul className="w-full grid lg:grid-cols-[repeat(3,350px)] md:grid-cols-[repeat(3,300px)] sl:grid-cols-[repeat(3,220px)] sm:grid-cols-[repeat(2, 220px)] ss:grid-cols-[repeat(2,200px)] grid-cols-[repeat(2,150px)] gap-y-[50px] justify-between">
{listdata.map((list, idx) => {
return (
<li key={list.product_id}>
<div
onClick={() =>
navigate(`/products/${list.product_id}`, {
state: { product: list },
})
}
className={`lg:w-[350px] lg:h-[350px] md:w-[300px] md:h-[300px] sl:w-[220px] sl:h-[220px] ss:w-[200px] ss:h-[200px] w-[150px] h-[150px] rounded-[10px] border-[1px] bg-center bg-cover cursor-pointer`}
style={{ backgroundImage: `url(${list.image})` }}
></div>
<p className="md:text-[16px] sm:text-[14px] text-[11px] text-subText font-spoqa">
{list.store_name}
</p>
<p
className={`w-full md:text-[18px] sm:text-[16px] text-[13px] text-mainText font-spoqa ${styles.textEllipsis}`}
>
{list.product_name}
</p>
<span
ref={idx === listdata.length - 1 ? lastItemRef : null}
className="inline-block md:text-[24px] sm:text-[22px] text-[14px] text-mainText font-spoqaBold"
>
{list.price.toLocaleString()}
</span>
<span className="inline sm:text-[16px] text-[13px] text-mainText font-spoqa">
{" "}
์›
</span>
</li>
);
})}
</ul>
</section>
);
}
API๋กœ ๋ถˆ๋Ÿฌ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋™์ ์œผ๋กœ background Image์˜ url์„ ์„ค์ •ํ•  ๋•Œ, tailwind๋กœ ์„ค์ • ์‹œ ์ด๋ฏธ์ง€๊ฐ€ ๋ Œ๋”๋ง ๋˜์ง€ ์•Š๋Š” ์ด์Šˆ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
tailwind์˜ ์˜ค๋ฅ˜๊ฐ€ ์žˆ๋Š” ๊ฒƒ์œผ๋กœ ๋ณด์—ฌ, ์ธ๋ผ์ธ ์†์„ฑ์œผ๋กœ background Image url์„ ์„ค์ •ํ•ด ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.



๐Ÿ“‚ํด๋”ํŠธ๋ฆฌ

๐Ÿ“ฆ src
 โ”ฃ๐Ÿ“‚ assets
 โ”ฃ๐Ÿ“‚ components
 โ”ƒ โ”ฃ๐Ÿ“‚ buttons
 โ”ƒ โ”ฃ๐Ÿ“‚ footer
 โ”ƒ โ”ฃ๐Ÿ“‚ modal
 โ”ƒ โ”ฃ๐Ÿ“‚ navBar
 โ”ƒ โ”ฃ NotFound.jsx
 โ”ƒ โ”ฃ NowLoading.jsx
 โ”ƒ โ”— SmNowLoading.jsx
 โ”ฃ๐Ÿ“‚ context
 โ”ฃ๐Ÿ“‚ pages
 โ”ƒ โ”ฃ๐Ÿ“‚ auth
 โ”ƒ โ”ฃ๐Ÿ“‚ home
 โ”ƒ โ”ฃ๐Ÿ“‚ myCart
 โ”ƒ โ”ฃ๐Ÿ“‚ payment
 โ”ƒ โ”ฃ๐Ÿ“‚ productDetail
 โ”ƒ โ”ฃ๐Ÿ“‚ productSearch
 โ”ƒ โ”ฃ๐Ÿ“‚ sellerCenter
 โ”ƒ โ”—๐Ÿ“‚ sellerProductsUpload
 โ”ฃ๐Ÿ“œ App.jsx
 โ”ฃ๐Ÿ“œ main.jsx
 โ”ฃ๐Ÿ“œ index.css
 โ”—๐Ÿ“œ style.js