-
Notifications
You must be signed in to change notification settings - Fork 8
[6주차] Team DiggIndie 백승선 & 조성아 과제 제출합니다. #9
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
base: main
Are you sure you want to change the base?
Conversation
이슈 템플릿 변경
Add a pull request template with sections for issues, work description, screenshots, and review requirements.
feat: landing 페이지 개발
feat: Header, Banner 개발
Revert "feat: Header, Banner 개발"
search 페이지 개발, 무한 로딩, skeleton 기능 포함
Feat/preview page
Merge Main
feat/ search [무한 스크롤 조건 수정]
Merge to Main
jungyungee
left a comment
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/comingSoon/page.tsx
Outdated
| @@ -0,0 +1,9 @@ | |||
| "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.
use client는 제거해도 될 것 같습니다..!
app/comingSoon/page.tsx
Outdated
| export default function ComingSoon() { | ||
| return ( | ||
| <div className="flex items-center justify-center min-h-screen bg-black w-[375px]"> | ||
| <div className="h-10 w-10 rounded-full border-4 border-white/30 border-t-white animate-spin" /> |
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해서 사용하면 코드의 재사용성을 낮출 수 있을 것 같아요..!
| @font-face { | ||
| font-family: 'SF Pro Display'; | ||
| src: url('/fonts/sf-pro-display-regular.woff2') format('woff2'); | ||
| font-weight: 400; | ||
| font-style: normal; | ||
| font-display: swap; | ||
| } | ||
|
|
||
| @font-face { | ||
| font-family: 'SF Pro Display'; | ||
| src: url('/fonts/sf-pro-display-medium.woff2') format('woff2'); | ||
| font-weight: 500; | ||
| font-style: normal; | ||
| font-display: swap; | ||
| } | ||
|
|
||
| @font-face { | ||
| font-family: 'SF Pro Display'; | ||
| src: url('/fonts/sf-pro-display-semibold.woff2') format('woff2'); | ||
| font-weight: 600; | ||
| font-style: normal; | ||
| font-display: swap; | ||
| } | ||
|
|
||
| @font-face { | ||
| font-family: 'SF Pro Display'; | ||
| src: url('/fonts/sf-pro-display-bold.woff2') format('woff2'); | ||
| font-weight: 700; | ||
| font-style: normal; | ||
| font-display: swap; | ||
| } | ||
|
|
||
| @font-face { | ||
| font-family: 'SF Pro Display'; | ||
| src: url('/fonts/sf-pro-display-heavy.woff2') format('woff2'); | ||
| font-weight: 800; | ||
| font-style: normal; | ||
| font-display: swap; | ||
| } |
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.
font.css와 중복되는것 같네요..!
app/search/page.tsx
Outdated
| // 검색어 변경 시 초기화 | ||
| useEffect(() => { | ||
| loadMovies(true); | ||
| }, [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.
현재는 타이핑 시마다 계속 API를 요청하게 되는데 과도하게 많이 요청될 수 있어서
(아래는 저희 팀 코드입니다)
useEffect(() => {
const handler = setTimeout(() => {
onSearchChange(query);
}, 300);
return () => clearTimeout(handler);
}, [query, onSearchChange]);
이런식으로 디바운스를 적용해서 지연시켜주면 더 좋을 것 같습니다.!
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창 관련 로직에 정말 많이 들어가서, 다음번에 검색창이나 로그인에서 이메일 검증 로직 등이 있을 때 사용해보세요!
+) 추가로 스로틀이란 개념도 있슴당
useDebounce 활용법
디바운스와 스로틀
| {/* Previews */} | ||
| <div className="px-4 flex flex-col"> | ||
| <span className="mt-8 text-[26px] leading-none font-bold self-start">Previews</span> | ||
| <span className="mt-2 text-sm text-[#E0E0E0] leading-relaxed text-left">{overview}</span> |
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.
| return ( | ||
| <div className="fixed bg-black bottom-0 w-[373px] flex justify-center z-900"> | ||
|
|
||
| <div className="bg-[#FFFFFF] mt-[18.12px] mb-[9.35px] h-[4.53px] w-[121.38px] rounded-[90.58px]" /> |
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 href="/home" className={getLinkClass('/home')}> | ||
| <HomeIcon className="w-[24px] h-[24px]" /> | ||
| <span>Home</span> | ||
| </Link> | ||
|
|
||
| <Link href="/search" className={getLinkClass('/search')}> | ||
| <Search className="w-[24px] h-[24px]" /> | ||
| <span>Search</span> | ||
| </Link> | ||
|
|
||
| <Link href="/comingSoon" className={getLinkClass('/comingSoon')}> | ||
| <ComingSoon className="w-[24px] h-[24px]" /> | ||
| <span>Coming Soon</span> | ||
| </Link> | ||
|
|
||
| <Link href="/downloads" className={getLinkClass('/downloads')}> | ||
| <Download className="w-[24px] h-[24px]" /> | ||
| <span>Downloads</span> | ||
| </Link> | ||
|
|
||
| <Link href="/more" className={getLinkClass('/more')}> | ||
| <Menu className="w-[24px] h-[24px]" /> | ||
| <span>More</span> | ||
| </Link> | ||
| </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.
작동에는 문제가 없지만, 배열로 두면 훨씬 깔끔한 코드가 될 수 있고, 유지보수에 유용합니다..!
const menuItems = [
{ href: '/home', label: 'Home', Icon: HomeIcon },
{ href: '/search', label: 'Search', Icon: SearchIcon },
{ href: '/downloads', label: 'Downloads', Icon: DownloadIcon },
{ href: '/comingsoon', label: 'Coming Soon', Icon: ComingSoonIcon },
{ href: '/more', label: 'More', Icon: MoreIcon },
];
| const src = path ? `${IMG_BASE}/${imgSize}${path}` : "/placeholder-portrait.png"; | ||
|
|
||
| return ( | ||
| <Link href={{ pathname: `/title/${item.id}`, query: { img: path } }} className={className} prefetch={false}> |
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로 전달하는 방식보다 link 만 넘겨주고, 상세페이지 내에서 직접 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.
스켈레톤 적용한 점 좋습니다..!
Wannys26
left a comment
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.
과제 하시느라 수고하셨습니다!
저는 이번에 Nextjs에서의 최적화 등에 많이 고민해보았는데,
코드 리뷰 하면서 다시 한번 생각해볼 수 있는 기회가 되었습니다!
밑은 공식문서 한글ver인데 한 번 읽어보시는거 추천드려용
app/search/page.tsx
Outdated
| // 검색어 변경 시 초기화 | ||
| useEffect(() => { | ||
| loadMovies(true); | ||
| }, [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.
디바운스는 input창 관련 로직에 정말 많이 들어가서, 다음번에 검색창이나 로그인에서 이메일 검증 로직 등이 있을 때 사용해보세요!
+) 추가로 스로틀이란 개념도 있슴당
useDebounce 활용법
디바운스와 스로틀
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.
지금 home 페이지 내의 모든 컴포넌트가 useEffect를 사용한 데이터 페칭으로 클라이언트 사이드 렌더링이 되어있는데,
데이터를 fetching 해오는 부분은 SSR 컴포넌트로 만드시는 걸 추천드립니다.
Banner를 제외하고는 home에 들어가는 컴포넌트들은 사실상 비슷한 레이아웃을 가지고 있기에,
공통 레이아웃만 CSR로 구성하시고,
나머지 컴포넌트들(ContinueWatching, Hollywood...) 같은 것은
SSR로 데이터만 fetching해와서 공통 레이아웃에 넣으시는 방식으로 리팩토링 하면 좋을거 같습니다.
한 페이지 내에 연결된 API가 많기에 로딩 속도가 느려지는데,
해당 방식으로 리팩토링 해보시면 "SSR이 왜 필요할까?"에 대한 것을 이해해보실 수 있을거에요!
| .filter((movie) => movie.poster_path) | ||
| .map((movie) => ( | ||
| <div key={movie.id} className="relative w-[103px] h-[161px] flex-shrink-0 group"> | ||
| <Thumbnail item={movie} imgSize="w500" className="absolute inset-0" /> |
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 위를 Thumbnail 컴포넌트가 absolute inset-0로 덮고 있어서, detail 페이지로 넘어가는 클릭 이벤트만 발생하고 있습니다..ㅠ
Thumbnail 컴포넌트가 스와이프 가능한 div 안에 있는 걸로 코드는 작성되어있지만,
실제로는 Thumbnail 컴포넌트가 모달처럼 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.
getOverview.ts를 제외하고는 전부 axios를 사용하신거 같은데,
fetch를 사용하시는 걸 더 추천드립니다!
이유로는
- Next.js 자동 캐싱: fetch는 Next.js에서 자동으로 캐싱되어 동일한 요청을 재사용
- Revalidation 제어: { next: { revalidate: 3600 } }로 캐시 수명을 설정 가능
- SSR/ISR 최적화: 서버 컴포넌트에서 fetch를 사용하면 빌드 타임이나 요청 시점에 데이터를 페칭하고 캐싱
home/page.tsx에서 코멘트 달은 서버 컴포넌트를 활용하라는 말이랑 연결되니 한 번 생각해보시면 좋을 거 같습니다!
Jy000n
left a comment
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나 consts들이 .tsx 안에 많이 선언/정의되어 있는데, 각각 type const 폴더 내로 한 번 정리해도 좋을 것 같습니다:) 과제 수고하셨습니당 😄
.github/ISSUE_TEMPLATE/커스텀-이슈-템플릿.md
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.
이슈 템플릿이랑 PR 형식 정해서 이렇게 남겨 놓는거 좋은 것 같아요 😊
| *::-webkit-scrollbar { | ||
| display: none; | ||
| } No newline at end of file |
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.
@layer base에 같이 둬도 될 것 같습니다 !
| @@ -0,0 +1,11 @@ | |||
| 'use client'; | |||
|
|
|||
| import LottieLogo from '../components/LottieLogo'; | |||
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.
절대경로 상대경로 혼용되어 있는데 절대경로로 통일해서 사용하시면 더 가독성 좋을 것 같아요 !
|
|
||
| return ( | ||
| <div className="flex flex-col min-h-screen bg-black w-[375px]"> | ||
| {/* 상단 고정 검색창 + Top search */} |
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 play from '@/public/icons/play.svg'; | ||
| import info from '@/public/icons/info.svg'; | ||
|
|
||
| const imgBase = process.env.NEXT_PUBLIC_TMDB_IMAGE_BASE || 'https://image.tmdb.org/t/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.
이 코드는 많이 쓰일 것 같은데 따로 constants 파일에 넣어도 될 것 같아요
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.
home페이지에 banner를 home/section/Banner.tsx를 사용하시는 것 같은데 해당 파일은 사용되지 않거나 필요 없으면 지우셔도 될 것 같습니당
| <Thumbnail item={movie} imgSize="w500" className="absolute inset-0" /> | ||
| {/* 진행률 표시*/} | ||
| <div className="absolute bottom-0 left-0 w-full h-1 bg-gray-700"> | ||
| <div className="h-1 bg-red-500" style={{ width: '70%' }} /> |
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.
w-[70%]로 하면 tailwind로 지정 가능한 거로 알고 있는데 안 됐나요..?
| export type WatchHistoryItem = { | ||
| id: number; // 더미 데이터 고유 ID | ||
| userName: string; // 사용자 이름 | ||
| contentId: number; // TMDB id | ||
| title: string; // 영화/TV 제목 | ||
| type: "movie" | "tv"; // 구분 | ||
| watchedAt: string; // 시청 날짜 (ISO 문자열) | ||
| }; |
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 폴더로 따로 빼는게 더 유용할 것 같아요!
| {data | ||
| .filter((movie) => movie.poster_path) // null 아닌 것만 | ||
| .slice(0, 5) // 5개만 표시 | ||
| .map((movie) => ( | ||
| <div key={movie.id} className="relative w-[154px] h-[251px] flex-shrink-0 group"> | ||
| <Thumbnail item={movie} imgSize="w500" className="absolute inset-0" /> | ||
| </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.
이 형식들이 많이 쓰이는데 따로 컴포넌트화 시켜도 좋을 것 같아요..! 영화 종류 같은거는 data={movies} 이런거로 props 전달하면 될 것 같아요

배포주소
1. 전반적인 협업 과정
GitHub 협업 순서
개발 중 오류 해결 방법
• 카톡 연락해서 문제를 해결하였습니다.
레포지토리 주소
레포지토리
issue, pr 등을 확인하실 수 있습니다. (closed 되어있음)
2. 업무 분배 및 구현 방법
업무 분배
승선-승선- navbar svgr방식 적용, 상세페이지 개발 ,spinner 개발, axios 적용
성아- search page 개발, skeleton 기능 개발, 무한 스크롤 기능 개발, 기존 fetch 방식을 axios 라이브러리를 이용하는 것으로 수정
구현 내용
백승선
-preview page: 넷플릭스 영화 사진을 누르면 해당 영화의 포스터가 배너로 걸리고 영화의 줄거리가 Preview 밑에 로드되는 previewPage.tsx를 만들었다.
조성아
search page : end point는 search/movie이고 lib/api/tdmb/movie.ts 안 fetchSearchMovie 함수에서 axios로 영화 데이터들을 불러오고 있다. 사용자가 검색창(SearchSection)에 입력한 텍스트(query)를 상태로 관리. query 값이 변경될 때마다 useEffect로 API 호출 실행한다. 검색어가 없을 경우 fetchPopularMovies()를 호출해 인기 영화 리스트를 대신 노출한다.
skeleton 기능 components/search/searchSkeleton.tsx 파일에 구현 해놓았고 search 페이지를 데이터 로드 완료 또는 실패시 setLoading을 불러온다. loading 상태가 true일 때는 Skeleton, false일 때는 실제 영화 데이터 목록을 표시.
무한 스크롤 기능 개발 : 초기 렌더링 시 첫 페이지 데이터 호출한다. 사용자가 스크롤을 내려 loaderRef 영역이 화면에 노출되면 다음 페이지 API 자동 요청한다. 추가 데이터가 없으면 “더 이상 조회된 영화가 없습니다.” 문구를 표시한다.
3. 느낀 점
백승선
조성아