Skip to content

[6주차] Groupeat 이승연 & 황영준 과제 제출합니다.#10

Open
YJ0623 wants to merge 70 commits into
CEOS-Developers:masterfrom
CEOS-13th-netflix:main
Open

[6주차] Groupeat 이승연 & 황영준 과제 제출합니다.#10
YJ0623 wants to merge 70 commits into
CEOS-Developers:masterfrom
CEOS-13th-netflix:main

Conversation

@YJ0623

@YJ0623 YJ0623 commented May 8, 2026

Copy link
Copy Markdown

배포 링크: next-netflix-23rd-altjiudda-yj0623s-projects.vercel.app

1. Skeleton UI

  • 스켈레톤 UI를 만듦으로써 UX 향상을 시도하였습니다. 홈 페이지의 영화 DB에서 API를 받아오는 과정이 짧지 않은 시간이었기 때문에, (데스크탑 환경은 제외하고)모바일 환경에서 사용자가 기다리는 동안 '보고 있을 화면'이 어때야 할 것인가에 대한 고민을 하였습니다. 이는 각 API 연동이 이루어지는 컴포넌트별로 스트리밍을 진행하려는 시도로 연결되었고, 스트리밍이 이루어지는 동안 각 컴포넌트의 위치는 변하면 안 됐기 때문에 스켈레톤UI의 너비-높이를 원래 렌더링되어야 하는 컴포넌트의 크기와 동일하게 설정하였습니다. 이때 저번 주에 언급된 Suspense옵션을 활용했습니다.

  • Suspense를 사용했으나, 비동기 함수가 사용되어 데이터 페칭을 진행하고, 이를 각 컴포넌트의 props로 넘겨주는 환경에서는 스트리밍이 가능하지 않았습니다. 기존에는 async와 Promise.all함수를 사용했기 때문에 모든 데이터 페칭이 완료되기 전까지는 페이지의 렌더링이 진행되지 않는 환경이었으니까요.

  • 따라서 이 문제를 해결하기 위해, 각 컴포넌트를 Wrapper로 감싸 '서버 컴포넌트화'시킨 뒤 해당 컴포넌트 내에서 데이터 페칭을 진행했습니다. 그렇게 데이터 페칭이 완료된 컴포넌트별로 함수 내부에 props가 전달되어 데이터 페칭이 먼저 완료되는 컴포넌트별로 렌더링이 진행되도록 스트리밍을 구현하였습니다.

변경 전 함수

        const top10KoreaData = await movieApi.getTop10KoreaToday();
        ///...중략
        <HeroCarousel movies={top10KoreaData.results} />

변경 이후 함수

        <Suspense fallback={<HeroSkeleton />}>
          <HeroWrapper />
        </Suspense>
        
        ...
        // HeroWrapper.tsx
        export default async function HeroWrapper() {
          const top10KoreaData = await movieApi.getTop10KoreaToday(); 
          return <HeroCarousel movies={top10KoreaData.results} />;
        }

2. Git Flow

협업 단계에서 서로 코드가 꼬이거나 새로운 기능을 개발했다 한들 머지를 할 때 오류가 발생하는 경우를 배제할 수 없었습니다. 이러한 위험성을 낮출 방법이 무엇인지 팀 멘토분에게 여쭤봤고, 추천받은 방법은 깃 플로우였습니다.
현재 과제를 진행하면서는 적극적으로 깃 플로우 전략을 사용하지는 않았지만, 현재 팀 프로젝트 레포지토리 내에서 main브랜치 / develop 브랜치 / feature 브랜치 등을 두어 브랜치별 역할을 수행 중입니다. 이러한 협업 과정은 커밋이 많아지고 기능이 복잡해질수록 더욱 필요하다 생각되어 다음 과제에는 직접 적용하기로 합의를 보았습니다.

3. 무한 스크롤 구현

-기존의 async 기반 서버 컴포넌트 구조만으로는 useInfiniteQuery, IntersectionObserver 같은 클라이언트 상태 관리 로직을 사용할 수 없다는 점을 알게 되었습니다.

-그래서 MovieSearchRow에서 처리하던 로직을 useMovieSearch, MovieSearchList로 분리하면서 클라이언트 중심 구조로 변경했고, TMDB 토큰이 서버 환경 변수라는 점 때문에 API Route를 중간에 두어 client → API route → TMDB 형태로 데이터를 가져오도록 수정했습니다.

-또한 협업을 하는 과제인 점을 고려하여, 추후 제 코드에서 문제가 생겼을 때 안정적인 대처를 위해 “더 보기” 버튼으로 먼저 페이지네이션이 정상 동작하는지 검증한 뒤 Intersection Observer를 붙이는 방식으로 단계적으로 구현하였습니다.

4. 상세 모달 구현

-전역 상태 기반 모달 구조와 브라우저 history UX를 함께 고민하게 되었습니다. 처음에는 모달이 전역 상태로 유지되어 브라우저 뒤로가기를 눌렀을 때 페이지 이동과 UX가 어색했지만, window.history.pushStatepopstate 이벤트를 이용해 “뒤로가기 → 모달 닫기” 흐름을 구현하면서 UX를 개선해보았습니다.

5. SVGR 적용

-SVG를 단순 이미지 파일처럼 사용하는 것이 아니라 React 컴포넌트처럼 import해서 사용해보았습니다. 특히 기존 SVG 내부에 고정 width/height가 들어있으면 Tailwind로 크기 제어가 잘 되지 않는 문제가 있었고, 이를 제거한 뒤 className으로 사이즈를 조절하는 방식이 더 유연하다는 점도 알게 되었습니다. 덕분에 아이콘을 디자인 시스템처럼 재사용할 수 있는 구조를 경험해보았습니다.

6. SEO metadata 구현

-App Router 기반의 SSR 환경에서 generateMetadata를 통해 title, description, openGraph 정보를 동적으로 생성하는 방식을 경험했습니다. 기존 클라이언트 모달 구조와 달리 서버 컴포넌트 기반으로 데이터를 가져와야 했고, app/detail/[mediaType]/[id] 형태의 dynamic route 구조를 새롭게 구성하게 되었습니다.

-또한 opengraph-image.tsxImageResponse를 이용해 영화 제목과 backdrop 이미지를 기반으로 동적 Open Graph 이미지를 생성하면서, SSR이 SEO와 공유 미리보기에 어떻게 활용되는지도 함께 공부해보게 되었습니다.

[영준]
팀원들이 열심히 준비해서 발표하는 자료들은 이미 알고 있던 것과 몰랐던 것들, 그리고 알고 있었지만 미처 신경쓰지 못했던 것들의 집합이었습니다. 갈수록 양질의 자료와 발표가 누적되며, 프로젝트에 직접 적용해보는 여러 시도들이 늘어갔고, 이는 양질의 웹 페이지가 만들어지는 경험으로 연결되고 있습니다. 스켈레톤UI를 컴포넌트의 역할에 따라 전략적으로 배치하고, 코드의 재사용 및 확장을 계속 고려하며 상태를 관리하는 등의 시도는 아마 혼자 공부했더라면 조금 늦게 접했을 지도 모른다는 생각을 하는 계기가 되었습니다. 좋은 과제와 자료 제공해주셔서 항상 감사합니다!!

YJ0623 and others added 30 commits April 27, 2026 22:23
Style: 텍스트 작업 완료
Pakage: loiite 추가
Co-authored-by: Copilot <copilot@github.com>
YJ0623 and others added 24 commits May 6, 2026 13:06
@YJ0623 YJ0623 changed the title [6주차] Groupeat 이승연, 황영준 과제 제출합니다. [6주차] Groupeat 이승연 & 황영준 과제 제출합니다. May 9, 2026

@waldls waldls left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6주차 과제 수고 많으셨습니다!

<main className="min-h-dvh bg-black text-white">
<h1>{detail.title}</h1>
</main>
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

현재 상세 페이지 접속 시 작품 제목만 노출되고 있어서, 피그마 디자인 시안을 참고하여 화면 퍼블리싱을 조금 더 보완해주시면 좋을 것 같습니다.
그리고 아래 두 가지 동선도 함께 반영해주시면 서비스 완성도가 훨씬 높아질 것 같습니다.

  • 홈 화면의 작품 카드 : 클릭 시 해당 경로로 이동
  • 검색 페이지 내 작품 아이템 : 클릭 시 해당 경로로 이동 (현재는 /search?q=...로 연결되고, 이 경로에 UI 작업을 하신 것으로 보입니다)

@YJ0623 YJ0623 May 10, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image 현재 상세 페이지 접속 시 작품 제목만 노출되고 있어서, 피그마 디자인 시안을 참고하여 화면 퍼블리싱을 조금 더 보완해주시면 좋을 것 같습니다. 그리고 아래 두 가지 동선도 함께 반영해주시면 서비스 완성도가 훨씬 높아질 것 같습니다.
  • 홈 화면의 작품 카드 : 클릭 시 해당 경로로 이동
  • 검색 페이지 내 작품 아이템 : 클릭 시 해당 경로로 이동 (현재는 /search?q=...로 연결되고, 이 경로에 UI 작업을 하신 것으로 보입니다)

오잉 이거 테스트할때는 상세페이지 퍼블리싱까지 되어있었던거같은데 확인해볼게요.. 커멘트 감사합니다

Comment on lines +13 to +39
const isDownRef = useRef(false);
const startXRef = useRef(0);
const scrollLeftRef = useRef(0);

if (!movies || movies.length === 0) return null;

return (
<section className="mt-8 px-4">
<h2 className="mb-4 text-2xl font-semibold text-white">Previews</h2>

<div
ref={scrollRef}
className="flex cursor-grab gap-[7px] overflow-x-auto scrollbar-hide active:cursor-grabbing"
onMouseDown={(e) => {
isDownRef.current = true;
startXRef.current = e.pageX;
scrollLeftRef.current = scrollRef.current?.scrollLeft ?? 0;
}}
onMouseLeave={() => {
isDownRef.current = false;
}}
onMouseUp={() => {
isDownRef.current = false;
}}
onMouseMove={(e) => {
if (!isDownRef.current || !scrollRef.current) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 드래그 로직은 다른 가로 스크롤 컴포넌트에서도 재사용될 가능성이 높아 보입니다. useDraggableScroll 같은 커스텀 훅으로 분리하시는 게 더 좋을 것 같습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 feature/movies/app/ 폴더 아래에 컴포넌트들이 나뉘어 있어 구조가 다소 분산되어 보입니다.
특히 API 로직인 useMovieDetail.ts가 컴포넌트 폴더에 포함되어 있어 역할 구분이 모호해질 수 있을 것 같아요.
components/ 폴더 아래에 common(공통), home, search 등 도메인 단위로 구조를 정리하면 가독성과 유지보수성이 더 좋아질 것 같습니다.

Comment on lines +13 to +14
}) {
const imageBaseUrl = 'https://image.tmdb.org/t/p/w500';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 도메인 주소는 다른 컴포넌트에서도 공통으로 사용될 것 같은데, 매번 선언하기보다 공통 상수로 관리하시는 편이 더 좋을 것 같습니다!

Comment on lines +25 to +28

const results = data?.pages.flatMap((page) => page.results) ?? [];

useEffect(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

results를 계산하는 flatMap 연산이 렌더링될 때마다 실행되고 있습니다. 데이터 양이 많아질 경우 성능에 영향을 줄 수 있으므로 useMemo로 감싸주면 더 효율적일 것 같습니다!

const results = useMemo(() => 
  data?.pages.flatMap((page) => page.results) ?? [], 
[data]);

Comment thread app/page.tsx
Comment on lines +6 to +7
import netflixAnimation from '@/public/lottie_netflix_logo.json';
import NetflexLogoIcon from '@/public/icon_netflix_logo.svg';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NetflexLogoIcon -> NetflixLogoIcon 으로 오타 수정해주셔야 더 명확할 것 같습니닷

Comment on lines +16 to +24

const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
error,
} = useMovieSearch(query);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 구조에서는 query가 한 글자 바뀔 때마다 useMovieSearch가 즉시 실행되어 API 요청이 발생합니다.
query 입력을 일정 시간 지연시킨 후 마지막에만 요청을 보내는 디바운싱을 적용하면 훨씬 효율적일 것 같습니다!

Comment on lines +33 to +39
<button
type="button"
onClick={onClose}
className="rounded bg-gray-600 px-5 py-2 text-sm text-white"
>
닫기
</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

에러 상태에서는 닫기 버튼이 있지만 정상적으로 데이터가 호출되면 따로 닫기 버튼이 없어 보입니다. Navbar가 보이거나 닫기 버튼이 정상적으로 데이터가 렌더링될 때도 보이면 UX적으로 더 좋을 것 같습니다!

@minsxo minsxo left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6주차 과제 수고 많으셨습니다!!🙌

Comment on lines +92 to +96
{/* 마지막 페이지 도달 시 안내 문구 */}
{!hasNextPage && results.length > 0 && (
<div className="text-gray-500 text-center mt-5">
모든 검색 결과를 불러왔습니다.
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

무한스크롤 끝났을 때 "모든 검색 결과를 불러왔습니다" 안내 띄우는 거 좋은 것 같아요!

Comment thread shared/lib/tmdbClient.ts
@@ -0,0 +1,34 @@
const BASE_URL = 'https://api.themoviedb.org/3';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getImageUrl(path, size) 같은 헬퍼로 분리해두면 이미지 경로나 사이즈 정책이 바뀔 때 한 곳만 수정할 수 있어서 유지보수하기 좋을 것 같습니다!

Comment thread components/SearchBar.tsx
Comment on lines +14 to +22
const timer = setTimeout(() => {
if (term.trim()) {
router.replace(`/search?q=${encodeURIComponent(term)}`);
} else {
router.replace(`/search`);
}
}, 500);
return () => clearTimeout(timer);
}, [term, router]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

검색어를 URL의 ?q= 파라미터로 관리해서 새로고침이나 링크 공유 후에도 검색 상태가 유지되는 점이 좋네요!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants