-
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 TIG 송은수 & 김승완 미션 제출합니다. #11
base: master
Are you sure you want to change the base?
[6주차] Team TIG 송은수 & 김승완 미션 제출합니다. #11
Conversation
1. nextJS 기본 설치 : create-next-app 2. tailwind, ts 설치
…ngess feat: Landing, MainHeader 컴포넌트 구현
1. components 에서 _components로 바꾸면 url path로 인식하지 않음
1. @를 이용한 절대 경로를 위해 설정 변경 2. 기존에 app디렉터리 내부에 _components 명으로 디렉터리 명을 지정했는데, 생각해보니까 그럴 필요 없이 src 디렉터리 하위에 바로 필요한 디렉터리들을 꽂으면 url path에 영향 안줘서 위치를 옮겨줬음 3. 앞으로 app 디렉터리는 우리가 주소 url path 조정할 거, assets는 이미지 원래 이런거 저장용인데 그냥 public에서 하고 있으니까 일단 신경 안써도 되고, context는 상태관리, hooks는 커스텀훔, pages도 일단은 크게 필요 없을거 같고 styles는 특수 스타일 관련 파일(이것도 필요하면), types는 타입스크립트 타입들 여기에 한번에 몰아넣고, utils는 유용한 함수 재사용성 있게 쓰자고 만든 디렉터리야
1. 재사용되는 컴포넌트가 main 화면의 헤더, 전체의 homeIndicator, tabBar 정도니까, 재사용성 있는 컴포넌트는 components 디렉터리에서 사용되는 가장 공통 최상위 디렉터리 명을 파고 만들어놓자고!
…ungwan Refactor : 파일 디렉토리 구조 변경, 절대경로를 위한 import 설정
1. public 디렉토리에도 접근할 수 있게 절대 경로 설정 2. HomeIndicatorBar를 아래에 absolute로 고정시키고 모든 페이지에 나타나는 TabBar도 배치. 해당 컴포넌트들은 components/all 디렉터리에 위치함
…ungwan Feat : 기본 레이아웃 설정
- justify-evenly -> justify-around로 변경 - Search svg 변경
…ngess feat: PreviewImage 컴포넌트 구현 merged By Seungwan
1. url path에 영향을 주지 않는 _private 디렉터리를 만들고, 그 아래에 영화 데이터를 받아오는 함수들을 작성 2. 해당 함수들을 서버 컴포넌트인 page.tsx 컴포넌트에서 async ~ await 문법을 이용해 리스트 렌더링 진행 3. 해당 posterpath에 접근 오류 나는 것을 next.config.mjs 설정 파일에서 해결해줌
1. npm i sharp 명령어, 즉 sharp 모듈 설치하면 nextJS가 이를 보고 해결 2. 하지만 내부 동작에 의해 production 환경의 종료가 정상적으로 진행되지 않음 -> macOS 기준 control+z로 강제 종료
…ungwan Feat : movieAPI 1차 연결 및 이미지 렌더링 성능 문제 in production 환경 해결
- style: CardSection의 title에 font-weight 부여 - fix: random 번호 0~9 => 1~10 으로 수정
- CardSection의 margin이 좌우에도 적용되어 있어 움직임 현상 있었던 거 수정 style: width 정의 수정 - body에서 375px로 정의했으니 이후 100%로 변경 fix: random 로직 수정 - random 번호 넘길때 +1 부여해 사진은 정상적으로 1~10번째꺼 넘기게 구현
…ngess �Feat: 나머지 기능 구현 merged by seungwan
…eungwan Feat : /movie-detail/{movieId} 페이지 완성
1. PreviewImage 컴포넌트에서 Image 컴포넌트의 부모 컴포넌트에 position 속성이 있어야 하는데, 중간에 라우팅을 위해 Link 컴포넌트를 넣다보니 이것이 깨졌음. Link 컴포넌트가 article 컴포넌트의 부모가 되게 함으로써 해결 2. ThumbnailImage 컴포넌트의 sizes 속성을 지정
1. next.config.js의 rewrites 설정을 통해 진행 => source에 부여된 속성을 fetch() 함수에서 쓰면 자동으로 원하는 외부 api 엔드포인트로 날려줌 2. 현재 사용하는 api 함수들이 일단 서버 컴포넌트에서 쓰이는 것, 클라이언트 컴포넌트에서 쓰이는 것 모두 있는데, 상관 없이 모두 rewrites()로 마스킹 해줌 3. query parameter를 싣어보내줘야 하는 것들은 has 속성으로 실제 api 엔드포인트에 잘 전달함 4. nodeJS 18 버전 이상부터 fetch() 함수를 사용할 때 url path의 도메인까지 full로 적어줘야 하는데, 개발 환경과 npm build & start를 이용한 로컬 production, 실제 배포 환경의 도메인은 모두 다름. 따라서 이를 .env, .env.development(개발환경), .env.production(실제 배포 환경), .env.production.local(로컬 프로덕션 환경)파일을 만들고 여기에 환경변수를 로드하여 진행했음
…eungwan 자잘한 Image 관련 속성 수정 & 중요한 api key 노출 사항 해결
…ongess feat: 무한스크롤 API 호출 SSG로 변경 merged by seungwan
…ongess fix: Search 페이지 검색기능 수정 merged by seungwan
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기 프론트 멘토 김현민이라고 합니다~!!!!
이번에 해주신 과제 너무 잘 봤습니다!!
전체적으로 Next를 잘 이해하신 깔끔한 코드인 것 같고, 제 의견들 살짝 남겨드렸습니다 ㅎㅎ
물론 제 의견이 절대 정답은 아니고 읽어보시고 참고하시면 좋을 것 같습니다.
이번 과제 너무 수고 많으셨어요~!!!!
<body className={inter.className}> | ||
<RecoilRootProvider>{children}</RecoilRootProvider> | ||
</body> |
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.
코드를 보니 각 페이지별 layout.tsx 에
<main className="flex-1 w-full overflow-y-scroll">{children}</main>;
이 부분이 많이 겹치는 것 같더라구여!
루트 layout.tsx 코드를
<body className={inter.className}> | |
<RecoilRootProvider>{children}</RecoilRootProvider> | |
</body> | |
<RecoilRootProvider> | |
<main className="flex-1 w-full overflow-y-scroll"> | |
{children} | |
</main> | |
</RecoilRootProvider> |
위처럼 설정해준다면 MainHeader를 가지고 있는 main 페이지의 세부 레이아웃만 따로 설정해주면 돼서 개발 cost가 줄어들 것 같습니다 ㅎㅎ
export const metadata: Metadata = { | ||
title: 'Songess-seungwan netlix clone by nextJS', | ||
description: | ||
'This website is netlix clone coding using nextJS and themovieDB API', | ||
}; |
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.
metadata 설정까지!!! 너무 멋집니다...
body { | ||
width: 375px; |
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.
서비스가 responsive 하지 않아서 코드를 보니 width 길이를 px 로 정해놓으셨네요!
저는 코드를 짤 때 평소 습관이 굉장히 중요하다고 생각을 하는데, 특히 이런 레이아웃과 관련된 크기를 설정할 때는 되도록 rem 단위를 사용하는 습관을 들이시면 좋을 것 같다는 개인적인 의견 드립니다 ㅎㅎ
물론 애초에 딱 하나의 크기에서만 서비스를 제공하겠다라는 기획에서는 전혀 상관 없겠지만, 기획은 언제든지 바뀔 수 있잖아요? 만약 갑자기 "우리 그냥 responsive 하게 바꾸자" 이렇게 나와버리면 그때서 화면에 따라 크기가 유동적으로 변해야 하는 부분을 체크하면서 수정하기에는 개발 코스트가 너무 커질 것 같아요.
처음부터 레이아웃 같은 화면 크기에 따라 유동적으로 변해야 하는 부분들은 파악한 뒤 rem 단위로(현재 코드에서는 예를 들어 body width, PreviewImage 컴포넌트 같은 레이아웃) / margin,padding 등 화면 크기가 바뀌어도 크기가 고정되면 좋겠는 부분들은 px 단위로 작업하는 습관을 들인다면 추후에 더 좋을 것 같습니다.
물론 과제를 할 때는 전혀 상관 없겠지만요 ㅎㅎ
|
||
export default function MainHeader() { | ||
return ( | ||
<header className="bg-transparent w-[338px] h-[57px] flex justify-between items-center z-10"> |
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.
스크롤을 내리는 순간 헤더의 background를 설정해 준다면 헤더가 좀 더 잘 보일거 같아요 ㅎㅎ
export default function MovieBar() { | ||
return ( | ||
<section className="w-full mt-[12px] mb-[22px] flex justify-evenly shrink-0"> | ||
<div className="flex flex-col items-center basis-12"> | ||
<Plus className="hover:cursor-pointer" /> | ||
<p className="text-[14px] leading-[20px]">My List</p> | ||
</div> | ||
<div className="w-[110px] h-[45px] flex gap-4 items-center justify-center bg-buttonGray rounded-md hover:cursor-pointer hover:bg-buttonHover"> | ||
<RightGo /> | ||
<p className="text-[20px] font-[600] text-black">Play</p> | ||
</div> | ||
<div className="flex flex-col items-center basis-12"> | ||
<Info className="hover:cursor-pointer" /> | ||
<p className="text-[14px] leading-[20px]">Info</p> | ||
</div> | ||
</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.
MovieBar 가 세 부분의 컴포넌트로 나뉘어 질 수 있으므로, 각각 MyListButton/ PlayButton / InfoButton 이렇게 3개의 컴포넌트로 생성해 준 뒤, 해당 컴포넌트를 파악하는데 중요한 데이터인 각 버튼의 텍스트를 외부에서 넘겨준다면 가독성에 더 좋을 것 같아요.
export default function MovieBar() { | |
return ( | |
<section className="w-full mt-[12px] mb-[22px] flex justify-evenly shrink-0"> | |
<div className="flex flex-col items-center basis-12"> | |
<Plus className="hover:cursor-pointer" /> | |
<p className="text-[14px] leading-[20px]">My List</p> | |
</div> | |
<div className="w-[110px] h-[45px] flex gap-4 items-center justify-center bg-buttonGray rounded-md hover:cursor-pointer hover:bg-buttonHover"> | |
<RightGo /> | |
<p className="text-[20px] font-[600] text-black">Play</p> | |
</div> | |
<div className="flex flex-col items-center basis-12"> | |
<Info className="hover:cursor-pointer" /> | |
<p className="text-[14px] leading-[20px]">Info</p> | |
</div> | |
</section> | |
); | |
} | |
<section className="w-full mt-[12px] mb-[22px] flex justify-evenly shrink-0"> | |
<MyListButton text="My List" /> | |
<PlayButton text="Play" /> | |
<InfoButton text="Info" /> | |
</section> |
import { useEffect, useState } from 'react'; | ||
|
||
export default function SearchInput() { | ||
const [showingMovies, setShowingMovies] = useRecoilState(showingMovie); |
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.
ezgif.com-video-to-gif-converter.mp4
테스트 해봤는데 검색 결과에 입력한 텍스트에 맞는 결과가 뜨지 않아서 현재 무한스크롤 동작에 오류가 있는 것 같아요!
그리고 현재 코드에서 불러온 atom 값을 다시 allMovies 라는 값을 초기화하는 데 사용하고 있는데 이 부분에 대해 제가 잘 이해하지 못한 것 같아요.
저만의 얕은 생각으로는 SearchInput 에서는 텍스트 값에 따라 맨 처음 pageNumber의 데이터로 atom 값을 변경하는 로직 / MovieSection 에서는 atom 값을 띄워주는 로직 & 화면 끝이 감지될 때 마다 pageNumber를 올려주고 atom 값을 업데이트하는 로직으로 구현하면 되지 않을까 싶네요!
위 로직으로 짠다면 SearchInput에는 atom의 값이 필요하지 않고, setter만 설정해 주면 되므로 useRecoilState이 아닌 useSetRecoilState만으로도 구현이 가능할 것 같네요!
useRecoilState는 atom을 구독하므로 atom 값이 변경될 때 마다 SearchInput 컴포넌트가 리렌더링 되지만, useSetRecoilState는 atom을 구독하지 않으므로 atom 값이 변경돼도 리렌더링 되지 않아서 성능 면에서도 더 효과적일 거 같아요.
물론 제가 짠 코드가 아니라서 캐치하지 못한 부분이 있을 수도 있는데 로직을 다시 한번 생각해보시면 제대로 동작하는 무한스크롤을 구현하실 수 있을 것 같습니다~!!
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 [inputValue, setInputValue] = useState('');
const handleInputChange = (e : ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
const searchQuery = e.target.value;
const searchResult = showingMovies.filter(movie => movie.title.includes(searchQuery));
setShowingMovies(searchQuery === '' ? showingMovies : searchResult);
};
이렇게 dom에 직접 접근하기보다 리액트 state로 관리하는 게 더 적합할 거 같아요. dom에 직접 접근하는 방식은 React의 선언적인 스타일과 어울리지 않고, DOM 조작이 React의 상태와 동기화되지 않을 수 있을 뿐더러 DOM 조작이 빈번하게 발생하면 유지보수에 어려움이 있을 수 있고 성능에 문제가 생길 수도 있어요.
혹시 제어 컴포넌트로 만들면 리렌더링이 과하게 발생한다는 이유 때문에 dom에 직접 접근하신 거라면, 과도한 리렌더링 문제는 debounce나 throttle을 이용하는 식으로 해결할 수 있을 거 같아요.
src/apis/getMovieNowPlaying.ts
Outdated
`${domainName}/api/topRatedMoviesByPage?pageNumber=${pageNumber}`, | ||
{ cache: 'force-cache' } | ||
); |
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.
topRatedMovie는 자주 바뀌는 데이터라서 'force-cache' 와는 어울리지 않는 api 인 것 같다는 개인적인 의견 드립니다 ㅎㅎ
<section className="w-full flex flex-col items-center shrink-0"> | ||
<Link href={`/movie-detail/${movieId}`}> | ||
<article className="w-[375px] h-[415px] overflow-hidden relative"> | ||
<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 를 적극 사용하신 점 너무 좋은 것 같아요!!!
interface SearchMovieCardProps { | ||
poster_path: string; | ||
title: string; | ||
id: string; | ||
} | ||
|
||
export default function SearchMovieCard({ | ||
poster_path, | ||
title, | ||
id, | ||
}: SearchMovieCardProps) { |
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.
types/movie.ts 파일에 설정해 놓으신 movieInfowithTitle 인터페이스와 겹치네요!!
interface를 하나 더 만들지 않아도 export 하신 movieInfowithTitle 가져다 쓰시면 될 것 같습니다 ~~
export interface movieInfo { | ||
posterPath: string; | ||
movieId: number; | ||
} | ||
|
||
export interface movieInfowithTitle { | ||
poster_path: string; | ||
title: string; | ||
id: string; | ||
} |
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.
types 폴더를 하나 만들어서 자주 사용되는 movie와 관련된 type을 정리해 놓으신 점 너무 좋은 것 같습니다 ㅎㅎ
…ongess fix: 무한스크롤 API호출 SSR 방식으로 변경 merged by seungwan
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 키 가리는 방법 배우고 가요~ 과제 수고하셨습니다!
src/components/main/SvgCard.tsx
Outdated
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="w-[146px] h-[76px] overflow-hidden relative shrink-0"> | ||
<Image | ||
src={`https://image.tmdb.org/t/p/w1280${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/w1280 이 부분 상수로 관리하면 좋을 거 같아요~
import { useEffect, useState } from 'react'; | ||
|
||
export default function SearchInput() { | ||
const [showingMovies, setShowingMovies] = useRecoilState(showingMovie); |
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 [inputValue, setInputValue] = useState('');
const handleInputChange = (e : ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
const searchQuery = e.target.value;
const searchResult = showingMovies.filter(movie => movie.title.includes(searchQuery));
setShowingMovies(searchQuery === '' ? showingMovies : searchResult);
};
이렇게 dom에 직접 접근하기보다 리액트 state로 관리하는 게 더 적합할 거 같아요. dom에 직접 접근하는 방식은 React의 선언적인 스타일과 어울리지 않고, DOM 조작이 React의 상태와 동기화되지 않을 수 있을 뿐더러 DOM 조작이 빈번하게 발생하면 유지보수에 어려움이 있을 수 있고 성능에 문제가 생길 수도 있어요.
혹시 제어 컴포넌트로 만들면 리렌더링이 과하게 발생한다는 이유 때문에 dom에 직접 접근하신 거라면, 과도한 리렌더링 문제는 debounce나 throttle을 이용하는 식으로 해결할 수 있을 거 같아요.
src/hooks/useGetMovieImg.tsx
Outdated
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.
자동완성이나 타입 안정성, 유지보수, 휴먼 에러방지 측면에서 any는 꼭 필요한 경우에만 사용하고, API response 타입을 명시적으로 지정�하는 연습을 해보셔도 좋을 거 같아요~
ActiveIcon: any; | ||
InactiveIcon: any; |
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 HomeGraySVG from '@public/svg/tabHomeGray.svg'; | ||
import HomeWhiteSVG from '@public/svg/tabHomeWhite.svg'; | ||
import SearchGraySVG from '@public/svg/tabSearchGray.svg'; | ||
import SearchWhiteSVG from '@public/svg/tabSearchWhite.svg'; | ||
import CommingSoonGraySVG from '@public/svg/tabCommingSoonGray.svg'; | ||
import CommingSoonWhiteSVG from '@public/svg/tabCommingSoonWhite.svg'; | ||
import DownloadsGraySVG from '@public/svg/tabDownloadsGray.svg'; | ||
import DownloadsWhiteSVG from '@public/svg/tabDownloadsWhite.svg'; | ||
import MoreGraySVG from '@public/svg/tabMoreGray.svg'; | ||
import MoreWhiteSVG from '@public/svg/tabMoreWhite.svg'; | ||
|
||
import { usePathname } from 'next/navigation'; |
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.
svgr을 사용하시는 만큼 svg의 fill, stroke 속성을 이용해서 색상 변경을 하는 것도 해보시면 좋을 거 같아용 ~
svgr
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 MovieSection() { | ||
const loaderRef = useRef<HTMLDivElement>(null); | ||
const searchInputRef = useRef<HTMLInputElement | null>(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.
useRef는 React 상태가 아니기 때문에 값이 변경되어도 컴포넌트를 재렌더링하지 않아요. 따라서 컴포넌트에서 최신 값을 필요로 하는 경우 문제가 생길 수 있어요. useRef는 렌더링과 무관하게 값을 저장하기에 최신 값이 아닌 값을 읽어올 수 있거든요. 또한 리랜더링을 해줘야하기 때문에 오히려 자동으로 랜더링이 일어나지 않는 부분이 단점으로 작용할 수도 있답니다~
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.
이번 과제도 너무 수고 많으셨습니다! 폴더구조랑 컴포넌트화 부분에서 많이 배워가요 ㅎㅎ 항상 느끼는 사실이지만 코드가 되게 깔끔하고 컴포넌트 재사용이 쉽게 개발하시는 것 같아요 👍 👍 👍
<TabItem key={tabBar.name} {...tabBar} /> | ||
))} | ||
</div> | ||
<div className="w-full h-[26.7]"></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.
px이 빠진 것 같아요..!?
<PreviewImage | ||
imageUrl={`https://image.tmdb.org/t/p/w1280${movie['posterPath']}`} | ||
square={title === 'Previews' ? false : true} | ||
key={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.
여기서 movie 배열 자체를 key 값으로 사용하기 보다는 key={movie['movieId']}
처럼 해서 movieId를 key 값으로 사용하는게 좋을 것 같아요. 적절한 고유 key는 불필요한 재랜더링을 막을 수 있고 시간복잡도도 줄일 수 있어서 성능 향상에 도움이 될 것 같아요!
<div className="flex flex-col w-full overflow-scroll gap-1 mb-[86px]"> | ||
{showingMovies.map((movie, idx) => ( | ||
<SearchMovieCard | ||
key={idx} |
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.
이 경우에도 배열의 index 보다는 영화의 고유 id를 key로 사용하는 것이 좋을 것 같아요!
index 값을 key 로 사용하는 방법은 key 를 정의하지 않으면 react 에서 자체적으로 하는 일이라고 합니다. 또 map()함수에서 data 의 index 는 변화하는 값이고, 배열 요소의 삭제, 이동, 삽입이 일어나면 index 는 변할 수 있어서 Index as key 는 자주 버그를 일으킬 수 있다고 합니다!
리액트 공식문서
className="object-cover" | ||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" | ||
/> | ||
; |
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.
영화 포스터를 가져오는 로직을 함수로 정의해서 사용하니 개발할 때 편리하셨을 것 같아요..!! 👍
|
||
<div className="mt-6 px-8 h-fit leading-[14.17px] text-[11.14px] tracking-[-0.03px] font-normal"> | ||
{previewContent === '' | ||
? 'There is no preview information!' |
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.
컴포넌트를 적당히 잘 분배해 페이지를 구성해서 코드리뷰하기 편했습니다.
과제 고생하셨습니다.!!
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-catch 블록을 추가하여 네트워크 요청이 실패했을 때 에러를 처리하도록 에러 핸들링 추가하는 것도 좋은 방법인 것 같습니다 ~!
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.
각 app의 폴더에 layout.tsx를 배치해서 해당 폴더 및 하위 폴더의 페이지에 공통 레이아웃을 정의한 점이 인상적이네요!
background-color: #f3f6f8; | ||
} | ||
|
||
body { |
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로 고정했었는데 그래도 과제 특성에 맞게 body를 고정시켜버리는 방법도 참신하네요.
setShowingMovies(res); | ||
} | ||
fetchMovieData(); | ||
}, [setShowingMovies]); |
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.
이 부분도 마찬가지로 초기 영화 데이터를 가져오는 부분에 에러 핸들링을 추가하면 예상치 못한 에러에 대응할 수 있을 것 같아요.
}, [setShowingMovies]); | |
useEffect(() => { | |
const fetchMovieData = async () => { | |
try { | |
const res = await getMovieTopRatedByPageNumber(1); | |
setShowingMovies(res); | |
} catch (error) { | |
console.error('Failed to fetch movies', error); | |
} | |
}; | |
fetchMovieData(); | |
}, [setShowingMovies]); |
}, [isLoading, loadMore]); | ||
|
||
return ( | ||
<div className="flex flex-col w-full overflow-scroll gap-1 mb-[86px]"> |
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.
overflow-scroll 보단 overflow-auto를 사용하는것이 더 몇가지 이점이 있어보입니다.
overflow-auto는 '컨텐츠가 부모 요소의 크기를 초과할 때만 스크롤바를 표시하는 것이 UX측면에서도 좋음', '스크롤바를 필요할 때만 렌더링하기 때문에 성능 측면에서 효율적' 인 특징이 있습니다!
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={imageUrl} | ||
fill | ||
className="object-cover" | ||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" |
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.
max-width 설정까지! 완전 세심하네요 배워갑니다..
<p className="text-base leading-8 w-fit hover:cursor-pointer">TV Shows</p> | ||
<p className="text-base leading-8 w-fit hover:cursor-pointer">Movies</p> | ||
<p className="text-base leading-8 w-fit hover:cursor-pointer">My List</p> |
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.
네비게이션은 <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.
PlayButton 따로 컴포넌트로 빼주는거 너무 좋은 거 같아요!
return ( | ||
<section className="w-full flex flex-col items-center h-full overflow-scroll shrink-0"> | ||
<BackgroundImage | ||
imageUrl={`https://image.tmdb.org/t/p/w1280${getMovieNowPlayingImgAndId[random]['posterPath']}`} |
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/w1280
이 부분이 반복적으로 사용되고 있으니 다른 파일에서 빼서 정의해도 좋을 것 같아요! 저희는 .env.local 파일에 작성해줬습니다
@layer components { | ||
.tabBarItem { | ||
@apply flex flex-col justify-center items-center hover:cursor-pointer w-[55px] h-[38px]; | ||
} | ||
|
||
.tabBarItemSVG { | ||
@apply flex-1 flex-shrink-0; | ||
} | ||
|
||
.tabBarItemSpan { | ||
@apply text-[8.2px] mt-[0.1rem]; | ||
} | ||
} |
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 HomeGraySVG from '@public/svg/tabHomeGray.svg'; | ||
import HomeWhiteSVG from '@public/svg/tabHomeWhite.svg'; | ||
import SearchGraySVG from '@public/svg/tabSearchGray.svg'; | ||
import SearchWhiteSVG from '@public/svg/tabSearchWhite.svg'; | ||
import CommingSoonGraySVG from '@public/svg/tabCommingSoonGray.svg'; | ||
import CommingSoonWhiteSVG from '@public/svg/tabCommingSoonWhite.svg'; | ||
import DownloadsGraySVG from '@public/svg/tabDownloadsGray.svg'; | ||
import DownloadsWhiteSVG from '@public/svg/tabDownloadsWhite.svg'; | ||
import MoreGraySVG from '@public/svg/tabMoreGray.svg'; | ||
import MoreWhiteSVG from '@public/svg/tabMoreWhite.svg'; | ||
|
||
import { usePathname } from 'next/navigation'; |
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
구현기능
느낀점 및 시간투자 부분
Recoil
page
컴포넌트는 서버 컴포넌트로 만들고, 상호작용이 필요한SearchInput
,MovieSection
컴포넌트만 클라이언트 컴포넌트로 구현하고 싶음.두 가지 이슈를 해결하기 위해
recoil
을 사용했다.recoil
는 CSR에서 사용할 수 있어서 클라이언트 컴포넌트를 선언해줘야 했다.app/layout.tsx
에서Recoil
을 도입하고 싶었는데 리액트처럼 그냥 컴포넌트 전체를 감싸버리면 오류가 발생했다.Metadata
데이터가 정의되어있는 최상위컴포넌트는 서버 컴포넌트로 만들 수 없었다.이를 해결하기 위해
RecoilRootProvider
컴포넌트를 만들어 그 안에서recoil
을 선언하고 묶어주었다.성능 최적화
Search
페이지에서 유저와의 상호작용이 필요한 것은input
박스와input
에 들어오는 인풋값에 따른 검색창이였다(무한스크롤을 위해서도). 성능 최적화를 위해 나머지부분은 서버단에서 처리하고 싶어서page
컴포넌트자체는 서버컴포넌트로 구현하고SearchInput
,MovieSection
은 클라이언트 컴포넌트로 만들어줘 유저와의 상호작용이 가능하게 만들었다.무한스크롤 구현
다른 라이브러리를 사용하지 않고 무한스크롤을 구현해보고 싶었다. Web API인
IntersectionObserver
를 사용해 무한스크롤을 구현했다.MovieSection
내부에서 영화리스트들 태그아래ref
를 단 태그를 만들고, 유저가 스크롤하여 모든 영화리스트들을 확인하고ref
단 태그가 보이면useEffect
에서firstEntry.isIntersecting
속성을 통해 감지하고loadMore()
를 호출해 다음 페이지의 영화리스트들을 가져오는 형식으로 구현했다.클라이언트 컴포넌트에서의 api key 노출 문제
use client
directive를 쓰지 않는 서버 컴포넌트는 브라우저 단에서 data를 fetching 하는 로직이 실행되지 않고 배포 서버에서 진행되기에 api key가 노출되지 않는다.하지만 클라이언트 컴포넌트는 javascript가 브라우저 단에서 실행되기 때문에 api key를 브라우저 개발자 도구의 네트워크 탭을 보면 나타난다는 문제가 발생하고 있었다. 이를 해결하기 위해 생각한 방법은 다음과 같다.
next.config.js
설정 파일의rewrites()
함수를 이용하여 외부 url로의 api 호출을 **"마스킹"**하는 것이다.위의 코드를 보면 여러 외부 url 엔드포인트로의 요청을 다른 url path로 숨겨주고 있다. 이렇게 작성한 로직은
fetch()
함수에서 사용되고 더 이상 api key가 노출되지 않는다는 것을 알 수 있다.Key Question
정적 라우팅(Static Routing)/동적 라우팅(Dynamic Routing)이란?
nextJS를 이용해 웹 애플리케이션을 만들 때, 고정된(정적인) url path를 가진 페이지가 있을 수도 있지만, 상황에 따라 달라지는 동적인 페이지가 있을 수도 있다.
전자는 정적 라우팅, 후자는 동적 라우팅이라고 불린다.
정적 라우팅의 경우에는 고정된 url path에 layout.tsx나 page.tsx 컴포넌트를 생성하기에 프로그래머가 이에 대응하는 값을 이용하여 웹 페이지를 렌더링할 수 있다. 하지만 동적 라우팅은 매번 세부 url path가 다르므로 이를 얻는 과정이 필요한데, 해당 컴포넌트가 클라이언트 컴포넌트 / 서버 컴포넌트인지에 따라 다르다.
usePathname()
훅과 객체 구조 분해 방식을 통해 이용할 수 있다.params
객체 속성이 전달된다. 동적으로 변화할 디렉터리 이름을[]
으로 감싸주고 해당하는 파일 이름을 변수명처럼 사용하면 된다. 디렉터리명을[...slug]
로 지어주면 /shop/a의 경우에는 ['a'], /shop/a/b 의 경우에는 ['a', 'b']처럼 사용할 수 있다.주목해서 볼만한 점은, 동적으로 변화하는 url path를 가지는 페이지의 경우에도
getStaticParams()
함수를 이용하여 미리 SSG(Static Site Generation) 방식으로 렌더링하여 사용자에게 로드되는 속도를 개선할 수 있다는 것이다.성능 최적화를 위해 사용한 방법
next/image
에서 제공되는 를 사용했다.를 써서 얻을 수 있는 이점
GET
요청에 따른 결과값을 확인해보면jpg
이지만 화면에 렌더링되어있는 이미지를 확인해보면webp
인걸 확인할 수 있다.paint
가 되었다가 이미지가 로드되면repaint
되면 화면이 밀리는 현상을 방지하기 위해placeholder
속성을 넣어줄 수 있다. 하지만 이는src
의 경로가jpg,png
등 정적일 때만 가능하다. 따라서 이 기능을 대체하기 위해sharp
라이브러리를 통해 해결했다.