Skip to content

๐ŸŽฌ ์˜ํ™” ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜๊ณ , ์˜ํ™” ๋ฆฌ๋ทฐ๋ฅผ ํ†ตํ•ด ์˜ํ™” ์ •๋ณด๋ฅผ ๊ณต์œ ํ•˜๋Š” ์‚ฌ์ดํŠธ

Notifications You must be signed in to change notification settings

NamJongtae/MovieWorld

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐ŸŽฌ MovieWorld

ํ…Œ์ŠคํŠธ ๊ณ„์ •

ID PW
[email protected] asdzxc123!

๋ฐฐํฌ URL : ๐ŸŽž MovieWorld

๐Ÿ™‹โ€โ™‚๏ธ ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ

thumbnail

  • MovieWorld๋Š” ๋‹ค์–‘ํ•œ ์˜ํ™” ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜๊ณ  ๋ฆฌ๋ทฐํ•  ์ˆ˜ ์žˆ๋Š” ์‚ฌ์ดํŠธ ์ž…๋‹ˆ๋‹ค.
  • ๋กœ๊ทธ์ธ ์—†์ด ๋‹ค์–‘ํ•œ ์˜ํ™” ์ •๋ณด๋ฅผ ๋ฌด๋ฃŒ๋กœ ์ œ๊ณต๋ฐ›์•„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์˜ํ™” ๋ฆฌ๋ทฐ๋ฅผ ํ†ตํ•ด ์˜ํ™” ์ •๋ณด๋ฅผ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์˜ํ™” ๋ฆฌ๋ทฐ ์Šคํฌ์ผ๋Ÿฌ ๋ฐฉ์ง€ ๊ธฐ๋Šฅ์ด ์žˆ์–ด ์˜ํ™” ๋ฆฌ๋ทฐ๋ฅผ ํ•„ํ„ฐ๋งํ•˜์—ฌ ์›์น˜์•Š์€ ์Šคํฌ์ผ๋Ÿฌ๋ฅผ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์›ํ•˜๋Š” ์˜ํ™”๋ฅผ ์ฐœํ•˜๊ณ , ์ €์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ“ƒ ๋ชฉ์ฐจ (ํด๋ฆญ ์‹œ ํ•ด๋‹น ๋ชฉ์ฐจ๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.)

๐Ÿ“† ๊ฐœ๋ฐœ๊ธฐ๊ฐ„

๊ฐœ๋ฐœ์™„๋ฃŒ: 2023.07.05 ~ 2023.08.07

ํ”„๋กœ์ ํŠธ ๊ฐœ๋ฐœ ์ดํ›„์—๋„ ์ƒˆ๋กœ ํ•™์Šตํ•œ ๋‚ด์šฉ์„ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์˜ค๋ฅ˜, ์ˆ˜์ •์‚ฌํ•ญ ์ˆ˜์ • ๋ฐ Redux ์ ์šฉ : 2023.08.07 ~ 2023.08.15

TypeScript ์ ์šฉ : 2023.09.02 ~ 2023.09.03

๐Ÿ”—Redux-toolkit Slice, Store ๊ตฌ์„ฑ ๋ฐ ์„ค๋ช…


โš™ ๊ฐœ๋ฐœํ™˜๊ฒฝ

ํ”„๋ก ํŠธ์—”๋“œ ๋ฒก์—”๋“œ ๋””์ž์ธ ๋ฐฐํฌ, ๊ด€๋ฆฌ
Html CSS JavaScript TypeScript

๐Ÿ”ฉ ๋ฒก์—”๋“œ ๊ตฌ์„ฑ

  • TheMovieDBAPI๋ฅผ ํ†ตํ•ด ์˜ํ™”์ •๋ณด๋ฅผ ๋ฐ›์•„์˜ค๋„๋ก ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • ๊ตฌํ˜„ ๊ธฐ๋Šฅ๋“ค(๋กœ๊ทธ์ธ, ์†Œ์…œ๋กœ๊ทธ์ธ, ํšŒ์›๊ฐ€์ž…, ์ด๋ฉ”์ผ|๋น„๋ฐ€๋ฒˆํ˜ธ ์ฐพ๊ธฐ, ์ฐœ, ๋ฆฌ๋ทฐ ๊ด€๋ จ ๊ธฐ๋Šฅ, ํ”„๋กœํ•„ ๋ณ€๊ฒฝ, ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ, ๋‚˜์˜ ์ฐœ ๋ชฉ๋ก, ๋‚˜์˜ ๋ฆฌ๋ทฐ ๋ชฉ๋ก, ๋กœ๊ทธ์•„์›ƒ)์€ firebase๋ฅผ ์ด์šฉํ•˜์—ฌ ๊ตฌํ˜„ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

โ›“ node_modules

๋ชจ๋“ˆ๋ช… ์šฉ๋„
axios ์„œ๋ฒ„ ํ†ต์‹ 
browser-image-compression ์ด๋ฏธ์ง€ ์••์ถ•
history ๋ชจ๋ฐ”์ผ ๋’ค๋กœ๊ฐ€๊ธฐ ๊ตฌํ˜„
lodash debouncing, throttling ์‚ฌ์šฉ
react-rotuer-dom ๋ผ์šฐํŒ… ๊ตฌํ˜„
react-intersetion-observer ๋ฌดํ•œ ์Šคํฌ๋กค ๊ตฌํ˜„
react-responsive ๋ฐ˜์‘ํ˜• ๊ตฌํ˜„
react-device-detect ๋ฐ˜์‘ํ˜• ๊ตฌํ˜„
redux ์ƒํƒœ๊ด€๋ฆฌ
redux-toolkit redux ํŽธ์˜์„ฑ, redux-toolkit thunk ์‚ฌ์šฉ
uuid ๊ณ ์œ  ์•„์ด๋”” ์ƒ์„ฑ
swiper ์Šฌ๋ผ์ด๋” ๊ตฌํ˜„
sweetAlert alert, confirm ์ปค์Šคํ…€

๐Ÿ›  ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌ

  • GitHub Issue
    • ๋น ๋ฅธ issue ์ƒ์„ฑ์„ ์œ„ํ•ด issue ํ…œํ”Œ๋ฆฟ์„ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.
    • issue label์„ ์ƒ์„ฑํ•˜์—ฌ ์–ด๋–ค ์ž‘์—…์„ ํžˆ๋Š”์ง€ ๊ตฌ๋ถ„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
    • issue๋ฅผ ํ†ตํ•ด ๊ตฌํ˜„ํ•  ๋‚ด์šฉ๊ณผ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ๋งŒ๋“ค์–ด ์–ด๋–ค ์ž‘์—…์„ ํ• ์ง€ ๋ฆฌ์ŠคํŠธ ๋งŒ๋“ค์–ด ๊ด€๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค.

issue

  • GitHub Project
    • ํ”„๋กœ์ ํŠธ ๋ณด๋“œ์˜ ์ด์Šˆ ๋ชฉ๋ก์„ ํ†ตํ•ด ๊ฐœ๋ฐœ ๊ณผ์ •๊ณผ ์ง„ํ–‰ ์ƒํ™ฉ์„ ํ•œ ๋ˆˆ์— ์•Œ์•„ ๋ณผ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

project

๐Ÿ“ƒ GitHub ์ปจ๋ฒค์…˜

์–ด๋–ค ์ž‘์—…์„ ํ–ˆ๋Š”์ง€ ํŒŒ์•…ํ•˜๊ธฐ ์œ„ํ•ด ์ปจ๋ฒค์…˜์„ ์ •ํ•˜์—ฌ commit๊ณผ isuue๋ฅผ ๊ด€๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค.

Fix : ์ˆ˜์ •์‚ฌํ•ญ๋งŒ ์žˆ์„ ๊ฒฝ์šฐ

Feat : ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์ด ์ถ”๊ฐ€ ๋˜๊ฑฐ๋‚˜ ์—ฌ๋Ÿฌ ๋ณ€๊ฒฝ ์‚ฌํ•ญ๋“ค์ด ์žˆ์„ ๊ฒฝ์šฐ

Style : ์Šคํƒ€์ผ๋งŒ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๊ฒฝ์šฐ

Docs : ๋ฌธ์„œ๋ฅผ ์ˆ˜์ •ํ•œ ๊ฒฝ์šฐ

Refactor : ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง์„ ํ•˜๋Š” ๊ฒฝ์šฐ

Remove : ํŒŒ์ผ์„ ์‚ญ์ œํ•˜๋Š” ์ž‘์—…๋งŒ ์ˆ˜ํ–‰ํ•œ ๊ฒฝ์šฐ

Rename : ํŒŒ์ผ ํ˜น์€ ํด๋”๋ช…์„ ์ˆ˜์ •ํ•˜๊ฑฐ๋‚˜ ์˜ฎ๊ธฐ๋Š” ์ž‘์—…๋งŒ์ธ ๊ฒฝ์šฐ

Relese : ๋ฐฐํฌ ๊ด€๋ จ ์ž‘์—…์ธ ๊ฒฝ์šฐ

Chore : ๊ทธ ์™ธ ๊ธฐํƒ€ ์‚ฌํ•ญ์ด ์žˆ์„ ๊ฒฝ์šฐ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.


๐Ÿ“ ๊ตฌํ˜„ ๊ธฐ๋Šฅ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ( ์ œ๋ชฉ ํด๋ฆญ ์‹œ ํ•ด๋‹น ๊ธฐ๋Šฅ ์ƒ์„ธ์„ค๋ช…์œผ๋กœ ์ด๋™๋ฉ๋‹ˆ๋‹ค. )

๐Ÿ”—์‹œ์ž‘ ํ™”๋ฉด ๐Ÿ”—๋กœ๊ทธ์ธ ๐Ÿ”—์†Œ์…œ ๋กœ๊ทธ์ธ
splash ๋กœ๊ทธ์ธ ์†Œ์…œ๋กœ๊ทธ์ธ
๐Ÿ”—๋ฉ”์ธ ํŽ˜์ด์ง€ ๐Ÿ”—์˜ํ™”์ •๋ณด-๋ฆฌ๋ทฐ(์ž‘์„ฑ,์ˆ˜์ •,์‚ญ์ œ,์‹ ๊ณ ) ๐Ÿ”—์˜ํ™”์ •๋ณด-์ฐœ, ๊ด€๋ จ์˜์ƒ
๋ฉ”์ธํŽ˜์ด์ง€ ์˜ํ™”์ •๋ณด-๋ฆฌ๋ทฐ(์ž‘์„ฑ,์ˆ˜์ •,์‚ญ์ œ,์‹ ๊ณ ) ์˜ํ™”์ •๋ณด-์ฐœ,๊ด€๋ จ์˜์ƒ
๐Ÿ”—์˜ํ™”์ •๋ณด-์Šคํฌ์ผ๋Ÿฌ, ํ•„ํ„ฐ ๐Ÿ”—๋งˆ์ดํŽ˜์ด์ง€-์ฐœ ๋ชฉ๋ก ๐Ÿ”—๋งˆ์ดํŽ˜์ด์ง€-๋ฆฌ๋ทฐ ๋ชฉ๋ก
์˜ํ™”์ •๋ณด-์Šคํฌ์ผ๋Ÿฌ,ํ•„ํ„ฐ ๊ฒ€์ƒ‰ํŽ˜์ด์ง€ ๋งˆ์ดํŽ˜์ด์ง€-์ฐœ
๐Ÿ”—๋งˆ์ดํŽ˜์ด์ง€-์ฐœ ๐Ÿ”—๋งˆ์ดํŽ˜์ด์ง€-ํ”„๋กœํ•„๋ณ€๊ฒฝ ๐Ÿ”—๋งˆ์ดํŽ˜์ด์ง€-๋น„๋ฐ€๋ฒˆํ˜ธ๋ณ€๊ฒฝ
๋งˆ์ดํŽ˜์ด์ง€-๋ฆฌ๋ทฐ ๋งˆ์ดํŽ˜์ด์ง€-ํ”„๋กœํ•„๋ณ€๊ฒฝ ๋งˆ์ดํŽ˜์ด์ง€-๋น„๋ฐ€๋ฒˆํ˜ธ๋ณ€๊ฒฝ

๐Ÿ”Ž ์ฃผ์š” ๊ธฐ๋Šฅ ์ฝ”๋“œ ๋ฐ ์„ค๋ช…

(1) customAxios

axios๋ฅผ customํ•˜์—ฌ baseURL๋ฅผ ์„ค์ •ํ•˜์—ฌ URL ์ค‘๋ณต ์„ค์ •์„ ํ”ผํ•˜๊ณ , ์ฝ”๋“œ๋ฅผ ๋‹จ์ถ•์‹œ์ผฐ์Šต๋‹ˆ๋‹ค.

import axios from "axios";

export const customAxios = axios.create({
  baseURL: process.env.REACT_APP_BASE_URL,
});

(2) API ํŒŒ์ผ

์‚ฌ์šฉํ•  API๋ฅผ ํ•˜๋‚˜์˜ ํŒŒ์ผ๋กœ ๋งŒ๋“ค์–ด์„œ ์ฝ”๋“œ ์ค‘๋ณต ์‚ฌ์šฉ์„ ํ”ผํ•˜๊ณ , ์ฝ”๋“œ๋ฅผ ๋‹จ์ถ• ์‹œ์ผœ, ์œ ์ง€๋ณด์ˆ˜๋ฅผ ์šฉ์ดํ•˜๊ฒŒ ํ•˜์˜€์Šต๋‹ˆ๋‹ค. movieAPI

import { customAxios } from "./customAxios";

// api_key, language params๋กœ ์„ค์ •
const api_key = process.env.REACT_APP_THEMOVIEDB_API_KEY;
const language = "ko-KR";

// ์˜ํ™” ๋น„๋””์˜ค ์ •๋ณด๊ฐ€ ํฌํ•จ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” API
export const getVideoData = async (id) => {
    const video = await customAxios.get(
      `movie/${id}`,
      {
        params: {
          api_key,
          language,
          append_to_response: "videos",
        },
      }
    );
    return video.data;
};

// Banner ํ™”๋ฉด์— ๋‚˜ํƒ€๋‚  ์˜ํ™” ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” API
export const getNowPlayingMovie = async () => {
    const res = await customAxios.get(`movie/now_playing`, {
      params: {
        api_key,
        language,
      },
    });
    const movieId =
      res.data.results[Math.floor(Math.random() * res.data.results.length)].id;
    return getVideoData(movieId);
};

// ์ตœ์‹ ์˜ ์˜ํ™” ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” API
export const getTrendingMovies = async (page = 1) => {
    const res = await customAxios.get(`/trending/movie/week`, {
      params: {
        api_key,
        language,
        page,
      },
    });
    return res.data.results;
};

// ์˜ํ™” ์ˆœ์œ„๊ฐ€ ๋†’์€ ์ˆœ์„œ๋Œ€๋กœ ์˜ํ™” ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” API
export const getTopRatedMovies = async (page = 1) => {
    const res = await customAxios.get("/movie/top_rated", {
      params: {
        api_key,
        language,
        page,
      },
    });
    return res.data.results;
};

// Action ์˜ํ™” ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” API
export const getActionMovies = async (page = 1) => {
    const res = await customAxios.get("/discover/movie?with_genres=28", {
      params: {
        api_key,
        language,
        page,
      },
    });
    return res.data.results;
};

// Comedy ์˜ํ™” ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” API
export const getComedyMovies = async (page = 1) => {
    const res = await customAxios.get("/discover/movie?with_genres=35", {
      params: {
        api_key,
        language,
        page,
      },
    });
    return res.data.results;
};

// Horror ์˜ํ™” ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” API
export const getHorrorMovies = async (page = 1) => {
    const res = await customAxios.get("/discover/movie?with_genres=27", {
      params: {
        api_key,
        language,
        page,
      },
    });
    return res.data.results;
};

// Romance ์˜ํ™” ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” API
export const getRomanceMovies = async (page = 1) => {
    const res = await customAxios.get("/discover/movie?with_genres=10749", {
      params: {
        api_key,
        language,
        page,
      },
    });
    return res.data.results;
};

// Documentary ์˜ํ™” ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” API
export const getDocumentaryMovies = async (page = 1) => {
    const res = await customAxios.get("/discover/movie?with_genres=99", {
      params: {
        api_key,
        language,
        page,
      },
    });
    return res.data.results;
};

// ๊ฒ€์ƒ‰ํ•œ ์˜ํ™” ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” API
export const getSearchData = async (keyword, page) => {
    const res = await customAxios.get(
      `/search/movie?include_adult=false&query=${keyword}`,
      {
        params: {
          api_key,
          language,
          page,
        },
      }
    );
    return res.data.results;
};

(3) ๋ฆฌ๋ทฐ-์ž‘์„ฑ, ์ˆ˜์ •, ์‚ญ์ œ, ์‹ ๊ณ 

๋ฆฌ๋ทฐ์— ๊ด€๋ จ๋œ ๊ธฐ๋Šฅ์€ firebase firestore๋ฅผ ํ†ตํ•ด db๋ฅผ ์„ค๊ณ„ํ•˜๊ณ  ์ง์ ‘ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

  • ๋ฆฌ๋ทฐ๋ชฉ๋ก์˜ db๊ตฌ์กฐ

    • reviewList ์ปฌ๋ ‰์…˜ ์•„๋ž˜ docs id๋กœ๋Š” movieId๋ฅผ ๋‘์–ด ๊ฐ ์˜ํ™” ๋ฐ์ดํ„ฐ๋ฅผ ๊ตฌ๋ถ„ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.
    • movieId Docs ์•„๋ž˜๋กœ ์˜ํ™”์— ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ๋“ค์ด ๋“ค์–ด๊ฐ€๋Š” review subColletion์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.
    • MovieInfo ๋ชจ๋‹ฌ ์ฐฝ์˜ ๋ฆฌ๋ทฐ๋ชฉ๋ก์„ ๋ฌดํ•œ์Šคํฌ๋กค ํŽ˜์ด์ง• ๊ธฐ๋Šฅ์œผ๋กœ ๋ฐ›์•„์˜ค๊ธฐ ์œ„ํ•ด subCollection์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์„ค๊ณ„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
    • subColletion ์•„๋ž˜ docs id๋กœ commentId๋ฅผ ์ฃผ์–ด reivew ๋ฐ์ดํ„ฐ๋ฅผ ๊ตฌ๋ถ„ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.
    • docs ์•„๋ž˜๋กœ๋Š” ๋ฆฌ๋ทฐ ์ •๋ณด๊ฐ€ ์ €์žฅ๋˜๋Š” field๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

ย ย  reviewList_db๊ตฌ์กฐ

  • ์œ ์ €์˜ db๊ตฌ์กฐ

    • user ์ปฌ๋ ‰์…˜ ์•„๋ž˜ docs id๋กœ ๊ฐ ๊ณ„์ •์˜ uid๋ฅผ ์ฃผ์–ด user ๋ฐ์ดํ„ฐ๋ฅผ ๊ตฌ๋ถ„ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.
    • docs ์•„๋ž˜๋กœ๋Š” user๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด๊ฐ€๋Š” field์™€ ๋ฆฌ๋ทฐ ๋ชฉ๋ก์˜ ์˜ํ™” ์ •๋ณด๊ฐ€ ๋“ค์–ด๊ฐ€๋Š” reviewListMovieInfo subCollection์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.
    • ๋งˆ์ดํŽ˜์ด์ง€์—์„œ ๋ฆฌ๋ทฐ๋ชฉ๋ก์—์„œ ์˜ํ™”๋ฐ์ดํ„ฐ๋ฅผ ๋ฌดํ•œ์Šคํฌ๋กค ํŽ˜์ด์ง• ๊ธฐ๋Šฅ์œผ๋กœ ๋ฐ›์•„์˜ค๊ธฐ ์œ„ํ•ด subColletion์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์„ค๊ณ„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
    • subColletion ์•„๋ž˜ docs id๋กœ commentId๋ฅผ ์ฃผ์–ด reivew ์˜ํ™” ๋ฐ์ดํ„ฐ๋ฅผ ๊ตฌ๋ถ„ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.
    • docs ์•„๋ž˜๋กœ๋Š” ์˜ํ™” ์ •๋ณด๊ฐ€ ์ €์žฅ๋˜๋Š” field๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

ย ย  user_db๊ตฌ์กฐ

  • ๋ฆฌ๋ทฐ์ž‘์„ฑ API์˜ ์„ธ ๊ฐ€์ง€ ์ฒ˜๋ฆฌ

    • reviewList docs ์ค‘ ํ•ด๋‹น ์˜ํ™”์— subColletion ์•„๋ž˜ docs์— ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑ
    • ๋งˆ์ดํŽ˜์ด์ง€์˜ ๋ฆฌ๋ทฐ๋ชฉ๋ก์—์„œ ํ•ด๋‹น ์˜ํ™” ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์œ„ํ•ด ๋ฆฌ๋ทฐํ•œ user ๋ฌธ์„œ์˜ ์•„๋ž˜ subCollection์ธ reviewListMovieInfo ์•„๋ž˜ docs์— ๋ฆฌ๋ทฐํ•œ ์˜ํ™”์˜ ์ •๋ณด๋ฅผ ์ €์žฅ
    • ํ•ด๋‹น ์œ ์ €๊ฐ€ ํ•ด๋‹น ์˜ํ™”์˜ ๋ฆฌ๋ทฐ ์œ ๋ฌด ํŒŒ์•…ํ•˜๊ธฐ ์œ„ํ•ด ์œ ์ € db์— reviewList์— ๋ฆฌ๋ทฐํ•œ movieID๋ฅผ ์ถ”๊ฐ€

=> ์œ„ ์„ธ ๊ฐ€์ง€ ์ž‘์—…๋“ค์€ ์ˆœ์„œ๊ฐ€ ์ƒ๊ด€์—†๊ธฐ ๋•Œ๋ฌธ์— ๋น ๋ฅธ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด promise.all๋ฅผ ์ด์šฉํ•˜์—ฌ ๋น„๋™๊ธฐ ์ž‘์—…๋“ค์„ ๋ณ‘๋ ฌ์ฒ˜๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค.

// ๋ฆฌ๋ทฐ ์ž‘์„ฑ API
export const addReview = async (movieData, reviewData) => {
    // reviewList ํ•ด๋‹น ์˜ํ™”์— ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€
    const reviewListRef = collection(db, "reviewList");
    const reviewDoc = doc(reviewListRef, String(movieData.id));
    const reviewRef = collection(reviewDoc, "review");

    const addReviewPromise = setDoc(doc(reviewRef, reviewData.id), {
      id: reviewData.id,
      uid: reviewData.uid,
      rating: reviewData.rating,
      contents: reviewData.contents,
      createdAt: reviewData.createdAt,
      spoiler: reviewData.spoiler,
      isBlock: false,
      reportCount: 0,
    });

    // ์œ ์ € db subCollection์— reviewํ•œ ์˜ํ™” ์ •๋ณด ์ €์žฅ
    const UserReviewListRef = collection(db, "userReviewList");
    const UserReviewListDoc = doc(UserReviewListRef, auth.currentUser.uid);
    const UserReviewRef = collection(UserReviewListDoc, "reviewMovie");

    const addUserReviewListPromise = setDoc(
      doc(UserReviewRef, reviewData.id),
      movieData
    );

    // ์œ ์ € db reviewList์— ๋ฆฌ๋ทฐํ•œ movieId ์ถ”๊ฐ€
    const userRef = doc(db, `user/${auth.currentUser.uid}`);
    const addUserReivewPromise = updateDoc(userRef, {
      reviewList: arrayUnion(movieData.id),
    });

    // ์ˆœ์„œ์— ์ƒ๊ด€์—†๋Š” ์ž‘์—… ๋ณ‘๋ ฌ์ฒ˜๋ฆฌ
    await Promise.all([
      addReviewPromise,
      addUserReviewListPromise,
      addUserReivewPromise,
    ]);
};
  • ๋ฆฌ๋ทฐ์‚ญ์ œ

    • ๋ฆฌ๋ทฐ์‚ญ์ œ API ๋˜ํ•œ ๋ฆฌ๋ทฐ ์ž‘์„ฑ์˜ ์œ„ ์„ธ ๊ฐ€์ง€ ์ž‘์—…๊ณผ ๊ฐ™์€ ์ž‘์—…์„ ํ†ตํ•ด ๋ฆฌ๋ทฐ๋ฅผ ์‚ญ์ œ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
// ๋ฆฌ๋ทฐ ์‚ญ์ œ API
export const removeReview = async (movieId, reviewId) => {
     // reviewList ํ•ด๋‹น ์˜ํ™”์— ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ ์‚ญ์ œ
    const reviewListRef = collection(db, "reviewList");
    const reviewDoc = doc(reviewListRef, String(movieId));
    const reviewRef = collection(reviewDoc, "review");
    const deleteReviewRef = doc(reviewRef, reviewId);
    const deleteReviewPromise = deleteDoc(deleteReviewRef);

    const UserRef = collection(db, "user");
    const UserReviewListMovieInfoDoc = doc(UserRef, auth.currentUser.uid);
    const UserReviewMovieInfoRef = collection(
      UserReviewListMovieInfoDoc,
      "reviewListMovieInfo"
    );

    // ์œ ์ € db subCollection์— reviewํ•œ ์˜ํ™” ์ •๋ณด ์‚ญ์ œ
    const deleteUserReviewListRef = doc(UserReviewMovieInfoRef, reviewId);
    const deleteUserReviewListPromise = deleteDoc(deleteUserReviewListRef);

    // ์œ ์ € db reviewList์— ๋ฆฌ๋ทฐํ•œ movieId ์‚ญ์ œ
    const userRef = doc(db, `user/${auth.currentUser.uid}`);
    const deleteUserReviewPromise = updateDoc(userRef, {
      reviewList: arrayRemove(movieId),
    });

    // ์ˆœ์„œ์— ์ƒ๊ด€์—†๋Š” ์ž‘์—… ๋ณ‘๋ ฌ์ฒ˜๋ฆฌ
    await Promise.all([
      deleteReviewPromise,
      deleteUserReviewListPromise,
      deleteUserReviewPromise,
    ]);
};
  • ๋ฆฌ๋ทฐ ์ˆ˜์ •

    • reviewList ํ•˜์œ„์˜ ํ•ด๋‹น๋˜๋Š” subColletion docs field์˜ rating, contents, spoiler ๊ฐ’์„ ์ˆ˜์ •
// ๋ฆฌ๋ทฐ ์ˆ˜์ • API
export const editReview = async (movieId, editData) => {
    const reviewListRef = collection(db, "reviewList");
    const reviewDoc = doc(reviewListRef, String(movieId));
    const reviewRef = collection(reviewDoc, "review");
    const updateReviewRef = doc(reviewRef, editData.id);

    await updateDoc(updateReviewRef, {
      rating: editData.rating,
      contents: editData.contents,
      spoiler: editData.spoiler,
    });
};
  • ๋ฆฌ๋ทฐ ์‹ ๊ณ 

    • reviewList ํ•˜์œ„์˜ ํ•ด๋‹น๋˜๋Š” subColletion์˜ docs field์˜ reportCount ๊ฐ’์„ 1์”ฉ ์ฆ๊ฐ€ ์‹œํ‚ต๋‹ˆ๋‹ค.
    • ๋งŒ์•ฝ reportCount๊ฐ€ 5 ์ด์ƒ์ธ ๊ฒฝ์šฐ isBlock๋ฅผ true์ฃผ์–ด ๋” ์ด์ƒ ๋ฆฌ๋ทฐ๋ชฉ๋ก์— ์ถœ๋ ฅ๋˜์ง€ ์•Š๋„๋ก block ์ฒ˜๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค.
// ๋ฆฌ๋ทฐ ์‹ ๊ณ  API
export const reviewReport = async (movieId, reviewData) => {
    // reviewList ์ปฌ๋ ‰์…˜์˜ ํ•ด๋‹น docsId(movieId)์˜ subCollection์˜ docsId(reviewId) ํ•ด๋‹น๋˜๋Š” ๋ฐ์ดํ„ฐ
    const reviewListRef = collection(db, "reviewList");
    const reviewDoc = doc(reviewListRef, String(movieId));
    const reviewRef = collection(reviewDoc, "review");
    const updateReviewRef = doc(reviewRef, reviewData.id);

    // ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ์˜ ์‹ ๊ณ ์ˆ˜๋ฅผ 1 ๋”ํ•ด์คŒ
    // ๋งŒ์•ฝ ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ์˜ ์‹ ๊ณ ์ˆ˜๊ฐ€ 5 ์ด์ƒ์ด ๋œ๋‹ค๋ฉด(ํ˜„์žฌ 4 => ํด๋ฆญ์‹œ 5์ด๋ฏ€๋กœ >=4 ์กฐ๊ฑด์‹ ์‚ฌ์šฉ) ํ•ด๋‹น ๋ฆฌ๋ทฐ block ์ฒ˜๋ฆฌ
    const reportReviewPromise = updateDoc(updateReviewRef, {
      isBlock: reviewData.reportCount >= 4 ? true : false,
      reportCount: increment(1),
    });

    // ์œ ์ €๊ฐ€ ์‹ ๊ณ ํ•œ ๋ฆฌ๋ทฐ์ธ์ง€ ์•„๋‹Œ์ง€ ํŒ๋‹จํ•˜๊ธฐ ์œ„ํ•ด reportList์— ํ•ด๋‹น reviewID๋ฅผ ์ถ”๊ฐ€
    const userRef = doc(db, `user/${auth.currentUser.uid}`);
    const addReportListPromise = updateDoc(userRef, {
      reportList: arrayUnion(reviewData.id),
    });
    await Promise.all[(reportReviewPromise, addReportListPromise)];
};

(4) ๋ฆฌ๋ทฐ ์Šคํฌ์ผ๋Ÿฌ ์ฒดํฌ ๊ธฐ๋Šฅ

  • ๋ฆฌ๋ทฐ ์ž‘์„ฑ์‹œ ์Šคํฌ์ผ๋Ÿฌ๊ฐ€ ํฌํ•จ๋œ ๊ธ€์„ ์ž‘์„ฑํ•˜๊ณ  ์‹ถ์€ ์œ ์ €๊ฐ€ ์กด์žฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์Šคํฌ์ผ๋Ÿฌ๊ฐ€ ํฌํ•จ๋œ ๋ฆฌ๋ทฐ๋ฅผ ์ž‘์„ฑ์‹œ์— ์Šคํฌ์ผ๋Ÿฌ๊ฐ€ ํฌํ•จ๋˜๋Š” ๊ธ€์ด ์žˆ๋‹ค๋Š” ์ฒดํฌ ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค์–ด ๋‹ค๋ฅธ ์œ ์ €๊ฐ€ ๋ฆฌ๋ทฐ๋ฅผ ๋ณผ ๋•Œ ๋ธ”๋ผ์ธ๋“œ ์ฒ˜๋ฆฌ ๋˜๋„๋ก ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

ย ย  ์Šคํฌ์ผ๋Ÿฌ ์ž‘์„ฑ

  • spoiler๋ผ๋Š” ๊ฐ’์„ ํ†ตํ•ด ํ•ด๋‹น ๋ฆฌ๋ทฐ๊ฐ€ spoiler๊ฐ€ ํฌํ•จ๋œ ๋ฆฌ๋ทฐ์ธ์ง€ ์•„๋‹Œ์ง€๋ฅผ ๊ตฌ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.
  • spoiler๊ฐ€ true ๊ฐ’์ด๋ฉด ๊ธฐ์กด contents ๋Œ€์‹  '์Šคํฌ์ผ๋Ÿฌ๊ฐ€ ํฌํ•จ๋œ ๋ฆฌ๋ทฐ์ž…๋‹ˆ๋‹ค.'๋ฅผ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.
  • ์˜†์— ์ œ๊ณต๋œ ๋ณด๊ธฐ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅผ ์‹œ ํ•ด๋‹น contents๊ฐ€ ์ถœ๋ ฅ๋˜๋„๋ก ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

ย ย  ์Šคํฌ์ผ๋Ÿฌ ๋ณด๊ธฐ

์•„๋ž˜ ์ฝ”๋“œ์—์„œ๋Š” reviewItem.spoiler๋ฅผ ํ†ตํ•ด ์Šคํฌ์ผ๋Ÿฌ๊ฐ€ ํฌํ•จ๋œ ๋ฆฌ๋ทฐ์ธ์ง€ ์•„๋‹Œ์ง€๋ฅผ ๊ตฌ๋ถ„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

showSpoiler ์ƒํƒœ๋ฅผ ํ†ตํ•ด ์Šคํฌ์ผ๋Ÿฌ ๋ฆฌ๋ทฐ๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

=> showSpoiler true ์ผ์‹œ '์Šคํฌ์ผ๋Ÿฌ๊ฐ€ ํฌํ•จ๋œ ๋ฆฌ๋ทฐ์ž…๋‹ˆ๋‹ค'๊ฐ€ ์ถœ๋ ฅ๋ฉ๋‹ˆ๋‹ค.

=> showSpoiler false ์ผ์‹œ ์›๋ž˜ ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ์˜ contents ๊ฐ’์œผ๋กœ ๋ณ€๊ฒฝํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 //                                 .
 //                                 .
 //                                 .
 //                               (์ƒ๋žต)

 <ReviewContents inactive={isBlock||(reviewItem.spoiler && !showSpoilerData)}>
            {reviewItem.isBlock
              ? "์‹ ๊ณ ์— ์˜ํ•ด ๋ธ”๋ผ์ธ๋“œ ์ฒ˜๋ฆฌ๋œ ๋ฆฌ๋ทฐ์ž…๋‹ˆ๋‹ค."
              : reviewItem.spoiler
              ? showSpoilerData
                ? reviewItem.contents
                : "์Šคํฌ์ผ๋Ÿฌ๊ฐ€ ํฌํ•จ๋œ ๋ฆฌ๋ทฐ์ž…๋‹ˆ๋‹ค."
              : reviewItem.contents}
            {reviewItem.spoiler && !showSpoilerData && (
              <ShowSpoilerBtn
                onClick={() => {
                  sweetConfirm(
                    "์Šคํฌ์ผ๋Ÿฌ๊ฐ€ ํฌํ•จ๋œ ๋ฆฌ๋ทฐ์ž…๋‹ˆ๋‹ค.\n์ •๋ง ํ™•์ธ ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?",
                    "ํ™•์ธ",
                    "์ทจ์†Œ",
                    () => setShowSpoilerData(true)
                  );
                }}
              >
                ๋ณด๊ธฐ
              </ShowSpoilerBtn>
            )}
</ReviewContents>

//                                (์ƒ๋žต)
//                                  .
//                                  .
//                                  .

(5) ๋ฌดํ•œ ์Šคํฌ๋กค

  • ๋ฌดํ•œ์Šคํฌ๋กค์„ ์ด์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ์ผ๋ถ€๋งŒ ๊ฐ€์ ธ์™€ ์„œ๋ฒ„์˜ ๋ถ€๋‹ด์„ ์ค„์ด๊ณ  ๋กœ๋”ฉ์†๋„๋ฅผ ๊ฐœ์„ ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • react-intersection-observer ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ด์šฉํ•˜์—ฌ ๋ฌดํ•œ์Šคํฌ๋กค์„ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • react-intersection-observer ๋ผ์ด๋ธŒ๋Ÿฌ์ด์˜ useView()์˜ ref๊ฐ’์„ ๊ด€์ฐฐ์š”์†Œ ref๊ฐ’์— ๋„ฃ์œผ๋ฉด ๊ด€์ฐฐ์š”์†Œ๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋งŒ์•ฝ ๊ด€์ฐฐ์š”์†Œ๊ฐ€ ํ™”๋ฉด ์ถœ๋ ฅ๋˜๋ฉด inView true๋กœ ํ™”๋ฉด์—์„œ ์‚ฌ๋ผ์ง„๋‹ค๋ฉด false๋กœ ๋ณ€๊ฒฝ๋˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
  • useEffect hook๋ฅผ ์ด์šฉํ•˜์—ฌ inView๊ฐ€ true ์ƒํƒœ์ผ ๋•Œ ๋‹ค์Œ ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ค๋„๋ก ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • hasMore๋ฅผ ํ†ตํ•ด ๋‹ค์Œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๋‹ค๋ฉด api ์š”์ฒญ์„ ์ผ์–ด๋‚˜์ง€ ์•Š๊ฒŒ ์กฐ๊ฑด์„ ๊ฑธ์–ด ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.
  • hasMore๋Š” ํ˜„์žฌ API์—์„œ ๋ฐ›์•„์˜จ ๋ฐ์ดํ„ฐ์˜ length์™€ limt๊ฐ€ ๊ฐ™์€์ง€ ๋น„๊ตํ•˜์—ฌ ๋‹ค์Œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š”์ง€ ํŒ๋‹จํ•ด์ค๋‹ˆ๋‹ค.
  • API ์š”์ฒญ์„ ๋ณด๋‚ผ ๋•Œ ๋งˆ๋‹ค page(res.docs[res.docs.length - 1] : ํ˜„์žฌ ๋ฐ›์•„์˜จ ๋ฐ์ดํ„ฐ docs์˜ ๊ฐ€์žฅ ๋งˆ์ง€๋ง‰ docs)๊ฐ’์„ ํŒŒ์•…ํ•˜์—ฌ firebase์˜ startAfter(ํ•ด๋‹น docs ์ดํ›„ docs๋ฅผ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” query) query๋ฅผ ์ด์šฉํ•˜์—ฌ ๋‹ค์Œ ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
// MypageMenu ์ปดํฌ๋„ŒํŠธ
import React, { useEffect, useState } from "react";
import { useMovieInfo } from "../../hook/useMovieInfo";
import { useInView } from "react-intersection-observer";
import { useMediaQuery } from "react-responsive";

import {
  fetchFirstLikeList,
  fetchLikeListPage,
  removeLike,
} from "../../firebase/likeAPI";
import {
  fetchFirstReviewMovieList,
  fetchReviewMovieListPage,
} from "../../firebase/reviewAPI";

import {
  InfiniteScrollTarget,
  MoiveListWrapper,
  MovieImgWrapper,
  MovieItem,
  MovieMenuBtn,
  MovieMenuItem,
  MovieMenuNav,
  MovieMenuTitle,
  MovieMenuUl,
  MovieMenuWrapper,
  MovieTitle,
  RemoveBtn,
} from "./mypage.style";
import ProgressiveImg from "../../compoents/commons/progressiveImg/ProgressiveImg";
import Blank from "../../compoents/commons/blank/Blank";
import Loading from "../../compoents/commons/loading/Loading";
import MovieInfo from "../../compoents/commons/Modal/MovieInfo.container";
import { sweetToast } from "../../sweetAlert/sweetAlert";

export default function MypageMenu() {
  const [data, setData] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [isOpenMovieInfo, setIsOpenMovieInfo, seletedMovie, onClickMovieInfo] =
    useMovieInfo(false);
  const [page, setPage] = useState("");
  const [hasMore, setHasMore] = useState(false);
  const limitPage = 20;
  const [ref, inview] = useInView();
  const [menu, setMenu] = useState("like");
  const [notData, setNotData] = useState(true);
  const isMoblie = useMediaQuery({ query: "(max-width:486px)" });

  const fetchFirstPage = async () => {
    setNotData(true);
    // menu๊ฐ€ like ์ผ์‹œ ์ข‹์•„์š” ๋ชฉ๋ก์„ ๋ฐ›์•„์˜ด
    // like๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉด ๋ฆฌ๋ทฐ ๋ชฉ๋ก์„ ๋ฐ›์•„์˜ด
    const res =
      menu === "like"
        ? await fetchFirstLikeList(limitPage)
        : await fetchFirstReviewMovieList(limitPage);
    const data = res.docs.map((el) => el.data());
    setData((prev) => [...prev, ...data]);
    setPage(res.docs[res.docs.length - 1]);
    setHasMore(res.docs.length === limitPage);
    setIsLoading(false);
    setNotData(false);
  };

  // ๋‹ค์Œ ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ด
  const fetchAddData = async () => {
    const res =
      menu === "like"
        ? await fetchLikeListPage(page, limitPage)
        : await fetchReviewMovieListPage(page, limitPage);
    const data = res.docs.map((el) => el.data());
    setData((prev) => [...prev, ...data]);
    setPage(res.docs[res.docs.length - 1]);
    setHasMore(res.docs.length === limitPage);
  };

  // menu ์ƒํƒœ๊ฐ€ ๋ฐ”๋€” ๋•Œ ๋งˆ๋‹ค ๋ฐ์ดํ„ฐ๋ฅผ ์ดˆ๊ธฐํ™”
  useEffect(() => {
    setData([]);
    fetchFirstPage();
  }, [menu]);

 // inView ์ƒํƒœ๊ฐ€ ๋ฐ”๋€” ๋•Œ ๋งˆ๋‹ค ๋‹ค์Œ ๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•œ๋‹ค๋ฉด ๋‹ค์Œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ด
  useEffect(() => {
    if (hasMore && inview) {
      fetchAddData();
    }
  }, [inview]);

//                                      (์ƒ๋žต)
//                                        .
//                                        .
//                                        .
                                    
  • ๋ฌดํ•œ ์Šคํฌ๋กค ์ ์šฉ ํ™”๋ฉด

ย ย  ๋ฌดํ•œ์Šคํฌ๋กค


(6) ๊ฒ€์ƒ‰ ๋””๋ฐ”์šด์‹ฑ

  • ๊ธฐ์กด ๊ฒ€์ƒ‰์‹œ onChange ์ด๋ฒคํŠธ์—์„œ input์˜ ๋ณ€ํ™”๊ฐ€ ๊ฐ์ง€๋  ๋•Œ ๋งˆ๋‹ค API์š”์ฒญ์ด ๋ฐœ์ƒํ•˜์—ฌ ๋ถˆํ•„์š”ํ•œ API ์š”์ฒญ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.
  • debunce์„ ํ†ตํ•ด ์„ค์ •ํ•œ ์‹œ๊ฐ„์ด ๊ฒฝ๊ณผํ•œ ์ดํ›„ ์ด๋ฒคํŠธ๊ฐ€ ํ˜ธ์ถœ๋˜์ง€ ์•Š์„ ๋•Œ ์ด๋ฒคํŠธ๋ฅผ ํ•œ ๋ฒˆ๋งŒ ํ˜ธ์ถœํ•˜๊ฒŒ ํ•ด์ฃผ์–ด ๋ถˆํ•„์š”ํ•œ API ํ˜ธ์ถœ์„ ๋ง‰์•„์ค๋‹ˆ๋‹ค.
  • lodash ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ debounce๋ฅผ ์ด์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • lodash debounce๋Š” ์ฒซ ๋ฒˆ์งธ ์ธ์ž๋กœ ์‹คํ–‰ํ•  ํ•จ์ˆ˜, ๋‘ ๋ฒˆ์งธ ์ธ์ž๋กœ ์‹œ๊ฐ„์„ ๋ฐ›์Šต๋‹ˆ๋‹ค.
  • debouncing ์ œ๋Œ€๋กœ ์ ์šฉ๋˜๋„๋ก useCallback๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ•จ์ˆ˜๊ฐ€ ๋‹ค์‹œ ์žฌ์ƒ์„ฑ๋˜๋Š” ๊ฒƒ์„ ๋ง‰์•„ ํ•จ์ˆ˜๊ฐ€ ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰๋˜๋„๋ก ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
 // ๊ฒ€์ƒ‰ ๋””๋ฐ”์šด์‹ฑ ์ ์šฉ ๊ฒ€์ƒ‰์–ด๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ ๊ฒ€์ƒ‰์–ด์— ํ•ด๋‹นํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅ
  const searchDebounce = useCallback(
    debounce(async (value) => {
      if (!value) {
      // ๊ฒ€์ƒ‰์–ด๊ฐ€ ์—†๋‹ค๋ฉด ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™”
        fetchFirstData();
        return;
      }
      const data = await fetchSearchMovie(value);
      setMovieData(data);
    }, 500),
    []
  );
  • ๊ฒ€์ƒ‰ ๋””๋ฐ”์šด์‹ฑ ์ ์šฉ ์ „

ย ย  ๊ฒ€์ƒ‰๋””๋ฐ”์šด์‹ฑ-์ ์šฉ์ „

  • ๊ฒ€์ƒ‰ ๋””๋ฐ”์šด์‹ฑ ์ ์šฉ ํ›„

ย ย  ๊ฒ€์ƒ‰๋””๋ฐ”์šด์‹ฑ-์ ์šฉํ›„


(7) ์ด๋ฏธ์ง€ ์ตœ์ ํ™”

โ‘  ์ด๋ฏธ์ง€ ์‚ฌ์ด์ฆˆ ์ตœ์†Œํ™”

  • ํ•„์š”ํ•œ ์ด๋ฏธ์ง€ ํฌ๊ธฐ ๋ณด๋‹ค ๋” ํฐ ์ด๋ฏธ์ง€๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ๋ฆฌ์†Œ์Šค ๋‚ญ๋น„๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.
  • ์ด๋ฏธ์ง€ ๋ฆฌ์†Œ์Šค ๋‚ญ๋น„๋ฅผ ์ตœ์†Œํ™” ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•  ํฌ๊ธฐ์— ๋งž๊ฒŒ ์ด๋ฏธ์ง€ ํฌ๊ธฐ๋ฅผ ์ตœ์†Œํ™” ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • ์ด๋ฏธ์ง€ ํ˜•์‹์€ svg ํ˜•์‹์˜ ์ด๋ฏธ์ง€๋ฅผ ์ด์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.
    • svg ํ˜•์‹ ์ด๋ฏธ์ง€๋Š” ๊ฐ„๋‹จํ•œ ์ด๋ฏธ์ง€์ธ ๊ฒฝ์šฐ png ํ˜•์‹๋ณด๋‹ค ์ด๋ฏธ์ง€ ์šฉ๋Ÿ‰์ด ์ž‘์œผ๋ฉฐ, ๋ ˆํ‹ฐ๋‚˜ ๋””์Šคํ”Œ๋ ˆ์ด์—์„œ๋„ ์ด๋ฏธ์ง€๊ฐ€ ๊นจ์ง€๋Š” ํ˜„์ƒ์ด ์—†์Šต๋‹ˆ๋‹ค.

โ‘ก imageCompression ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ด์šฉํ•˜์—ฌ ์ด๋ฏธ์ง€ ์••์ถ•

  • ์„œ๋ฒ„์— ์ด๋ฏธ์ง€๋ฅผ ์ „์†กํ•  ์‹œ ํ•„์š”ํ•œ ์ด๋ฏธ์ง€ ๋งŒํผ๋งŒ ์ตœ์†Œ๋กœ ์••์ถ•ํ•˜์—ฌ ์ด๋ฏธ์ง€ ๋ฆฌ์†Œ์Šค ๋‚ญ๋น„๋ฅผ ์ค„์ผ ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • ๋ณ„๋„์˜ imgCompression ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.
import imageCompression from "browser-image-compression"

export const imgCompression = async (file) => {
  try{
    const options = {
        maxSizeMB: 10, // ์ด๋ฏธ์ง€ ์ตœ๋Œ€ ์šฉ๋Ÿ‰
        maxWidthOrHeight: 256, // ์ด๋ฏธ์ง€ ์ตœ๋Œ€ ๋„ˆ๋น„ ๋ฐ ๋†’์ด
        useWebWorker: true, // webworker ์ ์šฉ ์œ ๋ฌด
        // webworker : ์›น ์›Œ์ปค API๊ฐ€ ๋ฉ€ํ‹ฐ ์Šค๋ ˆ๋”ฉ์„ ์ง€์›ํ•˜๊ฒŒ ๋˜์–ด ์›Œ์ปค๋ฅผ ์ด์šฉํ•˜๋ฉด ์›Œ์ปค์—์„œ ์ž‘์„ฑ๋œ ์Šคํฌ๋ฆฝํŠธ๋Š” 
        // ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ๋ถ„๊ธฐ๋˜์–ด ๋…๋ฆฝ๋œ ์Šค๋ ˆ๋“œ๋กœ ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฉ”๋ชจ๋ฆฌ ์ž์›์„ ํšจ์œจ์ ์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
    }
    // ์••์ถ•๋œ ์ด๋ฏธ์ง€ blob
    const compressedFileBlob = await imageCompression(file, options);
    // blob ํ˜•์‹์˜ ์ด๋ฏธ์ง€๋ฅผ file ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜
    const compressedFile = new File([compressedFileBlob], file.name, { type: file.type });
    // ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ด๋ฏธ์ง€๋ฅผ URL ์ƒ์„ฑ
    const preview = await imageCompression.getDataUrlFromFile(file);
    // ์••์ถ•๋œ ํŒŒ์ผ๊ณผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ด๋ฏธ์ง€๋ฅผ ๋ฐ˜ํ™˜
    return {compressedFile, preview}
  } catch(error) {
    console.log(error)
  }
}

ย ย  ์ด๋ฏธ์ง€์••์ถ•

  • ์ด๋ฏธ์ง€ ์••์ถ• ์ „

    • ์ด๋ฏธ์ง€ ํฌ๊ธฐ : 578MB
  • ์ด๋ฏธ์ง€ ์••์ถ• ํ›„

    • ์ด๋ฏธ์ง€ ํฌ๊ธฐ : 587MB => 72MB (515MB ์•ฝ 1/8 ํฌ๊ธฐ ๊ฐ์†Œ)

โ‘ข ์ ์ง„์  ๋กœ๋”ฉ ๊ธฐ๋ฒ• ๋ฐ lazy-loading๋ฅผ ํ†ตํ•œ ์ด๋ฏธ์ง€ ์ตœ์ ํ™”

  • ์ ์ง„์  ๋กœ๋”ฉ ๊ธฐ๋ฒ•๋ฅผ ํ†ตํ•ด ์ด๋ฏธ์ง€๊ฐ€ ๋กœ๋”ฉ๋  ๋•Œ ์›๋ณธ ์ด๋ฏธ์ง€ ๋Œ€์‹  ์ €ํ™”์งˆ์˜ ์ด๋ฏธ์ง€๋ฅผ ๋ณด์—ฌ์คŒ์œผ๋กœ์จ UX๋ฅผ ํ–ฅ์ƒ ์‹œ์ผฐ์Šต๋‹ˆ๋‹ค.

  • react-intersection-observer ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ด์šฉํ•˜์—ฌ lazy-loading๋ฅผ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

  • ์ด๋ฅผ ํ†ตํ•ด ์ด๋ฏธ์ง€๊ฐ€ ํ™”๋ฉด์—์„œ ๋‚˜ํƒ€๋‚  ๋•Œ ์ด๋ฏธ์ง€๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ์„ค์ •ํ•˜์—ฌ ๋กœ๋”ฉ์‹œ๊ฐ„์„ ๋‹จ์ถ• ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ์ด ๋‘ ๊ฐ€์ง€ ๊ธฐ๋ฒ•์„ ์ด๋ฏธ์ง€์— ์ ์šฉํ•˜๊ธฐ ์œ„ํ•ด ProgressiveImg ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค๊ณ , ์ด๋ฏธ์ง€์— ์ ์šฉ์‹œ์ผœ ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

  • ProgressvieImg Props

    • placeholderSrc : ์›๋ณธ ์ด๋ฏธ์ง€๊ฐ€ ๋กœ๋”ฉ๋˜๊ธฐ์ „ ๋ณด์—ฌ์ค„ ์ €ํ™”์งˆ์˜ ์ด๋ฏธ์ง€ url ์ž…๋‹ˆ๋‹ค.
    • src : ์›๋ณธ ์ด๋ฏธ์ง€์˜ url ์ž…๋‹ˆ๋‹ค.
    • ...props : ๊ทธ ์™ธ props๋ฅผ ๋ชจ๋‘ ๋ฐ›์•„์˜ค๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
// ProgressvieImg ์ปดํฌ๋„ŒํŠธ
import React, { useEffect, useState } from "react";
import { Img } from "./progressiveImg.style";
import { useInView } from "react-intersection-observer";

export default function ProgressiveImg({
  placeholderSrc,
  src,
  styles,
  ...props
}) {
  // ์ด๋ฏธ์ง€ src๋ฅผ ๊ด€๋ฆฌ
  const [imgSrc, setImgSrc] = useState(placeholderSrc || src);
  // ํ˜„์žฌ ๋กœ๋”ฉ์ด ์ƒํƒœ
  const [isLoading, setIsLoading] = useState(true);
  const { ref, inView } = useInView();
  useEffect(() => {
    // ์ด๋ฏธ์ง€๊ฐ€ ํ™”๋ฉด์—์„œ ๋ณด์ด๊ณ , imgSrc๊ฐ€ placholder์ด๋ฏธ์ง€ ์ผ๋•Œ ์ด๋ฏธ์ง€๋ฅผ ๋ฐ›์•„์˜ด
    if (inView && imgSrc === placeholderSrc) {
      const img = new Image();
      img.src = src;
      img.onload = () => {
        setImgSrc(src);
        setIsLazy(false);
      };
      img.onerror = () => {
        setImgSrc(
          resolveWebp(webpSupport, "/assets/webp/placeholderImg.webp", "svg")
        );
        setIsLazy(false);
      };
    }
  }, [src, inView]);

  return (
    <Img
      {...{ src: imgSrc, ...props }}
      isLazy={isLazy}
      // ๋กœ๋”ฉ ์ƒํƒœ์ผ ๋•Œ blurํšจ๊ณผ๋ฅผ ์ฃผ๊ธฐ์œ„ํ•ด ์‚ฌ์šฉ
      className={isLoading ? "lading" : "loaded"}
      ref={ref}
    />
  );
}

โ‘ฃ ์ตœ์‹  ์ด๋ฏธ์ง€ ํ˜•์‹ Webp ์ ์šฉ

  • WebP ์ด๋ฏธ์ง€๋Š” JPEG๋‚˜ PNG์— ๋น„ํ•ด ์••์ถ•๋ฅ ์ด ๋†’๊ณ , ๋” ์ž‘์€ ํŒŒ์ผ ํฌ๊ธฐ๋ฅผ ๊ฐ€์ง€๋ฉฐ, ๋†’์€ ํ’ˆ์งˆ์„ ์ œ๊ณตํ•˜๋Š” ์ด๋ฏธ์ง€ ํ˜•์‹์ž…๋‹ˆ๋‹ค.

  • Webp ์ด๋ฏธ์ง€ ํ˜•์‹์€ ๊ตฌ ๋ธŒ๋ผ์šฐ์ €๋Š” ์ง€์›ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ ์ง„์  ํ–ฅ์ƒ ๊ธฐ๋ฒ•์„ ์ด์šฉํ•˜์—ฌ ๋‹ค๋ฅด๊ฒŒ ์ฒ˜๋ฆฌํ•ด ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

  • Webp ์ด๋ฏธ์ง€๊ฐ€ ์ง€์›์ด ๋œ๋‹ค๋ฉด body ํƒœ๊ทธ์— webp classList๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ๊ณ , Webp ์ด๋ฏธ์ง€๊ฐ€ ์ง€์›๋˜์ง€ ์•Š๋Š”๋‹ค๋ฉด no-webp classList๋ฅผ ์ถ”๊ฐ€ํ•ด ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

  • body classList๋ฅผ ํ†ตํ•ด ์ด๋ฏธ์ง€ ํ˜•์‹์ด ๋‹ค๋ฅด๊ฒŒ ์ ์šฉ๋˜๋„๋ก ์ฒ˜๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค.

  • ๊ตฌ ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” svg ์ด๋ฏธ์ง€ ํ˜•์‹์ด ์ ์šฉ๋˜๋„๋ก ์ฒ˜๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค.

  • Webp๊ฐ€ ์ง€์›๋˜๋Š” ๋ธŒ๋ผ์šฐ์ €์—์„œ๋„ˆ Webp ์ด๋ฏธ์ง€๊ฐ€ ์ ์šฉ๋˜๋„๋ก ์ฒ˜๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค.

  • detectWebpSupport, resolveWebp ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด ์ด๋ฅผ ์ ์šฉ์‹œ์ผœ ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

  • detectWebpSupport

    • webpdata์— 1x1 ํ”ฝ์…€ ํฌ๊ธฐ์˜ Webp ํ˜•์‹์˜ ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ๋ฅผ base64๋กœ ์ธ์ฝ”๋”ฉํ•œ ๋ฌธ์ž์—ด์„ ํ• ๋‹นํ•ฉ๋‹ˆ๋‹ค.
    • ์ด๋ฏธ์ง€ ๋กœ๋”ฉ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜๊ฑฐ๋‚˜ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ callback ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
    • webp ์ด๋ฏธ์ง€๊ฐ€ ๋กœ๋”ฉ ๋˜๊ณ , webp์ด๋ฏธ์ง€ ์ง€์›์—ฌ๋ถ€ ํ™•์ธ์„ ๊ธฐ๋‹ค๋ฆฌ๊ธฐ ์œ„ํ•ด Promise๋ฅผ ์ด์šฉํ•ด ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.
    • image.src์— webpdata๋ฅผ ํ• ๋‹นํ•˜์—ฌ, ์ƒ์„ฑํ•œ ๋นˆ ์ด๋ฏธ์ง€ ๊ฐ์ฒด๊ฐ€ ํ•ด๋‹น Webp ์ด๋ฏธ์ง€๋ฅผ ๋กœ๋”ฉํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
    • callback ํ•จ์ˆ˜์—์„œ๋Š” event.type์ด "load"์ธ ๊ฒฝ์šฐ์™€ ์ด๋ฏธ์ง€์˜ ๋„ˆ๋น„(image.width)๊ฐ€ 1 ํ”ฝ์…€์ธ ๊ฒฝ์šฐ๋ฅผ ๊ฒ€์‚ฌํ•˜์—ฌ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ Webp ์ด๋ฏธ์ง€๋ฅผ ์ง€์›ํ•˜๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ํŒ๋ณ„ํ•ฉ๋‹ˆ๋‹ค.
    • ๋ธŒ๋ผ์šฐ์ €๊ฐ€ Webp ์ด๋ฏธ์ง€๋ฅผ ์ง€์›ํ•˜๋Š” ๊ฒฝ์šฐ document.body ์š”์†Œ์˜ classList์— "webp"๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
    • ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋ธŒ๋ผ์šฐ์ €๋ผ๋ฉด document.body ์š”์†Œ์˜ classList์— "no-webp"๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
  • resolveWebp

    • webpSupported: webp์ง€์› ์œ ๋ฌด, img : Webp ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ, fallbackExt : Webp ์ด๋ฏธ์ง€ ํ˜•์‹ ๋Œ€์‹  ์‚ฌ์šฉํ•  ์ด๋ฏธ์ง€ ํ˜•์‹
    • ext์— ์ด๋ฏธ์ง€ ํ˜•์‹์„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
    • webpSupported๊ฐ€ false์ธ ๊ฒฝ์šฐ, ext์ด webp์ธ ๊ฒฝ์šฐ์— webp ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ ๋Œ€์‹  webp ๋Œ€์‹  ์‚ฌ์šฉํ•  ์ด๋ฏธ์ง€ ํ˜•์‹ ๊ฒฝ๋กœ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
    • replace ๋ฉ”์„œ๋“œ๋ฅผ ์ด์šฉํ•ด์„œ ์ด๋ฏธ์ง€๊ฒฝ๋กœ๋ฅผ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค. /webp ์ œ๊ฑฐ ํ›„, .webp๋ฅผ ๋Œ€์ฒดํ•  ์ด๋ฏธ์ง€ ํ˜•์‹์œผ๋กœ ๊ต์ฒดํ•ฉ๋‹ˆ๋‹ค.
    • ํ˜„์žฌ ํ”„๋กœ์ ํŠธ์—์„œ ์‚ฌ์šฉ ์ค‘์ธ ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ ์˜ˆ์‹œ : img/assets/webp/webpImg.webp => img/assets/svgImg.svg
  export async function detectWebpSupport() {
  // webp ์ด๋ฏธ์ง€๊ฐ€ ๋กœ๋”ฉ ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฌ๊ธฐ ์œ„ํ•ด ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ
  return new Promise((resolve) => {
    const image = new Image();
    // 1px x 1px WebP ์ด๋ฏธ์ง€
    const webpdata =
      "data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA=";

    const callback = (event) => {
      // event.type์ด "load"์ธ ๊ฒฝ์šฐ์™€ ์ด๋ฏธ์ง€์˜ ๋„ˆ๋น„(image.width)๊ฐ€ 1 ํ”ฝ์…€์ธ ๊ฒฝ์šฐ๋ฅผ ๊ฒ€์‚ฌํ•˜์—ฌ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ WebP ์ด๋ฏธ์ง€๋ฅผ ์ง€์›ํ•˜๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ํŒ๋ณ„
      const result = event?.type === "load" && image.width === 1;
      if (result) {
        resolve(true); // WebP ์ง€์›๋จ
      } else {
        resolve(false); // WebP ์ง€์›๋˜์ง€ ์•Š์Œ
      }
    };

    image.onerror = callback;
    image.onload = callback;
    image.src = webpdata;
  });
}

  export const resolveWebp = (img, fallbackExt) => {
  const webpSupported = document.body.classList.contains("webp");
  // ์ด๋ฏธ์ง€ ํฌ๋งท
  const ext = img.split(".").pop();
  // webpSupported false, ext๊ฐ€ webp์ธ ๊ฒฝ์šฐ
  if (!webpSupported && ext === "webp") {
    return img.replace("/webp", "").replace(".webp", `.${fallbackExt}`);
  }
  return img;
};
  • ์ด๋ฏธ์ง€ ์ตœ์ ํ™” ์ „
    • ์ด๋ฏธ์ง€ ๋ฆฌ์†Œ์Šค : ์•ฝ 68MB
    • ๋กœ๋”ฉ ์†๋„ : 5.8์ดˆ

ย ย  ์ด๋ฏธ์ง€์ตœ์ ํ™”์ „

  • ์ด๋ฏธ์ง€ ์ตœ์ ํ™” ํ›„
    • ์ด๋ฏธ์ง€ ๋ฆฌ์†Œ์Šค : 67MB => 8.9MB (58MB ๋‹จ์ถ•)
    • ๋กœ๋”ฉ ์†๋„ : 5.8์ดˆ => 3.5์ดˆ (2.3์ดˆ ๋‹จ์ถ•)

ย ย  ์ด๋ฏธ์ง€์ตœ์ ํ™”ํ›„

(8) ์›น ์ ‘๊ทผ์„ฑ ํ‚ค๋ณด๋“œ ํฌ์ปค์‹ฑ ์ตœ์ ํ™”

  • ๋ชจ๋‹ฌ ์ฐฝ์—์„œ ํ‚ค๋ณด๋“œ ํฌ์ปค์‹ฑ ์ตœ์ ํ™”ํ•˜๊ธฐ ์œ„ํ•ด optKeyboardFocus ํ•จ์ˆ˜๋ฅผ ์ƒ์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค.
    • ๋ชจ๋‹ฌ์ฐฝ์—์„œ ํฌ์ปค์‹ฑ์ด ๋ฒ—์–ด๋‚˜์ง€ ์•Š๋„๋ก ์„ค์ •ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.
    • shift + tab ํ‚ค๋ฅผ ๋ˆŒ๋ €์„ ๊ฒฝ์šฐ(์ด์ „ ์š”์†Œ) ํฌ์ปค์‹ฑ์„ ๋ฐ›์„ ์š”์†Œ๋กœ ์ธ์ž๋กœ ๋ฐ›์€ previousTarget ์„ค์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
    • tab ํ‚ค๋ฅผ ๋ˆŒ๋ €์„ ๊ฒฝ์šฐ ํฌ์ปค์‹ฑ์„ ๋ฐ›์„ ์š”์†Œ๋กœ ์ธ์ž๋กœ ๋ฐ›์€ nextTarget์œผ๋กœ ์„ค์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค,
    • nextTarget์ด ํ•„์š”์—†๋Š” ๊ฒฝ์šฐ์—๋Š” previousTarget๋งŒ ์ ์šฉ๋˜๋„๋ก else if๋ฌธ nextTarget && ์กฐ๊ฑด์„ ์ถ”๊ฐ€ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.
    • previousTarget์€ ํ•„์š”์—†๋Š” ๊ฒฝ์šฐ๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์— ์ƒ๋žต๋˜๋Š” ๊ฒฝ์šฐ๋ฅผ ๊ณ ๋ คํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.
      • nextTarget๋งŒ ์žˆ๋Š” ๊ฒฝ์šฐ์—๋„ previousTarget์ด ํ•„์š” => e.preventDefault() ์ฒ˜๋ฆฌ๋กœ ์ด์ „ ์š”์†Œ ํฌ์ปค์‹ฑ(shift+tab)์ด ์ž‘๋™ํ•˜์ง€ ์•Š๊ณ  nextTarget.focus() ์ž‘๋™๋˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.
      • ์ด์ „ ์š”์†Œ(shift + tab), ๋‹ค์Œ ์š”์†Œ(tab) ํฌ์ปค์Šค ์ด๋™์‹œ tabํ‚ค๊ฐ€ ๊ฐ™์ด ์“ฐ์ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.
      • shift+tabํ‚ค๋ฅผ ๋ˆ„๋ฅด๋Š” ๊ฒฝ์šฐ๋„ ๋ณ„๋„์˜ ์กฐ๊ฑด ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
  • esc ํ‚ค๋ฅผ ๋ˆ„๋ฅผ ๊ฒฝ์šฐ ๋ชจ๋‹ฌ์ฐฝ์ด ๋‹ซํžˆ๋„๋ก ์ฒ˜๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • ๋˜ํ•œ selector ๋ฉ”๋‰ด๊ฐ€ ํ™œ์„ฑํ™” ๋˜๋ฉด ํฌ์ปค์‹ฑ์ด ๋ฒ—์–ด๋‚˜์ง€ ์•Š๋„๋ก ์ฒ˜๋ฆฌํ•˜์˜€์œผ๋ฉฐ, escํ‚ค๋ฅผ ๋ˆ„๋ฅด๋ฉด ๋‹ซํžˆ๋„๋ก ์ฒ˜๋ฆฌ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
// optKeyboardFocus ํ•จ์ˆ˜
// ํ‚ค๋ณด๋“œ ํฌ์ปค์Šค ์›น ์ ‘๊ทผ์„ฑ ์ตœ์ ํ™” ํ•จ์ˆ˜
// ์ฒซ ๋ฒˆ์งธ ์ธ์ž๋กœ๋Š” ์ด๋ฒคํŠธ
// ๋‘ ๋ฒˆ์งธ ์ธ์ž๋กœ ๋‹ค์Œ focus๋  ๋Œ€์ƒ, ์„ธ ๋ฒˆ์งธ ์ธ์ž๋กœ ์ด์ „ focus๋  ๋Œ€์ƒ
export const optKeyboardFocus = (e, previousTarget, nextTarget = null) => {
  if (e.shiftKey && e.keyCode === 9) {
    e.preventDefault();
    previousTarget.focus();
  } else if (nextTarget && e.keyCode === 9) {
    e.preventDefault();
    nextTarget.focus();
  }
};
  • ํ‚ค๋ณด๋“œ ํฌ์ปค์‹ฑ ์ตœ์ ํ™” ์ฒ˜๋ฆฌ ์ „
    • ํ‚ค๋ณด๋“œ ํฌ์ปค์‹ฑ์ด ๋ชจ๋‹ฌ ์ฐฝ ๋ฐ–์œผ๋กœ ๋ฒ—์–ด๋‚˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ย ย  ํ‚ค๋ณด๋“œ์ตœ์ ํ™”์ „

  • ํ‚ค๋ณด๋“œ ํฌ์ปค์‹ฑ ์ตœ์ ํ™” ์ฒ˜๋ฆฌ ํ›„
    • ํ‚ค๋ณด๋“œ ํฌ์ปค์‹ฑ์ด ๋ชจ๋‹ฌ ์ฐฝ ๋ฐ–์œผ๋กœ ๋ฒ—์–ด๋‚˜์ง€ ์•Š๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • escํ‚ค๋ฅผ ๋ˆŒ๋Ÿฌ ๋ชจ๋‹ฌ ์ฐฝ์ด ๋‹ซํžˆ๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ย ย  ํ‚ค๋ณด๋“œ์ตœ์ ํ™”ํ›„

(9) sweetAlert2

  • alert์ฐฝ๊ณผ confirm์ฐฝ์„ ์ปค์Šคํ…€ ํ•˜๊ธฐ์œ„ํ•ด sweetAlert2 ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™œ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • https://sweetalert2.github.io/ ์‚ฌ์ดํŠธ์˜ docs๋ฅผ ์ฐธ๊ณ ํ•˜์—ฌ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • ์›ํ•˜๋Š” ๋””์ž์ธ ์‚ฌ์šฉ๊ณผ ์ค‘๋ณต๋œ ์ฝ”๋“œ ์‚ฌ์šฉ์„ ์ค„์ด๊ธฐ ์œ„ํ•ด ๋ณ„๋„์˜ ํŒŒ์ผ์— sweetToast, sweetConfirm ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • sweetToast
    • alert์ฐฝ ๋Œ€์‹  ํ™œ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์ถ”๊ฐ€ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.
    • ์ธ์ž ๊ฐ’์€ title(alert ์ฐฝ์— ๋‚˜ํƒ€๋‚˜๋Š” ๋‚ด์šฉ), icon(alert์ฐฝ์— ๋‚˜ํƒ€๋‚˜๋Š” ์•„์ด์ฝ˜), timer(alert์ฐฝ์ด ์‚ฌ๋ผ์ง€๋Š” ์‹œ๊ฐ„)๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค.
    • Swal.fire๋ฅผ ์ด์šฉํ•˜์—ฌ sweetAlert2 ์ฐฝ์„ ์‚ฌ์šฉํ•˜๋ฉฐ, ์†์„ฑ์œผ๋กœ toast: ture(toast์ฐฝ ์‚ฌ์šฉ ์œ ๋ฌด), title(toast์ฐฝ์— ๋“ค์–ด๊ฐˆ ๋‚ด์šฉ), postiton(๋ฐฐ์น˜ํ•  ์œ„์น˜), showConfirmButton(ํ™•์ธ๋ฒ„ํŠผ ์œ ๋ฌด), icon(์•„์ด์ฝ˜ ์„ค์ •), timer(์ฐฝ์ด ์‚ฌ๋ผ์ง€๋Š” ์‹œ๊ฐ„ ์„ค์ •)๋ฅผ ๋„ฃ์–ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.
  • sweetConfirm
    • ์ธ์ž ๊ฐ’์€ title(confirm์ฐฝ ๋‚ด์šฉ), confirmButtonText(confirm์ฐฝ ํ™•์ธ ๋ฒ„ํŠผ ๋‚ด์šฉ), cancelButtonText(confirm์ฐฝ ์ทจ์†Œ ๋ฒ„ํŠผ ๋‚ด์šฉ)๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค.
    • Swal.fire๋ฅผ ์ด์šฉํ•˜์—ฌ sweetAlert2 ์ฐฝ์„ ์‚ฌ์šฉํ•˜๋ฉฐ, ์†์„ฑ์œผ๋กœ title(confirm์ฐฝ ๋‚ด์šฉ), showCancelButtonText(์ทจ์†Œ๋ฒ„ํŠผํ™œ์„ฑํ™” ์œ ๋ฌด), focusConfirm(confirm์ฐฝ ํฌ์ปค์‹ฑ ์œ ๋ฌด), confirmButtonText(confirm์ฐฝ ํ™•์ธ ๋ฒ„ํŠผ ๋‚ด์šฉ), cancelButtonText(confirm์ฐฝ ์ทจ์†Œ ๋ฒ„ํŠผ ๋‚ด์šฉ), showCloseButton(๋‹ซ๊ธฐ ๋ฒ„ํŠผ ํ™œ์„ฑํ™” ์œ ๋ฌด)๋ฅผ ๋„ฃ์–ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.
import Swal from "sweetalert2";
import "./sweetAlert.css";
import 'sweetalert2/dist/sweetalert2.min.css';
export const sweetToast = (title, icon = null, timer = 2000) => {
  return Swal.fire({
    toast: true,
    title,
    position: "top",
    showConfirmButton: false,
    icon,
    timer
  });
};

export const sweetConfirm = (
  title,
  confirmButtonText,
  cancelButtonText,
  cb
) => {
  return Swal.fire({
    title,
    showCancelButton: true,
    focusConfirm: true,
    confirmButtonText,
    cancelButtonText,
    showCloseButton: false,
  }).then(({ isConfirmed }) => {
    if (isConfirmed) {
      cb();
    }
  });
};
  • sweetAlert2 ์ ์šฉ ํ™”๋ฉด

ย ย  sweetAlert2์ ์šฉ

๐Ÿ”ซ ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

(1) Search ํŽ˜์ด์ง€์—์„œ ๊ฒ€์ƒ‰ ํ›„ ์š”์†Œ ํด๋ฆญ ์‹œ ์Šคํฌ๋กค์ด ์˜ฌ๋ผ๊ฐ€๋Š” ์ด์Šˆ

  • ์›์ธ : input์ฐฝ์˜ focus๊ฐ€ ์œ ์ง€๋œ ์ฑ„๋กœ ๋‹ค๋ฅธ ์š”์†Œ๋ฅผ ํด๋ฆญ ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์—, input ์š”์†Œ์˜ foucs๊ฐ€ ํ•ด์ œ ๋˜๋ฉด์„œ ์Šคํฌ๋กค์ด ์œ„๋กœ ์˜ฌ๋ผ๋Š” ๋ฌธ์ œ์˜€์Šต๋‹ˆ๋‹ค.
  • ํ•ด๊ฒฐ๋ฐฉ์•ˆ : input์˜ ๊ฒ€์ƒ‰์–ด ์ž…๋ ฅ์ด ๋๋‚˜๋ฉด blur ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ฃผ์–ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  // ๊ฒ€์ƒ‰ ๋””๋ฐ”์šด์‹ฑ ์ ์šฉ ๊ฒ€์ƒ‰์–ด๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ ๊ฒ€์ƒ‰์–ด์— ํ•ด๋‹นํ•˜๋Š” ๋ฐ์ดํ„ฐ ์ถœ๋ ฅ
  const searchDebounce = useCallback(
    debounce(async (value) => {
      if (!value) {
        fetchFirstData();
        return;
      }
      const data = await fetchSearchMovie(value);
      setMovieData(data);
      // ๊ฒ€์ƒ‰์ด ์™„๋ฃŒ๋œ ์ดํ›„ input๋ฅผ blur ์ฒ˜๋ฆฌ
      // blur ์ฒ˜๋ฆฌ๋ฅผ ํ•˜์ง€ ์•Š์œผ๋ฉด input์˜ ํฌ์ปค์Šค๊ฐ€ ์œ ์ง€๋œ ์ฑ„ ์Šคํฌ๋กค ๋‚ด๋ ค ๋‹ค๋ฅธ ์š”์†Œ ํด๋ฆญ ์‹œ 
      // ํฌ์ปค์Šค๊ฐ€ ํ•ด์ œ๋˜๋ฉด์„œ input์š”์†Œ๋ฅผ ์ฐพ์•„ ์Šคํฌ๋กค์ด ์œ„๋กœ ์˜ฌ๋ผ ์˜ค๊ธฐ ๋•Œ๋ฌธ 
      searchInputRef.current.blur();
    }, 500),
    []
  );
  • ์ด์Šˆ ํ•ด๊ฒฐ ์ „
    • ์Šคํฌ๋กค์„ ๋‚ด๋ฆฐ ํ›„ ํด๋ฆญ ์‹œ ์Šคํฌ๋กค์ด ์œ„๋กœ ์˜ฌ๋ผ ์˜ค๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

search์˜ค๋ฅ˜์ˆ˜์ •์ „

  • ์ด์Šˆ ํ•ด๊ฒฐ ํ›„
    • ์Šคํฌ๋กค์„ ๋‚ด๋ฆฐ ํ›„ ํด๋ฆญ ์‹œ ์Šคํฌ๋กค์ด ์œ ์ง€๋˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

search์˜ค๋ฅ˜ ํ•ด๊ฒฐํ›„

(2) Mypage ์ฐœ ๋ชฉ๋ก์—์„œ ์ฐœ ํ•ด์ œ ํ›„ ๋‹ค์‹œ ์ถ”๊ฐ€ ์‹œ ๊ธฐ์กด ์ฐœ ๋ชฉ๋ก์˜ ์ •๋ ฌ๊ณผ ๋‹ค๋ฅด๊ฒŒ ์ •๋ ฌ ๋˜๋Š” ์ด์Šˆ

  • ์›์ธ : firebase์˜ ์ •๋ ฌ ๊ธฐ์ค€์ด javascript ์ •๋ ฌ ๊ธฐ์ค€๊ณผ ๋‹ฌ๋ผ์„œ ๋ฐœ์ƒํ•œ ๋ฌธ์ œ์˜€์Šต๋‹ˆ๋‹ค.
  • ํ•ด๊ฒฐ๋ฐฉ์•ˆ : firebase์˜ ์ •๋ ฌ ๊ธฐ์ค€์— ๋งž๊ฒŒ sort ๋ฉ”์„œ๋“œ์˜ ์ •๋ ฌ ๊ธฐ์ค€์„ ์ ์šฉ ์‹œ์ผœ์ฃผ๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

MovieInfo.container ์ปดํฌ๋„ŒํŠธ onClickLike ํ•จ์ˆ˜ ์ฝ”๋“œ

 const onClickLike = async () => {
    if (!user) {
      return sweetToast("๋กœ๊ทธ์ธ ํ›„ ์ด์šฉ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค!", "warning");
    }
    if (!like) {
      setLike(true);
      await addLike(videoData);
      if (setMypageLikeData) {
        // firebase ์ •๋ ฌ๊ณผ ๊ฐ™์€ ์ˆœ์„œ๋ฅผ ๋งž์ถฐ์ฃผ๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•จ
        const patternNumber = /[0-9]/;
        const patternAlphabet = /[a-zA-Z]/;
        const patternHangul = /[ใ„ฑ-ใ…Ž|ใ…-ใ…ฃ|๊ฐ€-ํžฃ]/;
        const orderLevelDesc = [patternNumber, patternAlphabet, patternHangul];
        const getLevel = (str) => {
          const index = orderLevelDesc.findIndex((pattern) => pattern.test(str));
          // orderLevelDesc ๋ฐฐ์—ด์—์„œ ๋งŒ์กฑํ•˜๋Š” ํŒจํ„ด์˜ ์ธ๋ฑ์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•ด์คŒ
          return index;
        };
        setMypageLikeData((prev) =>
          [...prev, videoData].sort((a, b) => {
            // ์ฒซ๋ฒˆ์งธ ๋ฌธ์ž๋ฅผ ๋„ฃ์–ด์ค˜์„œ ๋งŒ์กฑ๋Š” ํŒจํ„ด์˜ ์ธ๋ฑ์Šค๋ฅผ ๋ฐ˜ํ™˜ ๋ฐ›์Œ
            const aLevel = getLevel(a.title.charAt(0));
            const bLevel = getLevel(b.title.charAt(0));
            // ์‹œ์ž‘ํ•˜๋Š” ๋ฌธ์ž์—ด์ด ๊ฐ™์€ ์ข…๋ฅ˜์ผ ๊ฒฝ์šฐ๋Š” ์œ ๋‹ˆ์ฝ”๋“œ ๊ฐ’์œผ๋กœ ์‚ฌ์ „์‹ ์ •๋ ฌ
            if (aLevel === bLevel) {
              return a.title.charCodeAt(0) - b.title.charCodeAt(0);
            }
            // ๋ฌธ์ž์—ด์ด ๊ฐ™์€ ์ข…๋ฅ˜๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ ์œ„ ํŒจํ„ด์— ๋‚˜์˜จ ์ˆœ์„œ๋Œ€๋กœ ์ •๋ ฌ
            return aLevel - bLevel;
          })
        );
      }
    } else {
      setLike(false);
      removeLike(videoData);
      if (setMypageLikeData) {
        setMypageLikeData((prev) =>
          prev.filter((item) => item.id !== videoData.id)
        );
      }
    }
  };
  • ์ด์Šˆ ํ•ด๊ฒฐ ์ „
    • ์ฐœ ๋ชฉ๋ก์„ ์ œ๊ฑฐ ํ›„ ๋‹ค์‹œ ์ถ”๊ฐ€ ํ•˜๋ฉด ์ •๋ ฌ ์ˆœ์„œ๊ฐ€ ๋‹ค๋ฅด๊ฒŒ ์ ์šฉ ๋˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ •๋ ฌ์˜ค๋ฅ˜์ˆ˜์ •์ „

  • ์ด์Šˆ ํ•ด๊ฒฐ ํ›„
    • ์ฐœ ๋ชฉ๋ก์„ ์ œ๊ฑฐ ํ›„ ๋‹ค์‹œ ์ถ”๊ฐ€ ํ•˜๋ฉด ์ •๋ ฌ ์ˆœ์„œ๊ฐ€ ๋™์ผํ•˜๊ฒŒ ์ ์šฉ ๋˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ •๋ ฌ์˜ค๋ฃจ์ˆ˜์ •ํ›„

=> ์œ„ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์œผ๋กœ ํ•ด๊ฒฐ ํ–ˆ์ง€๋งŒ ์ฐœ ๋ชฉ๋ก์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋งŽ์•„์งˆ ๊ฒฝ์šฐ ์„ฑ๋Šฅ ์ด์Šˆ๊ฐ€ ๋ฐœ์ƒํ•  ๋ฌธ์ œ๊ฐ€ ์žˆ์–ด, ์ถ”๊ฐ€์ ์ธ ๋ฐฉ๋ฒ•์„ ์ƒ๊ฐํ•˜์˜€์Šต๋‹ˆ๋‹ค.

=> ์ฐœ ๋ชฉ๋ก์„ ์ œํ•œํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ํ•ด๊ฒฐํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์ด ๋ฐฉ์‹์œผ๋กœ ์–ป์€ ์žฅ์ 

  • ์‚ฌ์šฉ์ž๊ฐ€ ์ฐœ ๋ชฉ๋ก์ด ๋งŽ์•„์งˆ ๊ฒฝ์šฐ ๊ด€๋ฆฌ๊ฐ€ ๋ณต์žกํ•ด์ง€๋Š” ๋ฌธ์ œ๋ฅผ ๋ฏธ๋ฆฌ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
  • ์ œํ•œ์„ ๋‘์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ฐ์ดํ„ฐ๊ฐ€ ๋งŽ์•„์ ธ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ๊ฒฝ์šฐ๊ฐ€ ์—†์–ด์ง‘๋‹ˆ๋‹ค.

์ด ๋ฐฉ์‹์œผ๋กœ ์–ป๋Š” ๋‹จ์ 

  • ์‚ฌ์šฉ์ž๊ฐ€ ์ฐœ ๋ชฉ๋ก์— ์ œํ•œ์„ ๋‘”๋‹ค๋ฉด ๋ถˆํŽธ์„ ๋Š๋ผ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋” ๋งŽ์€ ์ฐœ ๋ชฉ๋ก์„ ์š”๊ตฌํ•˜๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

MovieInfo.container ์ปดํฌ๋„ŒํŠธ fetchLike ํ•จ์ˆ˜, onClickLike ์ฝ”๋“œ

  const fetchLike = async () => {
    if (user) {
      const data = await getUser();
      // ์œ ์ € ๋ฐ์ดํ„ฐ likeList์—์„œ ํ˜„์žฌ ๋ฐ์ดํ„ฐ์˜ id์™€ ์ผ์น˜ํ•˜๋Š” ์ฐœ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ
      const isLike =
        data && data.likeList.find((likeId) => likeId === videoData.id);
      // ๊ฒฐ๊ณผ๋ฅผ boolean ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ›„ like ์ƒํƒœ์— ์ ์šฉ
      setLike(!!isLike);
      // likeList์˜ ๊ธธ์ด๊ฐ€ likListLimit ํด ๋•Œ๋ฅผ ๋น„๊ตํ•ด์„œ isExceed ์ƒํƒœ์— ์ ์šฉ
      setIsExceed(data.likeList.length > likListLimit);
    }
  };

const onClickLike = async () => {
    if (!user) {
      return sweetToast("๋กœ๊ทธ์ธ ํ›„ ์ด์šฉ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค!", "warning");
    }
    // ์ฐœ ๋ชฉ๋ก ์ตœ๋Œ€ ์ˆ˜๋ฅผ ์ดˆ๊ณผํ•˜๋ฉด ์ฐœ ๊ฐ€๋Šฅ์„ ๋ง‰๋Š”๋‹ค.
    if(isExceed) {
      return sweetToast("์ตœ๋Œ€ ์ฐœ ๋ชฉ๋ก ์ˆ˜๋ฅผ ์ดˆ๊ณผํ•˜์˜€์Šต๋‹ˆ๋‹ค.\n์ฐœ ๋ชฉ๋ก ์‚ญ์ œ ํ›„ ์ด์šฉํ•ด์ฃผ์„ธ์š”!", "warning");
    }
    if (!like) {
      setLike(true);
      await addLike(videoData);
      if (setMypageLikeData) {
        // firebase ์ •๋ ฌ๊ณผ ๊ฐ™์€ ์ˆœ์„œ๋ฅผ ๋งž์ถฐ์ฃผ๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•จ
        const patternNumber = /[0-9]/;
        const patternAlphabet = /[a-zA-Z]/;
        const patternHangul = /[ใ„ฑ-ใ…Ž|ใ…-ใ…ฃ|๊ฐ€-ํžฃ]/;
        const orderLevelDesc = [patternNumber, patternAlphabet, patternHangul];
        const getLevel = (str) => {
          const index = orderLevelDesc.findIndex((pattern) => pattern.test(str));
          // orderLevelDesc ๋ฐฐ์—ด์—์„œ ๋งŒ์กฑํ•˜๋Š” ํŒจํ„ด์˜ ์ธ๋ฑ์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•ด์คŒ
          return index;
        };
        setMypageLikeData((prev) =>
          [...prev, videoData].sort((a, b) => {
            // ์ฒซ๋ฒˆ์งธ ๋ฌธ์ž๋ฅผ ๋„ฃ์–ด์ค˜์„œ ๋งŒ์กฑ๋Š” ํŒจํ„ด์˜ ์ธ๋ฑ์Šค๋ฅผ ๋ฐ˜ํ™˜ ๋ฐ›์Œ
            const aLevel = getLevel(a.title.charAt(0));
            const bLevel = getLevel(b.title.charAt(0));
            // ์‹œ์ž‘ํ•˜๋Š” ๋ฌธ์ž์—ด์ด ๊ฐ™์€ ์ข…๋ฅ˜์ผ ๊ฒฝ์šฐ๋Š” ์œ ๋‹ˆ์ฝ”๋“œ ๊ฐ’์œผ๋กœ ์‚ฌ์ „์‹ ์ •๋ ฌ
            if (aLevel === bLevel) {
              return a.title.charCodeAt(0) - b.title.charCodeAt(0);
            }
            // ๋ฌธ์ž์—ด์ด ๊ฐ™์€ ์ข…๋ฅ˜๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ ์œ„ ํŒจํ„ด์— ๋‚˜์˜จ ์ˆœ์„œ๋Œ€๋กœ ์ •๋ ฌ
            return aLevel - bLevel;
          })
        );
      }
    } else {
      setLike(false);
      removeLike(videoData);
      if (setMypageLikeData) {
        setMypageLikeData((prev) =>
          prev.filter((item) => item.id !== videoData.id)
        );
      }
    }
  };

(3) WebpSupport ์ƒํƒœ๊ฐ€ ์ œ๋Œ€๋กœ ์ ์šฉ๋˜์ง€ ์•Š๋Š” ํ˜„์ƒ

  • ์›์ธ : detectWebpSupport ํ•จ์ˆ˜์—์„œ webp ์ด๋ฏธ์ง€ ๋กœ๋”ฉ ๋˜๊ธฐ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฌ๋Š” ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ฃผ์ง€ ์•Š์•„ ๋ฐœ์ƒํ•œ ๋ฌธ์ œ์˜€์Šต๋‹ˆ๋‹ค.
  • ํ•ด๊ฒฐ๋ฐฉ์•ˆ : detectWebpSupport ํ•จ์ˆ˜๋ฅผ promise๋ฅผ ์ด์šฉํ•˜์—ฌ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌํ•ด์ฃผ์–ด ํ•ด๊ฒฐํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๋ฌธ์ œ ๋ฐœ์ƒ ์ฝ”๋“œ

detectWebpSupport ํ•จ์ˆ˜ ์ฝ”๋“œ

export function detectWebpSupport() {
  const image = new Image();
  // 1px x 1px WebP ์ด๋ฏธ์ง€
  const webpdata = "data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA=";
  const callback = (event) => {
    // event.type์ด "load"์ธ ๊ฒฝ์šฐ์™€ ์ด๋ฏธ์ง€์˜ ๋„ˆ๋น„(image.width)๊ฐ€ 1 ํ”ฝ์…€์ธ ๊ฒฝ์šฐ๋ฅผ ๊ฒ€์‚ฌํ•˜์—ฌ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ WebP ์ด๋ฏธ์ง€๋ฅผ ์ง€์›ํ•˜๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ํŒ๋ณ„
    const result = event?.type === "load" && image.width === 1;
    if (result) {
      document.body.classList.add("webp");
    }
    else {
      document.body.classList.remove("webp");
    }
  };
  image.onerror = callback;
  image.onload = callback;
  image.src = webpdata;
}

App ์ปดํฌ๋„ŒํŠธ ์ค‘ detectWebpSupport ํ˜ธ์ถœ ์ฝ”๋“œ

useEffect(() => {
    detectWebpSupport();
    if (document.body.classList.contains("webp")) {
      setWebpSupport(true);
    }
  }, []);

๋ฌธ์ œ ํ•ด๊ฒฐ ํ›„ ์ฝ”๋“œ

detectWebpSupport ํ•จ์ˆ˜ ์ฝ”๋“œ

export async function detectWebpSupport() {
  // webp ์ด๋ฏธ์ง€๊ฐ€ ๋กœ๋”ฉ ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฌ๊ธฐ ์œ„ํ•ด ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ
  return new Promise((resolve) => {
    const image = new Image();
    // 1px x 1px WebP ์ด๋ฏธ์ง€
    const webpdata =
      "data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA=";

    const callback = (event) => {
      // event.type์ด "load"์ธ ๊ฒฝ์šฐ์™€ ์ด๋ฏธ์ง€์˜ ๋„ˆ๋น„(image.width)๊ฐ€ 1 ํ”ฝ์…€์ธ ๊ฒฝ์šฐ๋ฅผ ๊ฒ€์‚ฌํ•˜์—ฌ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ WebP ์ด๋ฏธ์ง€๋ฅผ ์ง€์›ํ•˜๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ํŒ๋ณ„
      const result = event?.type === "load" && image.width === 1;
      if (result) {
        document.body.classList.add("webp");
        resolve(true); // WebP ์ง€์›๋จ
      } else {
        document.body.classList.remove("webp");
        resolve(false); // WebP ์ง€์›๋˜์ง€ ์•Š์Œ
      }
    };

    image.onerror = callback;
    image.onload = callback;
    image.src = webpdata;
  });
}

App ์ปดํฌ๋„ŒํŠธ ์ค‘ detectWebpSupport ํ˜ธ์ถœ ์ฝ”๋“œ

  const checkWebp = async() => {
    const res = await detectWebpSupport();
    if (res) {
      setWebpSupport(true);
    } else {
      setWebpSupport(false);
    }
  }

  useEffect(() => {
    checkWebp();
  },[]);

(4) webp ์ด๋ฏธ์ง€๊ฐ€ ์ ์šฉ๋  ๋•Œ svg ์ด๋ฏธ์ง€ ๋ฆฌ์†Œ์Šค๋„ ๊ฐ™์ด ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋ฌธ์ œ

  • ์›์ธ : ๊ธฐ์กด ์ฝ”๋“œ์—์„œ webp๋ผ๋Š” classList๊ฐ€ ์—†์„ ๋•Œ svg ์ด๋ฏธ์ง€๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋„๋ก ํ–ˆ๋Š”๋ฐ, ์ฒ˜์Œ์—๋Š” body ํƒœ๊ทธ์— ์•„๋ฌด๋Ÿฐ className์ด ์—†๊ธฐ ๋•Œ๋ฌธ์— svg ์ด๋ฏธ์ง€๊ฐ€ ์ ์šฉ๋˜์–ด์„œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ resolveWebp ํ•จ์ˆ˜์—์„œ ์ธ์ž๋กœ ๋ฐ›๋Š” webpSupport ๊ฐ’์ด context๋กœ ์ €์žฅ๋œ ๊ฐ’์ด๊ธฐ ๋•Œ๋ฌธ์— ์ดˆ๊ธฐ๊ฐ’์ด null ์ฃผ์–ด์ ธ์„œ ์กฐ๊ฑด๋ฌธ์— ์˜ํ•ด svg ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ธฐ ๋•Œ๋ฌธ์— svg ์ด๋ฏธ์ง€๊ฐ€ ์ ์šฉ๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • ํ•ด๊ฒฐ๋ฐฉ์•ˆ : webp ์ด๋ฏธ์ง€๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด no-webp๋ผ๋Š” classList๋ฅผ ์ฃผ์–ด ์ดˆ๋ฐ˜ className์ด ์—†์„ ๊ฒฝ์šฐ์—๋Š” svg ์ด๋ฏธ์ง€๊ฐ€ ์ ์šฉ๋˜์ง€ ์•Š๋„๋ก ์ˆ˜์ •ํ•˜์—ฌ ํ•ด๊ฒฐํ•˜์˜€์Šต๋‹ˆ๋‹ค. resolveWebpํ•จ์ˆ˜์—์„œ webpSupport ๊ฐ’์ด null์ธ ๊ฒฝ์šฐ retrun ๋˜๋„๋ก ์ฒ˜๋ฆฌํ•˜์—ฌ context๊ฐ€ ์ดˆ๊ธฐ ๊ฐ’(null)์ผ๋•Œ svg ์ด๋ฏธ์ง€๊ฐ€ ์ ์šฉ๋˜๋Š” ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜์˜€์Šต๋‹ˆ๋‹ค.

webpSupport.js ์ฝ”๋“œ

  export async function detectWebpSupport() {
  // webp ์ด๋ฏธ์ง€๊ฐ€ ๋กœ๋”ฉ ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฌ๊ธฐ ์œ„ํ•ด ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ
  return new Promise((resolve) => {
    const image = new Image();
    // 1px x 1px WebP ์ด๋ฏธ์ง€
    const webpdata =
      "data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA=";

    const callback = (event) => {
      // event.type์ด "load"์ธ ๊ฒฝ์šฐ์™€ ์ด๋ฏธ์ง€์˜ ๋„ˆ๋น„(image.width)๊ฐ€ 1 ํ”ฝ์…€์ธ ๊ฒฝ์šฐ๋ฅผ ๊ฒ€์‚ฌํ•˜์—ฌ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ WebP ์ด๋ฏธ์ง€๋ฅผ ์ง€์›ํ•˜๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ํŒ๋ณ„
      const result = event?.type === "load" && image.width === 1;
      if (result) {
        document.body.classList.add("webp");
        resolve(true); // WebP ์ง€์›๋จ
      } else {
        document.body.classList.add("no-webp");
        resolve(false); // WebP ์ง€์›๋˜์ง€ ์•Š์Œ
      }
    };

    image.onerror = callback;
    image.onload = callback;
    image.src = webpdata;
  });

export const resolveWebp = (webpSupported, img, fallbackExt) => {
  // webpSupported null์ธ ๊ฒฝ์šฐ๋Š” context ์ดˆ๊ธฐ ๊ฐ’์ด๋ฏ€๋กœ return
  if(webpSupported===null) return;
  // ์ด๋ฏธ์ง€ ํฌ๋งท
  const ext = img.split(".").pop();
  // webpSupported false, ext๊ฐ€ webp์ธ ๊ฒฝ์šฐ
  if (!webpSupported && ext === "webp") {
    return img.replace("/webp", "").replace(".webp", `.${fallbackExt}`);
  }
  return img;
};

(5) ์ƒˆ๋กœ๊ณ ์นจ ์‹œ ์ž ๊น ๋™์•ˆ ์ด๋ฏธ์ง€๊ฐ€ ๋‚˜์˜ค์ง€ ์•Š๊ณ  alt๊ฐ€ ๋‚˜์˜ค๋Š” ํ˜„์ƒ

  • ์›์ธ : resolveWebp ํ•จ์ˆ˜์—์„œ webpSupported๊ฐ€ null์ธ ๊ฒฝ์šฐ ํ•จ์ˆ˜๋ฅผ ๋ฐ”๋กœ retrun ํ•˜์˜€๋Š”๋ฐ, ์ด ๊ฒฝ์šฐ์—๋Š” ์•„๋ฌด ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ๋„ ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฏธ์ง€๊ฐ€ ์ ์šฉ๋˜์ง€ ์•Š๊ณ , ์ด๋ฏธ์ง€ alt๊ฐ€ ์ถœ๋ ฅ๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • ํ•ด๊ฒฐ๋ฐฉ์•ˆ : webpSupported๋ฅผ ์ธ์ž๋กœ ๋ฐ›์ง€ ์•Š๊ณ  ํ•จ์ˆ˜ ์•ˆ์—์„œ ์„ ์–ธํ•˜์˜€์Šต๋‹ˆ๋‹ค.
export const resolveWebp = (webpSupported, img, fallbackExt) => {
 const webpSupported = document.body.classList.contains("webp");
 // ์ด๋ฏธ์ง€ ํฌ๋งท
 const ext = img.split(".").pop();
 // webpSupported false, ext๊ฐ€ webp์ธ ๊ฒฝ์šฐ
 if (!webpSupported && ext === "webp") {
   return img.replace("/webp", "").replace(".webp", `.${fallbackExt}`);
 }
 return img;
};

(6) webp ์ด๋ฏธ์ง€๊ฐ€ ์ ์šฉ๋  ๋•Œ svg ์ด๋ฏธ์ง€ ๋ฆฌ์†Œ์Šค๋„ ๊ฐ™์ด ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋ฌธ์ œ

  • ์›์ธ : ์ดˆ๊ธฐ body classList๊ฐ€ ์—†์„ ๋•Œ resolveWebp ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • ํ•ด๊ฒฐ๋ฐฉ์•ˆ : detectWebpSupport ํ•จ์ˆ˜ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋ฅผ ์ด์šฉํ•˜์—ฌ app ์ปดํฌ๋„ŒํŠธ์— detectWebpSupport ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜๊ณ  ๋‚˜์„œ body์˜ classList ๋ถ€์—ฌ ํ›„ webpChecked ์ƒํƒœ๋ฅผ ํ†ตํ•ด ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง์„ ๊ฑธ์–ด์ฃผ์–ด body์— classList๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ์—๋งŒ ๋ Œ๋”๋ง ๋  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์—ฌ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜์˜€์Šต๋‹ˆ๋‹ค.

detectWebpSupport ํ•จ์ˆ˜

export async function detectWebpSupport() {
  // ์ด๋ฏธ์ง€๊ฐ€ webp ์ง€์›์œ ๋ฌด๋ฅผ ํŒŒ์•…ํ•˜๊ณ  ๋ Œ๋”๋ง ๋  ์ˆ˜ ์žˆ๋„๋ก ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ
  return new Promise((resolve) => {
    const image = new Image();
    // 1px x 1px WebP ์ด๋ฏธ์ง€
    const webpdata =
      "data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA=";

    const callback = (event) => {
      // event.type์ด "load"์ธ ๊ฒฝ์šฐ์™€ ์ด๋ฏธ์ง€์˜ ๋„ˆ๋น„(image.width)๊ฐ€ 1 ํ”ฝ์…€์ธ ๊ฒฝ์šฐ๋ฅผ ๊ฒ€์‚ฌํ•˜์—ฌ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ WebP ์ด๋ฏธ์ง€๋ฅผ ์ง€์›ํ•˜๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ํŒ๋ณ„
      const result = event?.type === "load" && image.width === 1;
      if (result) {
        resolve(true); // WebP ์ง€์›๋จ
      } else {
        resolve(false); // WebP ์ง€์›๋˜์ง€ ์•Š์Œ
      }
    };

    image.onerror = callback;
    image.onload = callback;
    image.src = webpdata;
  });
}

App ์ปดํฌ๋„ŒํŠธ

//                           '
//                           '
//                           '
//                         (์ƒ๋žต)

// webp ์ง€์›์œ ๋ฌด๊ฐ€ ํ™•์ธ ๋˜์—ˆ์„๋•Œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋ง ์‹œํ‚ค์œ„ํ•ด ์‚ฌ์šฉ
 const [webpChecked, setWebpChecked] = useState(false);

 const checkwebp = async () => {
   const webpSupport = await detectWebpSupport();
   if (webpSupport) {
     // webp๊ฐ€ ์ง€์›๋œ๋‹ค๋ฉด body์— webp classList์ถ”๊ฐ€
     document.body.classList.add("webp");
   } else {
     // webp๊ฐ€ ์ง€์›๋˜์ง€ ์•Š๋Š”๋‹ค๋ฉด body์— no-webp classList์ถ”๊ฐ€
     document.body.classList.add("no-webp");
   }
   // webp ์ง€์›์œ ๋ฌด๊ฐ€ ํ™•์ธ๋˜์—ˆ๋‹ค๋ฉด true๋กœ ์„ค์ •
   setWebpChecked(true);
 };

 useEffect(() => {
   checkwebp();
 }, []);

//                         (์ƒ๋žต)
//                           '
//                           '
//                           '

About

๐ŸŽฌ ์˜ํ™” ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜๊ณ , ์˜ํ™” ๋ฆฌ๋ทฐ๋ฅผ ํ†ตํ•ด ์˜ํ™” ์ •๋ณด๋ฅผ ๊ณต์œ ํ•˜๋Š” ์‚ฌ์ดํŠธ

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages