JSX 컴포넌트 가이드
AI 캐릭터 채팅용 JSX 컴포넌트 직접 만들기. Props, State, 버튼 이벤트까지 단계별로 배우는 고급 사용자 가이드입니다.
주의사항
이 가이드는 직접 JSX를 커스텀하려는 고급 유저용 가이드입니다!
간단한 딸깍 JSX는 Gems를 활용해 주세요.
(아니면 아래 가이드를 제미나이나 GPT에 복붙하고 만들어 달라고 하셔도 됩니다)
컴포넌트 앞에 export default가 있다면 꼭 삭제 부탁드립니다!
해당 내용이 있으면 오류가 발생할 수 있습니다.
이 가이드에서 배울 것
가장 간단한 컴포넌트 만들기
props로 데이터 받기
상태(state) 사용하기
버튼으로 채팅에 메시지 보내기
실전 컴포넌트 만들기
준비물
캐릭터 편집기의 "컴포넌트" 탭
1단계: Hello World
가장 간단한 컴포넌트부터 시작합니다.
코드 복사하기
export default function HelloWorld() {
return (
<Card>
<CardContent className="p-4">
<p>안녕하세요!</p>
</CardContent>
</Card>
);
}결과
회색 테두리가 있는 카드 안에 "안녕하세요!"가 표시됩니다.
핵심 포인트
export default function 컴포넌트이름()- 항상 이 형태로 시작return (...)- 화면에 보여줄 내용Card,CardContent- 미리 제공되는 UI 컴포넌트
2단계: Props로 데이터 받기
외부에서 데이터를 전달받아 표시해봅시다.
코드 복사하기
export default function Greeting({ name = "모험가" }) {
return (
<Card>
<CardContent className="p-4">
<p className="text-lg">안녕하세요, <strong>{name}</strong>님!</p>
</CardContent>
</Card>
);
}AI가 호출할 때
<Greeting name="유니" />결과
"안녕하세요, 유니님!" 이 표시됩니다.
핵심 포인트
{ name = "모험가" }- 중괄호로 props 받기,=뒤는 기본값{name}- JSX 안에서 변수 사용할 때는 중괄호로 감싸기기본값을 꼭 설정하세요! (AI가 값을 안 줄 수도 있음)
3단계: 여러 개의 Props 사용하기
여러 데이터를 받아서 카드 형태로 보여줍니다.
코드 복사하기
export default function CharacterCard({
name = "캐릭터",
level = 1,
job = "모험가"
}) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<User className="h-5 w-5" />
{name}
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="flex justify-between">
<span className="text-muted-foreground">레벨</span>
<Badge>{level}</Badge>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">직업</span>
<span>{job}</span>
</div>
</CardContent>
</Card>
);
}AI가 호출할 때
<CharacterCard name="유니" level={15} job="마법사" />결과
┌─────────────────────────┐
│ 👤 유니 │
├─────────────────────────┤
│ 레벨 [15] │
│ 직업 마법사 │
└─────────────────────────┘핵심 포인트
문자열은
name="유니", 숫자는level={15}형태로 전달User- 사람 모양 아이콘 (Lucide)Badge- 작은 라벨 컴포넌트space-y-2- 자식 요소들 사이에 세로 간격
4단계: 객체와 배열 Props
복잡한 데이터를 다뤄봅시다.
코드 복사하기
export default function StatusBar({
hp = { current: 100, max: 100 },
mp = { current: 50, max: 50 }
}) {
return (
<Card>
<CardContent className="p-4 space-y-3">
{/* HP 바 */}
<div>
<div className="flex justify-between text-sm mb-1">
<span className="flex items-center gap-1">
<Heart className="h-4 w-4 text-red-500" />
HP
</span>
<span>{hp.current} / {hp.max}</span>
</div>
<Progress value={hp.current} max={hp.max} />
</div>
{/* MP 바 */}
<div>
<div className="flex justify-between text-sm mb-1">
<span className="flex items-center gap-1">
<Zap className="h-4 w-4 text-blue-500" />
MP
</span>
<span>{mp.current} / {mp.max}</span>
</div>
<Progress value={mp.current} max={mp.max} />
</div>
</CardContent>
</Card>
);
}AI가 호출할 때
<StatusBar
hp={{ current: 75, max: 120 }}
mp={{ current: 30, max: 80 }}
/>결과
┌─────────────────────────┐
│ ❤️ HP 75 / 120 │
│ [████████░░░░░░░] │
│ │
│ ⚡ MP 30 / 80 │
│ [████░░░░░░░░░░░] │
└─────────────────────────┘핵심 포인트
객체 props:
hp={{ current: 75, max: 120 }}- 중괄호가 두 겹!바깥쪽
{}- JSX 표현식안쪽
{}- 객체 리터럴
hp.current,hp.max- 점(.)으로 객체 속성 접근Progress- 진행률 바 컴포넌트
5단계: 배열 데이터 표시하기 (map)
목록을 반복해서 표시합니다.
코드 복사하기
export default function SkillList({
skills = []
}) {
return (
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Sparkles className="h-4 w-4" />
스킬 목록
</CardTitle>
</CardHeader>
<CardContent>
{skills.length === 0 ? (
<p className="text-sm text-muted-foreground">스킬이 없습니다</p>
) : (
<div className="space-y-2">
{skills.map((skill, index) => (
<div
key={index}
className="flex items-center justify-between p-2 rounded bg-muted/50"
>
<span>{skill.name}</span>
<Badge variant="outline">MP {skill.cost}</Badge>
</div>
))}
</div>
)}
</CardContent>
</Card>
);
}AI가 호출할 때
<SkillList
skills={[
{ name: "파이어볼", cost: 15 },
{ name: "힐링", cost: 10 },
{ name: "번개", cost: 20 }
]}
/>결과
┌─────────────────────────┐
│ ✨ 스킬 목록 │
├─────────────────────────┤
│ ┌─────────────────────┐ │
│ │ 파이어볼 [MP 15] │ │
│ └─────────────────────┘ │
│ ┌─────────────────────┐ │
│ │ 힐링 [MP 10] │ │
│ └─────────────────────┘ │
│ ┌─────────────────────┐ │
│ │ 번개 [MP 20] │ │
│ └─────────────────────┘ │
└─────────────────────────┘핵심 포인트
skills.map((skill, index) => ...)- 배열의 각 항목을 JSX로 변환key={index}- 반복문에서 필수! 각 항목을 구분하는 IDskills.length === 0 ? A : B- 조건부 렌더링 (비어있으면 A, 아니면 B)bg-muted/50- 배경색에 50% 투명도
6단계: 상태(State) 사용하기
클릭하면 바뀌는 인터랙티브 컴포넌트를 만듭니다.
코드 복사하기
export default function Counter() {
const [count, setCount] = React.useState(0);
return (
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-center gap-4">
<Button
variant="outline"
size="sm"
onClick={() => setCount(count - 1)}
>
-
</Button>
<span className="text-2xl font-bold w-12 text-center">
{count}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setCount(count + 1)}
>
+
</Button>
</div>
</CardContent>
</Card>
);
}결과
┌─────────────────────────┐
│ [-] 0 [+] │
└─────────────────────────┘버튼을 클릭하면 숫자가 증가/감소합니다.
핵심 포인트
React.useState(0)- 상태 생성 (초기값 0)[count, setCount]- [현재값, 값변경함수]onClick={() => setCount(count + 1)}- 클릭 시 실행할 함수주의:
useState아니고React.useState로 써야 함!
7단계: 채팅에 메시지 보내기
버튼을 누르면 채팅에 메시지를 보내는 기능입니다.
코드 복사하기
export default function ActionButtons({
actions = ["공격한다", "방어한다", "도망친다"]
}) {
return (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-base">행동을 선택하세요</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{actions.map((action, index) => (
<Button
key={index}
variant="outline"
className="w-full justify-start"
onClick={() => sendMessage(action)}
>
<span className="text-muted-foreground mr-2">{index + 1}.</span>
{action}
</Button>
))}
</CardContent>
</Card>
);
}AI가 호출할 때
<ActionButtons
actions={["검으로 베기", "마법 시전", "물약 사용", "도망치기"]}
/>결과
┌─────────────────────────┐
│ 행동을 선택하세요 │
├─────────────────────────┤
│ [1. 검으로 베기 ] │
│ [2. 마법 시전 ] │
│ [3. 물약 사용 ] │
│ [4. 도망치기 ] │
└─────────────────────────┘버튼을 클릭하면 해당 텍스트가 채팅에 입력됩니다!
핵심 포인트
sendMessage("텍스트")- 채팅에 메시지 전송하는 함수사용자가 버튼을 클릭하면 → 해당 텍스트로 AI에게 응답
인터랙티브 스토리/게임에 활용
8단계: 탭 UI 만들기
여러 정보를 탭으로 구분해서 보여줍니다.
코드 복사하기
export default function CharacterTabs({
stats = { str: 10, dex: 10, int: 10 },
equipment = { weapon: "없음", armor: "없음" },
gold = 0
}) {
return (
<Card>
<CardContent className="p-4">
<Tabs defaultValue="stats">
<TabsList className="w-full">
<TabsTrigger value="stats" className="flex-1">스탯</TabsTrigger>
<TabsTrigger value="equip" className="flex-1">장비</TabsTrigger>
<TabsTrigger value="gold" className="flex-1">재화</TabsTrigger>
</TabsList>
<TabsContent value="stats" className="mt-4 space-y-2">
<div className="flex justify-between">
<span className="flex items-center gap-2">
<Sword className="h-4 w-4 text-red-400" /> 힘
</span>
<span className="font-bold">{stats.str}</span>
</div>
<div className="flex justify-between">
<span className="flex items-center gap-2">
<Zap className="h-4 w-4 text-green-400" /> 민첩
</span>
<span className="font-bold">{stats.dex}</span>
</div>
<div className="flex justify-between">
<span className="flex items-center gap-2">
<Sparkles className="h-4 w-4 text-blue-400" /> 지능
</span>
<span className="font-bold">{stats.int}</span>
</div>
</TabsContent>
<TabsContent value="equip" className="mt-4 space-y-2">
<div className="flex justify-between">
<span className="flex items-center gap-2">
<Sword className="h-4 w-4" /> 무기
</span>
<span>{equipment.weapon}</span>
</div>
<div className="flex justify-between">
<span className="flex items-center gap-2">
<Shield className="h-4 w-4" /> 방어구
</span>
<span>{equipment.armor}</span>
</div>
</TabsContent>
<TabsContent value="gold" className="mt-4">
<div className="flex items-center justify-center gap-2 text-2xl">
<Coins className="h-6 w-6 text-yellow-500" />
<span className="font-bold">{gold}</span>
<span className="text-muted-foreground text-base">G</span>
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
);
}AI가 호출할 때
<CharacterTabs
stats={{ str: 18, dex: 14, int: 12 }}
equipment={{ weapon: "강철 검", armor: "가죽 갑옷" }}
gold={2500}
/>핵심 포인트
Tabs- 탭 컨테이너,defaultValue로 처음 선택된 탭 지정TabsList- 탭 버튼들의 컨테이너TabsTrigger- 각 탭 버튼,value로 구분TabsContent- 탭 내용,value가 같은 Trigger와 연결됨
실전 예제: 종합 상태창
배운 것을 모두 활용한 완성형 컴포넌트입니다.
코드 복사하기
export default function GameStatus({
character = {
name: "캐릭터",
level: 1,
job: "모험가"
},
hp = { current: 100, max: 100 },
mp = { current: 50, max: 50 },
exp = { current: 0, max: 100 },
stats = { str: 10, dex: 10, int: 10, luk: 10 },
gold = 0,
location = "마을"
}) {
// HP 퍼센트에 따른 색상
const hpPercent = (hp.current / hp.max) * 100;
const hpColor = hpPercent > 50 ? "bg-green-500" : hpPercent > 25 ? "bg-yellow-500" : "bg-red-500";
return (
<Card className="w-full">
{/* 캐릭터 정보 헤더 */}
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2">
<User className="h-5 w-5 text-primary" />
{character.name}
</CardTitle>
<div className="flex items-center gap-2">
<Badge variant="secondary">{character.job}</Badge>
<Badge>Lv.{character.level}</Badge>
</div>
</div>
<div className="flex items-center gap-1 text-sm text-muted-foreground">
<MapPin className="h-3 w-3" />
{location}
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* HP/MP/EXP 바 */}
<div className="space-y-2">
{/* HP */}
<div>
<div className="flex justify-between text-xs mb-1">
<span className="flex items-center gap-1">
<Heart className="h-3 w-3 text-red-500" /> HP
</span>
<span>{hp.current}/{hp.max}</span>
</div>
<div className="h-2 bg-muted rounded-full overflow-hidden">
<div
className={cn("h-full transition-all", hpColor)}
style={{ width: `${hpPercent}%` }}
/>
</div>
</div>
{/* MP */}
<div>
<div className="flex justify-between text-xs mb-1">
<span className="flex items-center gap-1">
<Zap className="h-3 w-3 text-blue-500" /> MP
</span>
<span>{mp.current}/{mp.max}</span>
</div>
<Progress value={mp.current} max={mp.max} />
</div>
{/* EXP */}
<div>
<div className="flex justify-between text-xs mb-1">
<span className="flex items-center gap-1">
<Star className="h-3 w-3 text-yellow-500" /> EXP
</span>
<span>{exp.current}/{exp.max}</span>
</div>
<Progress value={exp.current} max={exp.max} />
</div>
</div>
<Separator />
{/* 스탯 그리드 */}
<div className="grid grid-cols-4 gap-2 text-center">
<div className="p-2 rounded bg-muted/50">
<div className="text-xs text-muted-foreground">STR</div>
<div className="font-bold text-red-400">{stats.str}</div>
</div>
<div className="p-2 rounded bg-muted/50">
<div className="text-xs text-muted-foreground">DEX</div>
<div className="font-bold text-green-400">{stats.dex}</div>
</div>
<div className="p-2 rounded bg-muted/50">
<div className="text-xs text-muted-foreground">INT</div>
<div className="font-bold text-blue-400">{stats.int}</div>
</div>
<div className="p-2 rounded bg-muted/50">
<div className="text-xs text-muted-foreground">LUK</div>
<div className="font-bold text-purple-400">{stats.luk}</div>
</div>
</div>
{/* 골드 */}
<div className="flex items-center justify-end gap-1">
<Coins className="h-4 w-4 text-yellow-500" />
<span className="font-bold">{gold.toLocaleString()}</span>
<span className="text-muted-foreground text-sm">G</span>
</div>
</CardContent>
</Card>
);
}AI가 호출할 때
<GameStatus
character={{ name: "유", level: 25, job: "대마법사" }}
hp={{ current: 180, max: 250 }}
mp={{ current: 45, max: 200 }}
exp={{ current: 7500, max: 10000 }}
stats={{ str: 12, dex: 18, int: 35, luk: 15 }}
gold={125000}
location="마법사 길드"
/>자주 하는 실수와 해결법
1. "React is not defined" 에러
// ❌ 잘못된 코드
const [count, setCount] = useState(0);
// ✅ 올바른 코드
const [count, setCount] = React.useState(0);2. 숫자를 따옴표로 감싸기
// ❌ 잘못된 코드 - level이 문자열 "15"가 됨
<Component level="15" />
// ✅ 올바른 코드 - level이 숫자 15가 됨
<Component level={15} />3. 객체 전달 시 중괄호 한 겹만 사용
// ❌ 잘못된 코드
<Component data={ name: "유니" } />
// ✅ 올바른 코드 - 중괄호 두 겹!
<Component data={{ name: "유" }} />4. map에서 key 빼먹기
// ❌ 잘못된 코드 - 콘솔에 경고 발생
{items.map((item) => (
<div>{item}</div>
))}
// ✅ 올바른 코드
{items.map((item, index) => (
<div key={index}>{item}</div>
))}5. 기본값 없이 props 사용
// ❌ 위험한 코드 - hp가 없으면 에러
export default function Status({ hp }) {
return <div>{hp.current}</div>;
}
// ✅ 안전한 코드 - 기본값 설정
export default function Status({ hp = { current: 100, max: 100 } }) {
return <div>{hp.current}</div>;
}6. import 문 사용하기
// ❌ 잘못된 코드 - import는 자동 제거됨
import { Heart } from 'lucide-react';
// ✅ 올바른 코드 - 그냥 바로 사용
<Heart className="h-4 w-4" />사용 가능한 것들 요약
UI 컴포넌트
Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter
Badge, Button, Progress, Separator
Tabs, TabsList, TabsTrigger, TabsContent
Avatar, AvatarImage, AvatarFallback아이콘 (Lucide)
Heart, Star, Sword, Shield, Zap, Droplet
User, Crown, Coins, Gem, Trophy, Package
MapPin, Map, Clock, Calendar, Settings
Sun, Moon, Cloud, Flame, Snowflake, Wind
Sparkles, Smile, Meh, Frown, ActivityReact 훅
React.useState, React.useMemo, React.useCallback
React.useEffect, React.useRef, React.useContext유틸리티
cn() - 클래스 이름 조건부 병합
sendMessage() - 채팅에 메시지 전송
deepMerge() - 객체 깊은 병합사용 불가
fetch, localStorage, eval, import
외부 이미지 URL, 외부 라이브러리다음 단계
간단한 것부터 시작: Hello World → Props 추가 → 상태 추가
복사해서 수정: 예제를 복사한 후 조금씩 바꿔보기
에러 읽기: 빨간 에러 메시지가 뭘 고치라는지 확인
테스트 입력 활용: 컴포넌트 에디터의 "테스트 입력"으로 미리보기