์ด๋ฒ์ ๋ฒจ๋ก๊ทธ์์ ํฐ์คํ ๋ฆฌ๋ก ๋ธ๋ก๊ทธ๋ฅผ ์ฎ๊ธฐ๋ฉด์ ์ด์ ์ ์์ฑํ๋ ํ๋ก์ ํธ ๊ด๋ จ ๋ธ๋ก๊ทธ๋ฅผ ์ฎ๊ฒจ ์ ์ด๋ณธ๋ค.
์ ์ด ์ฐํฉ ๋์๋ฆฌ CEOS์ ํ๋ก ํธ์๋ ์คํฐ๋ ๋ง์ง๋ง ๊ณผ์ ๋ก next js๋ฅผ ํ์ฉํ ๋ทํ๋ฆญ์ค ํด๋ก ์ฝ๋ฉ ํ๋ก์ ํธ๋ฅผ ์งํํ๋ค.
ํด๋น ํ๋ก์ ํธ์์๋ ๋ฉ์ธ ํ์ด์ง, searchPage๋ง ๊ตฌํํ๋ค.
๋ฐฐํฌ๋งํฌ: https://next-netflix-16th-pre-folio-front.vercel.app/
GITHUB: https://github.com/Pre-folio/next-netflix-16th
I. ํด๋ ๊ตฌ์กฐ
ํด๋ ๊ตฌ์กฐ๋ ๋ค์๊ณผ ๊ฐ๋ค.
src
|-api
|-components
|-elements (๊ณต์ ์ปดํฌ๋ํธ)
|-homePage
|-landingPage
|-searchPage
|-icons
|-pages
|-home
|-index.tsx
|-[id].tsx
|-search
|-index.tsx
|-states
|-styles
II. HomePage
React-Query ๋์ ๊ธฐ
์ด๋ฒ ํ๋ก์ ํธ์์๋ ์๋ก์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํด๋ณด๊ณ ์ถ์ด react-query๋ฅผ ๋์
ํ๋ค.
์ด์ ์ ๋
ธ๋ง๋์ฝ๋ ๊ฐ์๋ฅผ ํตํด react-query๋ฅผ ์ ๊น ๊ณต๋ถํ ์ ์ด ์๋๋ฐ, ๋ค์ ๊ธ์์ ๋ณผ ์ ์๋ค.
React-Query ๊ณต๋ถ์๋ฃ
1. React-query ๋์
์ฐ์ api ํด๋๋ฅผ ๋ฐ๋ก ๋ง๋ค์ด api ํธ์ถ ํจ์๋ค์ ๋ฐ๋ก ๊ด๋ฆฌํ๋ค.
export const getNowPlaying = () => {
return client.get(`movie/now_playing?api_key=${API_KEY}`).then((res) => res.data);
};
export const getTopRated = () => {
return client.get(`movie/top_rated?api_key=${API_KEY}`).then((res) => res.data);
};
export const getPopular = () => {
return client.get(`movie/popular?api_key=${API_KEY}`).then((res) => res.data);
};
export const getUpcoming = () => {
return client.get(`movie/upcoming?api_key=${API_KEY}`).then((res) => res.data);
};
2. ์๋ฒ์ฌ์ด๋ ๋ ๋๋ง
react-query์ ์๋ฒ์ฌ์ด๋ ๋ ๋๋ง์๋ ๋ ๊ฐ์ง ๋ฐฉ๋ฒ์ด ์๋ค. (InitialData, Hydration)
์ด์ค ๋๋ Hydration ๋ฐฉ๋ฒ์ ์ฌ์ฉํ๋ค.
Hydrate๋?
- Next.js์์๋ Pre-Rendering ๋ ์น ํ์ด์ง๋ฅผ ํด๋ผ์ด์ธํธ์๊ฒ ๋จผ์ ๋ณด๋ด๊ณ , React๊ฐ ๋ฒ๋ค๋ง ๋ ์๋ฐ์คํฌ๋ฆฝํธ ์ฝ๋๋ค์ ํด๋ผ์ด์ธํธ์๊ฒ ์ ์ก
- Next.js๋ก ์ ์๋ ์นํ์ด์ง๋ฅผ ๋ฐฉ๋ฌธํ๊ฒ ๋๋ฉด ๋งจ ์ฒ์ document ํ์ ์ ํ์ผ์ ์ ์ก๋ฐ๊ณ , ๊ทธ ์ดํ์ ๋ ๋๋ง ๋ React.js ํ์ผ๋ค์ด Chunk ๋จ์๋ก ๋ค์ด๋ก๋๋๋ ๊ฒ์ ํ์ธํ ์ ์์
- ์ ๋ฆฌ์กํธ ์ฝ๋๋ค์ด ์ด์ ์ ๋ณด๋ด์ง HTML DOM ์์ ์์ ํ๋ฒ ๋ ๋ ๋๋ง ํ๋ฉด์ ๋ ๋๋ง์ ํ๊ฒ ๋๋๋ฐ ์ด ๊ณผ์ ์ Hydrate๋ผ๊ณ ํฉ๋๋ค.
Hydration ๋ฐฉ์์ ์ฌ์ฉํ๋ฉด getServersideProps์์ prefetch๋ฅผ ํตํด ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ ๋ค, queryClient๋ฅผ dehydrateํ์ฌ props์ dehydratedState๋ก ์ค๋ค.
export async function getServerSideProps() {
const queryClient = new QueryClient();
await queryClient.prefetchQuery(['now-playing'], getNowPlaying);
await queryClient.prefetchQuery(['top-rated'], getTopRated);
await queryClient.prefetchQuery(['popular'], getPopular);
await queryClient.prefetchQuery(['up-coming'], getUpcoming);
return {
props: {
dehydratedState: dehydrate(queryClient),
},
};
getServerSideProps() ํจ์ ์์ prefetchQuery๋ฅผ ์ฌ์ฉํ๊ณ , props์ dehydratedState๋ฅผ ์ฃผ์ด returnํ๋ค.
const { isLoading: nowPlayingLoading, data: nowPlayingData } = useQuery(['now-playing'], getNowPlaying);
const { isLoading: topRatedLoading, data: topRatedData } = useQuery(['top-rated'], getTopRated);
const { isLoading: popularLoading, data: popularData } = useQuery(['popular'], getPopular);
const { isLoading: upComingLoading, data: upComingData } = useQuery(['up-coming'], getUpcoming);
setNowPlayingMovies(nowPlayingData.results);
setTopRatedMovies(topRatedData.results);
setPopularMovies(popularData.results);
setUpComingMovies(upComingData.results);
์ดํ์๋ useQuery๋ก ๊ฐ ํจ์์ isLoading ๊ฐ๊ณผ data ๊ฐ์ ๋ฐ์์๋ค. (useQuery๋ ๊ธฐ๋ณธ์ ์ผ๋ก isLoading๊ณผ data ๊ฐ์ ๋ฐํํ๋ค.)
const์ ์ธ์ ๋ค์ ์ฐ์ธ ๋ฐ์ดํ๋ ๊ทธ ์์ฑ์ ์ด๋ฆ์ ๋ถ์ฌํ๋ค๋ ๋ป์ด๋ค. (js ๋ฌธ๋ฒ)
React-Query ์ฌ์ฉํด ๋ณด๋ ์ข์ ์
1. isLoading๊ฐ๋ ํจ๊ป ๋ฐํํด ์ค - ๋ฐ๋ก set ํ ํ์ ์์ด์ ํธ๋ฆฌํจ
2. ์บ์ฑ ๊ธฐ๋ฅ์ด ์์ด์ ํ๋ฒ ๋ค์ด๊ฐ ํ์ด์ง ๋ค์ ๋ค์ด๊ฐ๋ฉด ๋ก๋ฉ์๊ฐ ์์ด ๋ฐ๋ก ๋ณด์
3. ๋์ผ ๋ฐ์ดํฐ๋ฅผ ์ฌ๋ฌ ๋ฒ ์์ฒญํ๋ฉด ์์์ ๊ฑธ๋ฌ์ ํ ๋ฒ๋ง ์์ฒญ → ํจ์จ up
4. ๋ฆฌ์กํธ ํ ๋ค์ด๋ ์ฌ์ฉ ๊ตฌ์กฐ๊ฐ ๋น์ทํด์ ๋ฐฐ์ฐ๊ธฐ ์ฉ์ดํจ
III. SearchPage
1. ์๋ฒ์ฌ์ด๋ ๋ ๋๋ง์ ์ด๋ป๊ฒ?
๊ฒฐ๋ก ๋ถํฐ ๋งํ์๋ฉด ํด๋ฆญ์ผ๋ก ๊ฒ์ํ๋ ๋ฐฉ์๋ ์๋ ์ค์๊ฐ ๊ฒ์ ๊ธฐ๋ฅ์ ์๋ฒ์ฌ์ด๋ ๋ ๋๋ง์ผ๋ก ํ๋ ค๋ ๊ฒ์ ๋ง๋ ์ ๋๋ ์ผ์ด์๋ค.
์๋ฒ์ฌ์ด๋ ๋ ๋๋ง ๋ฐฉ์์ ์๋ฒ์ html ํ์ผ์ ์ ์ฅํด ๋์๋ค๊ฐ, ํ์ ํด๋ผ์ด์ธํธ์ ์์ฒญ์ ๋ฐ๊ณ html ํ์ผ์ ๋ณํํ์ฌ ๋ธ๋ผ์ฐ์ ์ ์ถ๋ ฅํ๋ ๋ฐฉ์์ด๋ค. html ํ์ผ์ ์๋ฒ์ ๋ฏธ๋ฆฌ ์ ์ฅํด์ผ ํ๋๋ฐ, ์ค์๊ฐ์ผ๋ก ๊ณ์ api๋ฅผ ์๋ก ์์ฒญํด์ ํ์ด์ง๋ฅผ ๋ง๋ค๊ฒ ์ํค๋ ๋ฐฉ๋ฒ์ ๋ง์ง ์๋ ๋ฐฉ๋ฒ์ด์๋ค.
useEffect(() => {
setIsLoading(true);
if (debouncedSearchWord) {
searchMovies(searchWord).then((res) => {
setSearchedMovies(res.data.results);
setIsLoading(false);
return res.data;
});
} else {
getPopular().then((res) => {
setSearchedMovies(res.results);
setIsLoading(false);
return res.data;
});
}
}, [debouncedSearchWord]);
๊ทธ๋์ ์์ฑ๋ ์ฝ๋. ์ผ๋ฐ์ ์ธ api ํธ์ถ๊ณผ ๋ค๋ฅผ ๊ฒ ์๋ค.
์ด๊ธฐ์ ์์ฑ๋ ์ฝ๋๋ผ ์ค์๊ฐ์ผ๋ก ๋ฐ์ดํฐ ๋ถ๋ฌ์ค๊ธฐ ๊ธฐ๋ฅ์๋ง ์ง์คํ๋ค. (์คํฌ๋กค, ์ค์ผ๋ ํค X)
2. ์ค์ผ๋ ํค ์ด๋ฏธ์ง
useQuery๊ฐ ๋ฐ์์ค๋ ๊ฐ ์ค isLoading ๊ฐ์ ํตํด, ํด๋น ๊ฐ์ด true์ธ์ง false ์ธ์ง์ ๋ฐ๋ผ true๋ฉด ์ค์ผ๋ ํค ์ด๋ฏธ์ง๋ฅผ, false๋ฉด ์ํ์ ํด๋นํ๋ ์ธ๋ค์ผ ์ด๋ฏธ์ง๋ฅผ ๋์์ค ์ ์์๋ค.
let arr = new Array(20).fill(1);
const {
getBoard,
getNextPage,
getBoardIsSuccess,
getNextPageIsPossible,
isLoading
} = useInfiniteScrollSearchQuery(debouncedSearchWord);
<div>
<ListTitle>Top Searches</ListTitle>
{!isLoading
? // ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋๋ฐ ์ฑ๊ณตํ๊ณ ๋ฐ์ดํฐ๊ฐ 0๊ฐ๊ฐ ์๋ ๋ ๋ ๋๋ง
getBoardIsSuccess && getBoard!.pages
? getBoard!.pages.map((page_data: any, page_num: any) => {
const board_page = page_data.board_page;
return board_page?.map((item: any, idx: any) => {
if (
// ๋ง์ง๋ง ์์์ ref ๋ฌ์์ฃผ๊ธฐ
getBoard!.pages.length - 1 === page_num &&
board_page.length - 1 === idx
) {
return (
// ๋ง์ง๋ง ์์์ ref ๋ฃ๊ธฐ ์ํด div๋ก ๊ฐ์ธ๊ธฐ
<div ref={ref} key={item.board_id}>
<SkeletonItem key={item.board_id} />
</div>
);
} else {
return <SearchItem key={item.board_id} {...item} />;
}
});
})
: null
: arr.map((arr, index) => {
return <SkeletonItem key={index} />;
})}
</div>
3. Debounce
์ ๋ ฅ ํ ์ผ์ ์๊ฐ์ด ์ง๋ ๋ค์ ๋ ๋๋ง ํ๊ฒ๋ ๋ง๋ค๊ณ ์ถ์ด ๋ง๋ ๊ธฐ๋ฅ์ด๋ค.
hooks ํด๋ ์๋์ useDebounse.tsx ํ์ผ์ ๋ง๋ค์ด ๋ค์๊ณผ ๊ฐ์ด ์ฝ๋๋ฅผ ์์ฑํ๋ค.
import { useState, useEffect } from 'react';
export const useDebounce = (value: string, delay: number) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
};
props๋ก value์ delay๋ฅผ ๋ฐ์ ๋ค์๊ณผ ๊ฐ์ด ๊ฐ๊ณผ ์๊ฐ์ ์ ๋ ฅํด ์ฃผ์ด ์ฌ์ฉํ ์ ์๋ค.
const debouncedSearchWord = useDebounce(searchWord, 500);
4. ๋ฌดํ ์คํฌ๋กค
์ฐ์ react-query์ ๋ด์ฅํจ์์ธ useInfiniteQuery์ ๊ดํ ๊ณต๋ถ๋ฅผ ๋ง์ด ํ๋ค.
๊ณต๋ถ์๋ฃ
๋ฌดํ ์คํฌ๋กค ๊ธฐ๋ฅ์ ๊ตฌํํ ๋ ์ฌ์ฉํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ ๋ค์๊ณผ ๊ฐ๋ค:
useInfiniteQuery(react-query), useInView(react-intersection-observer)
1. useInfiniteQuery
์ฐ์ useInfiniteScrollQuery๋ผ๋ ์ปดํฌ๋ํธ ํ์ผ์ ํ๋ ์์ฑํ์ฌ ๋ค์๊ณผ ๊ฐ์ด ์์ฑํ๋ค.
import { useInfiniteQuery } from '@tanstack/react-query';
import client from '../../api/client';
const API_KEY = process.env.NEXT_PUBLIC_TMDB_API_KEY;
export function useInfiniteScrollSearchQuery(debouncedSearchWord: string) {
const getSearchData = async ({ pageParam = 1, searchWord = debouncedSearchWord }) => {
const res = await client.get(`search/movie/?api_key=${API_KEY}&query=${searchWord}&page=${pageParam}`);
return {
board_page: res.data.results,
current_page: pageParam,
isLast: res.data.total_pages === pageParam, //๋ฏธ์ณค๋ค.
current_word: debouncedSearchWord,
};
};
const {
data: getBoard,
fetchNextPage: getNextPage,
isSuccess: getBoardIsSuccess,
hasNextPage: getNextPageIsPossible,
isLoading: isLoading,
} = useInfiniteQuery(['search', debouncedSearchWord], getSearchData, {
getNextPageParam: (lastPage: any, pages: any) => {
if (!lastPage.isLast) return lastPage.current_page + 1;
return undefined;
},
});
return {
getBoard,
getNextPage,
getBoardIsSuccess,
getNextPageIsPossible,
isLoading,
};
}
ํด๋น ์ฝ๋๋ ๊ฒ์ ํ ๊ฒ์์ด๊ฐ ๋ฐ๋ ๋๋ง๋ค api๋ฅผ ๋ถ๋ฌ์ค๊ณ , ๊ทธ๊ณณ์ ๋ฌดํ์คํฌ๋กค ๊ธฐ๋ฅ์ ๋ฃ๋ ๊ฒ์ด๋ค.
์ฐ์ ์ ์ผ๋ก ๊ธฐ๋ฅ ๊ตฌํ ํ๋ ๊ฒ์ ์ง์คํ์ฌ api ๋ฐ๋ ๊ฒ๋ ํจ๊ป ์ฝ๋ ์์ ๋ฃ์๋ค…(์ถํ ๋ฆฌํฉํ ๋ง ์์ )
- ์ฒ์์ getSearchData ํจ์๋ฅผ ํตํด api๋ฅผ ํธ์ถํ๊ณ , return ๊ฐ์ผ๋ก
board_page (ํ์ด์ง์ ๊ฒฐ๊ณผ),
current_page (ํ์ฌ ํ์ด์ง: 1๋ถํฐ 1์ฉ ์ฆ๊ฐ๋๋ ๊ฐ),
isLast (๋ง์ง๋งํ์ด์ง์ธ์ง ์๋์ง ํ์ธํด ์ฃผ๋ boolean ๊ฐ),
hasNextPage (๋ค์ ํ์ด์ง๋ฅผ ๊ฐ์ง๊ณ ์๋์ง ์๋ ค์ฃผ๋ boolean ๊ฐ),
isLoading(๋ก๋ฉ ์ค์ธ์ง ํ์ธํด ์ฃผ๋ boolean ๊ฐ)์ ๋ฐ์๋ค. - ๋ค์์ผ๋ก๋ useInfiniteQuery ํจ์๋ฅผ ์ฌ์ฉํ๋ค.
ํด๋น ํจ์๋ ๋ค์๊ณผ ๊ฐ์ props๋ฅผ ๊ฐ์ง๋ค.
useInfiniteQuery(queryKey: ๊ณ ์ ์ key๊ฐ, queryFunction: ํจ์, option: ๊ทธ ์ธ ์ต์ ๋ค)
option ํญ๋ชฉ์ getNextPageParam์ ์๋ ์ฝ๋์ ๊ฐ์ด ์์ฑํด ์ฃผ๋ฉด props ํญ๋ชฉ์ lastPage์ pages๋ ์ฝ๋ฐฑ ํจ์์์ ๋ฆฌํดํ ๊ฐ์ ์๋ฏธํ๋ค๊ณ ํ๋ค.
(lastPage: ์ง์ ์ ๋ฐํ๋ ๋ฆฌํด๊ฐ, pages: ์ฌํ ๋ฐ์์จ ์ ์ฒด ํ์ด์ง)
const {
data: getBoard,
fetchNextPage: getNextPage,
isSuccess: getBoardIsSuccess,
hasNextPage: getNextPageIsPossible,
isLoading: isLoading,
} = useInfiniteQuery(['search', debouncedSearchWord], getSearchData, {
getNextPageParam: (lastPage: any, pages: any) => {
if (!lastPage.isLast) return lastPage.current_page + 1;
return undefined;
},
});
์ฌ๊ธฐ์ ๋ก๋ฉ๋ ํ์ด์ง๊ฐ ๋ง์ง๋ง ํ์ด์ง์ธ์ง ๊ฒ์ฌ ํ์ ๋ง์ผ๋ฉด undefined๋ฅผ, ์๋๋ฉด current_page ๊ฐ์ +1์ ํด์ฃผ์ด ๋ค์ ํ์ด์ง๊ฐ ๋ฐํ๋๋๋ก ํ์๋ค.
์ด์ธ, useInfiniteQuery๋ฅผ ๊ณต๋ถํ๋ฉฐ ์๊ฒ ๋ ๋ค๋ฅธ option๋ค์ ๊ดํด์๋ ์์ฑํด๋ณธ๋ค..(์๊น์์)
- ์๋ก์ด ์ฟผ๋ฆฌ ์ธ์คํด์ค๊ฐ ๋ง์ดํธ ํ์ ๋ (refetchOnMount)
- ์๋ฅผ ๋ค์ด ๋ฐ์ดํฐ๋ฅผ fetch ํ๋ ์ปดํฌ๋ํธ๊ฐ ์๊ณ , ์ด ์ปดํฌ๋ํธ๊ฐ ๋ค๋ฅธ ๋ฐ์์๋ ๋ง์ดํธ ๋๋ฉด ์บ์ ๋ ๋ฐ์ดํฐ๋ฅผ ์ฐ๋ ๊ฒ ์๋๋ผ ๊ทธ๋ ๋๋ค์ refetch ํ๋ค๋ ๋ป.
- ์๋๊ฐ refocus๋์ ๋ (refetchOnWindowFocus)
- ๋คํธ์ํฌ๊ฐ ๋ค์ ์ฐ๊ฒฐ๋์ ๋ (refetchOnReconnect)
- refetch interval ์ค์ ์ ํด์คฌ์ ๋ (refetchInterval)
์ฌ์ฉ์์
const {
data: getBoard,
fetchNextPage: getNextPage,
isSuccess: getBoardIsSuccess,
hasNextPage: getNextPageIsPossible,
isLoading: isLoading,
}: any = useInfiniteQuery(['popular'], getInitialData, {
refetchonMount: true,
refetchOnWindowFocus: true,
...
},
});
์๋๋ ๊ฒ์ ๋จ์ด๊ฐ ๋ณ๊ฒฝ๋ ๋๋ง๋ค useInfiniteQuery๋ฅผ ์ด๋ป๊ฒ ์ฌํธ์ถํ๋ฉด ์ข์ ์ง์ ๊ดํ ๊ณ ๋ฏผ์ ์ ๋ง ์ค๋ซ๋์ ํ๋ค.
์ฐพ๋ค๊ฐ ์๋ useInfiniteQuery์ ์ฒซ ์ธ์์ธ queryKey์ ๋ฐฐ์ด๋ก ๋ณ์๋ฅผ ๋ฃ์ผ๋ฉด ๋ณ์๊ฐ ๋ฌ๋ผ์ง ๋๋ง๋ค ๋ ๋๋ง ๋๋ค๊ณ ํ๋๋ผ…
(๋์ค์ ์๊ณ ๋ณด๋ key๊ฐ์ด ๋ณํด์ ๋ค์ ์บ์ฑ์ ๊ด๋ฆฌํ๋ ค๊ณ ๋ ๋๋ง ํ๋ค๊ณ ํ๋ค.)
<๊ฒฐ๊ณผ>
useInfiniteQuery(['search', debouncedSearchWord], getSearchData)
์ ์ฌ์ฉํ๋ ๊ฒ์์ด๊ฐ ๋ฐ๋ ๋๋ง๋ค ๋ ๋๋ง ๋์๋ค!!
2. useInview
๋ค์์ react-intersection-observer ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ค์น ํ import ํ useInView๋ผ๋ ํจ์์ด๋ค.
- ์ฌ์ฉ๋ฒ
const [ref, isView] = useInView()
์๋ ์ฝ๋์์ div์ ref prop์ useInView์ ref๋ฅผ ์ฃผ๋ฉด,
์ฌ์ฉ์๊ฐ div ์์๋ฅผ ๋ณด๋ฉด isView๊ฐ true, ์ ๋ณด๋ฉด false๊ฐ ๋ฐํ๋๋ค.
const {
getBoard,
getNextPage,
getBoardIsSuccess,
getNextPageIsPossible,
isLoading
} = useInfiniteScrollSearchQuery(debouncedSearchWord);
useEffect(() => {
if (isView && getNextPageIsPossible) {
getNextPage();
}
}, [isView, getBoard]);
์์ ๊ฐ์ ์ฝ๋๋ก useInfiniteScrollSearchQuery ํจ์๋ฅผ import ํ๊ณ return ํ ๊ฐ๋ค์ ๋ฐ์์๋ค.
์ดํ์๋ useEffect ํจ์๋ฅผ ์ฌ์ฉํ์ฌ ๋ค์ ํ์ด์ง๋ฅผ ๊ฐ์ ธ์ค๋ ๊ฒ์ด ๊ฐ๋ฅํ๋ฉด (ํด๋น ํ์ด์ง๊ฐ ๋ง์ง๋ง ํ์ด์ง๊ฐ ์๋๋ผ๋ฉด) ๋ค์ ํ์ด์ง๋ฅผ ๋ฐ์์ฌ ์ ์๋๋ก ํ์๋ค.
์ด๋ ๊ฒ ์ด๋ฒ netflix ํด๋ก ์ฝ๋ฉ ํ๋ก์ ํธ์์ ์ฌ์ฉํ ๊ธฐ์ ๋ค์ ์ ๋ฆฌํด ๋ณด์๋ค.
๋๋ฌด ๋ง์ ํจ์จ์ ์ธ ๊ธฐ์ ๋ค์ ์ฌ์ฉํด ๋ณด๊ณ ๊ฒฝํํด ๋ดค๋ค๋ ์ ์์ ์ข์ ๊ฒฝํ์ด์๋ ๊ฒ ๊ฐ๋ค.
'๐ป ํ๋ก์ ํธ > ๐งธ TOY-PROJECTS' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[DeepLook] 5. ๋ฐฑ์๋ ์ฐ๊ฒฐ (0) | 2023.06.21 |
---|---|
[DeepLook] 4. ๋ชจ๋ธ ์ ์ ๋ฐ ํ์ต (0) | 2023.06.21 |
[DeepLook] 3. ์ ์ฒ๋ฆฌ (haar-cascade ์๊ณ ๋ฆฌ์ฆ) (0) | 2023.06.20 |
[DeepLook] 2. AI ์์ ์ค๊ณ ๊ณผ์ / ํฌ๋กค๋ง (0) | 2023.06.20 |
[DeepLook] 1. ์์ํ๊ฒ ๋ ๊ณ๊ธฐ / ํ๋ก์ ํธ IA (0) | 2023.06.20 |