🐨CoalaCoding
DocsExamplesTry itBoardB반
🐨CoalaCoding

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

문서

  • JavaScript
  • Web Publishing
  • React
  • Python

커뮤니티

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

기타

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

04. 공통 UI 컴포넌트 만들기 (UI.jsx)

Button, Spinner, Modal, Container — 프로젝트 전체에서 재사용할 공통 컴포넌트를 만든다

코드 블록의 Try it Yourself 버튼으로 직접 실행할 수 있다.

구문

💡TIP

Gemini CLI로 구현하기 — 공통 UI 컴포넌트

  • 프롬프트: gemini "src/components/UI.jsx 파일을 작성해줘. Button(variant, className, children, ...props), Spinner(message, full, className), Modal(onClose, children), Container(className, children) 4개의 공통 컴포넌트가 필요해. Tailwind CSS를 사용하고, Button의 variant별 스타일은 src/index.css에 @layer components로 정의해줘. [추가 요구사항]"
    • 사용 가이드: [추가 요구사항]에 필요한 변형이 있으면 추가하고, 없으면 삭제한다. CSS 부분은 src/index.css에 붙여넣는다.

1. 왜 공통 UI 컴포넌트를 먼저 만들까?

이 프로젝트에서는 버튼, 로딩 표시, 모달(팝업), 컨테이너(감싸는 영역) 가 여러 페이지에서 반복된다. 이것들을 UI.jsx 한 파일에 모아두면, 필요한 곳에서 가져다 쓰기만 하면 된다.

컴포넌트사용처
Button헤더 검색, 뒤로가기, 예고편 보기, 페이지네이션
Spinner데이터 로딩 중 표시
Modal예고편 영상 팝업
Container섹션 공통 레이아웃

2. Button — 버튼 컴포넌트

src/components/ 폴더 안에 UI.jsx 파일을 새로 만든다. 이 파일에 4개의 컴포넌트를 순서대로 작성한다. 먼저 Button부터 시작한다. variant prop으로 버튼 종류를 지정하면 CSS 클래스 btn-{variant}가 적용된다.

export function Button({ variant = "primary", className = "", children, ...props }) {
  return (
    <button className={`btn-${variant} ${className}`} {...props}>
      {children}
    </button>
  );
}
줄설명
1{ variant, className, children, ...props } — 부모 컴포넌트가 전달하는 **Props(프롭스)**이다. <Button variant="danger">삭제</Button>처럼 쓰면 variant에 "danger", children에 "삭제"가 들어온다. variant의 기본값은 "primary"이다.
3템플릿 리터럴로 btn-${variant} 클래스명을 동적으로 생성한다. variant="danger"이면 btn-danger 클래스가 적용된다. {...props}로 나머지 속성을 button 태그에 전달한다.

버튼 스타일은 src/index.css에 CSS 클래스로 정의한다. 아래 내용을 index.css 파일에 추가한다.

@layer components {
  .btn-primary   { @apply bg-yellow-400 text-black hover:bg-yellow-300; }
  .btn-danger    { @apply bg-red-600 text-white hover:bg-red-500; }
  .btn-ghost     { @apply text-white hover:text-yellow-400; }
  .btn-secondary { @apply bg-gray-800 text-white hover:bg-gray-700 disabled:opacity-30; }
}
구문설명
@layer componentsTailwind CSS의 레이어 시스템이다. base(기본 태그 스타일) → components(컴포넌트 클래스) → utilities(유틸리티 클래스) 순으로 우선순위가 높아진다. 커스텀 컴포넌트 클래스는 components 레이어에 작성한다.
@applyCSS 파일 안에서 Tailwind 유틸리티 클래스를 그대로 사용하는 지시어이다. @apply bg-yellow-400은 background-color: #facc15와 동일하다.
.btn-primary<Button variant="primary">일 때 적용되는 클래스이다. btn-${variant} 패턴으로 자동 연결된다.

3. Spinner — 로딩 표시

같은 UI.jsx 파일에서, Button 함수 바로 아래에 이어서 Spinner를 작성한다. full prop이 true이면 화면 전체를 덮는 로딩 화면을 렌더링하고, false이면 텍스트만 표시한다.

export function Spinner({ message = "불러오는 중...", full = false, className = "" }) {
  if (full) {
    return (
      <div className="bg-black min-h-screen flex items-center justify-center">
        <p className="text-white text-2xl animate-pulse">{message}</p>
      </div>
    );
  }
  return <p className={`text-white text-xl ${className}`}>{message}</p>;
}
줄설명
1message(표시 텍스트), full(전체 화면 여부), className(추가 클래스)을 받는다. 모두 기본값이 있다.
2-7full이 true이면 검정 배경으로 화면 전체를 채우고 가운데에 메시지를 표시한다.
4min-h-screen — 최소 높이를 화면 전체 높이로 설정한다. flex — 자식 요소를 유연하게 배치하는 레이아웃 방식이다. items-center(세로 가운데)와 justify-center(가로 가운데)를 합치면 화면 정중앙에 배치된다.
5animate-pulse — 깜빡거리는 효과이다. 로딩 중이라는 느낌을 준다.
9full이 false이면 텍스트만 간단히 표시한다.

4. Modal — 팝업 컴포넌트

같은 UI.jsx 파일에서, Spinner 함수 바로 아래에 이어서 Modal을 작성한다. fixed inset-0으로 화면 전체를 덮는 오버레이를 생성하고, 그 위에 콘텐츠를 가운데 배치하는 팝업 컴포넌트이다.

export function Modal({ onClose, children }) {
  return (
    <div className="fixed inset-0 z-50 bg-black/90 flex items-center justify-center p-4">
      <div className="relative">
        <Button variant="ghost" onClick={onClose} className="absolute -top-10 right-0 text-2xl">
          <FontAwesomeIcon icon={faXmark} />
        </Button>
        {children}
      </div>
    </div>
  );
}
줄설명
1onClose(닫기 함수)와 children(팝업 안에 표시할 내용)을 받는다.
3fixed inset-0으로 화면 전체를 덮고, bg-black/90으로 어두운 배경을 만든다.
5-7우상단에 X 닫기 버튼을 배치한다. 클릭하면 onClose가 호출된다.
8children 자리에 예고편 영상 등이 들어온다.

5. Container — 섹션 레이아웃

같은 UI.jsx 파일에서, Modal 함수 바로 아래에 마지막 컴포넌트인 Container를 작성한다. container mx-auto로 최대 너비를 제한하고 가로 중앙에 배치하는 공통 레이아웃 컴포넌트이다.

export function Container({ className = "", children }) {
  return (
    <section className={`px-11 ${className}`}>
      <div className="container mx-auto">
        {children}
      </div>
    </section>
  );
}
줄설명
1className = ""은 기본값이다. 사용하는 곳에서 추가 클래스를 넘기지 않으면 빈 문자열이 된다.
3px-11 — 좌우에 약 44px의 여백(패딩)을 준다.
4container — 화면 크기에 따라 최대 너비가 자동으로 제한된다. mx-auto — 좌우 마진을 자동으로 같게 맞춰서 가운데 정렬한다. 이 두 클래스는 Tailwind에서 레이아웃을 잡을 때 가장 자주 쓰는 조합이다.

6. 전체 코드 확인

import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faXmark } from "@fortawesome/free-solid-svg-icons";

export function Button({ variant = "primary", className = "", children, ...props }) {
  return (
    <button className={`btn-${variant} ${className}`} {...props}>
      {children}
    </button>
  );
}

export function Spinner({ message = "불러오는 중...", full = false, className = "" }) {
  if (full) {
    return (
      <div className="bg-black min-h-screen flex items-center justify-center">
        <p className="text-white text-2xl animate-pulse">{message}</p>
      </div>
    );
  }
  return <p className={`text-white text-xl ${className}`}>{message}</p>;
}

목차

  • 구문