๐ BANANA๋ ์ฌ์ฉํ์ง ์์ง๋ง ๊ฐ์น๊ฐ ์๋ ํจ์ ์ ํ์ ๋๋๋ ์ปค๋ฎค๋ํฐ ์น์ฌ์ดํธ์ ๋๋ค.
๐ 2์ธ ํ ํ๋ก์ ํธ ( 2023.03 ~ (ํ์ฌ develop/back branch ์์ ์์
์ค)
๐ ํ๋ก์ ํธ ๋ชฉํ : ์ฌํ์ฉ & ์ฌ์ฌ์ฉ ์ธ์ ์ฆ๋ ๋ฐ ํ๊ฒฝ ์นํ์ ์ธ ์๋น๋ฌธํ ์กฐ์ฑ
์ท์ ์ฐ๋ฆฌ ์ํ์์ ํ์์ ์ธ ์์ ์ค ํ๋์ด์ง๋ง, ์ท ์ฐ์
์ ๋ง์ ํ๊ฒฝ ๋ฌธ์ ๋ฅผ ์ผ๊ธฐํฉ๋๋ค.
์ฌ์ ์ ์กฐ ๊ณผ์ ์์ ๋ฐ์ํ๋ ๋๋์ ๋ฌผ ์ฌ์ฉ๊ณผ ํํ ๋ฌผ์ง ๋ฐฐ์ถ, ์ท ์์ฐ๊ณผ ์๋น๋ก ์ธํ ํ๊ธฐ๋ฌผ์ ์ฆ๊ฐ๋ ํ๊ฒฝ์ ์ฌ๊ฐํ ์ํฅ์ ๋ฏธ์น๊ณ ์์ต๋๋ค.
์ด์ ๋ํ ํด๊ฒฐ์ฑ
์ค ํ๋๋ "์ฌํ์ฉ"๊ณผ "์ฌ์ฌ์ฉ"์
๋๋ค.
์
์ง ์์ ์ท์ ๋ค๋ฅธ ์ฌ๋๋ค๊ณผ ๊ณต์ ํ๊ณ ๋๋ํ๋ ๊ฒ์ ํ๊ฒฝ ์นํ์ ์ธ ์๋น ๋ฌธํ๋ฅผ ์กฐ์ฑํ๊ณ , ์์์ ํจ์จ์ ์ธ ์ฌ์ฉ์ ์ด์งํ ์ ์๋ ์ข์ ๋ฐฉ๋ฒ์
๋๋ค.
BANANA ๋๋ ์ปค๋ฎค๋ํฐ๋ก ํ์ฉํจ์ผ๋ก์จ ํ๊ฒฝ์ ๋ํ ์ธ์๊ณผ ๊ด์ฌ์ ๋์ผ ์ ์์ต๋๋ค.
๋๊ตฌ๋ ์์ ์ด ์
์ง ์์ ์ท์ ๊ธฐ๋ถํ๊ณ , ํ์๋ก ํ๋ ์ฌ๋๋ค์๊ฒ ๋๋ํ ์ ์๋ ํ๋ซํผ์ ์ ๊ณตํฉ๋๋ค.
ํฉ์ง๋ | ๊นํ์ง |
---|---|
@hwangJN | @hyeonjy |
wlsk401@gmail.com | hg024246@gmail.com |
โ๏ธ ์ฌ์ฉ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
- Styled-components : ์ปดํฌ๋ํธ ๊ธฐ๋ฐ ์คํ์ผ๋ง
- React Query : ๋ฐ์ดํฐ ์ํ๊ด๋ฆฌ (useQuery, useMutation)
- Recoil : ๋ก๊ทธ์ธ ์ํ ๊ด๋ฆฌ
- jsonwebtoken : ๋ก๊ทธ์ธ ์ ์ ๊ถํ ๋ถ์ฌ
- React-device-detect : BrowserView ์ MobileView ๋ก ๋๋์ด ์์
- React-hook-form : form ์ ๊ตฌํ ๋ฐ ์ ํจ์ฑ ๊ฒ์ฌ
- React Swiper & Slick : ์ด๋ฏธ์ง Slider ๊ตฌํ
- React-loading-skeleton : ๋ก๋ฉ UI&UX ๊ฐ์
- React-js-pagination : ํ์ด์ง๋ค์ด์
๐FIGMA
- OAuth 2.0 ๊ธฐ๋ฐ ์์ ๋ก๊ทธ์ธ ๋ฐ React-hook-form ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ฉํ ํ์๊ฐ์ ๊ธฐ๋ฅ์ ๋๋ค.(์นด์นด์ค & ๊ตฌ๊ธ)
- JWT(+Refresh token)์ ํตํ ์ ์ ๊ถํ ๋ถ์ฌํ์ผ๋ฉฐ Recoil ์ ํตํด ๋ก๊ทธ์ธ ์ํ๋ฅผ ๊ด๋ฆฌํฉ๋๋ค.
- React-hook-form ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ฉํ ์ ํจ์ฑ ๊ฒ์ฌ ํ ์๋ฌ ๋ฉ์ธ์ง๊ฐ ํ์๋ฉ๋๋ค.
- ์ด๋ฏธ์ง ํ์ผ ์ถ๊ฐ & ์ญ์ & ๋ํ์ฌ์ง์ ํ์ธํ ์ ์์ต๋๋ค.
- React-Swiper & Slick Library๋ฅผ ์ฌ์ฉํ์์ต๋๋ค.
- ์ด๋ฏธ์ง๋ฅผ ํฌ๊ฒ ๋ณด๋ ๊ฒฝ์ฐ modal ํํ๋ก ๋ํ๋๋๋ก ๊ตฌํํ์ต๋๋ค.
- React Query ์ Optimistic Update๋ฅผ ํ์ฉํ์์ต๋๋ค
- ์๋ฒ ์์ฒญ ์๋ฃ ์ UI๋ฅผ ์ ๋ฐ์ดํธ๋ฅผ ํตํด ๋น ๋ฅธ ๋ฐ์์๋๋ฅผ ๋๋ ์ ์์ต๋๋ค.
- ๋ก๊ทธ์ธ ์ ์ ์ ๋ง์ดํ์ด์ง์์ ๋๋ ๋ชฉ๋ก์ ํ์ธํฉ๋๋ค.
- React-loading-skeleton library๋ฅผ ํตํด ์ค์ผ๋ ํค ์ปดํฌ๋ํธ๋ฅผ ๊ตฌํํ์ต๋๋ค.
- ๋ค๋ฅธ ์ ์ ์ ๋๋ ๋ชฉ๋ก, ์ ์ ๊ฐ ๋ฐ์ ํ๊ธฐ๋ฅผ ํ์ธํ ์ ์์ต๋๋ค.
- ๊ฒ์ ํค์๋๊ฐ ์ ๋ชฉ ํน์ ๋ด์ฉ์ ํฌํจ๋๋ ๊ฒ์๋ฌผ์ ๊ฒ์ํ ์ ์์ต๋๋ค.
- ๋ฑ๋ก์, ์กฐํ์ ๋ฑ ๊ฒ์๋ฌผ์ ์ ๋ ฌํ ์ ์์ต๋๋ค.
๐ ๊ฒ์๋ฌผ(post) ์์ - 1. ๊ธฐ์กด ์ด๋ฏธ์ง ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ฐ์ ธ์ค๊ธฐ
- ๊ฒ์๋ฌผ ์์ฑ ์์๋ ๋ก์ปฌ์์ ์ ๋ก๋ํ ์ด๋ฏธ์ง์ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ์ํด createObjectURL ๋ก ์ ๊ทผํ์ง๋ง, ๊ฒ์๋ฌผ ์์ ์์๋ ์๋ฒ๋ก๋ถํฐ ์ ๋ฌ๋ฐ์ base64 ํ์ ์ ์ด๋ฏธ์ง ๋ฐ์ดํฐ๋ฅผ URL๋ก ๋ณํํ๋๋ฐ ์คํจํ๋ค.
- ๋ฐฉ๋ฒ์ ๋ฐ๊ฟ FileReader ๊ฐ์ฒด๋ฅผ ํตํด ๋ก์ปฌ์์ ์ ๋ก๋๋๋ ์ด๋ฏธ์ง๋ฅผ base64 ํ์ ๋ก ์ฒ๋ฆฌํ๋ค.
๊ฐ์ ์ด์ - ๊ธฐ์กด ์ด๋ฏธ์ง๊ฐ ๋จ์ง ์์
// ๊ธฐ์กด ์ด๋ฏธ์ง ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋์ฐ๊ธฐ
function base64ToImgUrl(base64) {
const blob = new Blob([base64], { type: 'image/jpeg' });
const url = URL.createObjectURL(blob);
return url;
}
useEffect(() => {
if (state) {
//....
let imgurls = [];
for (let i = 0; i < state.item.imgs.length; i++) {
imgurls.push(base64ToImgUrl(state.item.imgs[i].data)); // base64 ๋ฐ์ดํฐ - > ObjectURL
}
setImgURLs(imgurls);
}
}, [state]);
// ๋ก์ปฌ ์ด๋ฏธ์ง ์
๋ก๋ํ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋์ฐ๊ธฐ
for (let i = 0; i < imageLists.length; i++) {
const currentImageUrl = URL.createObjectURL(imageLists[i]);
imageUrlLists.push(currentImageUrl);
}
//...
setImgURLs(imageUrlLists);
//...
// ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ด๋ฏธ์ง ์ปดํฌ๋ํธ
<ImgPreview src={imgURL} />
๊ฐ์ ์ดํ
// ๊ธฐ์กด ์ด๋ฏธ์ง ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋์ฐ๊ธฐ
//....
let imgurls = [];
for (let i = 0; i < state.item.imgs.length; i++) {
imgurls.push(state.item.imgs[i].data); // base64 ๋ฐ์ดํฐ
}
setImgURLs(imgurls);
//....
// ๋ก์ปฌ ์ด๋ฏธ์ง ์
๋ก๋
const reader = new FileReader();
reader.onload = () => {
const base64Data = reader.result;
setImgURLs((prevImgs) => [...prevImgs, base64Data.split(",")[1]]); // ์ด๋ฏธ์ง ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ ์ฅ(base64ํ์)
};
//...
// ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ด๋ฏธ์ง ์ปดํฌ๋ํธ
<ImgPreview src={`data:image/jpeg;base64, ${imgURL}`} />
๐ ๊ฒ์๋ฌผ(post) ์์ - 2. ์ด๋ฏธ์ง ์ญ์
- ์ด๋ค ๊ธฐ์กด ์ด๋ฏธ์ง๊ฐ ์ญ์ ๋์๊ณ ์ ์ง๋๋ฉฐ, ๋ก์ปฌ์์ ์ด๋ค ์ด๋ฏธ์ง๊ฐ ์ถ๊ฐ&์ญ์ ๋๋์ง์ ๋ํด ์ฒ๋ฆฌํด์ผ ํ๋ค.
- ์ฒ์์๋ ๋ฐ์ดํฐ๋ฅผ ๋ชจ๋ ์ง์ด ๋ค์ ์ ๋ฐ์ดํธ ๋ ๋ฐ์ดํฐ๋ฅผ ๋ค์ insert ํ๋ ๋ฐฉ๋ฒ์ ์ ํํ๋ค๊ฐ, ๊ธฐ์กด ์ด๋ฏธ์ง ๋ฐ์ดํฐ(base64ํ์)์ ํ์ผ ํํ๋ก ์ ํํ๋๋ฐ ์ด๋ ค์์ ๊ฒช์๋ค.
- ๋ค๋ฅธ ๋ฐฉ๋ฒ์ผ๋ก, ์ญ์ ํ ๋ฐ์ดํฐ์ ํ์ผ๋ช ๊ณผ ์๋ก ์ถ๊ฐํ ํ์ผ๋ง ์๋ฒ์ ์ ์กํ๋ ๋ฐฉ๋ฒ์ ํํ๋ค
const [imgURLs, setImgURLs] = useState([]); /**์ด๋ฏธ์ง ๋ฏธ๋ฆฌ๋ณด๊ธฐ */
const [imgFileName, setImgFileName] = useState([]); /** ๊ธฐ์กด ์ด๋ฏธ์ง ํ์ผ๋ช
(์์ ) */
const [deleteFileList, setDeleteFileList] = useState([]); /** ์ญ์ ๋ ํ์ผ๋ช
*/
useEffect(() => {
if (state) {
//.......
let imgurls = [];
let imgfilesname = [];
for (let i = 0; i < state.item.imgs.length; i++) {
imgurls.push(state.item.imgs[i].data); //base64 ๋ฐ์ดํฐ - ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ด๋ จ
imgfilesname.push(state.item.imgs[i].filename); // ๊ธฐ์กด ์ด๋ฏธ์ง ํ์ผ๋ช
}
setImgURLs(imgurls);
setImgFileName(imgfilesname);
}
}, [state]);
//...
// ์ด๋ฏธ์ง ์ญ์ ์ ์คํ ํจ์
const handleDelete = (index) => {
setImgURLs(imgURLs.filter((_, idx) => idx !== index)); // ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ฒ๋ฆฌ
if (index < imgFileName.length) { // ๊ธฐ์กด ์ด๋ฏธ์ง ๋ฐ์ดํฐ ์ญ์
setDeleteFileList((prev) => [...prev, imgFileName[index]]);
setImgFileName(imgFileName.filter((_, idx) => idx !== index));
} else { // ๋ก์ปฌ์์ ์ถ๊ฐ๋ ์ด๋ฏธ์งํ์ผ ์ญ์
setImgFile(
imgFile.filter((_, idx) => idx + imgFileName.length !== index)
);
}
};
๐ ํ์ผ๋ช ์ค๋ณต ์ ์ฅ ๋ฒ๊ทธ
- ๊ฒ์๋ฌผ ์์ฑ์ ์ ๋ก๋ ๋๋ ์ด๋ฏธ์ง๋ node.js์ multer ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก ์ ์ฅ
- ์ ๋ก๋ ์๊ฐ(Date.now())์ผ๋ก ํ์ผ ์ด๋ฆ์ ๊ตฌ๋ถํ๋ ค ํ์์ผ๋ ํฌ๊ธฐ๊ฐ ์์ ํ์ผ(ex:.png)์ ๊ฒฝ์ฐ ์ฒ๋ฆฌ์๋๊ฐ ๋นจ๋ผ ๊ฐ์ ์๊ฐ์ ์ฒ๋ฆฌ๋์ด ๋์ผํ ํ์ผ์ด๋ฆ์ผ๋ก ์ ์ฅ๋๋ ๋ฌธ์ ๋ฐ์
๊ฐ์ ์
filename: function (req, file, cb) {
const extension = path.extname(file.originalname);
const filename = `${Date.now()}${extension}`;
cb(null, filename);
},
๊ฐ์ ์ดํ
- ์ ๋ก๋ ์๊ฐ๊ณผ ๊ธฐ์กด ํ์ผ๋ช ์ ํผํฉํ์ฌ ํ์ผ๋ช ์ ์์ ํจ
- ๊ธฐ์กด ํ์ผ๋ช ์ด ํ๊ธ์ผ ๊ฒฝ์ฐ ๋ฌธ์๊ฐ ๊นจ์ง๋ ๊ฒฝ์ฐ๋ฅผ multer๋ฅผ 1.4.4 ๋ฒ์ ์ผ๋ก ๋ค์ด๊ทธ๋ ์ด๋ํ์ฌ ํด๊ฒฐ
filename: function (req, file, cb) {
const extension = path.extname(file.originalname);
const name = file.originalname.split(".")[0];
const filename = `${Date.now()}${name}${extension}`;
cb(null, filename);
},
๐ Modal ๊ด๋ จ ๋ฒ๊ทธ ํด๊ฒฐ
useEffect(() => {
const body = document.querySelector("body");
if (imgFullModal || activeGrade) {
body.classList.add("no-scroll");
} else if (!imgFullModal && !activeGrade) {
body.classList.remove("no-scroll");
}
return ()=>body.classList.remove("no-scroll");
}, [imgFullModal, activeGrade]);
-
๋ชจ๋ฌ ์ฌ์ฉ ์ ์คํฌ๋กค ๋ฐฉ์ง
-
์ธ๋ง์ดํธ์(return) ์คํฌ๋กค ๋ฐฉ์ง๋ฅผ ์ ๊ฑฐํด ์ฃผ์ง ์์ ๊ฒฝ์ฐ, ๋ชจ๋ฌ active ์ํ์์ ๋ค๋ฅธ ํ์ด์ง๋ก ์ด๋ ์ ์ฌ์ ํ ์คํฌ๋กค์ด ๋งํ์๋ ์ํฉ์ด ๋ฐ์
๐ React-Swiper currentIdx ๋ฒ๊ทธ ํด๊ฒฐ
// swiper onSlideChange ์ - ํ์ฌ ์ด๋ฏธ์ง์ ์ธ๋ฑ์ค ์ ์ฅ ํจ์
const handleSlideChange = (swiper) => {
setImgCurrentIdx(swiper.realIndex);
};
//...
<StyledSwiper
//...
loop={true}
onSlideChange={handleSlideChange}
>
</StyledSwiper>
- ๊ธฐ์กด์ ์ฌ์ฉํ๋ swiper.activeIndex๋ Swiper ์ปดํฌ๋ํธ๊ฐ loop ๋ชจ๋์ผ ๊ฒฝ์ฐ์ ์ ํํ ์ธ๋ฑ์ค๋ฅผ ๋ฐํํ์ง ๋ชปํจ
- swiper.realIndex๋ก ๋์ฒด
๐ useMutation - ์ฐ(์์ด์ฝ) ์ํ ๋ณ๊ฒฝ
const queryClient = useQueryClient();
const { mutate: mutateHeart } = useMutation(
(heart) => heartChangeApi(heart),
{
// ์๋ฒ ์์ฒญ ์๋ฃ ํ ์
๋ฐ์ดํธ ์๋ฃ๋ ์ต์ ์ ๋ณด๋ฅผ ํ๋ฉด์ ๊ทธ๋ฆฌ๋ ๊ฒฝ์ฐ
// onSuccess: () => {
// queryClient.invalidateQueries(["postDatail", postId);
// },
// ์ตํฐ๋ฏธ์คํฑ ์
๋ฐ์ดํธ
onMutate: async (newData) => {
const previousHeartData = queryClient.getQueryData([
"postDatail",
postId,
]);
queryClient.setQueryData(["postDatail", postId], (olddata) => {
return { ...olddata, heart: !newData.heart };
});
return previousHeartData; //์์ฒญ ์คํจํ ๊ฒฝ์ฐ ๊ธฐ์กด ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉ
},
//์์ฒญ์ด ์คํจํ ๊ฒฝ์ฐ ์ด์ ์ํ ์ ์ง
onError: (rollback) => rollback(),
}
);
- mutation ์ฑ๊ณต ํ ์ฟผ๋ฆฌ๋ฅผ ์ ๋ฐ์ดํธ ํ๋ ๋ฐฉ๋ฒ์์ ์ตํฐ๋ฏธ์คํฑ ์ ๋ฐ์ดํธ๋ก ๋ณ๊ฒฝํจ์ ๋ฐ๋ผ ๋น ๋ฅธ ๋ฐ์์๋