ID | PW |
---|---|
[email protected] | asdzxc123! |
๋ฐฐํฌ URL : ๐ MovieWorld
- 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 ๊ตฌ์ฑ ๋ฐ ์ค๋ช
ํ๋ก ํธ์๋ | ๋ฒก์๋ | ๋์์ธ | ๋ฐฐํฌ, ๊ด๋ฆฌ |
---|---|---|---|
- TheMovieDBAPI๋ฅผ ํตํด ์ํ์ ๋ณด๋ฅผ ๋ฐ์์ค๋๋ก ํ์์ต๋๋ค.
- ๊ตฌํ ๊ธฐ๋ฅ๋ค(๋ก๊ทธ์ธ, ์์ ๋ก๊ทธ์ธ, ํ์๊ฐ์ , ์ด๋ฉ์ผ|๋น๋ฐ๋ฒํธ ์ฐพ๊ธฐ, ์ฐ, ๋ฆฌ๋ทฐ ๊ด๋ จ ๊ธฐ๋ฅ, ํ๋กํ ๋ณ๊ฒฝ, ๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ, ๋์ ์ฐ ๋ชฉ๋ก, ๋์ ๋ฆฌ๋ทฐ ๋ชฉ๋ก, ๋ก๊ทธ์์)์ firebase๋ฅผ ์ด์ฉํ์ฌ ๊ตฌํ ํ์์ต๋๋ค.
๋ชจ๋๋ช | ์ฉ๋ |
---|---|
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๋ฅผ ํตํด ๊ตฌํํ ๋ด์ฉ๊ณผ ์ฒดํฌ๋ฆฌ์คํธ๋ฅผ ๋ง๋ค์ด ์ด๋ค ์์ ์ ํ ์ง ๋ฆฌ์คํธ ๋ง๋ค์ด ๊ด๋ฆฌํ์์ต๋๋ค.
- GitHub Project
- ํ๋ก์ ํธ ๋ณด๋์ ์ด์ ๋ชฉ๋ก์ ํตํด ๊ฐ๋ฐ ๊ณผ์ ๊ณผ ์งํ ์ํฉ์ ํ ๋์ ์์ ๋ณผ ์ ์์์ต๋๋ค.
์ด๋ค ์์ ์ ํ๋์ง ํ์ ํ๊ธฐ ์ํด ์ปจ๋ฒค์ ์ ์ ํ์ฌ commit๊ณผ isuue๋ฅผ ๊ด๋ฆฌํ์์ต๋๋ค.
Fix
: ์์ ์ฌํญ๋ง ์์ ๊ฒฝ์ฐ
Feat
: ์๋ก์ด ๊ธฐ๋ฅ์ด ์ถ๊ฐ ๋๊ฑฐ๋ ์ฌ๋ฌ ๋ณ๊ฒฝ ์ฌํญ๋ค์ด ์์ ๊ฒฝ์ฐ
Style
: ์คํ์ผ๋ง ๋ณ๊ฒฝ๋์์ ๊ฒฝ์ฐ
Docs
: ๋ฌธ์๋ฅผ ์์ ํ ๊ฒฝ์ฐ
Refactor
: ์ฝ๋ ๋ฆฌํฉํ ๋ง์ ํ๋ ๊ฒฝ์ฐ
Remove
: ํ์ผ์ ์ญ์ ํ๋ ์์
๋ง ์ํํ ๊ฒฝ์ฐ
Rename
: ํ์ผ ํน์ ํด๋๋ช
์ ์์ ํ๊ฑฐ๋ ์ฎ๊ธฐ๋ ์์
๋ง์ธ ๊ฒฝ์ฐ
Relese
: ๋ฐฐํฌ ๊ด๋ จ ์์
์ธ ๊ฒฝ์ฐ
Chore
: ๊ทธ ์ธ ๊ธฐํ ์ฌํญ์ด ์์ ๊ฒฝ์ฐ ์ฌ์ฉํฉ๋๋ค.
๐ ๊ตฌํ ๊ธฐ๋ฅ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ( ์ ๋ชฉ ํด๋ฆญ ์ ํด๋น ๊ธฐ๋ฅ ์์ธ์ค๋ช ์ผ๋ก ์ด๋๋ฉ๋๋ค. )
๐์์ ํ๋ฉด | ๐๋ก๊ทธ์ธ | ๐์์ ๋ก๊ทธ์ธ |
---|---|---|
๐๋ฉ์ธ ํ์ด์ง | ๐์ํ์ ๋ณด-๋ฆฌ๋ทฐ(์์ฑ,์์ ,์ญ์ ,์ ๊ณ ) | ๐์ํ์ ๋ณด-์ฐ, ๊ด๋ จ์์ |
---|---|---|
๐์ํ์ ๋ณด-์คํฌ์ผ๋ฌ, ํํฐ | ๐๋ง์ดํ์ด์ง-์ฐ ๋ชฉ๋ก | ๐๋ง์ดํ์ด์ง-๋ฆฌ๋ทฐ ๋ชฉ๋ก |
---|---|---|
๐๋ง์ดํ์ด์ง-์ฐ | ๐๋ง์ดํ์ด์ง-ํ๋กํ๋ณ๊ฒฝ | ๐๋ง์ดํ์ด์ง-๋น๋ฐ๋ฒํธ๋ณ๊ฒฝ |
---|---|---|
axios๋ฅผ customํ์ฌ baseURL๋ฅผ ์ค์ ํ์ฌ URL ์ค๋ณต ์ค์ ์ ํผํ๊ณ , ์ฝ๋๋ฅผ ๋จ์ถ์์ผฐ์ต๋๋ค.
import axios from "axios";
export const customAxios = axios.create({
baseURL: process.env.REACT_APP_BASE_URL,
});
์ฌ์ฉํ 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;
};
๋ฆฌ๋ทฐ์ ๊ด๋ จ๋ ๊ธฐ๋ฅ์ firebase firestore๋ฅผ ํตํด db๋ฅผ ์ค๊ณํ๊ณ ์ง์ ๊ตฌํํ์์ต๋๋ค.
-
- reviewList ์ปฌ๋ ์ ์๋ docs id๋ก๋ movieId๋ฅผ ๋์ด ๊ฐ ์ํ ๋ฐ์ดํฐ๋ฅผ ๊ตฌ๋ถํด์ฃผ์์ต๋๋ค.
- movieId Docs ์๋๋ก ์ํ์ ๋ฆฌ๋ทฐ ๋ฐ์ดํฐ๋ค์ด ๋ค์ด๊ฐ๋ review subColletion์ด ์กด์ฌํฉ๋๋ค.
- MovieInfo ๋ชจ๋ฌ ์ฐฝ์ ๋ฆฌ๋ทฐ๋ชฉ๋ก์ ๋ฌดํ์คํฌ๋กค ํ์ด์ง ๊ธฐ๋ฅ์ผ๋ก ๋ฐ์์ค๊ธฐ ์ํด subCollection์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ค๊ณํ์์ต๋๋ค.
- subColletion ์๋ docs id๋ก commentId๋ฅผ ์ฃผ์ด reivew ๋ฐ์ดํฐ๋ฅผ ๊ตฌ๋ถํด์ฃผ์์ต๋๋ค.
- docs ์๋๋ก๋ ๋ฆฌ๋ทฐ ์ ๋ณด๊ฐ ์ ์ฅ๋๋ field๊ฐ ์กด์ฌํฉ๋๋ค.
-
- user ์ปฌ๋ ์ ์๋ docs id๋ก ๊ฐ ๊ณ์ ์ uid๋ฅผ ์ฃผ์ด user ๋ฐ์ดํฐ๋ฅผ ๊ตฌ๋ถํด์ฃผ์์ต๋๋ค.
- docs ์๋๋ก๋ user๋ฐ์ดํฐ๊ฐ ๋ค์ด๊ฐ๋ field์ ๋ฆฌ๋ทฐ ๋ชฉ๋ก์ ์ํ ์ ๋ณด๊ฐ ๋ค์ด๊ฐ๋ reviewListMovieInfo subCollection์ด ์กด์ฌํฉ๋๋ค.
- ๋ง์ดํ์ด์ง์์ ๋ฆฌ๋ทฐ๋ชฉ๋ก์์ ์ํ๋ฐ์ดํฐ๋ฅผ ๋ฌดํ์คํฌ๋กค ํ์ด์ง ๊ธฐ๋ฅ์ผ๋ก ๋ฐ์์ค๊ธฐ ์ํด subColletion์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ค๊ณํ์์ต๋๋ค.
- subColletion ์๋ docs id๋ก commentId๋ฅผ ์ฃผ์ด reivew ์ํ ๋ฐ์ดํฐ๋ฅผ ๊ตฌ๋ถํด์ฃผ์์ต๋๋ค.
- docs ์๋๋ก๋ ์ํ ์ ๋ณด๊ฐ ์ ์ฅ๋๋ field๊ฐ ์กด์ฌํฉ๋๋ค.
-
- 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)];
};
- ๋ฆฌ๋ทฐ ์์ฑ์ ์คํฌ์ผ๋ฌ๊ฐ ํฌํจ๋ ๊ธ์ ์์ฑํ๊ณ ์ถ์ ์ ์ ๊ฐ ์กด์ฌํ๊ธฐ ๋๋ฌธ์ ์คํฌ์ผ๋ฌ๊ฐ ํฌํจ๋ ๋ฆฌ๋ทฐ๋ฅผ ์์ฑ์์ ์คํฌ์ผ๋ฌ๊ฐ ํฌํจ๋๋ ๊ธ์ด ์๋ค๋ ์ฒดํฌ ๊ธฐ๋ฅ์ ๋ง๋ค์ด ๋ค๋ฅธ ์ ์ ๊ฐ ๋ฆฌ๋ทฐ๋ฅผ ๋ณผ ๋ ๋ธ๋ผ์ธ๋ ์ฒ๋ฆฌ ๋๋๋ก ๊ตฌํํ์์ต๋๋ค.
- 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>
// (์๋ต)
// .
// .
// .
- ๋ฌดํ์คํฌ๋กค์ ์ด์ฉํ์ฌ ๋ฐ์ดํฐ๋ฅผ ์ผ๋ถ๋ง ๊ฐ์ ธ์ ์๋ฒ์ ๋ถ๋ด์ ์ค์ด๊ณ ๋ก๋ฉ์๋๋ฅผ ๊ฐ์ ํ๊ธฐ ์ํด ์ฌ์ฉํ์์ต๋๋ค.
- 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]);
// (์๋ต)
// .
// .
// .
- ๋ฌดํ ์คํฌ๋กค ์ ์ฉ ํ๋ฉด
- ๊ธฐ์กด ๊ฒ์์ 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),
[]
);
- ๊ฒ์ ๋๋ฐ์ด์ฑ ์ ์ฉ ์
- ๊ฒ์ ๋๋ฐ์ด์ฑ ์ ์ฉ ํ
- ํ์ํ ์ด๋ฏธ์ง ํฌ๊ธฐ ๋ณด๋ค ๋ ํฐ ์ด๋ฏธ์ง๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ๋ฆฌ์์ค ๋ญ๋น๊ฐ ๋ฉ๋๋ค.
- ์ด๋ฏธ์ง ๋ฆฌ์์ค ๋ญ๋น๋ฅผ ์ต์ํ ํ๊ธฐ ์ํด ์ฌ์ฉํ ํฌ๊ธฐ์ ๋ง๊ฒ ์ด๋ฏธ์ง ํฌ๊ธฐ๋ฅผ ์ต์ํ ํ์์ต๋๋ค.
- ์ด๋ฏธ์ง ํ์์ svg ํ์์ ์ด๋ฏธ์ง๋ฅผ ์ด์ฉํ์์ต๋๋ค.
- svg ํ์ ์ด๋ฏธ์ง๋ ๊ฐ๋จํ ์ด๋ฏธ์ง์ธ ๊ฒฝ์ฐ png ํ์๋ณด๋ค ์ด๋ฏธ์ง ์ฉ๋์ด ์์ผ๋ฉฐ, ๋ ํฐ๋ ๋์คํ๋ ์ด์์๋ ์ด๋ฏธ์ง๊ฐ ๊นจ์ง๋ ํ์์ด ์์ต๋๋ค.
- ์๋ฒ์ ์ด๋ฏธ์ง๋ฅผ ์ ์กํ ์ ํ์ํ ์ด๋ฏธ์ง ๋งํผ๋ง ์ต์๋ก ์์ถํ์ฌ ์ด๋ฏธ์ง ๋ฆฌ์์ค ๋ญ๋น๋ฅผ ์ค์ผ ์ ์๋๋ก ํ์์ต๋๋ค.
- ๋ณ๋์ 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 ํฌ๊ธฐ ๊ฐ์)
-
์ ์ง์ ๋ก๋ฉ ๊ธฐ๋ฒ๋ฅผ ํตํด ์ด๋ฏธ์ง๊ฐ ๋ก๋ฉ๋ ๋ ์๋ณธ ์ด๋ฏธ์ง ๋์ ์ ํ์ง์ ์ด๋ฏธ์ง๋ฅผ ๋ณด์ฌ์ค์ผ๋ก์จ 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 ์ด๋ฏธ์ง๋ 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์ด ๋จ์ถ)
- ๋ชจ๋ฌ ์ฐฝ์์ ํค๋ณด๋ ํฌ์ปค์ฑ ์ต์ ํํ๊ธฐ ์ํด 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ํค๋ฅผ ๋๋ฌ ๋ชจ๋ฌ ์ฐฝ์ด ๋ซํ๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค.
- 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 ์ ์ฉ ํ๋ฉด
- ์์ธ : 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),
[]
);
- ์ด์ ํด๊ฒฐ ์
- ์คํฌ๋กค์ ๋ด๋ฆฐ ํ ํด๋ฆญ ์ ์คํฌ๋กค์ด ์๋ก ์ฌ๋ผ ์ค๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค.
- ์ด์ ํด๊ฒฐ ํ
- ์คํฌ๋กค์ ๋ด๋ฆฐ ํ ํด๋ฆญ ์ ์คํฌ๋กค์ด ์ ์ง๋๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค.
(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)
);
}
}
};
- ์์ธ : 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();
},[]);
- ์์ธ : ๊ธฐ์กด ์ฝ๋์์ 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;
};
- ์์ธ : 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;
};
- ์์ธ : ์ด๊ธฐ 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();
}, []);
// (์๋ต)
// '
// '
// '