-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[6주차] Team Couplelog 김민영 & 안혜연 미션 제출합니다. #13
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
안녕하세요~~ 19기 프론트 멘토 김현민 이라고합니다!!!
전체적으로 너무 깔끔하고 정돈된 코드 잘 봤습니다 ㅎㅎ
너무 잘 정리된 코드들을 보며 저도 배워가는 점이 있었던 것 같습니다.
이번 과제 너무 수고 많으셨어요~!!! 👍
@@ -0,0 +1,30 @@ | |||
name: git push into another repo to deploy to vercel |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요금 부과를 막기 위해 개인 레포로 이동하는 과정을 Github Actions로 작성하신 점 멋있네요 ㅎㅎ
/> | ||
)} | ||
</div> | ||
<div className="w-full h-full absolute bg-gradient-to-b from-transparent via-transparent to-black"></div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요 부분 컴포넌트로 생성해 주면 좀 더 코드가 깔끔해질 것 같아요 ㅎㅎ
Today.tsx 에서도 동일한 코드를 사용중이여서 재사용성도 좋아질 것 같습니다~~
<div className="w-full h-full absolute bg-gradient-to-b from-transparent via-transparent to-black"></div> | |
<GradientStyle /> |
return ( | ||
<div> | ||
<div className="w-full h-[415px] relative"> | ||
<div className="absolute inset-0"> | ||
{randomMovie && ( | ||
<Image | ||
fill | ||
src={`https://image.tmdb.org/t/p/original${randomMovie.poster_path}`} | ||
alt={randomMovie.title} | ||
style={{ objectFit: 'cover' }} | ||
priority | ||
/> | ||
)} | ||
</div> | ||
<div className="w-full h-full absolute bg-gradient-to-b from-transparent via-transparent to-black"></div> | ||
</div> | ||
<section className="flex flex-row justify-center gap-[5px] pt-0.5"> | ||
<TopNImg /> | ||
{randomIndex !== null && ( | ||
<div className="fonts-today">Top {randomIndex + 1} in Korea Today</div> | ||
)} | ||
</section> | ||
<section className="flex justify-between w-full h-[45px] mt-[11px] pl-[54px] pr-[62px]"> | ||
<div className="flex flex-col items-center"> | ||
<PlusImg /> | ||
<div className="fonts-mainicon">My List</div> | ||
</div> | ||
<PlayBtn width={6.91406} /> | ||
<div className="flex flex-col items-center"> | ||
<InfoImg /> | ||
<div className="fonts-mainicon">Info</div> | ||
</div> | ||
</section> | ||
</div> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Today.tsx 컴포넌트에서 이미지 띄우기 / ...in Korea Today 텍스트 띄우기 /버튼 바 띄우기 이렇게 여러 성격의 기능들이 들어가 있어서 컴포넌트를 좀만 더 생성해줘서 세분화해주면 더 가독성에 좋을 것 같아요~!!
return ( | |
<div> | |
<div className="w-full h-[415px] relative"> | |
<div className="absolute inset-0"> | |
{randomMovie && ( | |
<Image | |
fill | |
src={`https://image.tmdb.org/t/p/original${randomMovie.poster_path}`} | |
alt={randomMovie.title} | |
style={{ objectFit: 'cover' }} | |
priority | |
/> | |
)} | |
</div> | |
<div className="w-full h-full absolute bg-gradient-to-b from-transparent via-transparent to-black"></div> | |
</div> | |
<section className="flex flex-row justify-center gap-[5px] pt-0.5"> | |
<TopNImg /> | |
{randomIndex !== null && ( | |
<div className="fonts-today">Top {randomIndex + 1} in Korea Today</div> | |
)} | |
</section> | |
<section className="flex justify-between w-full h-[45px] mt-[11px] pl-[54px] pr-[62px]"> | |
<div className="flex flex-col items-center"> | |
<PlusImg /> | |
<div className="fonts-mainicon">My List</div> | |
</div> | |
<PlayBtn width={6.91406} /> | |
<div className="flex flex-col items-center"> | |
<InfoImg /> | |
<div className="fonts-mainicon">Info</div> | |
</div> | |
</section> | |
</div> | |
); | |
} | |
return <div> | |
<BigMovieImage /> -> 이미지 띄우기 | |
<Text text={`Top ${randomIndex + 1} in Korea Today`} className="fonts-today" /> -> ...in Korea Today 텍스트 | |
<ButtonBar /> -> MyList, Play, info 을 가지고 있는 버튼 바 | |
</div> |
( in Korea Today 부분은 이미 짜주신 것 처럼 div 태그에 해주셔도 상관 없는데, 저는 개인적으로 Text 컴포넌트를 하나 파서 저런식으로 넘겨주면 가독성 측면에서 더 좋아지는 거 같더라구여 ㅎㅎ)
Text 컴포넌트 예시(twMerge 라이브러리 사용) ->
return <p className={twMerge("초기 설정해 준 style", className)}>
{text}
</p>
<div className="flex flex-col items-center"> | ||
<PlusImg /> | ||
<div className="fonts-mainicon">My List</div> | ||
</div> | ||
<PlayBtn width={6.91406} /> | ||
<div className="flex flex-col items-center"> | ||
<InfoImg /> | ||
<div className="fonts-mainicon">Info</div> | ||
</div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
여기서 My List와 Info 도 PlayBtn 처럼 특정 기능을 수행하고, 디자인도 겹치네요!!
버튼을 파악하기에 중요한 데이터인 버튼의 텍스트들을 외부에서 넘겨서 컴포넌트로 생성해주면 좋을거 같아요 ㅎㅎ
<div className="flex flex-col items-center"> | |
<PlusImg /> | |
<div className="fonts-mainicon">My List</div> | |
</div> | |
<PlayBtn width={6.91406} /> | |
<div className="flex flex-col items-center"> | |
<InfoImg /> | |
<div className="fonts-mainicon">Info</div> | |
</div> | |
<BarDarkBtn icon={<PlusImg>} text="My List" /> | |
<PlayBtn width={6.91406} /> | |
<BarDarkBtn icon={<InfoImg>} text="Info" /> |
BarDarkBtn 컴포넌트 예시 ->
return <button className={twMerge("flex flex-col items-center", className)}>
{icon}
<div className={twMerge("fonts-mainicon", textClassName)}>My List</div>
</button>
title: string; | ||
} | ||
|
||
export const trendingMovies: trendingMoviesTypes[] = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
디테일 너무 고생하셨네여 ㅋㅋㅋㅋㅋㅋㅋ 짱입니다
export default function Page() { | ||
const [searchedMovies, setSearchedMovies] = useState([]); | ||
|
||
const handleSearch = async (query: string) => { | ||
try { | ||
const data = await getSearchedMovies(query); | ||
setSearchedMovies(data.results); | ||
} catch (error) { | ||
console.error('에러 발생:', error); | ||
} | ||
}; | ||
|
||
return ( | ||
<section className="flex flex-col pt-11 h-full"> | ||
<div className="sticky top-0 z-20"> | ||
<SearchBar onSearch={handleSearch} /> | ||
<p className="py-5 pl-2.5 fonts-bigtitle">Top Searches</p> | ||
</div> | ||
<div className="flex-1 overflow-auto"> | ||
{searchedMovies.length > 0 ? <Movies searchedMovies={searchedMovies} /> : <Trending />} | ||
</div> | ||
<FooterNav /> | ||
</section> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현재는 query 가 바뀔 때 마다 바로바로 api 호출을 모두 시도하고, 그때마다 페이지 전체가 리렌더링되는 상황인거 같아요!!
query 가 바뀌더라도 어느정도 api 호출을 줄여줄 수 있는 debounce 및 thottle 사용 / 과도한 리렌더링 방지를 위한 로직(useMemo, useCallback, React.memo) 에 대해 고민해보시면 좋을 것 같아요~!!
|
||
export default function Header() { | ||
// 메뉴 항목을 배열로 정의 | ||
const menuItems = ["TV Shows", "Movies", "My List"]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
메뉴 항목을 배열로 정의한 후 map 으로 처리하신 로직 좋은거 같아요 ㅎㅎ 유지보수에도 좋은 코드 같습니다!!
{poster_path && ( | ||
<Image | ||
fill | ||
src={`https://image.tmdb.org/t/p/original${poster_path}`} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://image.tmdb.org/t/p/original
해당 엔드포인트가 많은 파일에서 사용되고 있어서 따로 변수로 생성해준 뒤 export 해주면 더 좋을 거 같아요~!!
interface PlayBtnProps { | ||
width: number; | ||
handlePlayBtn?: () => void; | ||
} | ||
|
||
export default function PlayBtn(props: PlayBtnProps) { | ||
const { width, handlePlayBtn } = props; | ||
|
||
return ( | ||
<button | ||
style={{ width: `${width}rem` }} | ||
className={`flex justify-center items-center h-[45px] gap-4 rounded-md bg-btn-gray`} | ||
onClick={handlePlayBtn}> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
width 처럼 css 스타일을 외부에서 받아야 할 때 twMerge 라이브러리를 사용하시면 더 편리할 것 같아요!! 혹시 추가적인 css 스타일을 더 받아와야 할 때에도 더욱 유연하게 대처도 가능할 거 같아요 ㅎㅎ
interface PlayBtnProps { | |
width: number; | |
handlePlayBtn?: () => void; | |
} | |
export default function PlayBtn(props: PlayBtnProps) { | |
const { width, handlePlayBtn } = props; | |
return ( | |
<button | |
style={{ width: `${width}rem` }} | |
className={`flex justify-center items-center h-[45px] gap-4 rounded-md bg-btn-gray`} | |
onClick={handlePlayBtn}> | |
interface PlayBtnProps { | |
handlePlayBtn?: () => void; | |
className?: string; | |
} | |
export default function PlayBtn(props: PlayBtnProps) { | |
const { handlePlayBtn, className } = props; | |
return ( | |
<button | |
className={twMerge(`flex justify-center items-center h-[45px] gap-4 rounded-md bg-btn-gray`, className)} | |
onClick={handlePlayBtn}> |
외부에서 className prop 에 예를들어 w-[20rem] 이렇게만 적어주셔도 돼서 편리하고, 추가적인 스타일도 넘겨야 할 때 유연하게 대처할 수 있을 것 같습니다 ㅎㅎ
passHref | ||
> | ||
<div className="min-w-[102px] h-[102px] relative object-cover rounded-full overflow-hidden cursor-pointer"> | ||
<Image |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Next/Image 를 적극적으로 사용하시는 점 너무 좋네요~~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
안녕하세요! 이번 과제도 너무 수고 많으셨어요! 항상 pr을 정성스럽게 올려주셔서 pr 읽는 맛이 쏠쏠합니당 ㅎㅎ 브랜치 전략 보고 본격적으로 협업 연습하신 것 같아서 멋있었어요! 좋은 코드 보고 많이 배워가요 감사합니다 👍 ❤️
<div className="flex flex-col w-full"> | ||
<DetailTop poster_path={movie.poster_path} /> | ||
<div className="pt-3.5 pl-8 pr-8"> | ||
<PlayBtn width={18.9375}/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
버튼이 모바일 뷰에서는 예쁘게 위치해 있는데, 전체화면에서는 한쪽으로 치우쳐 있어요..!
저는 그래서 화면 비율에 따라서 가로나 세로로 쭉 늘릴 수 있는 컴포넌트들은 너비나 높이에 상수 값을 주는 대신에, padding
을 주고 너비를 100%
로 해서 스타일링 하는데, 이렇게 하면 크게 신경쓰지 않아도 돼서 편하더라구요! 이 방법도 좋은 방법이라고 생각해요! :)
<div> | ||
<div className="flex flex-col w-full"> | ||
<DetailTop poster_path={movie.poster_path} /> | ||
<div className="pt-3.5 pl-8 pr-8"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<div className="pt-3.5 pl-8 pr-8"> | |
<div className="pt-3.5 pl-8 pr-8"> |
여기에서는 이렇게 고쳐주면 가운데로 정렬돼요!
그런데 이렇게 바꾸면 밑에 overview 부분도 조정해야 될 것 같아요 😿
import dynamic from 'next/dynamic'; | ||
|
||
export default function page() { | ||
const Landing = dynamic(() => import('@/components/lottie/Landing'), { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dynamic import
사용하신 거 멋있어요! 👍
</FooterBtn> | ||
<FooterBtn text="More" isCurrentPage={pathname === '/more'}> | ||
<MoreIcon /> | ||
</FooterBtn> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저도 처음에 푸터 요소들을 나열해서 구현했었는데, 이번 과제 리팩토링하면서 요소들을 배열로 처리해서 map
함수로 랜더링하도록 바꿔봤어요! 이렇게 하니까 중복되는 코드를 줄일 수 있었어요!
이런 방법도 생각해보시면 좋을 것 같아요! 👍
fill | ||
src={`https://image.tmdb.org/t/p/original${randomMovie.poster_path}`} | ||
alt={randomMovie.title} | ||
style={{ objectFit: 'cover' }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
테일윈드에서 objectFit: 'cover'
을 사용할 때 이렇게 바꿔서 사용하면 된다구 합니다!!
저도 승완님이 알려주셨어요 ㅎㅎ 👍 💯
승완님의 리뷰
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
깜짝 놀랐어여 ㅎㅎ 디테일 최고.... 👍
{searchedMovies && | ||
searchedMovies.map((movie: MovieTypes) => ( | ||
<div key={movie.id} className="flex bg-search-gray"> | ||
<article className={'w-[300px] h-[140px] overflow-hidden relative shrink-0 rounded-r-lg'}> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
모바일뷰에서 searchedMovie
가 잘려서 보여요..!! 아무래도 이미지 크기 때문인 것 같은데,,, 한 번 확인해보시면 좋을 것 같아요!!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
추가적인 구현으로 검색페이지에 실시간 검색어 기능과 검색하면 나타나는 영화리스트까지 구현한 거 너무 멋있습니다!
커플로그팀 매주 항상 체계적인 코드 좋은 자극 받고갑니다. 과제 고생하셨습니다!!
// now playing 영화 데이터 반환하는 함수. | ||
export async function getNowPlayingMovie() { | ||
const url = `https://api.themoviedb.org/3/movie/now_playing?api_key=${process.env.NEXT_PUBLIC_MOVIE_API_KEY}`; | ||
const nowPlayingMovieResponse = await fetch(url, { cache: 'no-store' }); | ||
|
||
const nowPlayingMovieData = await nowPlayingMovieResponse.json(); | ||
|
||
return nowPlayingMovieData.results; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
API 호출 함수들의 중복되는 fetch 호출을 fetchMovies 함수로 추출하여 중복 코드를 제거하면 좋을 것 같습니다.
// now playing 영화 데이터 반환하는 함수. | |
export async function getNowPlayingMovie() { | |
const url = `https://api.themoviedb.org/3/movie/now_playing?api_key=${process.env.NEXT_PUBLIC_MOVIE_API_KEY}`; | |
const nowPlayingMovieResponse = await fetch(url, { cache: 'no-store' }); | |
const nowPlayingMovieData = await nowPlayingMovieResponse.json(); | |
return nowPlayingMovieData.results; | |
} | |
async function fetchMovies(url: string) { | |
const response = await fetch(url, { cache: 'no-store' }); | |
if (!response.ok) { | |
throw new Error(``); | |
} | |
const data = await response.json(); | |
return data.results; | |
} | |
export const getNowPlayingMovie = () => | |
fetchMovies(`https://api.themoviedb.org/3/movie/now_playing?api_key=${API_KEY}`); | |
const res = await fetch(url); | ||
const data = await res.json(); | ||
|
||
return data; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fetch 호출 후 응답이 성공적인지 확인하고, 실패 시 에러를 던지도록 짜면 안정성을 높이는데 도움이 될 것 같습니다.
const res = await fetch(url); | |
const data = await res.json(); | |
return data; | |
}; | |
const res = await fetch(url); | |
if (!res.ok) { | |
throw new Error(``); | |
} | |
return await res.json(); |
{wrapperItems.map((item, index) => ( | ||
<Wrapper | ||
key={index} | ||
title={item.title} | ||
fetchType={item.fetchType} | ||
/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저번과제에서 이 부분을 리팩토링했네요..!
<div> | ||
<Lottie | ||
animationData={netflixAnimation} | ||
style={{ display: 'flex', width: '100vw', height: '80vh', paddingTop: '8rem' }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이부분도 tailwindcss로 충분히 구현가능한 부분이예요!
style={{ display: 'flex', width: '100vw', height: '80vh', paddingTop: '8rem' }} | |
className="flex w-screen h-[80vh] pt-32" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이번주 과제도 수고많았어요..!👍
<section> | ||
<p className="fonts-details mt-6">{overview}</p> | ||
</section> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이부분 오버플로우 처리해줘도 좋을거 같아요...!
passHref | ||
> | ||
<div className="min-w-[103px] h-[161px] relative object-cover cursor-pointer"> | ||
<Image |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{searchedMovies && | ||
searchedMovies.map((movie: MovieTypes) => ( | ||
<div key={movie.id} className="flex bg-search-gray"> | ||
<article className={'w-[300px] h-[140px] overflow-hidden relative shrink-0 rounded-r-lg'}> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<input | ||
type="text" | ||
className="block fonts-input bg-transparent px-2 focus:outline-none w-screen" | ||
placeholder="Search for a show, movie, genre, etc." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
배포링크
6주차 미션: Next-Netflix
🌱 필수, 선택 구현
🪴 Team
👨💻 팀원 정보 및 역할
👨💻 팀원 역할 분담
GET
GET
📄 브랜치 전략
[Issue 먼저 생성하고 해당 이슈 번호 브랜치 생성]
main
: 최종 Merge를 하는 곳 (배포 브랜치)develop
: 개발할때 Merge하는 곳feature
: 기능을 개발하면서 각자 페이지별로 사용할 브랜치test
: 개인 연습 브랜치feature/#이슈번호/페이지/기능설명
📚 커밋 컨밴션
커밋 단위는 반드시 최소한의 작업 단위로 쪼개서, 한 PR당 10커밋 이상 넘어가지 않도록 합니다.
📄 리팩토링
[layout]
root layout
(common layout)
📦app
┣ 📂(commonLayout)
┃ ┣ 📂details
┃ ┃ ┗ 📂[id]
┃ ┃ ┃ ┗ 📜page.tsx
┃ ┣ 📂home
┃ ┃ ┗ 📜page.tsx
┃ ┣ 📂search
┃ ┃ ┗ 📜page.tsx
┃ ┗ 📜layout.tsx
┣ 📜layout.tsx
┗ 📜page.tsx
[env]
Next.js에서는 환경 변수 이름이 NEXT_PUBLIC_으로 시작해야 클라이언트 사이드에서도 환경 변수에 접근할 수 있다
배포 시 웹서버에서 발생하는 문제 : 배포할 때 프로젝트 설정에 env 넣어주어야 한다
Error: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.
[코드 리팩토링]
[SSR]
🛠 기술 스택
🎀 pr 상세
🔗 PR Full ver.
🧨 KEY QUESTIONS
[정적 라우팅(Static Routing)/동적 라우팅(Dynamic Routing)이란?]
정적 라우팅은 하나의 고정된 페이지로 라우팅되는 것을 의미한다. 사용자의 요청에 따라 해당 경로에 정의된 페이지를 제공한다.
동적 라우팅은 미리 정의된 URL 주소로만 라우팅하는 것이 아니라 사용자가 접근한 경로 혹은 특정 값에 따라 동적인 라우팅을 제공하고 싶을 때 사용할 수 있는 방식이다. 단일 페이지, 하나의 컴포넌트를 사용하여 변화하는 데이터를 수용할 수 있는 페이지를 제작할 수 있다. 동적 라우팅은 URL의 일부를 변수로 사용하여 같은 페이지 레이아웃을 다양한 데이터와 함께 재사용할 수 있는 큰 장점이 있다.
위의 이미지와 같이 대괄호
[]
로 시작하는 폴더를 생성하여 그 안에page.tsx
를 만들어 '/details/123' '/details/456' 과 같은 url을 만들 수 있다. '/details/1/565544'와 같은 주소도 만들 수 있다.[성능 최적화를 위해 사용한 방법]
이번주가 둘 다 너무 바빴습니다 ㅜ 시간 내서 둘이 다시한번 SSR 구현해보도록 하겠습니다