Type something to search...

05. App.jsx — 레이아웃 구성과 데이터 가져오기

요약

Gemini CLI로 구현하기 — App.jsx 레이아웃 & 데이터 로딩

  1. npx gemini
  2. 1
    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 인스턴스 경로]에서 가져와줘.
  3. 사용 가이드: [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 파일을 열고 임시 코드를 모두 지운 뒤 아래를 작성한다.

src/App.jsx
1
import { useState, useEffect } from "react";
2
import { Outlet } from "react-router";
3
import { Header } from "./components/Header.jsx";
4
import { Footer } from "./components/Footer.jsx";
5
import api from "./api/axios";
6
7
export default function App() {
8
const [now, setNow] = useState(null);
9
const [popular, setPopular] = useState(null);
10
const [topRated, setTopRated] = useState(null);
설명
1useState(유즈스테이트)와 useEffect(유즈이펙트) — React의 Hook(훅)이다. 훅은 컴포넌트에 특별한 능력을 부여하는 도구이다. useState는 컴포넌트 내부에 상태 값을 저장하고, 값이 바뀌면 화면을 자동으로 다시 렌더링하는 훅이다. useEffect는 컴포넌트에서 함수의 실행 시점을 제어하는 훅이다.
2Outlet(아울렛) — 리액트 라우터의 children path(패스)로 연결된 컴포넌트를 렌더링(렌더링)해준다.
3-4HeaderFooter를 별도 파일에서 가져온다. 다음 편에서 만든다.
53편에서 만든 TMDB API 인스턴스를 가져온다.
8-10영화 데이터를 담을 상태 변수 3개(now, popular, topRated)를 선언한다. 초기값은 모두 null(아직 아무것도 없음)이다.
초기값의미
null아직 API를 호출하지 않은 상태이다. 로딩 중이라고 판단할 수 있다.
[]API를 호출했지만 결과가 0건인 상태이다.

null이면 로딩 스피너를 표시하고, []이면 “결과 없음”을 표시하는 식으로 구분할 수 있다.


3. 2단계 — API 데이터 로딩

같은 App.jsx 파일에서, const [topRated, ... 줄 바로 아래에 이어서 작성한다. 이 코드는 앱이 처음 실행될 때 TMDB 서버에 “현재 상영작, 인기작, 최고 평점 영화를 한꺼번에 보내줘”라고 요청하는 부분이다.

src/App.jsx
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
}, []);
의존성 배열실행 시점
[] (빈 배열)컴포넌트가 처음 화면에 나타날 때 1번만
[id]id 값이 바뀔 때마다 매번
없음 (생략)컴포넌트가 렌더링될 때 매번 (비추천)

4. 3단계 — 화면 렌더링

같은 App.jsx 파일에서, useEffect 블록 바로 아래에 이어서 작성한다. 이 부분은 “데이터가 아직 도착하지 않았는지 확인하고, 도착한 데이터를 하위 페이지에 택배처럼 보내주는” 역할이다. <>(Fragment, 프래그먼트)는 여러 요소를 감싸되 실제 HTML 태그를 추가하지 않는 투명 포장지이다.

src/App.jsx
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-7ctx 객체에 영화 데이터 3종과 loading 상태를 담는다. &#124;&#124;(OR 연산자)는 왼쪽 값이 null, undefined, 0 등 거짓으로 판단되는 값일 때 오른쪽 값을 반환한다. now가 아직 null이면 []를 반환하므로 하위 컴포넌트가 빈 배열을 안전하게 받을 수 있다.
11Header 컴포넌트를 상단에 배치한다.
12Outletcontextctx 객체를 전달한다. 하위 페이지(Home, MovieDetail 등)에서 이 데이터를 받아서 사용한다.
13Footer 컴포넌트를 하단에 배치한다.

하위 컴포넌트에서 useOutletContext() 훅으로 꺼낸다.

1
// Home.jsx에서 (7편에서 작성)
2
import { useOutletContext } from "react-router";
3
4
export function Home() {
5
const { now, popular, topRated, loading } = useOutletContext();
6
}

아직 만들지 않았으므로 에러를 방지하기 위해 빈 파일을 만들어 둔다.

src/components/Header.jsx
1
export function Header() {
2
return <header>헤더 (준비중)</header>;
3
}
src/components/Footer.jsx
1
export function Footer() {
2
return <footer>푸터 (준비중)</footer>;
3
}

5. 전체 코드

src/App.jsx
1
import { useState, useEffect } from "react";
2
import { Outlet } from "react-router";
3
import { Header } from "./components/Header.jsx";
4
import { Footer } from "./components/Footer.jsx";
5
import api from "./api/axios";
6
7
export 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
}