05. App.jsx — 레이아웃 구성과 데이터 가져오기
헤더, 푸터, 데이터 로딩, Outlet을 활용한 전체 레이아웃을 만든다
코드 블록의 Try it Yourself 버튼으로 직접 실행할 수 있다.
구문
💡TIP
Gemini CLI로 구현하기 — App.jsx 레이아웃 & 데이터 로딩
- 프롬프트:
gemini "src/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 파일을 열고 임시 코드를 모두 지운 뒤 아래를 작성한다.
import { useState, useEffect } from "react";
import { Outlet } from "react-router";
import { Header } from "./components/Header.jsx";
import { Footer } from "./components/Footer.jsx";
import api from "./api/axios";
export default function App() {
const [now, setNow] = useState(null);
const [popular, setPopular] = useState(null);
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(아직 아무것도 없음)이다. |
3. 2단계 — API 데이터 로딩
같은 App.jsx 파일에서, const [topRated, ... 줄 바로 아래에 이어서 작성한다. 이 코드는 앱이 처음 실행될 때 TMDB 서버에 "현재 상영작, 인기작, 최고 평점 영화를 한꺼번에 보내줘"라고 요청하는 부분이다.
async function loadMovie() {
const [res1, res2, res3] = await Promise.all([
api.get("movie/now_playing"),
api.get("tv/airing_today"),
//api.get("movie/popular"),
api.get("movie/top_rated"),
]);
setNow(res1.data.results.filter((m) => m.poster_path));
setPopular(res2.data.results.filter((m) => m.poster_path));
setTopRated(res3.data.results.filter((m) => m.poster_path));
}
useEffect(() => {
loadMovie();
}, []);
const [res1, res2, res3] = await Promise.all([
api.get("movie/now_playing"),
api.get("movie/popular"),
api.get("movie/top_rated"),
]);
Promise.all(프로미스 올)은 여러 API 요청을 동시에 보내고, 모두 끝나면 결과를 한꺼번에 받는다.
| 방식 | 요청 시간 |
|---|---|
순차 실행 (await 3번) | 1초 + 1초 + 1초 = 3초 |
Promise.all | 가장 느린 1개 기준 = 약 1초 |
요청 3개를 순서대로 보내면 각각의 응답을 기다려야 하지만, Promise.all로 동시에 보내면 모두 끝날 때까지 한 번만 기다리면 된다.
4. 3단계 — 화면 렌더링
같은 App.jsx 파일에서, useEffect 블록 바로 아래에 이어서 작성한다. 이 부분은 "데이터가 아직 도착하지 않았는지 확인하고, 도착한 데이터를 하위 페이지에 택배처럼 보내주는" 역할이다. <>(Fragment, 프래그먼트)는 여러 요소를 감싸되 실제 HTML 태그를 추가하지 않는 투명 포장지이다.
const loading = now === null || popular === null || topRated === null;
const ctx = {
now: now || [],
popular: popular || [],
topRated: topRated || [],
loading,
};
return (
<>
<Header />
<Outlet context={ctx} />
<Footer />
</>
);
}
| 줄 | 설명 |
|---|---|
| 1 | 세 상태 중 하나라도 null이면 아직 로딩 중이다. |
| 2-7 | ctx 객체에 영화 데이터 3종과 loading 상태를 담는다. ||(OR 연산자)는 왼쪽 값이 null, undefined, 0 등 거짓으로 판단되는 값일 때 오른쪽 값을 반환한다. now가 아직 null이면 []를 반환하므로 하위 컴포넌트가 빈 배열을 안전하게 받을 수 있다. |
| 11 | Header 컴포넌트를 상단에 배치한다. |
| 12 | Outlet의 context에 ctx 객체를 전달한다. 하위 페이지(Home, MovieDetail 등)에서 이 데이터를 받아서 사용한다. |
| 13 | Footer 컴포넌트를 하단에 배치한다. |
5. 전체 코드
import { useState, useEffect } from "react";
import { Outlet } from "react-router";
import { Header } from "./components/Header.jsx";
import { Footer } from "./components/Footer.jsx";
import api from "./api/axios";
export default function App() {
const [now, setNow] = useState(null);
const [popular, setPopular] = useState(null);
const [topRated, setTopRated] = useState(null);
async function loadMovie() {
const [res1, res2, res3] = await Promise.all([
api.get("movie/now_playing"),
api.get("tv/popular"),
api.get("movie/top_rated"),
]);
setNow(res1.data.results.filter((m) => m.poster_path));
setPopular(res2.data.results.filter((m) => m.poster_path));
setTopRated(res3.data.results.filter((m) => m.poster_path));
}
useEffect(() => {
loadMovie();
}, []);
const loading = now === null || popular === null || topRated === null;
const ctx = {
now: now || [],
popular: popular || [],
topRated: topRated || [],
loading,
};
return (
<>
<Header />
<Outlet context={ctx} />
<Footer />
</>
);
}