🐨CoalaCoding
DocsExamplesTry itBoardB반B반
🐨CoalaCoding

개발자를 위한 한국어 웹 기술 문서

문서

  • JavaScript
  • Web Publishing
  • React
  • Python

커뮤니티

  • 게시판
  • 예제 모음
  • Try it 에디터

기타

  • GitHub
  • 관리자
© 2026 CoalaCoding. All rights reserved.
  • GOFLIX 프로젝트 소개와 개발환경 설정
  • React 진입점과 라우팅 설정
  • Axios로 TMDB API 연결하기
  • 공통 UI 컴포넌트 만들기 (UI.jsx)
  • App.jsx — 레이아웃 구성과 데이터 가져오기
  • Header와 Footer 만들기
  • Home.jsx — 메인 페이지 완성하기
  • Section과 Card — 영화 카드 목록 만들기
  • MovieDetail — 영화 상세 페이지 만들기
  • Category, ErrorPage 완성하기
  • AI 챗봇 연동하기
  • Swiper_캐러셀_적용과_프로젝트_마무리
  • GOFLEX Gemini CLI 바이브코딩 프롬프트 템플릿
  • 무비앱-완료본
  1. 홈
  2. 문서
  3. React
  4. 프로젝트: 영화앱
  5. App.jsx — 레이아웃 구성과 데이터 가져오기

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

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();
  }, []);

Promise.all 설명

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로 동시에 보내면 모두 끝날 때까지 한 번만 기다리면 된다.

filter 설명

setNow(res1.data.results.filter((m) => m.poster_path));

.filter()로 포스터 이미지(poster_path)가 있는 영화만 골라낸다. 포스터가 없으면 화면에 빈 카드가 생기기 때문이다.

useEffect의 빈 배열 []은 무슨 뜻인가?
의존성 배열실행 시점
[] (빈 배열)컴포넌트가 처음 화면에 나타날 때 1번만
[id]id 값이 바뀔 때마다 매번
없음 (생략)컴포넌트가 렌더링될 때 매번 (비추천)

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

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

// Home.jsx에서 (7편에서 작성)
import { useOutletContext } from "react-router";

export function Home() {
  const { now, popular, topRated, loading } = useOutletContext();
}
Header.jsx / Footer.jsx 임시 파일

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

export function Header() {
  return <header>헤더 (준비중)</header>;
}
export function Footer() {
  return <footer>푸터 (준비중)</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 />
    </>
  );
}

목차

  • 구문