-
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주차] Reddigg 미션 제출합니다. #16
base: master
Are you sure you want to change the base?
Conversation
feat: movie-detail page 구현
Feature/12 search page
Feature/12 search page
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 적용과, 무한 스크롤 적용 방식 등 리뷰하면서 많이 배워갔습니다 :)
이번 주 과제 고생많으셨습니다~
src/app/home/page.tsx
Outdated
<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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Link
태그에서 이런 식으로 쿼리파라미터를 지정해서 넘겨줄 수 있는 점 배워갑니다!
찾아보니까 데이터 노출을 방지하기 위해 as
라는 속성을 이용하면 URL 를 마스킹 할 수도 있다고 해서 참고하시면 좋을 거 같아요!
query데이터 이동방법
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.
as를 쓰니까 왜인지 useSearchParam이 읽지를 못하더라구요... ㅠㅠ
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.
저도 배워갑니다!
src/app/search/page.tsx
Outdated
const [topRatedData] = await Promise.all([ | ||
tmdbApi.get(requests.fetchTopRated), | ||
]); |
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.
이렇게 바꿀 수 있을 거 같습니다!
const [topRatedData] = await Promise.all([ | |
tmdbApi.get(requests.fetchTopRated), | |
]); | |
const topRatedData = await tmdbApi.get(requests.fetchTopRated); |
src/app/search/page.tsx
Outdated
} catch (error) { | ||
console.error("Failed to fetch movies:", error); | ||
} |
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.
try-cath문으로 에러처리까지 고려하신 점 잘하신 거 같아요:)
src/app/search/page.tsx
Outdated
}; | ||
|
||
const filteredMovies = topRated.filter((movie) => | ||
includesChosung(movie.title, searchQuery), |
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.
함수 이름이 엣지넘치네요 ㅎㅎㅋㅋㅋㅋㅋㅋㅋㅋ 완전 직관적이에요
src/app/utils/movieFunctions.tsx
Outdated
<Link | ||
href={{ | ||
pathname: `/movie-detail/${movie.id}`, | ||
query: { path: movie.backdrop_path, overview: movie.overview }, | ||
}} | ||
> |
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.
검색 페이지에서 클릭하면 상세 화면으로 이동하게 구현하신 점 좋아요 👍
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
여러 비동기 작업들을 한번에 이행하기 위해서 Promise.all
을 적절히 사용하신 것 같습니다:)
//현재 페이지 변경-> 영화 데이터 불러옴 | ||
useEffect(() => { | ||
if (currentPage > 1 && hasMore) | ||
fetchMoviesFromPage(currentPage).then((newMovies) => { | ||
setTopRated((prevMovies) => [...prevMovies, ...newMovies]); | ||
}); | ||
}, [currentPage]); |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
굉장히 여러가지를 신경써서 코드를 작성하신 것 같아요~ useCallback
을 통해 isLoading
이 변경되지 않으면 함수가 생성되지 않게 최적화를 진행하시고 IntersectionObserver
을 통해 마지막 요소가 화면에서 교차될 때 currentPage
의 상태를 변하게 하여 무한 스크롤 구현하신 점 배워갑니다!
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.
바쁘실텐데 고생 많이 하셨습니다.
재미있고 도움되는 코드 였습니다. 오늘도 역시 많이 배워갑니다 ~~~!!
tmdbApi.get(requests.fetchPopular), | ||
tmdbApi.get(requests.fetchTrending), | ||
tmdbApi.get(requests.fetchHorrorMovies), | ||
]); |
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.
저는 처음보는 promise.all이네요! 깊이가 깊지 않다보니, 사용하면서 배우는 경우가 많은데, 비동기 작업을 이행하는 것 공부해보고 싶네요 ~~
const fetchMoviesFromPage = async (page: number) => { | ||
setIsLoading(true); | ||
const response = await tmdbApi.get<{ | ||
results: Movie[]; |
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.
여기서 axios와 async/await의 조합을 사용하셨는데, axios는 HTTP 요청을 간편하게 만들어주고, async/await를 통해 이러한 비동기 요청을 동기적으로 처리할 수 있게 해주는 게 맞나요? 혹시 이러한 조합을 선택하신 이유가 있을까요? 저는 사용하지 않아서, 제가 이해한 것이 맞는지, 추가로 배울 점이 있다면 알고 싶어요 ~~~
<Link | ||
href={{ | ||
pathname: `/movie-detail/${movie.id}`, | ||
query: { path: movie.backdrop_path, overview: movie.overview }, | ||
}} | ||
> |
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.
한 주동안 수고하셨습니다~~ 전체적으로 디테일과 최적화를 고려하셔서 좋은 결과물이 나온 것 같아요!!
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
따로 라이브러리 사용하지 않고 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요부분에서 renderMovieLists라는 함수를 사용해주셨는데, 원래 해당 함수는 리스트 형태의 영화 데이터를 렌더링해주기 위해 만든 함수 같아요! 근데 아마 리팩토링하면서 변화가 있었던 것 같아요, 이렇게 바꾸신 김에 해당 함수를 사용하지 않고 그냥 따로 컴포넌트로 빼서 사용하는 것이 가독성에 좋을 것 같아요! 컴포넌트로 구성한다면 react memo를 통해 memoization을 할 수도 있으니까요!
isActive: boolean; | ||
children: JSX.Element; | ||
text: string; | ||
onClick?: () => void; |
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.
onClick 이벤트 prop을 전달하는 과정에서 서로 다른 onClick 이벤트가 같이 정의되어 있을 수도 있기 때문에 용도에 맞게 네이밍해주시면 더 좋을 것 같아요~~ 예를 들어, handleClickNavButton 과 같은 형태로요!
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.
이유는 모르겠는데, navbar의 현재 페이지의 아이콘을 클릭해서(예를 들어, 현재 home인데, home 버튼을 눌렀을 때) url의 이동이 없는데도 불구하고, 페이지가 리렌더링 되는 것 같아요.. 계속 이유를 찾아보려고 했는데 잘 모르겠네요ㅜ 저희꺼를 포함해서 다른분들꺼는 같은페이지 버튼을 눌러도 리렌더링이 발생하지 않는데 왜 그런 것인지 함 찾아보면 좋을 것 같습니다!
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.
한주동안 고생 많으셨습니다~~ 코드의 디테일이 매우 좋네요 👍👍
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저희는 javascript를 사용했는데, typescript로 movie interface 따로 선언하여 상위 컴포넌트에서 fetch 데이터를 한번에 관리해주는게 깔끔합니다!
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.
utils 함수에 render 관련 함수들을 모아준게 가독성이 좋습니다!!
); | ||
}; | ||
|
||
const NavBarContainer = styled.nav` |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
위에서 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 호출을 하였다.