-
Notifications
You must be signed in to change notification settings - Fork 14
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주차] SNIFF 미션 제출합니다. #12
base: master
Are you sure you want to change the base?
Conversation
구현 도중, client component로 선언한 LandingLogo에서 계속 document가 undefined 이라는 에러가 발생했다. 아마, server side에서 렌더링 하면서 dom을 구성하는 도중에 발생하는 오류인 것 같다. 따라서, next의 dynamic loading 기능을 사용하여 해당 컴포넌트를 server side에서 아예 렌더링하지 않도록 설정해주었더니 에러가 해결됐다.
feat: netflix landing 구현
navbar가 랜딩 시에는 표시되지 않아야 하므로, pathname에 따라 navbar가 조건부 렌더링되도록 설정했다. 이를 위해 middleware.ts에서 routing시 pathname을 cookie에 저장하도록 설정했고, root layout에서 저장된 pathname에 따라 조건부로 렌더링되도록 하였다.
svgr을 통해 svg 파일을 다루고, 이를 index.ts에 모아 쉽게 컴포넌트 형태로 사용할 수 있도록 구현했다.
기존에는 cookie로 pathname을 기록하여 layout에서 렌더링 조건을 달아 렌더링 여부를 결정했는데, 어차피 navbar 컴포넌트를 client component로 정의하므로, 해당 컴포넌트 내에서 usePathname을 통해 pathname을 확인하여 랜딩 페이지라면 렌더링하지 않도록 하는 방법을 사용했다.
[feat] navbar 구현
아직 구현되지 않은 페이지의 라우팅을 막고, common 폴더를 만들어 디렉토리를 이동했다.
Link(a) 로 변경하여 클릭시 어딘가로 이동하도록 설정했다. 추후, 홈화면 윗쪽의 포스터 표시를 클릭한 content의 포스터로 보여주도록 할 예정이다.
그 과정에서 tailwind의 plugin을 사용하여 scroll-hide라는 custom style을 정의했다.
기존에는 횡이동 스크롤로만 움직일 수 있었으나, 기능상 마우스로 드래그하는 것이 꼭 필요하기 때문에 useState와 useRef를 사용하여 마우스 drag를 구현했다. 그 과정에서 해당 컴포넌트를 client component로 변경했다.
데스크탑 환경에서 마우스 hover시 넘기기 버튼이 보여지도록 구현했다.
drag를 통해 슬라이더를 이동시킬 수 있도록 했으나, 데스크탑 환경에서의 사용성이 좋은 것 같지 않아, 버튼을 통해 움직이도록 수정했다.
[feat] 홈화면 슬라이더 컴포넌트 구현
[feat] home top 구현
검색어가 없을 떄의 default 데이터를 설정해야 하므로, popular category의 영화들을 불러올 수 있도록 구현했다.
검색 페이지 하단 padding을 추가했고, 제목이 2줄 이상이 될 때 ...으로 표시하도록 구현했다.
구현하면서 input 컴포넌트 내부에서 query를 저장하는 state를 없애고, page의 root 컴포넌트에서 선언한 keyword state를 통해 검색어가 비었는지 판단하기로 결정했다. api 로딩 중에 어색한 flow를 방지하기 위해 init이라는 state를 선언했다. 추후에 loading이나 스켈레톤 컴포넌트를 활용할 수 있을 것 같다.
Image의 onLoad 속성을 사용하여 로딩이 완료되기 전에는 skeleton component를 display 하도록 구현했다. skeleton component는 pulse animation을 사용하여 실제로 로딩이 진행중인 느낌을 전달한다.
[feat] search 페이지 구현 완료
�[feat] detail페이지 구현완료
[chore] minor 에러 수정
[chore] minor 한 변경사항 적용
[chore] minor한 버그 수정
추가한줄알았는데, 까먹었었음.
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.
전체적으로 수준높은 코드 잘 봤습니다. 코드리뷰를 하면서 많은 것들을 배울 수 있었습니다. 시간상 양질의 코드리뷰를 못해드린 것 같아 죄송하고 정말 수고 많으셨습니다!!
@@ -0,0 +1,85 @@ | |||
/* eslint-disable react-hooks/exhaustive-deps */ |
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.
이 페이지는 최상단에 use client 를 선언하셨는데 시간이 없어 리팩토링을 하지 못한것인지 특별한 이유가 있는지 개인적으로 궁금하네요 ㅎㅎ
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.
페이지 내에서 리엑트 훅을 사용하려면 csr로 페이지를 구성해야 하는 것 같드라구요!
|
||
useEffect(() => { | ||
if (!keyword) { | ||
(async () => { | ||
// next의 fetch는 같은 요청에 대해 caching을 진행하므로, | ||
// 첫 요청 이후에는 caching된 데이터를 가져올 것(?) | ||
const defaultMovies = await getMovies('popular'); | ||
setMovies(defaultMovies); | ||
setPageNum(1); | ||
setTotalPageNum(1); | ||
setInit(false); | ||
})(); | ||
} | ||
}, [keyword]); | ||
|
||
useEffect(() => { | ||
if (pageNum > 1) { | ||
(async () => { | ||
const data = await getSearchedMovies(keyword, pageNum); | ||
setMovies([...movies, ...data.results]); | ||
})(); | ||
} | ||
}, [pageNum]); | ||
|
||
useEffect(() => { | ||
if (inview && pageNum < totalPageNum) { | ||
setPageNum(pageNum + 1); | ||
} | ||
}, [inview]); |
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.
필요한 부분을 세부 클라이언트 컴포넌트에 선언한다면 최상단에 'use client'를 없앨수도 있겠네요 ㅎㅎ 저희도 시간 때문에 서치페이지 'use client'로 선언해버렸는데 이제 리팩토링 해봐야됩니다 ㅋㅋㅋㅋㅋㅋ
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.
약간 귀찮아서 걍 페이지 자체를 csr로 구성했는데 ssr로 구성하고 내부에 csr 컴포넌트를 넣는 것이 성능적으로 뛰어나다면 앞으로 그렇게 해봐야겠네요ㅎㅎ
setInit={setInit} | ||
handleOnChangeQuery={useDebounce(handleOnChangeQuery, 500)} | ||
/> |
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.
debounce 를 통해 과도한 데이터 fetching 방지하신 디테일 좋네요 ㅎㅎ
> | ||
<Image | ||
src={url + content.poster_path} | ||
alt={content.title} | ||
className={imageClasses} | ||
width={width} | ||
height={isPreview ? width : height} | ||
/> |
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 를 통한 최적화 좋은 것 같습니다 ㅎㅎ
export const useDebounce = <T extends (...args: any[]) => any>( | ||
fn: T, | ||
delay: number, |
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.
제네릭 함수 사용 좋네요 ㅎㅎ
<button className="px-[2px]">TV Shows</button> | ||
<button className="px-[2px]">Movies</button> | ||
<button className="px-[2px]">My List</button> |
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.
스타일이 같은 태그가 3번 중복되므로 텍스트를 리스트 변수로 묶어서 map 함수로 렌더링하는 것도 좋을 것 같네요 ㅎㅎ
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.
요것도 귀찮아서 그냥 수기로 썼는데, 확실히 그렇게 하는게코드 중복에 도움될 것 같아요~~
<ContentsSlider | ||
title="Preview" | ||
isRanking={false} | ||
isPreview={true} | ||
contents={Nowplaying} | ||
/> | ||
<ContentsSlider title="Nigeria Today" isRanking={true} contents={Top} /> | ||
<ContentsSlider title="Popular" isRanking={false} contents={Popular} /> | ||
<ContentsSlider title="Upcoming" isRanking={false} contents={Upcoming} /> |
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 함수로 렌더링 한다면 가독성과 추후 슬라이더 추가시에도 편리할 것 같다는 개인적인 의견 드립니다 ㅎㅎ
<ContentsSlider | |
title="Preview" | |
isRanking={false} | |
isPreview={true} | |
contents={Nowplaying} | |
/> | |
<ContentsSlider title="Nigeria Today" isRanking={true} contents={Top} /> | |
<ContentsSlider title="Popular" isRanking={false} contents={Popular} /> | |
<ContentsSlider title="Upcoming" isRanking={false} contents={Upcoming} /> | |
const sliderData=[ | |
{ | |
title: "Preview", | |
movieData: await getMovies('now_playing'), | |
isRanking: false, | |
isPreview:true | |
}, | |
{ | |
title: "Nigeria Today", | |
movieData: await getMovies('top_rated'), | |
isRanking: true, | |
isPreview:false | |
}, | |
{ | |
title: "Popular", | |
movieData: await getMovies('popular'), | |
isRanking: false, | |
isPreview:false | |
}, | |
{ | |
title: "Upcoming", | |
movieData: await getMovies('upcoming'), | |
isRanking: false, | |
isPreview:false | |
} | |
] | |
.... | |
{sliderData.map((data,index)=>( | |
<ContentsSlider | |
key={data.title} | |
title={data.title} | |
isRanking={data.isRanking} | |
isPreview={data.isPreview} | |
contents={data.movieData} | |
/> | |
))} |
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.
저도 배워갑니다~!
- 팀 repo 그대로 vercel에 배포하려면 요금을 지불해야 함 | ||
- 따라서, 팀 repo를 오대균 github에 fork 하여, 오대균 repo에서 배포 함 | ||
- 수동으로 배포하면 번거로우니 다음과 같은 flow로 배포되도록 설정함 | ||
> 1. team repo master branch에 push 발생 | ||
> 2. 오대균 repo의 master branch에 변경사항 자동으로 적용 | ||
> 3. 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.
저희는 vercel 을 사용한적이 없어 아직 팀 레포 그대로 배포하도 공짜로 사용 가능하여 그냥 했는데, github action 을 이용하여 이를 해결하신점 매우 멋지네요!!!^^
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.
코드 보면서 많이 배워갑니다:) 과제 수고 많으셨어요:)
setInit(false); | ||
})(); | ||
} | ||
}, [keyword]); |
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.
지금 보니까 vercel 뭐 가격 정책때문에 이미지 요청이 막히는 것 같네요ㅋㅋㅋ.. 뭐 무슨 limit이 있다고 했는데 오늘 딱 그 limit에 도달했나봐요 함 해결해볼게요ㅜㅜ
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 이미지 optimization 설정을 꺼주니까 해결되었네요!
</div> | ||
</Link> | ||
); | ||
}; |
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.
제 화면에서만 그런건지 모르겠는데, 검색을 했을 때 영화가 다 뜨지 않고, 개발자 도구 콘솔창에는 402 오류가 뜨네요🥲
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="absolute w-2/5 h-full flex items-center justify-center bg-[#545454]"> | ||
<Image | ||
src={defaultImageUrl} |
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.
스니프팀도 포스터가 없을 때에는 defaultImageUrl 뜨게 해놓았군요!! 좋은 것 같습니다:)
}, delay); | ||
return result; | ||
}; | ||
}; |
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.
onChange 이벤트를 debouncing하도록 검색 기능하신 점 너무 좋은 것 같습니다! 저도 다음에 사용해봐야겠어요:)
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.
이번주 과제 수고하셨습니다~ 성능 최적화를 위해 고민하신게 느껴지는 코드였습니다 :) 두분 다 너무 수고하셨어요~~ㅎㅎ
); | ||
}; | ||
|
||
export default SearchResultElement; |
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.
저도 검색을 했을때 보니까 payment required 가 뜨는 것 같네요.. 구글링 해봤는데 정확한 이유는 찾지 못했어요 ㅜ 아래 링크는 찾아본건데 참고해보셔도 될 것 같아요..!!
https://jha-memo.tistory.com/199
를 쓰고 리뷰를 마무리하는 와중에 다시 새로고침해보니까 헤결이 되었네요..!! 빠르게 찾아서 해결하셨군요ㅋㅋㅋ👍
return ( | ||
<Link href={clickUrl}> | ||
<div className="relative"> | ||
{isLoading ? <SkeletonElement /> : null} |
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.
loading 중일때 skeletonElement뜨게 하는 아이디어 베워갑니다~!
const handleOnChangeQuery = async ( | ||
e: React.ChangeEvent<HTMLInputElement>, | ||
) => { | ||
const query = e.target.value.trim(); | ||
if (query) { | ||
const data = await getSearchedMovies(query); | ||
setMovies(data.results); | ||
setPageNum(data.page); | ||
setTotalPageNum(data.total_pages); | ||
} | ||
setKeyword(query); | ||
if (!query) setInit(true); | ||
}; |
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.
handleChangeQuery를 통해 검색어 변화가 있을때 이렇게 처리를 할수도 있겠네요..! 저희는 불러온 리스트에서 필터링을 했었는데, 이런 방법도 있군요!
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.
오늘도 구현하신 코드가 깔끔하고 배워갈 내용이 많은 것 같습니다! 수고하셨습니다~
import { getMovies } from '@/utils/Api'; | ||
|
||
interface BrowseProps { | ||
searchParams: { [key: string]: string | undefined }; |
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.
저는 useSearchParams를 사용했는데 직접 구현하신 거 좋은 것 같습니다
<ContentsSlider | ||
title="Preview" | ||
isRanking={false} | ||
isPreview={true} | ||
contents={Nowplaying} | ||
/> | ||
<ContentsSlider title="Nigeria Today" isRanking={true} contents={Top} /> | ||
<ContentsSlider title="Popular" isRanking={false} contents={Popular} /> | ||
<ContentsSlider title="Upcoming" isRanking={false} contents={Upcoming} /> |
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.
저도 배워갑니다~!
import dynamic from 'next/dynamic'; | ||
|
||
const page = () => { | ||
const Logo = dynamic(() => import('@/components/LandingLogo'), { |
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을 이용해서 구현해봐야겠어요~!
검색과 디테일 페이지 구현
[key Feature]
배포
SNIFF-NETFLIX
[Key Question]
정적 라우팅(Static Routing)/동적 라우팅(Dynamic Routing)이란?
정적 라우팅 하나의 페이지가 하나의 url 요청에만 매칭되는 것입니다.
동적 라우팅 하나의 페이지가 여러 개의(하지만 지정된 형식의) url에 매칭되는 것입니다.
app router을 통해 next 어플리케이션을 개발한다면, app 폴더 내부에서 폴더를 만들고, 해당 폴더에 page.ts(x)를 만든다면 폴더 명으로 정적 라우팅이 가능합니다.
폴더명을 괄호([something])에 넣어 지정하면, 다이나믹 라우팅 기능을 사용할 수 있습니다. 저희가 구현한 앱에서 각 영화의 디테일 페이지는 /movies/123342(영화id) 와 같은 주소로 접근할 수 있습니다. 이 때 영화 id에 따라 다른 정보를 보여주도록 하기 위해, 디렉토리를 movies/[id]와 같이 설정하고, page.tsx 파일을 만들어주었습니다.
해당 파일 안에서는 params라는 prop을 통해서 id에 접근할 수 있고, 이 id를 통해 tmdb의 api를 사용하여 영화 정보를 얻어올 수 있는 것입니다.
성능 최적화를 위해 사용한 방법
2023-11-17.1.38.47.mov