Conversation
feat: movie-detail page 구현
Feature/12 search page
Feature/12 search page
rmdnps10
left a comment
There was a problem hiding this comment.
tailwindcss 적용과, 무한 스크롤 적용 방식 등 리뷰하면서 많이 배워갔습니다 :)
이번 주 과제 고생많으셨습니다~
| <ImageBox className="pl-[0.75rem] w-full flex flex-col"> | ||
| <h3 className="pl-[0.25rem] text-white mt-[14px] mb-[1.44rem] text-popular font-bold"> |
There was a problem hiding this comment.
Next에서 CSS적용할 때, SSR 방식으로 구현하는 점을 생각하면 Tailwind css 사용하는 게 제일 좋은 방법인 거 같아요:) 막상도입하는데 쉽지 않으셨을 거 같은데, 고생많으셨습니다!
| <Link | ||
| href={{ | ||
| pathname: `/movie-detail/${movie.id}`, | ||
| query: { path: movie.backdrop_path, overview: movie.overview }, | ||
| }} | ||
| > |
There was a problem hiding this comment.
Link 태그에서 이런 식으로 쿼리파라미터를 지정해서 넘겨줄 수 있는 점 배워갑니다!
찾아보니까 데이터 노출을 방지하기 위해 as 라는 속성을 이용하면 URL 를 마스킹 할 수도 있다고 해서 참고하시면 좋을 거 같아요!
query데이터 이동방법
There was a problem hiding this comment.
as를 쓰니까 왜인지 useSearchParam이 읽지를 못하더라구요... ㅠㅠ
| const [topRatedData] = await Promise.all([ | ||
| tmdbApi.get(requests.fetchTopRated), | ||
| ]); |
There was a problem hiding this comment.
이렇게 바꿀 수 있을 거 같습니다!
| const [topRatedData] = await Promise.all([ | |
| tmdbApi.get(requests.fetchTopRated), | |
| ]); | |
| const topRatedData = await tmdbApi.get(requests.fetchTopRated); |
| } catch (error) { | ||
| console.error("Failed to fetch movies:", error); | ||
| } |
There was a problem hiding this comment.
try-cath문으로 에러처리까지 고려하신 점 잘하신 거 같아요:)
| }; | ||
|
|
||
| const filteredMovies = topRated.filter((movie) => | ||
| includesChosung(movie.title, searchQuery), |
| <Link | ||
| href={{ | ||
| pathname: `/movie-detail/${movie.id}`, | ||
| query: { path: movie.backdrop_path, overview: movie.overview }, | ||
| }} | ||
| > |
There was a problem hiding this comment.
검색 페이지에서 클릭하면 상세 화면으로 이동하게 구현하신 점 좋아요 👍
| const [ | ||
| nowPlayingData, | ||
| topRatedData, | ||
| popularData, | ||
| trendingData, | ||
| horrorData, | ||
| ] = await Promise.all([ | ||
| tmdbApi.get(requests.fetchNowPlaying), | ||
| tmdbApi.get(requests.fetchTopRated), | ||
| tmdbApi.get(requests.fetchPopular), | ||
| tmdbApi.get(requests.fetchTrending), | ||
| tmdbApi.get(requests.fetchHorrorMovies), | ||
| ]); |
There was a problem hiding this comment.
여러 비동기 작업들을 한번에 이행하기 위해서 Promise.all을 적절히 사용하신 것 같습니다:)
| //현재 페이지 변경-> 영화 데이터 불러옴 | ||
| useEffect(() => { | ||
| if (currentPage > 1 && hasMore) | ||
| fetchMoviesFromPage(currentPage).then((newMovies) => { | ||
| setTopRated((prevMovies) => [...prevMovies, ...newMovies]); | ||
| }); | ||
| }, [currentPage]); |
There was a problem hiding this comment.
currentPage의 상태에 따라 fetch를 하여 topRatedMovies 를 반영하여 무한 스크롤을 구현하신 점 멋있습니다 👍
| const lastMovieElementRef = useCallback( | ||
| (node: Element | null) => { | ||
| if (observer.current) observer.current.disconnect(); | ||
| observer.current = new IntersectionObserver((entries) => { | ||
| if (entries[0].isIntersecting && hasMore) { | ||
| setCurrentPage((prevPage) => prevPage + 1); | ||
| } | ||
| }); | ||
| if (node) observer.current.observe(node); | ||
| }, | ||
| [isLoading], | ||
| ); |
There was a problem hiding this comment.
굉장히 여러가지를 신경써서 코드를 작성하신 것 같아요~ useCallback을 통해 isLoading이 변경되지 않으면 함수가 생성되지 않게 최적화를 진행하시고 IntersectionObserver을 통해 마지막 요소가 화면에서 교차될 때 currentPage의 상태를 변하게 하여 무한 스크롤 구현하신 점 배워갑니다!
flowerseok
left a comment
There was a problem hiding this comment.
바쁘실텐데 고생 많이 하셨습니다.
재미있고 도움되는 코드 였습니다. 오늘도 역시 많이 배워갑니다 ~~~!!
| tmdbApi.get(requests.fetchPopular), | ||
| tmdbApi.get(requests.fetchTrending), | ||
| tmdbApi.get(requests.fetchHorrorMovies), | ||
| ]); |
There was a problem hiding this comment.
저는 처음보는 promise.all이네요! 깊이가 깊지 않다보니, 사용하면서 배우는 경우가 많은데, 비동기 작업을 이행하는 것 공부해보고 싶네요 ~~
| const fetchMoviesFromPage = async (page: number) => { | ||
| setIsLoading(true); | ||
| const response = await tmdbApi.get<{ | ||
| results: Movie[]; |
There was a problem hiding this comment.
여기서 axios와 async/await의 조합을 사용하셨는데, axios는 HTTP 요청을 간편하게 만들어주고, async/await를 통해 이러한 비동기 요청을 동기적으로 처리할 수 있게 해주는 게 맞나요? 혹시 이러한 조합을 선택하신 이유가 있을까요? 저는 사용하지 않아서, 제가 이해한 것이 맞는지, 추가로 배울 점이 있다면 알고 싶어요 ~~~
| <Link | ||
| href={{ | ||
| pathname: `/movie-detail/${movie.id}`, | ||
| query: { path: movie.backdrop_path, overview: movie.overview }, | ||
| }} | ||
| > |
oooppq
left a comment
There was a problem hiding this comment.
한 주동안 수고하셨습니다~~ 전체적으로 디테일과 최적화를 고려하셔서 좋은 결과물이 나온 것 같아요!!
| useEffect(() => { | ||
| const fetchMovies = async () => { | ||
| try { | ||
| // API 요청을 한번에 보냄 | ||
| const [ | ||
| nowPlayingData, | ||
| topRatedData, | ||
| popularData, | ||
| trendingData, | ||
| horrorData, | ||
| ] = await Promise.all([ | ||
| tmdbApi.get(requests.fetchNowPlaying), | ||
| tmdbApi.get(requests.fetchTopRated), | ||
| tmdbApi.get(requests.fetchPopular), | ||
| tmdbApi.get(requests.fetchTrending), | ||
| tmdbApi.get(requests.fetchHorrorMovies), | ||
| ]); | ||
|
|
||
| // 상태 한번에 업데이트 | ||
| setNowPlaying(nowPlayingData.data.results); | ||
| setTopRated(topRatedData.data.results); | ||
| setPopular(popularData.data.results); | ||
| setTrending(trendingData.data.results); | ||
| setHorror(horrorData.data.results); | ||
| } catch (error) { | ||
| console.error("Failed to fetch movies:", error); | ||
| } | ||
| }; | ||
|
|
||
| fetchMovies(); | ||
| }, []); |
There was a problem hiding this comment.
ssr을 공부하고 있는 만큼 server side에서 fetching하는 방법을 사용했어도 좋았겠지만, 기능상에는 큰 문제가 없기 때문에 잘 구현하신 것 같아요~~ 다만, 해당 fetching 작업이 client side에서 발생하기 때문에, api를 통해 영화 정보를 가져오는 시간동안 해당 페이지의 text들만 미리 렌더링되어 화면이 버벅되는 모습을 볼 수 있어요!
이런 식으로요! 이를 막기 위해서 state를 통해 loading flag을 설정해주거나, 스켈레톤 컴포넌트를 구성해두는 방법이 있을 것 같아요 한 번 생각해보시면 좋을 것 같아요~
| observer.current = new IntersectionObserver((entries) => { | ||
| if (entries[0].isIntersecting && hasMore) { | ||
| setCurrentPage((prevPage) => prevPage + 1); | ||
| } | ||
| }); | ||
| if (node) observer.current.observe(node); |
There was a problem hiding this comment.
따로 라이브러리 사용하지 않고 intersection observer을 활용해서 직접 구현하신거 대단해요~~
| <div | ||
| key={movie.id} | ||
| ref={ | ||
| filteredMovies.length === index + 1 | ||
| ? lastMovieElementRef | ||
| : undefined | ||
| } | ||
| > | ||
| {/* {renderMovieLists(filteredMovies)} */} | ||
| {renderMovieLists([movie])} | ||
| </div> | ||
| ))} |
There was a problem hiding this comment.
요부분에서 renderMovieLists라는 함수를 사용해주셨는데, 원래 해당 함수는 리스트 형태의 영화 데이터를 렌더링해주기 위해 만든 함수 같아요! 근데 아마 리팩토링하면서 변화가 있었던 것 같아요, 이렇게 바꾸신 김에 해당 함수를 사용하지 않고 그냥 따로 컴포넌트로 빼서 사용하는 것이 가독성에 좋을 것 같아요! 컴포넌트로 구성한다면 react memo를 통해 memoization을 할 수도 있으니까요!
| isActive: boolean; | ||
| children: JSX.Element; | ||
| text: string; | ||
| onClick?: () => void; |
There was a problem hiding this comment.
onClick 이벤트 prop을 전달하는 과정에서 서로 다른 onClick 이벤트가 같이 정의되어 있을 수도 있기 때문에 용도에 맞게 네이밍해주시면 더 좋을 것 같아요~~ 예를 들어, handleClickNavButton 과 같은 형태로요!
There was a problem hiding this comment.
이유는 모르겠는데, navbar의 현재 페이지의 아이콘을 클릭해서(예를 들어, 현재 home인데, home 버튼을 눌렀을 때) url의 이동이 없는데도 불구하고, 페이지가 리렌더링 되는 것 같아요.. 계속 이유를 찾아보려고 했는데 잘 모르겠네요ㅜ 저희꺼를 포함해서 다른분들꺼는 같은페이지 버튼을 눌러도 리렌더링이 발생하지 않는데 왜 그런 것인지 함 찾아보면 좋을 것 같습니다!
| const [topRated, setTopRated] = useState<Movie[]>([]); | ||
| const [popular, setPopular] = useState<Movie[]>([]); | ||
| const [nowPlaying, setNowPlaying] = useState<Movie[]>([]); | ||
| const [trending, setTrending] = useState<Movie[]>([]); | ||
| const [horror, setHorror] = useState<Movie[]>([]); |
There was a problem hiding this comment.
저희는 javascript를 사용했는데, typescript로 movie interface 따로 선언하여 상위 컴포넌트에서 fetch 데이터를 한번에 관리해주는게 깔끔합니다!
There was a problem hiding this comment.
utils 함수에 render 관련 함수들을 모아준게 가독성이 좋습니다!!
| ); | ||
| }; | ||
|
|
||
| const NavBarContainer = styled.nav` |
| const [ | ||
| nowPlayingData, | ||
| topRatedData, | ||
| popularData, | ||
| trendingData, | ||
| horrorData, | ||
| ] = await Promise.all([ | ||
| tmdbApi.get(requests.fetchNowPlaying), | ||
| tmdbApi.get(requests.fetchTopRated), | ||
| tmdbApi.get(requests.fetchPopular), | ||
| tmdbApi.get(requests.fetchTrending), | ||
| tmdbApi.get(requests.fetchHorrorMovies), |
There was a problem hiding this comment.
Promise.all 사용해서 한번에 get 요청을 할 수 있다는 걸 배워갑니다~!
| const lastMovieElementRef = useCallback( | ||
| (node: Element | null) => { | ||
| if (observer.current) observer.current.disconnect(); | ||
| observer.current = new IntersectionObserver((entries) => { | ||
| if (entries[0].isIntersecting && hasMore) { | ||
| setCurrentPage((prevPage) => prevPage + 1); | ||
| } | ||
| }); | ||
| if (node) observer.current.observe(node); | ||
| }, | ||
| [isLoading], | ||
| ); |
There was a problem hiding this comment.
위에서 currentPage를 state로 관리하며 IntersectionObserver를 이용하여 무한스크롤 구현하신게 인상 깊습니다!! 덕분에 새로 하나 배워갑니다!

[6주차]
[배포링크]
next-netflix-18th
[Key Questions]
정적 라우팅(Static Routing)/동적 라우팅(Dynamic Routing)이란?
- 정적라우팅
정적 라우팅이란, 고정된 페이지로 라우팅하는 것. /home 페이지가 대표적인 예시로, 접속시 랜더링 페이지 후 home 페이지로 라우팅된다. 이러한 방식으로 이동하는 것을 정적 라우팅한다고 표현한다. 자연스럽게 우리가 만든 파일 이름을 주소로 라우팅되는 것이 정적 라우팅이다.
- 동적라우팅
동적 라우팅은 가변적인 페이지로 라우팅하는 것. 영화 디테일 상세보기 같은 것이 대표적인 예시로, 실제 소스코드는 한 페이지지만 movie id에 따라서 주소가 변경된다. 대괄호를 이용해 동적 라우팅을 할 수 있는데, [movieId] 로 파일명을 만들어주고, pathname:
/movie-detail/${movie.id}, 로 설정하여 라우팅이 가능하다.성능 최적화를 위해 사용한 방법
무한 스크롤 구현을 위한
IntersectionObserver사용→ 사용자가 스크롤을 맨 아래로 내리면 추가 데이터를 불러오는 무한 스크롤 기능을 구현했다. 전체데이터를 불러오기보다, 스크롤 이벤트를 감지하여 필요시에 api 호출을 하였다.