04. 공통 UI 컴포넌트 만들기 (UI.jsx)
요약
Gemini CLI로 구현하기 — 공통 UI 컴포넌트
npx gemini-
1src/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}가 적용된다.
1export function Button({ variant = "primary", className = "", children, ...props }) {2 return (3 <button className={`btn-${variant} ${className}`} {...props}>4 {children}5 </button>6 );7}| 줄 | 설명 |
|---|---|
| 1 | { variant, className, children, ...props } — 부모 컴포넌트가 전달하는 **Props(프롭스)**이다. <Button variant="danger">삭제</Button>처럼 쓰면 variant에 "danger", children에 "삭제"가 들어온다. variant의 기본값은 "primary"이다. |
| 3 | 템플릿 리터럴로 btn-${variant} 클래스명을 동적으로 생성한다. variant="danger"이면 btn-danger 클래스가 적용된다. {...props}로 나머지 속성을 button 태그에 전달한다. |
1// 이렇게 전달하면2<Button variant="danger" onClick={handleClick} disabled={true}>삭제</Button>3
4// Button 내부에서 이렇게 풀린다5// variant = "danger"6// className = ""7// children = "삭제"8// props = { onClick: handleClick, disabled: true }...props는 명시적으로 꺼내지 않은 나머지 속성 전부를 하나의 객체로 모은다. 이것을 <button {...props}>로 전달하면 onClick, disabled 등이 그대로 button 태그에 적용된다.
버튼 스타일은 src/index.css에 CSS 클래스로 정의한다. 아래 내용을 index.css 파일에 추가한다.
1@layer components {2 .btn-primary { @apply bg-yellow-400 text-black hover:bg-yellow-300; }3 .btn-danger { @apply bg-red-600 text-white hover:bg-red-500; }4 .btn-ghost { @apply text-white hover:text-yellow-400; }5 .btn-secondary { @apply bg-gray-800 text-white hover:bg-gray-700 disabled:opacity-30; }6}| 구문 | 설명 |
|---|---|
@layer components | Tailwind CSS의 레이어 시스템이다. base(기본 태그 스타일) → components(컴포넌트 클래스) → utilities(유틸리티 클래스) 순으로 우선순위가 높아진다. 커스텀 컴포넌트 클래스는 components 레이어에 작성한다. |
@apply | CSS 파일 안에서 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이면 텍스트만 표시한다.
1export function Spinner({ message = "불러오는 중...", full = false, className = "" }) {2 if (full) {3 return (4 <div className="bg-black min-h-screen flex items-center justify-center">5 <p className="text-white text-2xl animate-pulse">{message}</p>6 </div>7 );8 }9 return <p className={`text-white text-xl ${className}`}>{message}</p>;10}| 줄 | 설명 |
|---|---|
| 1 | message(표시 텍스트), full(전체 화면 여부), className(추가 클래스)을 받는다. 모두 기본값이 있다. |
| 2-7 | full이 true이면 검정 배경으로 화면 전체를 채우고 가운데에 메시지를 표시한다. |
| 4 | min-h-screen — 최소 높이를 화면 전체 높이로 설정한다. flex — 자식 요소를 유연하게 배치하는 레이아웃 방식이다. items-center(세로 가운데)와 justify-center(가로 가운데)를 합치면 화면 정중앙에 배치된다. |
| 5 | animate-pulse — 깜빡거리는 효과이다. 로딩 중이라는 느낌을 준다. |
| 9 | full이 false이면 텍스트만 간단히 표시한다. |
4. Modal — 팝업 컴포넌트
같은 UI.jsx 파일에서, Spinner 함수 바로 아래에 이어서 Modal을 작성한다. fixed inset-0으로 화면 전체를 덮는 오버레이를 생성하고, 그 위에 콘텐츠를 가운데 배치하는 팝업 컴포넌트이다.
1export function Modal({ onClose, children }) {2 return (3 <div className="fixed inset-0 z-50 bg-black/90 flex items-center justify-center p-4">4 <div className="relative">5 <Button variant="ghost" onClick={onClose} className="absolute -top-10 right-0 text-2xl">6 <FontAwesomeIcon icon={faXmark} />7 </Button>8 {children}9 </div>10 </div>11 );12}| 줄 | 설명 |
|---|---|
| 1 | onClose(닫기 함수)와 children(팝업 안에 표시할 내용)을 받는다. |
| 3 | fixed inset-0으로 화면 전체를 덮고, bg-black/90으로 어두운 배경을 만든다. |
| 5-7 | 우상단에 X 닫기 버튼을 배치한다. 클릭하면 onClose가 호출된다. |
| 8 | children 자리에 예고편 영상 등이 들어온다. |
5. Container — 섹션 레이아웃
같은 UI.jsx 파일에서, Modal 함수 바로 아래에 마지막 컴포넌트인 Container를 작성한다. container mx-auto로 최대 너비를 제한하고 가로 중앙에 배치하는 공통 레이아웃 컴포넌트이다.
1export function Container({ className = "", children }) {2 return (3 <section className={`px-11 ${className}`}>4 <div className="container mx-auto">5 {children}6 </div>7 </section>8 );9}| 줄 | 설명 |
|---|---|
| 1 | className = ""은 기본값이다. 사용하는 곳에서 추가 클래스를 넘기지 않으면 빈 문자열이 된다. |
| 3 | px-11 — 좌우에 약 44px의 여백(패딩)을 준다. |
| 4 | container — 화면 크기에 따라 최대 너비가 자동으로 제한된다. mx-auto — 좌우 마진을 자동으로 같게 맞춰서 가운데 정렬한다. 이 두 클래스는 Tailwind에서 레이아웃을 잡을 때 가장 자주 쓰는 조합이다. |
6. 전체 코드 확인
- Button + Spinner
- Modal + Container
1import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";2import { faXmark } from "@fortawesome/free-solid-svg-icons";3
4export function Button({ variant = "primary", className = "", children, ...props }) {5 return (6 <button className={`btn-${variant} ${className}`} {...props}>7 {children}8 </button>9 );10}11
12export function Spinner({ message = "불러오는 중...", full = false, className = "" }) {13 if (full) {14 return (15 <div className="bg-black min-h-screen flex items-center justify-center">16 <p className="text-white text-2xl animate-pulse">{message}</p>17 </div>18 );19 }20 return <p className={`text-white text-xl ${className}`}>{message}</p>;21}1export function Modal({ onClose, children }) {2 return (3 <div className="fixed inset-0 z-50 bg-black/90 flex items-center justify-center p-4">4 <div className="relative">5 <Button variant="ghost" onClick={onClose} className="absolute -top-10 right-0 text-2xl">6 <FontAwesomeIcon icon={faXmark} />7 </Button>8 {children}9 </div>10 </div>11 );12}13
14export function Container({ className = "", children }) {15 return (16 <section className={`px-11 ${className}`}>17 <div className="container mx-auto">18 {children}19 </div>20 </section>21 );22}