๐๋ฐฐํฌ๋งํฌ / ๐gif test ๋ฐฐํฌ๋งํฌ
์์ ๊ธฐ๊ฐ: 2023.07.26 - 2023.08.04(2์ฃผ)
# ํ๊ณ
์ข์ ๊ธฐํ๋ก GGNZ ์ด๋ฒคํธ ํ์ด์ง๋ฅผ ์ ์ํ๊ฒ ๋์๋ค. ๊ฐ๋ฐ์ ์์ Canvas๋ฅผ ํ์ตํ์์ง๋ง, ์ญ์๋ ์ฝ์ง ์์ ์์ ์ด์๋ค. gif ํ์ผ์ ๋ค๋ฃจ๊ธฐ๊ฐ ์ฝ์ง ์์์ ๋์ฑ ๊ทธ๋ฌ๋ค. ์ฒ์ ๊ณํ๋๋ก gif๋ก ๋ง๋ค๊ณ animated gif ํํ๋ก ์ ์ฅํ๋ ํํ๊ฐ ์๋ png๋ก ๋ ์ด์ด๋ฅผ ์์ customized Olchaeneez๋ฅผ ๋ง๋ค๊ณ png ์ ์ฅํ๋ ํํ๊ฐ ๋์ด ์์ฌ์์ด ๋ง์ด ๋จ๋๋ค. ํ์ฌ๋ sub domain์ ๋ฐ๋ก ๋ฐ์, gif๋ก ์์ ํ ์ ์๋ ํํ๋ฅผ ์๋ํด๋ณด๊ณ ์๋ ์ค์ด๋ค.
## ๊ธฐ์ ์ ์ธ ๋ฌธ์ ์ 1 - canvas animated gif renderinng
canvas๋ animated gif๋ฅผ ์ง์ํ์ง ์์ผ๋ฏ๋ก ์ ์ ์ธ ์ด๋ฏธ์ง๋ง ๋ณด์ฌ์ฃผ๋ ์ด์๊ฐ ์์๋ค. ๋์์ด๋๋ถ๊ป ๋ฐ์ ๋ฐ์ดํฐ๋ animated gif์ด๋ฏ๋ก, ์ด๋ฅผ ๋ ๋๋ง ํ๊ธฐ ์ํด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํด ํด๊ฒฐํ๊ณ ์ ํ๋ค. ํ์ง๋ง ์ถ๊ฐ์ ์ธ ๋ฌธ์ ๊ฐ ์์๋ค. ๋จ์ํ ํ๋์ gif ํ์ผ์ ๋ ๋๋ง ํ๋ ๊ฒ์ด ์๋, ์ฌ๋ฌ์ฅ์ animated gif ์ด๋ฏธ์ง๋ฅผ canvas์ ์์์ผ ํ๊ณ ์์ธ ์ด๋ฏธ์ง๊ฐ ๋์์ ํน์ ์์ญ์ ๋ณด์ฌ์ ธ์ผํ๋ค.
gif๋ฅผ ์ด๋ ๊ฒ ๋ค๋ค๋ณธ๊ฑด ์ฒ์์ด์ด์ ์ฝ 3์ผ๊ฐ ์ฌ๋ฌ gif๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์๋ํด๋ณด์๋ค. ๊ทธ ์ค gifencoder์ ๊ฒฝ์ฐ, ๊ฐ๊ฐ์ ์ด๋ฏธ์ง๊ฐ ์๊ฐ์ฐจ๋ฅผ ๋๊ณ ์์ฐจ์ ์ผ๋ก ๋ ๋๋ง ๋์ด ์ฌ์ฉํ ์ ์์๋ค. ์ฌ๋ฌ ์๋ ๋์ gifuct์ fabric ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํด ์ฌ๋ฌ animated gif ์ด๋ฏธ์ง๋ฅผ ๋ ๋๋ง ํ ์ ์์๋ค.
## ๊ธฐ์ ์ ์ธ ๋ฌธ์ ์ 2 - canvas animated gif motion
23.08.04
canvas์ gif animation์ ๋ ๋๋งํ๋๋ฐ๋ ์ฑ๊ณตํ์ง๋ง, ๊ฐ๊ฐ์ gif ์ด๋ฏธ์ง๋ค์ ๋์์ด ์๋ก ์ผ์นํ์ง ์๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค. setTimeout์ ์ฌ์ฉํด๋ณด๊ธฐ๋ ํ๊ณ ์ด๋ฏธ์ง๋ฅผ ๋ชจ๋ ๊ฐ์ ธ์จ ํ ์ด๋ฏธ์ง๋ฅผ ๋ ๋๋ง ํ๊ฒ ํด ๋ณด๊ธฐ๋ ํ์ง๋ง, ๋ฌธ์ ๊ฐ ํด๊ฒฐ๋์ง ์์๋ค(dev ํ๊ฒฝ์์๋ setTimeout์ผ๋ก ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์์ง๋ง, ๋ฐฐํฌ์์๋ ๋ค์ ๋์ผํ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค).
์ถ๊ฐ๋ก canvas์ animated gif์ ๋ ์ด์ด๊ฐ ์์ผ ์๋ก, ์บ๋ฆญํฐ์ ๋์์ด ๋๋ ค์ง๊ณ ๋ ๋๋ง ๋๋๋ฐ ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆฐ๋ค๋ ๋ฌธ์ ๊ฐ ์์๋ค. ํด๋น ํ๋ก์ ํธ๋ฅผ ์ํด ์ถ๊ฐ์ ์ผ๋ก canvas์ ๋ํด ํ์ตํ์ง๋ง, canvas ์ฌ์ฉ์ ํฌ๊ธฐํ๊ณ figure tag์ ์ด๋ฏธ์ง๋ฅผ ๋ ๋๋ง ํ๋ ๋ฐฉ์์ผ๋ก ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ค.(23.08.04)
23.08.19
8์ ๋ง์ GGNZ ์คํ๋ผ์ธ ํ์ฌ๊ฐ ์๋ค๋ ๋ง์, ๋ค์ ํ ๋ฒ ํด๋น ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ณ ์ ๋์ ํด ๋ณด์๋ค. ๋จผ์ , ๋ถํธ์บ ํ ๋ฉํ ๋๊ป์ ์กฐ์ธํด์ฃผ์ ๋ด์ฉ์ ๋ฐํ์ผ๋ก ๋ชจ๋ ์ด๋ฏธ์ง๋ฅผ ๊ฐ์ ธ์จ ํ์ canvas์ ๊ทธ๋ฆฌ๊ณ ์ ํ๋ค. ์ด์ ์๋ Promise.all()์ ์ฌ์ฉํด์ canvas์ ์ฌ๋ฌ์ฅ์ animated gif๋ฅผ ๊ทธ๋ฆฌ๋ ์๋๋ฅผ ํ์์ง๋ง, ๋์์ด ์ ๊ฐ๊ฐ์ด์ด์ ์ด๋ฏธ์ง ์์ฒด๊ฐ ๋ถ์์ ํด๋ณด์ด๋ ๋ฌธ์ ๊ฐ ์์๋ค. ํ์ง๋ง ์ด๋ฒ์๋ fabric์ ๋ด์ฅ ํจ์์ธ 'requestAnimFrame()'์ ์ฌ์ฉํด ๋ชจ๋ gif ํ์ผ์ ๊ฐ์ ธ์จ ํ canvas์ ๊ทธ๋ฆฌ๋ ์ฝ๋๋ฅผ ์งฐ๋ค. ์๋ฒฝํ์ง ์์ง๋ง, ์ด์ ๋ณด๋ค ๊ฐ gif frame์ด ๋ฐ๋ก ๋ ผ๋ค๋ ๋๋์ ์ค์๋ค.
* fabric.util.requestAnimFrame
๋ธ๋ผ์ฐ์ ์๊ฒ ๋ค์ ํ๋ฉด ๊ทธ๋ฆฌ๊ธฐ๊ฐ ์ผ์ด๋๊ธฐ ์ ์ ํน์ ํจ์๋ฅผ ์คํํ๋๋ก ์์ฒญํ๋ ๊ธฐ๋ฅ์ผ๋ก ์ฃผ๋ก ์ ๋๋ฉ์ด์ ์ ๋ง๋ค ๋ ์ฌ์ฉ๋๋ค.
์๋ฅผ ๋ค์ด, ์ฐ๋ฆฌ๊ฐ ์์ง์ด๋ ๊ณต์ ํ๋ฉด์ ๊ทธ๋ฆฌ๊ณ ์ถ๋ค๊ณ ๊ฐ์ ํด๋ณด์. ๊ณต์ด ์์ง์ด๋ ๊ฒ์ฒ๋ผ ๋ณด์ด๊ฒ ํ๋ ค๋ฉด ๊ณต์ ๊ณ์ํด์ ์๋ก์ด ์์น์ ๊ทธ๋ ค์ผ ํ๋ค. ์ด๋ฅผ ์ํด requestAnimationFrame์ ์ฌ์ฉํ๋ฉด, ๋ธ๋ผ์ฐ์ ๊ฐ ํ๋ฉด์ ๋ค์ ๊ทธ๋ฆฌ๊ธฐ ์ง์ ์ ๊ณต์ ์๋ก์ด ์์น์ ๊ทธ๋ฆฌ๋ ํจ์๋ฅผ ์คํํ๋๋ก ์์ฒญํ ์ ์๋ค.
์ด๋ฐ ์์ผ๋ก requestAnimationFrame์ ์ฌ์ฉํ๋ฉด, ์ ๋๋ฉ์ด์ ์ด ๋ถ๋๋ฝ๊ฒ ๋ณด์ด๊ฒ ํ ์ ์๋ค. ์๋ํ๋ฉด ๋ธ๋ผ์ฐ์ ๊ฐ ํ๋ฉด์ ๊ทธ๋ฆฌ๋ ํ์ด๋ฐ์ ๋ง์ถฐ ์ ๋๋ฉ์ด์ ์ ์ ๋ฐ์ดํธํ๊ธฐ ๋๋ฌธ์ด๋ค.
fabric.util.requestAnimFrame์ fabric.js ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ ์ ๊ณตํ๋ requestAnimationFrame์ wrapper ํจ์๋ค. ์ด ํจ์๋ฅผ ์ฌ์ฉํ๋ฉด fabric.js๋ฅผ ์ฌ์ฉํ์ฌ Canvas์ ๊ทธ๋ ค์ง ๊ฐ์ฒด๋ค์ ์ ๋๋ฉ์ด์ ์ ์ฝ๊ฒ ์ ์ดํ ์ ์์ต๋๋ค.
## ์๋ก ๋ฐฐ์ด stack 1 - ํด๋ ๋ด ๋ชจ๋ ํ์ผ import (Webpack require.context)
layer ํด๋ ํ์์ ํด๋ ๋ด์ ์๋ ๋ชจ๋ ์ด๋ฏธ์ง ํ์ผ์ import ํด์์ผ ํ๋ค. ํ์ผ ์์ด ๋ณดํต 50๊ฐ๋ผ ํ๋์ฉ import ํ๊ธฐ์๋ ๋ฌด๋ฆฌ๊ฐ ์์๊ธฐ์ ์ดํ ๋์ ๊ตฌ๊ธ๋งํ์ฌ ๋ฐฉ๋ฒ์ ์ฐพ์ ์ ์์๋ค. webpack์ require.context()๋ฅผ ์ฌ์ฉํด ํน์ ์์น์ ํด๋์ ์ ๊ทผํด ํ์์ ๋ชจ๋ ํ์ผ์ importํ ์ ์๊ฒํด ์ฃผ๋, ์ ๋ง ํธ๋ฆฌํ๊ฒ ์ฌ์ฉํ๋ค.
const imgUrl: IImgUrl = {
base: {
base: require.context("/public/ํ์ผ๊ฒฝ๋ก", false, /\.(png|gif)$/),
},
...
}
const markdownContext: __WebpackModuleApi.RequireContext =
imgUrl[์์ํด๋][ํ์ํด๋];
const importAll = (requireContext: __WebpackModuleApi.RequireContext) => {
const keys = requireContext.keys();
const images: IModule[] = keys.map((key: string) => {
return requireContext(key) as IModule;
});
return images.map((el) => el.default.src).slice(images.length / 2);
};
const importFile = () => {
return require(`/ํ์ผ๊ฒฝ๋ก)
.default;
};
return fileName ? importFile() : importAll(markdownContext);
reference) https://eams.dev/post/require-context
## ์๋ก ๋ฐฐ์ด stack 2 - canvas API (Github, ๋ฐฐํฌ์ฌ์ดํธ)
๋ณธ๊ฒฉ์ ์ธ ๊ฐ๋ฐ์ ๋ค์ด๊ฐ๊ธฐ ์ , canvas API์ ๋ํด ํ์ตํ๋ค. ์ด์ ์ ๋ถํธ์บ ํ์์ ์บ๋ฒ์ค ๊ฐ์๋ฅผ ์๊ฐํ๋ฉฐ canvas API๋ฅผ ์ฌ์ฉํด ์ง๋ ์ด ๊ฒ์์ ๋ง๋ ๊ฒฝํ์ ์์ง๋ง, canvas API ์์ฒด๊ฐ ์์ํด์ ์ ์ฒด์ ์ธ ์ดํด๋ณด๋ค๋ ๊ฐ์๋ฅผ ๋ฐ๋ผ๊ฐ๊ธฐ์๋ง ๊ธ๊ธํ๋ค. ๋๋ถ์ ์ด๋ฒ์๋ MDN ๋ฌธ์๋ฅผ ํตํด ์ ์ฒด์ ์ธ ๊ฐ๋ ์ ์ดํดํ๊ณ ๋ ธ๋ง๋์ฝ๋ ๊ฐ์๋ฅผ ํตํด ๊ฐ๋จํ๊ฒ ํ ์ดํ๋ก์ ํธ๋ก ๊ทธ๋ฆผํ์ ๊ตฌํํด๋ณด์๋ค.
์ฒ์์๋ canvas, context ๋ฑ ์ฒ์ ๋ค๋ค๋ณด๋ ๊ฒ๋ค์ด๋ผ ์์ํ์ง๋ง, ํ ์ดํ๋ก์ ํธ์ ํ๋, ๋ ์ฉ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ฉด์ ์ง์ ๊ทธ๋ฆผํ์ ๊ธฐ๋ฅ์ ๊ตฌํํ ์ ์๋ค๋ ์ ์ด ํฅ๋ฏธ๋กญ๊ฒ ๋ค๊ฐ์๋ค.
* ๊ธฐ๋ฅ : ๋ธ๋ฌ์ฌ ํฌ๊ธฐ์กฐ์ , ๋ฐฐ๊ฒฝ์ ๋ฃ๊ธฐ, ์ง์ฐ๊ฐ, ์บ๋ฒ์ค ๋น์ฐ๊ธฐ, ์ด๋ฏธ์ง ํ์ผ ๋ฃ๊ธฐ, ์บ๋ฒ์ค ์ด๋ฏธ์ง ์ ์ฅ
## ์๋ก ๋ฐฐ์ด stack 3 - canvas image ์ ์ฅ - png
canvas์ animated gif๋ฅผ ์๋ฌ์์ด ๋ ๋๋ง ํ์ง ๋ชปํด, ๋ณด๋ค ์์ ์ ์ธ png ํํ์ ์ด๋ฏธ์ง๋ฅผ ํ๋ฉด์ ๋ณด์ฌ์ฃผ๊ณ ์ด๋ฅผ ์ฌ์ฉ์๊ฐ ๋ก์ปฌ์ ์ ์ฅํ ์ ์๊ฒ ๊ฐ๋ฐํ๋ค. ํด๋น ๋ถ๋ถ์ html2canvas ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํด ์ฝ๊ฒ ๊ตฌํํ ์ ์์๋ค. ํนํ ์ด๋ฏธ์ง๊ฐ ๋ค์ด์๋ tag๊ฐ canvas tag๊ฐ ์๋๋๋ผ๋ png ํํ๋ก ์ด๋ฏธ์ง๋ฅผ ์ ์ฅํ ์ ์๋ ๋ถ๋ถ์ด ์ ๊ธฐํ๋ค.
if (figureRef.current) {
html2canvas(figureRef.current).then((canvas) => {
const link = document.createElement("a");
link.download = "image.png";
link.href = canvas.toDataURL();
link.click();
});
}
## ์๋ก ๋ฐฐ์ด stack 4 - canvas image ์ ์ฅ - webm
animated gif๋ฅผ canvas์ ๊ทธ๋ฆฐ ํ, animation ํํ๋ก ๋ก์ปฌ์ ์ ์ฅํ ์ ์๋๋ก ์๋ํด ๋ณด์๋ค. ์ด๊ธฐ์๋ ccapture ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ์ ๋ํด ์์๋ณด๋ค๊ฐ stackoverflow์์ canvas capturestream๊ณผ mediaRecoder๋ฅผ ์ฌ์ฉํด canvas์ ๊ทธ๋ ค์ง animation์ webm ํํ๋ก ์ ์ฅํ๊ณ ์๋ค๋ ๊ธ์์ ํํธ๋ฅผ ์ป์ด ์ฝ๋๋ฅผ ์์ฑํ๋ค.
HTMLCanvasElement์ capturestream()์ ์ฌ์ฉํด canvas์ ๊ทธ๋ ค์ง midia stream์ captureํ ํ ์ด๋ฅผ video ํ์์ผ๋ก ๊ธฐ๋กํ๋ค. ์ดํ chunks ๋ฐฐ์ด์ ๋ด์ Blob๊ฐ์ฒด๋ฅผ ์์ฑํ ํ ์ฌ์ฉ์์ ๋ก์ปฌ์ ์ ์ฅํ๋๋ก ํ๋ค.
const canvas = canvasRef.current as HTMLCanvasElement;
if (canvas) {
const stream = canvas.captureStream(); // ์บ๋ฒ์ค์์ ๋ฏธ๋์ด ์คํธ๋ฆผ์ ์บก์ฒ
const mediaRecorder = new MediaRecorder(stream, { // ์บก์ฒํ ์คํธ๋ฆผ์ ์ฌ์ฉํด ์๋ก์ด MediaRecorder instance ์์ฑ
mimeType: "video/webm", // instance๋ video/webm ํ์์ ๋น๋์ค๋ก ๊ธฐ๋กํจ
});
const chunks: Blob[] = []; // ๊ธฐ๋ก๋ ๋น๋์ค ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ ๋ฐฐ์ด ์์ฑ
// dataavailable ์ด๋ฒคํธ ํธ๋ค๋ฌ ์ค์
// ํธ๋ค๋ฌ๋ media data๊ฐ ์ฌ์ฉ๊ฐ๋ฅํด์ง ๋ ๋ง๋ค ํธ์ถ๋์ด ๋ฐ์ดํฐ๋ฅผ chunks ๋ฐฐ์ด์ ์ถ๊ฐ
mediaRecorder.ondataavailable = (event) => {
chunks.push(event.data);
};
// stop() ์ด๋ฒคํธ ํธ๋ค๋ฌ ์ค์
// ๋ฏธ๋์ด ๊ธฐ๋ก์ด ์ค์ง๋ ๋ ํธ์ถ๋๋ฉฐ, ์ด๋, chunks ๋ฐฐ์ด์ ์ ์ฅ๋ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํด ์๋ก์ด Blob ๊ฐ์ฒด๋ฅผ ์์ฑ ๋ฐ ๋ค์ด๋ก๋ ๋งํฌ ์ ๊ณต
mediaRecorder.onstop = () => {
const blob = new Blob(chunks, { type: "video/webm" });
const downloadLink = document.createElement("a");
downloadLink.href = URL.createObjectURL(blob);
downloadLink.download = "์ ๋ชฉ์์.webm";
downloadLink.click();
};
mediaRecorder.start(); // ๋ฏธ๋์ด ๊ธฐ๋ก ์์
setTimeout(() => {
mediaRecorder.stop();
}, 1600);
}
reference)
Record animation of canvas with a long time
I have to record animation from canvas and the animation takes about 2 minutes. I'm using MediaRecorder and canvas captureStream to record the canvas, everything works well, but we have to wait for 2
stackoverflow.com
## ์๋ก ๋ฐฐ์ด stack 5 - canvas image ์ ์ฅ - gif
webm ํํ๋ก ์ ์ฅํ๋ ๊ฒ์ผ๋ก test ํ์ด์ง ๊ฐ๋ฐ์ ๋ง๋ฌด๋ฆฌํ๋ ค ํ๋ค. ํ์ง๋ง, ์ฌ์ฉ์ค์ธ ์์ดํฐ safari์์๋ webm ํ์ผ์ ๋ค์ด๋ก๋ํ ์ ์๋๊ฒ์ผ๋ก ๋ณด์ฌ์ก๋ค. ๊ทธ๋์ ์ด์ฉ ์ ์์ด gif๋ก ์ ์ฅํ๋ ํํ๋ก ๋ค์ ๊ฐ๋ฐ์ ํ๋ค. webm์ ๊ฒฝ์ฐ ์ฌ์ฉ์๊ฐ ์ ์ฅ๋ฒํผ์ ๋๋ฅด๋ฉด ํ๊ท 1์ด ๋ด๋ก canvas์ ๊ทธ๋ ค์ง animation์ ๋ก์ปฌ์ ์ ์ฅํ ์์๋ค. ํ์ง๋ง, gif์ ๊ฒฝ์ฐ, ์๊ฐ๋ณด๋ค ๊ธด ์๊ฐ์ด ์ง๋ ํ์์ผ animated gif๊ฐ ๊ทธ๋ ค์ก๊ธฐ์, ux๋ฅผ ๊ณ ๋ คํ๋ค๋ฉด, gif๋ก ์ ์ฅํ๋ ๊ฒ์ ์ง์ํ๊ณ ์ถ์๋ค. ํ์ง๋ง, ๋ถ๊ฐํผํ๋ค.
gif.js ๋ฅผ ์ฌ์ฉํด canvas์ animated gif๋ฅผ ์ ์ฅํ๋ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ค. ํ์ง๋ง ์ ์ฅ ๋ฒํผ์ ๋๋ฅด๋ ๋ค์ ์๋ฌ๊ฐ ๋ฐ์ํ๋ค.
Failed to load resource: the server responded with a status of 404 (Not Found)
node_modules ํด๋ ๋ด @types ํด๋์ gif.js ํด๋์ gif.worker.js ํ์ผ์ด ์์ง๋ง, ์๋ฒ๊ฐ ์์ฒญํ ๋ฆฌ์์ค๋ฅผ ์ฐพ์ ์ ์๋ค๋ ์๋ฌ๊ฐ ๋ฐ์ํ๋ค. ์ฒซ ๋ฒ์งธ ๋ฐฉ๋ฒ์ผ๋ก web-loader๋ฅผ ์ค์นํ๊ณ next.config.js ํ์ผ์ ์์ ํ ํ ๋ค์ save ๋ฒํผ์ ํด๋ฆญํด๋ดค์ง๋ง, ์ฌ์ ํ ๋์ผํ ์๋ฌ๊ฐ ๋ฐ์ํ๋ค.
๋ ๋ฒ์งธ ๋ฐฉ๋ฒ์ผ๋ก public ํด๋์ gif.worker.js๋ฅผ ๋ฃ์ด์ฃผ๋ save ๋ฒํผ์ ๊ธฐ๋ฅ์ด ์ ์์ ์ผ๋ก ๋์ํ๋ค.
'๊ฐ๋ฐ ๊ณต๋ถ > Projects' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Project) ์๋ฐ์ ์ด์ปค๋จธ์ค (0) | 2023.07.31 |
---|---|
Practice) ๊ฐ์ธํ๋ก์ ํธ - Today's Clothing (0) | 2023.05.28 |
Practice) React Shop ํ๊ณ (0) | 2023.04.20 |
Practice) React Shop - Day 3 (0) | 2023.04.17 |
Practice) React Shop - Day 2 (0) | 2023.04.15 |