05. App.jsx — 레이아웃 구성과 데이터 가져오기
요약
Gemini CLI로 구현하기 — App.jsx 레이아웃 & 데이터 로딩
npx gemini-
1src/App.jsx를 작성해줘. TMDB API에서 movie/now_playing, tv/airing_today, movie/top_rated 데이터를 Promise.all로 동시에 가져와야 해. useState로 3개의 상태를 관리하고, useEffect로 앱 시작 시 한 번만 호출해줘. 가져온 데이터는 Outlet의 context로 하위 페이지에 전달하고, Header와 Footer도 포함해줘. axios 인스턴스는 [axios 인스턴스 경로]에서 가져와줘.
- 사용 가이드:
[axios 인스턴스 경로]를./api/axios로 바꾼다. API 엔드포인트가 다르면tv/airing_today부분을 수정한다.
1. App.jsx의 역할
App.jsx는 GOFLEX 앱의 뼈대 역할을 한다. 이 파일이 담당하는 일은 세 가지이다.
- TMDB API에서 영화 데이터 3종(현재 상영, 인기, 최고 평점)을 가져온다
- 상단에
Header, 하단에Footer를 배치한다 - 하위 페이지가 렌더링되는
Outlet에 데이터를 전달한다
| 순서 | 만들 기능 | 핵심 개념 |
|---|---|---|
| 1단계 | import + 상태 변수 | useState |
| 2단계 | API 데이터 로딩 | Promise.all + useEffect |
| 3단계 | 화면 렌더링 | Outlet context |
2. 1단계 — import + 상태 변수
src/App.jsx 파일을 열고 임시 코드를 모두 지운 뒤 아래를 작성한다.
1import { useState, useEffect } from "react";2import { Outlet } from "react-router";3import { Header } from "./components/Header.jsx";4import { Footer } from "./components/Footer.jsx";5import api from "./api/axios";6
7export default function App() {8 const [now, setNow] = useState(null);9 const [popular, setPopular] = useState(null);10 const [topRated, setTopRated] = useState(null);| 줄 | 설명 |
|---|---|
| 1 | useState(유즈스테이트)와 useEffect(유즈이펙트) — React의 Hook(훅)이다. 훅은 컴포넌트에 특별한 능력을 부여하는 도구이다. useState는 컴포넌트 내부에 상태 값을 저장하고, 값이 바뀌면 화면을 자동으로 다시 렌더링하는 훅이다. useEffect는 컴포넌트에서 함수의 실행 시점을 제어하는 훅이다. |
| 2 | Outlet(아울렛) — 리액트 라우터의 children path(패스)로 연결된 컴포넌트를 렌더링(렌더링)해준다. |
| 3-4 | Header와 Footer를 별도 파일에서 가져온다. 다음 편에서 만든다. |
| 5 | 3편에서 만든 TMDB API 인스턴스를 가져온다. |
| 8-10 | 영화 데이터를 담을 상태 변수 3개(now, popular, topRated)를 선언한다. 초기값은 모두 null(아직 아무것도 없음)이다. |
| 초기값 | 의미 |
|---|---|
null | 아직 API를 호출하지 않은 상태이다. 로딩 중이라고 판단할 수 있다. |
[] | API를 호출했지만 결과가 0건인 상태이다. |
null이면 로딩 스피너를 표시하고, []이면 “결과 없음”을 표시하는 식으로 구분할 수 있다.
3. 2단계 — API 데이터 로딩
같은 App.jsx 파일에서, const [topRated, ... 줄 바로 아래에 이어서 작성한다. 이 코드는 앱이 처음 실행될 때 TMDB 서버에 “현재 상영작, 인기작, 최고 평점 영화를 한꺼번에 보내줘”라고 요청하는 부분이다.
1 async function loadMovie() {2 const [res1, res2, res3] = await Promise.all([3 api.get("movie/now_playing"),4 api.get("tv/airing_today"),5 //api.get("movie/popular"),6 api.get("movie/top_rated"),7 ]);8 setNow(res1.data.results.filter((m) => m.poster_path));9 setPopular(res2.data.results.filter((m) => m.poster_path));10 setTopRated(res3.data.results.filter((m) => m.poster_path));11 }12
13 useEffect(() => {14 loadMovie();15 }, []);- Promise.all 설명
- filter 설명
1const [res1, res2, res3] = await Promise.all([2 api.get("movie/now_playing"),3 api.get("movie/popular"),4 api.get("movie/top_rated"),5]);Promise.all(프로미스 올)은 여러 API 요청을 동시에 보내고, 모두 끝나면 결과를 한꺼번에 받는다.
| 방식 | 요청 시간 |
|---|---|
순차 실행 (await 3번) | 1초 + 1초 + 1초 = 3초 |
Promise.all | 가장 느린 1개 기준 = 약 1초 |
요청 3개를 순서대로 보내면 각각의 응답을 기다려야 하지만, Promise.all로 동시에 보내면 모두 끝날 때까지 한 번만 기다리면 된다.
1setNow(res1.data.results.filter((m) => m.poster_path));.filter()로 포스터 이미지(poster_path)가 있는 영화만 골라낸다. 포스터가 없으면 화면에 빈 카드가 생기기 때문이다.
| 의존성 배열 | 실행 시점 |
|---|---|
[] (빈 배열) | 컴포넌트가 처음 화면에 나타날 때 1번만 |
[id] | id 값이 바뀔 때마다 매번 |
| 없음 (생략) | 컴포넌트가 렌더링될 때 매번 (비추천) |
4. 3단계 — 화면 렌더링
같은 App.jsx 파일에서, useEffect 블록 바로 아래에 이어서 작성한다. 이 부분은 “데이터가 아직 도착하지 않았는지 확인하고, 도착한 데이터를 하위 페이지에 택배처럼 보내주는” 역할이다. <>(Fragment, 프래그먼트)는 여러 요소를 감싸되 실제 HTML 태그를 추가하지 않는 투명 포장지이다.
1 const loading = now === null || popular === null || topRated === null;2 const ctx = {3 now: now || [],4 popular: popular || [],5 topRated: topRated || [],6 loading,7 };8
9 return (10 <>11 <Header />12 <Outlet context={ctx} />13 <Footer />14 </>15 );16}| 줄 | 설명 |
|---|---|
| 1 | 세 상태 중 하나라도 null이면 아직 로딩 중이다. |
| 2-7 | ctx 객체에 영화 데이터 3종과 loading 상태를 담는다. ||(OR 연산자)는 왼쪽 값이 null, undefined, 0 등 거짓으로 판단되는 값일 때 오른쪽 값을 반환한다. now가 아직 null이면 []를 반환하므로 하위 컴포넌트가 빈 배열을 안전하게 받을 수 있다. |
| 11 | Header 컴포넌트를 상단에 배치한다. |
| 12 | Outlet의 context에 ctx 객체를 전달한다. 하위 페이지(Home, MovieDetail 등)에서 이 데이터를 받아서 사용한다. |
| 13 | Footer 컴포넌트를 하단에 배치한다. |
하위 컴포넌트에서 useOutletContext() 훅으로 꺼낸다.
1// Home.jsx에서 (7편에서 작성)2import { useOutletContext } from "react-router";3
4export function Home() {5 const { now, popular, topRated, loading } = useOutletContext();6}아직 만들지 않았으므로 에러를 방지하기 위해 빈 파일을 만들어 둔다.
1export function Header() {2 return <header>헤더 (준비중)</header>;3}1export function Footer() {2 return <footer>푸터 (준비중)</footer>;3}5. 전체 코드
1import { useState, useEffect } from "react";2import { Outlet } from "react-router";3import { Header } from "./components/Header.jsx";4import { Footer } from "./components/Footer.jsx";5import api from "./api/axios";6
7export default function App() {8 const [now, setNow] = useState(null);9 const [popular, setPopular] = useState(null);10 const [topRated, setTopRated] = useState(null);11
12 async function loadMovie() {13 const [res1, res2, res3] = await Promise.all([14 api.get("movie/now_playing"),15 api.get("tv/popular"),16 api.get("movie/top_rated"),17 ]);18 setNow(res1.data.results.filter((m) => m.poster_path));19 setPopular(res2.data.results.filter((m) => m.poster_path));20 setTopRated(res3.data.results.filter((m) => m.poster_path));21 }22
23 useEffect(() => {24 loadMovie();25 }, []);26
27 const loading = now === null || popular === null || topRated === null;28 const ctx = {29 now: now || [],30 popular: popular || [],31 topRated: topRated || [],32 loading,33 };34
35 return (36 <>37 <Header />38 <Outlet context={ctx} />39 <Footer />40 </>41 );42}