가이드

JSX 컴포넌트 가이드

AI 캐릭터 채팅용 JSX 컴포넌트 직접 만들기. Props, State, 버튼 이벤트까지 단계별로 배우는 고급 사용자 가이드입니다.


주의사항

이 가이드는 직접 JSX를 커스텀하려는 고급 유저용 가이드입니다!

간단한 딸깍 JSX는 Gems를 활용해 주세요.

Gems에서 바로 만들기

(아니면 아래 가이드를 제미나이나 GPT에 복붙하고 만들어 달라고 하셔도 됩니다)

컴포넌트 앞에 export default가 있다면 꼭 삭제 부탁드립니다!

해당 내용이 있으면 오류가 발생할 수 있습니다.

이 가이드에서 배울 것

  1. 가장 간단한 컴포넌트 만들기

  2. props로 데이터 받기

  3. 상태(state) 사용하기

  4. 버튼으로 채팅에 메시지 보내기

  5. 실전 컴포넌트 만들기

준비물

  • 캐릭터 편집기의 "컴포넌트" 탭


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} - 반복문에서 필수! 각 항목을 구분하는 ID

  • skills.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, Activity

React 훅

React.useState, React.useMemo, React.useCallback
React.useEffect, React.useRef, React.useContext

유틸리티

cn()        - 클래스 이름 조건부 병합
sendMessage() - 채팅에 메시지 전송
deepMerge()  - 객체 깊은 병합

사용 불가

fetch, localStorage, eval, import
외부 이미지 URL, 외부 라이브러리

다음 단계

  1. 간단한 것부터 시작: Hello World → Props 추가 → 상태 추가

  2. 복사해서 수정: 예제를 복사한 후 조금씩 바꿔보기

  3. 에러 읽기: 빨간 에러 메시지가 뭘 고치라는지 확인

  4. 테스트 입력 활용: 컴포넌트 에디터의 "테스트 입력"으로 미리보기