[6주차] IPX 김민서 & 남기림 과제 제출합니다.#15
Conversation
[REFACTOR] 홈 화면 렌더링 최적화 및 UI 개선
[FEAT] 상세 페이지 구현
Dev- 상세 페이지 및 화면 UI 최적화 master로 merge
feat: 검색페이지에서 상세페이지로의 연결 추가
[FEAT] 검색 페이지 구현
[CHORE] 컴포넌트 디테일 수정
[CHORE] migrate from npm to pnpm
Chore/migrate
[CHORE] npm -> pnpm 변경
chore: Webpack에서 TurboPack으로 마이그레이션 적용
| fontSize: "12px", | ||
| fontWeight: 400, | ||
| lineHeight: "135%", | ||
| letterSpacing: "-0.24px", |
There was a problem hiding this comment.
global.css에서 토큰 불러오는 게 앞으로도 훨씬 편하실거예요~~!!
| <MovieRow title="US TV Shows" medias={usTVShows.results} /> | ||
| <div className="flex flex-col gap-6 mt-4"> | ||
| <Suspense fallback={<RowSkeleton />}> | ||
| <AsyncMovieRow title="Popular on Netflix" fetcher={fetchPopular} /> |
There was a problem hiding this comment.
다른 분 리뷰에도 달았던 내용인데요, const로 title과 fetcher, slice, large를 분리하고, 이미 props에 slice?와 large? 타입을 잘 정의해주셨다 보니 상수 배열 내의 요소를 map함수로 뿌리고 관리하면 훨씬 간결한 코드가 될 수 있겠다는 생각을 했습니다.
| return <div className="w-full h-[415px] bg-neutral-900 animate-pulse" />; | ||
| } | ||
|
|
||
| function RowSkeleton({ large }: { large?: boolean }) { |
There was a problem hiding this comment.
skeleton을 그 스켈레톤이 들어갈 컴포넌트와 동일한 디렉터리에 보관하는 게(혹은 skeletons폴더에서 components폴더와 같은 트리를 만들어서) 좋은 컨벤션이라고 하더라구요. 같은 코드 안에 작성하다 보면 너무 길어질 수 있으니 다른 파일로 이동을 권장드립니다.!!
|
|
||
| const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => { | ||
| const q = e.target.value; | ||
| setQuery(q); |
There was a problem hiding this comment.
해당 코드에서는 타자를 하나하나 칠 때마다 전부 setQuery가 호출되어 비효율이 발생합니다. 예를 들어 '세오스'를 검색한다면 [ㅅ, 세, 세ㅇ, 세오, 세오ㅅ, 세오스] 가 전부 호출되고, 만약 이 코드가 api연동으로 연결된다면 '세오ㅅ' 검색 결과가 '세오스'보다 늦게 도착하면 검색 결과도 안 나올 수 있어요. 따라서 debounce함수(useEffect와 setTimeout함수의 조합)을 만들어야 할 것 같습니다.
나중에 프로젝트를 진행하실 때에도 사용자로부터 입력받아 그 입력을 CRUD하는 경우가 많을 텐데요, 방어 로직 구현(ex: 디바운스 함수 등)을 꼭 구현하셨으면 합니다~~
There was a problem hiding this comment.
svg 아이콘을 사용하는 방법 중에 svgr 태그도 있으니 참고해보세요!
| }, 3000); | ||
| return () => clearInterval(interval); | ||
| }, [medias.length]); | ||
|
|
There was a problem hiding this comment.
media.length보다 media자체가 의존성 배열에 있는 게 더 좋아보입니다 !
|
|
||
| if (!media?.poster_path) { | ||
| return <div className={`${sizeClass} rounded-[2px] bg-white shrink-0`} />; | ||
| } |
There was a problem hiding this comment.
bg-white 보다는 로고나 아이콘이 포함된 스켈레톤 형식도 고려해보시면 좋을 것 같습니다!
| if (page === 1 || results.length > 0) return; | ||
| fetchTopSearches(page).then((data) => { | ||
| setTopSearches((prev) => [...prev, ...data.results]); | ||
| setHasMore(data.page < data.total_pages); | ||
| }); | ||
| }, [page]); |
There was a problem hiding this comment.
6주차 과제하시느라 고생많으셨습니당!!
앱 실행 결과, 검색 결과를 지운 뒤 Top Searches로 돌아올 때 기존 pagination 상태가 초기화되지 않아 동일한 Top Searches 데이터가 다시 추가되면서 중복 렌더링되는 것 같습니다.
검색 모드에서 Top Searches 모드로 전환될 때 page/topSearches/hasMore 상태를 초기화하거나, append 전에 중복 id를 제거 처리하면 더 안정적일 것 같습니다!
| <BottomNav /> | ||
| </main> | ||
| ); | ||
| } |
There was a problem hiding this comment.
홈 화면을 HeroBanner, Previews, ContinueWatching, MovieRow로 나누고 각 섹션을 Suspense로 감싼 구조가 좋아 보입니다. 페이지 구성 요소가 잘 보이는 것 같네용!
| /> | ||
| <Image src="/icons/icon-x.svg" alt="x" width={16} height={16} /> | ||
| </header> | ||
| ); |
There was a problem hiding this comment.
오른쪽 X 아이콘이 현재는 장식 이미지처럼 보여서, 검색어 삭제 기능을 위해서는 button으로 감싸고 aria-label을 추가하면 좋을 것 같아요!
| <section className="px-4 py-3"> | ||
| <h2 className="text-heading2 text-white mb-3 antialiased"> | ||
| Continue Watching for Girim Nam | ||
| </h2> |
There was a problem hiding this comment.
Continue Watching for Girim Nam이 하드코딩되어 있어서 재사용성이 조금 떨어질 수 있을 것 같습니다. 사용자 이름을 prop으로 받으면 더 좋을 것 같네용!
배포링크
6주차 개발 내용