Skip to content

[5주차] ORLO 김홍엽 & 오유진 과제 제출합니다.#8

Open
Yeobi00 wants to merge 47 commits intoCEOS-Developers:masterfrom
CEOS-CONX:master
Open

[5주차] ORLO 김홍엽 & 오유진 과제 제출합니다.#8
Yeobi00 wants to merge 47 commits intoCEOS-Developers:masterfrom
CEOS-CONX:master

Conversation

@Yeobi00
Copy link
Copy Markdown

@Yeobi00 Yeobi00 commented May 2, 2026

배포 링크

🔗 https://ceos-week5-next-netflix-23rd.vercel.app/

협업 과정

1. Git Branch 전략

Branch 역할
main 개인 레포로의 푸시 및 배포 전용 브랜치
dev 개발 통합 브랜치. 모든 feature 브랜치가 머지되는 기준 브랜치
feature/* 이슈 단위로 분기되는 기능 개발 브랜치
  • main은 배포 가능한 상태만 유지하며, 실제 개발 작업은 dev 브랜치를 기준으로 이루어졌습니다.
  • 새로운 기능은 dev에서 분기한 feature/* 브랜치에서 작업한 뒤, PR을 통해 다시 dev로 머지하는 방식으로 진행했습니다.

 

2. 작업 흐름

  1. 이슈 생성 — Issue Template을 활용해 작업 단위를 명확히 정의
  2. 브랜치 분기 — 해당 이슈 번호를 기반으로 feature/* 브랜치 생성
  3. 개발 및 커밋 — 기능 단위로 작업 후 커밋
  4. PR 생성dev 브랜치를 대상으로 Pull Request 작성
  5. 코드 리뷰 후 머지 — 리뷰어 승인 후 dev에 반영

 

3. 패키지 매니저 선정과 그 이유

pnpm

  • 엄격한 의존성 관리
    • npm은 호이스팅으로 인해 package.json에 명시하지 않은 패키지도 암묵적으로 사용할 수 있는 유령 의존성 문제가 있음.
    • pnpm은 격리된 node_modules 구조를 사용하여 이를 원천 차단하기 때문에, 의존성 관계가 항상 명확함.
  • 빠른 설치 속도
    • pnpm은 Content-Addressable Stroage 방식으로 패키지를 글로벌 스토어에 저장하고 링크로 연결하기 때문에, 같은 패키지를 다시 설치할 때 다운로드 없이 링크만 생성하여 npm 대비 설치 속도가 빠름.

 

4. 디렉토리 구조

📦 next-netflix-23rd
├── 📂 .github
│   ├── 📂 ISSUE_TEMPLATE              # 이슈 템플릿 (feature, bug, chore 등)
│   ├── 📄 pull_request_template.md    # PR 템플릿
│   └── 📂 workflows
│       └── 📄 deploy.yml              # CI/CD 배포 워크플로우
│
├── 📂 .husky                          # Git hooks (commit-msg, pre-commit, pre-push)
├── 📂 public                          # 정적 파일
│
├── 📂 src
│   ├── 📂 api
│   │   ├── 📄 axios.ts                # Axios 인스턴스 (TMDB baseURL, API key 설정)
│   │   ├── 📄 requests.ts             # TMDB API 엔드포인트 정의
│   │   └── 📄 movieService.ts         # 영화 데이터 fetch 함수
│   │
│   ├── 📂 app
│   │   ├── 📄 layout.tsx              # 루트 레이아웃
│   │   ├── 📄 page.tsx                # 메인 페이지
│   │   ├── 📄 globals.css             # 글로벌 스타일
│   │   └── 📄 favicon.ico
│   │
│   ├── 📂 assets
│   │   ├── 📂 fonts                   # Pretendard 폰트
│   │   ├── 📂 icons                   # SVG 아이콘 (play, add, home 등)
│   │   └── 📂 lottie                  # Netflix 인트로 애니메이션
│   │
│   ├── 📂 components
│   │   ├── 📂 common                  # 공통 컴포넌트 (TopNav, BottomNav, Layout, HomeIndicator)
│   │   ├── 📂 landing                 # 스플래시 화면 (SplashScreen)
│   │   └── 📂 main                    # 메인 페이지 컴포넌트 (Featured, MovieRow, Previews)
│   │
│   └── 📂 types
│       └── 📄 movie.ts                # 영화 데이터 타입 정의
│
├── 📄 .prettierrc.json                # Prettier 설정
├── 📄 .coderabbit.yml                 # CodeRabbit 코드 리뷰 설정
└── 📄 commitlint.config.js            # 커밋 메시지 린트 규칙

 

5. 업무 분담

김홍엽

  • 프로젝트 초기 세팅
  • Github 템플릿 및 배포 워크플로우 구성
  • CodeRabbit 설정
  • 랜딩 페이지 구현
  • 메인 페이지 섹션별 영화 데이터 연동 및 렌더링

오유진

  • 타이포그래피 및 색상 설정
  • Husky/lint-staged 설정
  • TMDB API 연동
  • 공통 레이아웃 및 상·하단 navBar 구현
  • Featured, Previews 컴포넌트 구현

 

6. 소통 방식

프로젝트 초기에 비대면 회의를 통해 전체적인 방향성을 정립했습니다.업무 분담뿐만 아니라 패키지 매니저, 커밋 메시지 컨벤션, PR 컨벤션, Issue/Branch 관리 규칙 등 프로젝트 세팅 전반에 대해 충분히 논의한 뒤 작업에 착수했습니다.

작업 기간 동안에는 카카오톡을 활용해 진행 상황을 주기적으로 공유했습니다. 각자의 작업 현황을 수시로 공유하며 전체 일정에 차질이 없도록 조율했습니다.

데모데이 프로젝트에 앞서, 해보고 싶은 것들과 소통 방식을 시도해보며 추후에 취사선택할 수 있도록 많이 해보려고 했습니다.

또한 이번 과제가 데모데이 프로젝트의 사전 단계인 만큼, 다양한 협업 방식과 도구를 적극적으로 시도해보는 데 의의를 두었습니다. 이슈 템플릿, PR 템플릿, 커밋 컨벤션, CodeRabbit 자동 리뷰, Husky 기반 Git Hooks 등 여러 협업 장치를 도입했습니다.

 

Review Questions

React 18 버전의 변경점

1. Concurrent Rendering

Before

  • 렌더링이 시작되면 중단할 수 없는 동기적 작업
  • 컴포넌트 트리의 렌더링이 시작되면 끝까지 메인 스레드를 점유하며, 도중에 사용자 입력이 들어와도 현재 렌더링이 끝날 때까지 기다려야 했음.
  • 모든 업데이트가 동일한 우선순위로 처리되어, 긴급한 입력과 무거운 백그라운드 렌더링을 구분할 수 없었음.
import ReactDOM from 'react-dom';

const container = document.getElementById('root');
ReactDOM.render(<App />, container);

After (React 18)

  • 동시성 렌더러가 도입되어, 렌더링을 시작 -> 일시중지 -> 재개 -> 폐기 할 수 있게 됨.
  • 백그라운드에서 새로운 화면을 준비하면서도 메인 스레드를 블로킹하지 않게 됨.
import { createRoot } from 'react-dom/client';

const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />);

 

2. Automatic Batching

Before

  • React 이벤트 핸들러 안에서만 batch 처리됨.
  • 비동기 작업 안에서 여러 state를 업데이트할 때마다 불필요한 리렌더링 발생
// React 17 - 이벤트 핸들러 내부: batch O (1번 렌더링)
function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
}

// React 17 - setTimeout 내부: batch X (2번 렌더링)
setTimeout(() => {
  setCount(c => c + 1); // 리렌더링 1
  setFlag(f => !f);     // 리렌더링 2
}, 1000);

// React 17 - Promise 내부: batch X (2번 렌더링)
fetch('/api').then(() => {
  setCount(c => c + 1); // 리렌더링 1
  setFlag(f => !f);     // 리렌더링 2
});

After (React 18)

  • 모든 출처의 업데이트가 자동으로 batch 처리됨.
  • 주의: createRoot를 사용해야 적용되며, 기존 ReactDOM.render를 그대로 쓰면 이전과 동일하게 동작
// React 18 - 어디서든 batch O (모두 1번 렌더링)
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
}, 1000);

fetch('/api').then(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
});

 

3. Lazy Loading과 Suspense 컴포넌트

  • React.lazy와 Suspense의 조합 자체는 이전부터 존재했지만, React 18 이후에 SSR까지 동작하도록 확장됨.

Before

  • 클라이언트 사이드에서는, React.lazy로 감싼 컴포넌트가 실제 렌더링이 시도되는 시점에 동적으로 코드가 로드되고, 그동안 Suspense의 fallback이 표시되며 잘 동작했음.
  • 하지만, SSR API(renderToString, renderToNodeStream)가 모두 동기적으로 동작해서, Suspense의 핵심 동작인 Promise를 기다리는 것이 불가능했음.
  • 그래서 클라이언트 전용 앱에서는 React.lazy를 쓰고, SSR이 필요하면 @loadable/component 같은 서드파티 라이브러리를 쓰는 식으로 이원화되어 있었음.
// React 17 - 클라이언트에서는 동작
import { lazy, Suspense } from 'react';

const MarkdownPreview = lazy(() => import('./MarkdownPreview'));

function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(true)}>Show</button>
      {show && (
        <Suspense fallback={<Loading />}>
          <MarkdownPreview />
        </Suspense>
      )}
    </>
  );
}

// React 17 — 같은 코드를 SSR에서 쓰면?
import { renderToString } from 'react-dom/server';
const html = renderToString(<App />); // ❌ Suspense 관련 에러

// 그래서 SSR이 필요하면 loadable-components로 우회
import loadable from '@loadable/component';
const MarkdownPreview = loadable(() => import('./MarkdownPreview'));
// → SSR 호환되지만 React 외부 라이브러리에 의존하게 됨

After (React 18)

  • 새로운 API인 renderToPipeableStream을 도입하면서 SSR에서 Suspense를 제대로 지원하게 되어, React.lazy가 별도의 라이브러리 없이 SSR과 자연스럽게 통합됨.
  • Streaming HTML + Selective Hydration과 결합되어 SSR에서의 동작이 더 강력해짐.
    • Streaming HTML: 페이지 전체가 준비될 때까지 기다리지 않고, 준비된 부분부터 HTML 청크로 전송
      • 느린 컴포넌트는 로 감싸면 fallback을 먼저 보내고, 데이터 준비 완료 후 실제 콘텐츠를 추가 청크로 스트리밍
    • Selective Hydration: 전체 JS 번들 로드를 기다리지 않고, 준비된 부분부터 hydration 시작
      • 사용자가 상호작용한 영역을 우선적으로 hydration
// 클라이언트 코드 - React 17과 동일
import { lazy, Suspense } from 'react';

const Comments = lazy(() => import('./Comments'));
const Sidebar = lazy(() => import('./Sidebar'));

function App() {
  return (
    <Layout>
      <Header />
      <Suspense fallback={<SidebarSkeleton />}>
        <Sidebar />
      </Suspense>
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments />
      </Suspense>
    </Layout>
  );
}

// 서버 코드 - renderToString → renderToPipeableStream으로 교체
import { renderToPipeableStream } from 'react-dom/server';

app.get('/', (req, res) => {
  const { pipe } = renderToPipeableStream(<App />, {
    bootstrapScripts: ['/main.js'],
    onShellReady() {
      // Suspense 바깥의 "shell" 부분이 준비되면 즉시 스트리밍 시작
      res.statusCode = 200;
      res.setHeader('Content-Type', 'text/html');
      pipe(res);
    },
  });
});

 

4. 서버 컴포넌트와 클라이언트 컴포넌트

  • React 18에서 기반이 마련되었고, Next.js 13 App Router에서 프로덕션 사용이 가능해졌으며, React 19에서 안정화됨.

등장 배경

  • 기존 모델의 한계: 모든 컴포넌트 코드가 클라이언트 번들에 포함되어 사이즈 부담
  • 데이터 fetching은 useEffect 기반이라 waterfall 문제 발생
  • DB/파일 시스템 등 백엔드 자원에 직접 접근할 수 없어 API 레이어 필수

서버 컴포넌트

  • 클라이언트 앱이나 SSR 서버와 분리된 환경에서, 번들링 전에 미리 렌더링되는 새로운 컴포넌트 타입
  • 서버에서만 실행되고, 렌더링 결과만 클라이언트로 전송
  • 컴포넌트의 JS 코드 자체는 클라이언트 번들에 포함되지 않음.
// app/posts/page.jsx - Server Component
import { db } from '@/lib/db';
import LikeButton from './LikeButton';

export default async function PostList() {
  const posts = await db.query('SELECT * FROM posts'); // DB 직접 접근
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>
          <h2>{post.title}</h2>
          <LikeButton postId={post.id} /> {/* Client Component */}
        </li>
      ))}
    </ul>
  );
}

클라이언트 컴포넌트

  • 브라우저에서 실행되고, 상호작용이 가능한 컴포넌트
  • 파일 최상단에 "use client" 선언
// LikeButton.jsx - Client Component
'use client';
import { useState } from 'react';

export default function LikeButton({ postId }) {
  const [liked, setLiked] = useState(false);
  return (
    <button onClick={() => setLiked(!liked)}>
      {liked ? '❤️' : '🤍'}
    </button>
  );
}
항목 서버 컴포넌트 클라이언트 컴포넌트
실행 위치 서버 (요청 시마다, 또는 빌드 타임) 브라우저
useState, useReducer
useEffect, useLayoutEffect
이벤트 핸들러 (onClick 등)
브라우저 API (window, localStorage)
async/await (컴포넌트 자체) ❌ (제한적)
DB, 파일시스템, 환경변수 직접 접근
클라이언트 번들 크기 0 (포함 안 됨) 포함됨
서버 전용 라이브러리 사용
Context 사용 ❌ (직접 생성 불가)

Yeobi00 and others added 30 commits April 28, 2026 19:35
chore: 프로젝트 초기 세팅
chore: 개인 레포 미러링 배포 워크플로우 추가
feature: tmdb api 연동 및 환경변수 설정
Comment thread src/api/axios.ts
@@ -0,0 +1,10 @@
import axios from 'axios';

const instance = axios.create({
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

https://pestudent.tistory.com/96
Next.js에서는 axios 대신 fetch 메서드를 통해 api 연동을 구현하는 것이 권장 사항입니다!

const isActive = pathname === item.href;

return (
<Link
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Next.js의 이점인 Link를 usePathname과 묶어 사용하신 점 좋습니다. SEO측면에서 용이하다고 합니다.

Copy link
Copy Markdown

@GirimNam GirimNam left a comment

Choose a reason for hiding this comment

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

이번주차 코드리뷰를 함께 하게 되어 영광입니다!

<section className="overflow-hidden py-3">
<h2 className="text-heading2 mb-3 px-4 text-white">{title}</h2>

<div className="no-scrollbar flex gap-[7px] overflow-x-auto px-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.

스크롤바 숨김 처리를 의도하신 것 같은데, 실제로는 가로 스크롤바가 보입니다. tailwind-scrollbar-hide 플러그인이 package.json에 없어서 그런 것 같아요! 혹시 의도하신 게 아니라면 플러그인 추가하시거나 globals.css에 직접 CSS로 처리하는 방법으로 수정하시는게 좋을 것 같아요!

Image

Comment thread src/app/page.tsx
<div className="flex flex-col">
<Featured />
<Previews />
<MovieRow title="Continue Watching for You" fetchKey="getContinueWatching" variant="tall" />
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

const로 title, fetchKey, variant?를 관리해서 map으로 MovieRow를 뿌렸다면 어땠을까 싶어요!

/>

<div className="relative h-[55vh] w-full">
<Image
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

이런 컴포넌트는 w-full이기 때문에 뷰포트 크기가 작아져도(세로가 줄어도) 큰 영향이 없지만, 여러 컴포넌트를 동시에 vh로 관리하면서 Image 속성을 fill로 설정하면 위험할 수도 있습니다!

animationData={netflixIntro}
loop={false}
onComplete={() => {
sessionStorage.setItem('splashShown', 'true');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

sessionstorage 사용해서 새로고침해도 로고가 안뜨게 하는 부분이 너무 좋은거 같아요!

Comment thread src/app/page.tsx
<MovieRow title="TV Thrillers & Mysteries" fetchKey="getThrillerMovies" />
<MovieRow title="US TV Shows" fetchKey="getComedyMovies" />
</div>
</SplashScreen>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

스플래쉬를 별도의 페이지가 아니라 status에 따른 조건부 랜더링으로 표현하신게 너무 좋은거 같아요!

Comment thread src/app/layout.tsx
return (
<html lang="ko" className="font-sans">
<body className="bg-black antialiased">
<Layout>{children}</Layout>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

레이아웃을 별도의 컴포넌트로 분리하신거 좋은거 같아용!

Copy link
Copy Markdown

@minseo0614 minseo0614 left a comment

Choose a reason for hiding this comment

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

5주차 수고 많으셨습니다 🙌

Comment on lines +29 to +38
<div className="relative h-[415px] w-full">
<Image
src={`https://image.tmdb.org/t/p/original${movie.poster_path}`}
alt={movie.title || movie.name || ''}
fill
sizes="100vw"
className="object-cover"
priority
/>
<div className="absolute inset-0 bg-linear-to-t from-black via-transparent to-transparent" />
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

배너 이미지 위에 어두운 그라데이션 살짝 깔아서 어떤 포스터가 와도 하단 텍스트가 잘 보이게 처리하신 부분 너무 좋은 것 같아요!

import { movieService } from '@/api/movieService';
import { Movie } from '@/types/movie';

type MovieServiceKey = keyof typeof movieService;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MovieRow가 fetchKey 문자열만 받아서 어떤 카테고리든 동작하게 만드신 거, 새 row 추가할 때 한 줄로 끝나서 좋은 것 같아요!

Comment on lines +11 to +17
function getInitialStatus(): 'splash' | 'done' {
if (typeof window === 'undefined') return 'splash';
return sessionStorage.getItem('splashShown') ? 'done' : 'splash';
}

export default function SplashScreen({ children }: SplashScreenProps) {
const [status, setStatus] = useState<'splash' | 'done'>(getInitialStatus);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

splash 띄울지 말지를 useEffect 안이 아니라 useState 초기값에서 바로 결정하면 첫 진입 때 깜빡임 없이 분기되어서 좋은 것 같아요!

Comment thread src/types/movie.ts
Comment on lines +10 to +15
export interface MovieResponse {
page: number;
results: Movie[];
total_pages: number;
total_results: number;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

total_pages 필드를 MovieResponse 인터페이스에 명시해둔 점이 좋았습니다. 무한 스크롤 구현 시 page < total_pages 조건 등으로 다음 페이지 요청 가능 여부를 판단할 수 있어, 불필요한 API 호출을 방지하는 데 도움이 될 것 같습니다.

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.

6 participants