3강. JSX 문법과 컴포넌트

React의 핵심인 컴포넌트를 만드는 법과, 이를 위해 사용하는 JSX 문법을 완벽히 익힌다.

1. 컴포넌트와 JSX의 관계

컴포넌트란?

컴포넌트(Component)는 React에서 UI를 만드는 기본 단위다. 레고 블록처럼 작은 부품들을 조립해서 전체 화면을 만든다고 생각하면 된다.

JSX란?

JSX(JavaScript XML)는 자바스크립트 안에서 HTML처럼 생긴 코드를 작성할 수 있게 해주는 문법이다.

둘의 관계

핵심 개념:
컴포넌트는 "무엇"이고, JSX는 "어떻게"다.

컴포넌트: UI를 만드는 함수 (설계도)
JSX: 컴포넌트 안에서 UI를 표현하는 문법 (재료와 조립법)

컴포넌트는 JSX를 반환(return)함으로써 화면에 무엇을 그릴지 정의한다.
// 컴포넌트: Greeting이라는 함수
function Greeting() {
  // JSX: 이 컴포넌트가 그릴 UI를 JSX 문법으로 작성
  return <h1>안녕하세요!</h1>;
}

// 위 JSX는 실제로 이렇게 변환됨
function Greeting() {
  return React.createElement('h1', null, '안녕하세요!');
}

2. 컴포넌트 완전 정복

컴포넌트의 기본 구조

React 컴포넌트는 함수형 컴포넌트를 기본으로 사용한다. (과거에는 클래스형도 있었지만 지금은 거의 사용 안 함)

// 가장 간단한 컴포넌트
function Hello() {
  return <div>안녕!</div>;
}

// 화살표 함수로도 작성 가능
const Hello = () => {
  return <div>안녕!</div>;
};

// return이 한 줄이면 괄호 생략 가능
const Hello = () => <div>안녕!</div>;

컴포넌트 작성 규칙 (매우 중요!)

규칙 1: 대문자로 시작
컴포넌트 이름은 반드시 대문자로 시작해야 한다.

올바른 예: Header, MyButton, UserProfile
잘못된 예: header, myButton, userProfile

이유: React는 소문자로 시작하면 일반 HTML 태그로 인식한다.
<div>는 HTML div 태그, <Div>는 Div 컴포넌트
규칙 2: JSX 반환
함수는 반드시 JSX를 return해야 한다. 아무것도 반환하지 않으면 오류 발생.

// 올바른 예
function Button() {
  return <button>클릭</button>;
}

// 오류 발생 - 아무것도 반환 안 함
function Button() {
  console.log("버튼");
}
규칙 3: 파일당 하나의 기본 내보내기
다른 파일에서 사용하려면 export default로 내보내야 한다.

// Button.jsx
function Button() {
  return <button>클릭</button>;
}

export default Button; // 다른 파일에서 import 가능

// 또는 한 줄로
export default function Button() {
  return <button>클릭</button>;
}

컴포넌트 사용하기

만든 컴포넌트는 HTML 태그처럼 사용한다. 이를 "컴포넌트 렌더링"이라고 한다.

// Button 컴포넌트 정의
function Button() {
  return <button>클릭하세요</button>;
}

// App 컴포넌트에서 Button 사용
function App() {
  return (
    <div>
      <h1>내 앱</h1>
      <Button />  {/* Button 컴포넌트를 HTML 태그처럼 사용 */}
      <Button />  {/* 여러 번 재사용 가능 */}
    </div>
  );
}

컴포넌트 안에서 로직 작성하기

컴포넌트는 자바스크립트 함수이므로, return 전에 일반 JS 코드를 작성할 수 있다.

function Greeting() {
  // return 전에 자바스크립트 로직 작성
  const name = "철수";
  const hour = new Date().getHours();
  const greeting = hour < 12 ? "좋은 아침" : "안녕하세요";

  // JSX에서 변수 사용
  return (
    <div>
      <h1>{greeting}, {name}님!</h1>
      <p>현재 시각은 {hour}시입니다.</p>
    </div>
  );
}
[실습 1] 자기소개 컴포넌트 만들기
src/Profile.jsx 파일을 만들고 다음 내용을 작성:

function Profile() {
  const name = "홍길동";
  const age = 25;
  const hobby = "독서";

  return (
    <div style={{ padding: '20px', border: '1px solid #ccc' }}>
      <h2>{name}의 프로필</h2>
      <p>나이: {age}세</p>
      <p>취미: {hobby}</p>
    </div>
  );
}

export default Profile;
그리고 App.jsx에서 불러와서 사용:
import Profile from './Profile';

function App() {
  return <Profile />;
}

3. JSX 문법 완전 정복

JSX는 HTML처럼 보이지만 실제로는 자바스크립트다. 그래서 HTML과는 다른 규칙들이 있다.

JSX의 본질

// 우리가 작성하는 JSX
const element = <h1 className="title">안녕!</h1>;

// Vite(Babel)가 변환한 실제 자바스크립트
const element = React.createElement(
  'h1',
  { className: 'title' },
  '안녕!'
);

JSX는 결국 React.createElement() 함수를 쉽게 쓰기 위한 문법 설탕(Syntactic Sugar)이다.

JSX 규칙 1: 하나의 최상위 태그

컴포넌트는 반드시 하나의 덩어리만 반환해야 한다. 형제 태그를 나란히 둘 수 없다.

// 오류 발생! - 두 개의 형제 태그
function App() {
  return (
    <h1>제목</h1>
    <p>내용</p>
  );
}

// 해결책 1: div로 감싸기
function App() {
  return (
    <div>
      <h1>제목</h1>
      <p>내용</p>
    </div>
  );
}

// 해결책 2: Fragment 사용 (권장)
function App() {
  return (
    <>  {/* Fragment: 화면에 렌더링되지 않는 투명한 태그 */}
      <h1>제목</h1>
      <p>내용</p>
    </>
  );
}

// Fragment의 완전한 형태 (key가 필요할 때만 사용)
import { Fragment } from 'react';

function App() {
  return (
    <Fragment>
      <h1>제목</h1>
      <p>내용</p>
    </Fragment>
  );
}
Fragment를 쓰는 이유:
불필요한 <div>가 많아지면 HTML 구조가 복잡해지고 CSS 스타일링이 어려워진다.
Fragment(<></>)는 실제 DOM에 렌더링되지 않으므로 깔끔한 HTML 구조를 유지할 수 있다.

JSX 규칙 2: 모든 태그는 닫아야 함

HTML에서는 일부 태그를 닫지 않아도 됐지만, JSX에서는 모든 태그를 반드시 닫아야 한다.

// HTML에서는 OK, JSX에서는 오류
<input type="text">
<br>
<img src="image.jpg">

// JSX에서는 이렇게 닫아야 함 (Self-closing tag)
<input type="text" />
<br />
<img src="image.jpg" />
<hr />
<meta charset="UTF-8" />

JSX 규칙 3: 중괄호로 자바스크립트 표현식 삽입

중괄호 {} 안에 자바스크립트 표현식(expression)을 넣을 수 있다.

function Welcome() {
  const name = "철수";
  const age = 20;
  const isAdult = age >= 18;

  return (
    <div>
      {/* 변수 */}
      <h1>안녕, {name}!</h1>

      {/* 계산식 */}
      <p>10년 후 나이: {age + 10}</p>

      {/* 삼항 연산자 */}
      <p>{isAdult ? "성인" : "미성년자"}</p>

      {/* 함수 호출 */}
      <p>대문자 이름: {name.toUpperCase()}</p>

      {/* 배열 (자동으로 join됨) */}
      <p>{['사과', '바나나', '오렌지']}</p>
    </div>
  );
}
중요: 표현식만 가능!
중괄호 안에는 값을 반환하는 표현식만 넣을 수 있다.

가능: 변수, 계산식, 삼항 연산자, 함수 호출
불가능: if문, for문, switch문 등의 문장(statement)

// 오류! - if는 표현식이 아님
return <div>{if (true) { "참" }}</div>;

// 대신 삼항 연산자 사용
return <div>{true ? "참" : "거짓"}</div>;

JSX 규칙 4: 속성명 주의사항

JSX는 자바스크립트이므로, HTML 속성명이 약간 다르다.

HTML JSX 이유
class className class는 JS 예약어
for htmlFor for는 JS 예약어
onclick onClick 카멜케이스 사용
tabindex tabIndex 카멜케이스 사용
// HTML 방식 (JSX에서는 오류!)
<div class="container">
  <label for="name">이름</label>
  <button onclick="handleClick()">클릭</button>
</div>

// JSX 방식
<div className="container">
  <label htmlFor="name">이름</label>
  <button onClick={handleClick}>클릭</button>
</div>

JSX 규칙 5: 인라인 스타일은 객체로

HTML에서는 문자열로 스타일을 지정했지만, JSX에서는 객체로 지정한다.

// HTML 방식
<div style="color: red; font-size: 20px;">텍스트</div>

// JSX 방식 (객체 사용, 속성명은 카멜케이스)
<div style={{ color: 'red', fontSize: '20px' }}>텍스트</div>
//           ↑ 첫 번째 중괄호: JS 표현식
//            ↑ 두 번째 중괄호: 객체 리터럴

// 변수로 분리하면 더 깔끔
const myStyle = {
  color: 'red',
  fontSize: '20px',
  backgroundColor: '#f0f0f0'  // background-color → backgroundColor
};

return <div style={myStyle}>텍스트</div>;

JSX 규칙 6: 주석 작성법

function App() {
  return (
    <div>
      {/* JSX 안에서 주석: 중괄호 안에 /*  */ 사용 */}
      <h1>제목</h1>

      {/*
        여러 줄 주석도 가능
        이렇게 작성합니다
      */}

      {// 한 줄 주석도 가능 (덜 일반적)
      }
    </div>
  );
  // JSX 밖에서는 일반 자바스크립트 주석
}
[실습 2] JSX 문법 연습
src/Card.jsx 파일을 만들고 JSX 문법을 연습해보자:

function Card() {
  const title = "React 학습";
  const progress = 75;
  const completed = progress >= 100;

  const cardStyle = {
    border: '2px solid #007bff',
    borderRadius: '10px',
    padding: '20px',
    margin: '10px',
    backgroundColor: completed ? '#d4edda' : '#fff3cd'
  };

  return (
    <>
      {/* Fragment 사용 */}
      <div style={cardStyle}>
        <h2 className="card-title">{title}</h2>
        <p>진행률: {progress}%</p>

        {/* 진행 바 */}
        <div style={{
          width: '100%',
          height: '20px',
          backgroundColor: '#e0e0e0',
          borderRadius: '10px'
        }}>
          <div style={{
            width: `${progress}%`,
            height: '100%',
            backgroundColor: '#007bff',
            borderRadius: '10px'
          }} />
        </div>

        {/* 조건부 렌더링 */}
        <p>{completed ? "✅ 완료!" : "🔄 진행 중"}</p>
      </div>
    </>
  );
}

export default Card;

4. 컴포넌트 분리 실습

하나의 파일(App.jsx)에 모든 코드를 넣는 것은 좋지 않다. 역할별로 파일을 나누면 코드 관리가 쉬워진다.

왜 컴포넌트를 분리하나?

컴포넌트 분리 원칙

언제 분리해야 할까?

1. 재사용될 것 같은 UI: 버튼, 카드, 입력 폼 등
2. 독립적인 기능: 로그인 폼, 검색 바, 장바구니 등
3. 너무 길어진 컴포넌트: 100줄 이상이면 분리 고려
4. 명확한 책임이 있는 UI: 헤더, 푸터, 사이드바 등

실습: 웹사이트 레이아웃 만들기

[실습 3] 컴포넌트 분리하기

1단계: 컴포넌트 파일 생성
src 폴더에 다음 파일들을 만든다:

src/Header.jsx
function Header() {
  return (
    <header style={{
      backgroundColor: '#282c34',
      padding: '20px',
      color: 'white'
    }}>
      <h1>내 웹사이트</h1>
      <nav>
        <a href="#home" style={{ color: 'white', margin: '0 10px' }}>홈</a>
        <a href="#about" style={{ color: 'white', margin: '0 10px' }}>소개</a>
        <a href="#contact" style={{ color: 'white', margin: '0 10px' }}>연락</a>
      </nav>
    </header>
  );
}

export default Header;
src/MainContent.jsx
function MainContent() {
  return (
    <main style={{
      padding: '40px',
      minHeight: '500px'
    }}>
      <h2>메인 콘텐츠</h2>
      <p>여기에 주요 내용이 들어갑니다.</p>

      <div style={{
        display: 'grid',
        gridTemplateColumns: 'repeat(3, 1fr)',
        gap: '20px',
        marginTop: '20px'
      }}>
        <div style={{ border: '1px solid #ddd', padding: '20px' }}>
          <h3>카드 1</h3>
          <p>첫 번째 카드 내용</p>
        </div>
        <div style={{ border: '1px solid #ddd', padding: '20px' }}>
          <h3>카드 2</h3>
          <p>두 번째 카드 내용</p>
        </div>
        <div style={{ border: '1px solid #ddd', padding: '20px' }}>
          <h3>카드 3</h3>
          <p>세 번째 카드 내용</p>
        </div>
      </div>
    </main>
  );
}

export default MainContent;
src/Footer.jsx
function Footer() {
  const year = new Date().getFullYear();

  return (
    <footer style={{
      backgroundColor: '#f0f0f0',
      padding: '20px',
      textAlign: 'center',
      borderTop: '1px solid #ddd'
    }}>
      <p>© {year} 내 웹사이트. All rights reserved.</p>
      <div>
        <a href="#privacy" style={{ margin: '0 10px' }}>개인정보처리방침</a>
        <a href="#terms" style={{ margin: '0 10px' }}>이용약관</a>
      </div>
    </footer>
  );
}

export default Footer;
2단계: App.jsx에서 조립하기
import Header from './Header';
import MainContent from './MainContent';
import Footer from './Footer';

function App() {
  return (
    <div style={{
      display: 'flex',
      flexDirection: 'column',
      minHeight: '100vh'
    }}>
      <Header />
      <MainContent />
      <Footer />
    </div>
  );
}

export default App;

컴포넌트 import/export 완전 이해

// 기본 내보내기 (Default Export) - 파일당 1개만 가능
export default function Button() {
  return <button>클릭</button>;
}

// 불러올 때 - 이름을 마음대로 정할 수 있음
import Button from './Button';
import MyButton from './Button';  // 이름 변경 가능
import Btn from './Button';       // 이름 변경 가능


// 이름있는 내보내기 (Named Export) - 여러 개 가능
export function Button() {
  return <button>클릭</button>;
}

export function Input() {
  return <input />;
}

// 불러올 때 - 정확한 이름을 사용해야 함
import { Button, Input } from './Components';
import { Button as MyBtn } from './Components';  // as로 이름 변경 가능


// 혼합 사용
export default function Button() { /* ... */ }
export function SmallButton() { /* ... */ }

// 불러올 때
import Button, { SmallButton } from './Button';

5. 조건부 렌더링

상황에 따라 다른 UI를 보여줘야 할 때가 많다. JSX에서는 자바스크립트의 조건 연산자를 활용한다.

방법 1: 삼항 연산자 (조건 ? 참 : 거짓)

가장 많이 사용하는 방법. 참/거짓 두 가지 경우를 모두 처리할 때 사용한다.

function LoginButton() {
  const isLoggedIn = false;

  return (
    <div>
      {isLoggedIn ? (
        <button>로그아웃</button>
      ) : (
        <button>로그인</button>
      )}
    </div>
  );
}

// 복잡한 UI도 가능
function UserGreeting() {
  const user = { name: "철수", level: "프리미엄" };
  const isLoggedIn = true;

  return (
    <div>
      {isLoggedIn ? (
        <div>
          <h2>환영합니다, {user.name}님!</h2>
          <span>회원 등급: {user.level}</span>
        </div>
      ) : (
        <div>
          <h2>로그인이 필요합니다</h2>
          <button>로그인하기</button>
        </div>
      )}
    </div>
  );
}

방법 2: AND 연산자 (&&)

조건이 참일 때만 보여주고, 거짓이면 아무것도 안 보여줄 때 사용한다.

function Notification() {
  const hasNewMessages = true;
  const messageCount = 5;

  return (
    <div>
      <h1>알림</h1>

      {/* messageCount가 0보다 크면 표시 */}
      {messageCount > 0 && (
        <div>새 메시지 {messageCount}개</div>
      )}

      {/* hasNewMessages가 true면 표시 */}
      {hasNewMessages && <span>🔔 새 알림이 있습니다</span>}
    </div>
  );
}

// 주의: 0은 화면에 렌더링됨!
function Example() {
  const count = 0;

  return (
    <div>
      {/* 🚨 잘못된 예: 0이 화면에 그대로 표시됨 */}
      {count && <span>개수: {count}</span>}

      {/* ✅ 올바른 예: 명확한 불리언 값 사용 */}
      {count > 0 && <span>개수: {count}</span>}
    </div>
  );
}

방법 3: 변수에 저장하기

조건이 복잡하면 JSX 밖에서 처리하고 변수에 저장하는 것이 더 깔끔하다.

function Dashboard() {
  const user = {
    name: "김철수",
    age: 17,
    isPremium: false
  };

  // JSX 밖에서 조건 처리
  let content;

  if (user.age < 18) {
    content = <p>미성년자는 이용할 수 없습니다.</p>;
  } else if (user.isPremium) {
    content = (
      <div>
        <h2>프리미엄 회원 전용 콘텐츠</h2>
        <p>모든 기능을 이용할 수 있습니다.</p>
      </div>
    );
  } else {
    content = (
      <div>
        <h2>일반 회원 콘텐츠</h2>
        <button>프리미엄 업그레이드</button>
      </div>
    );
  }

  return (
    <div>
      <h1>대시보드</h1>
      {content}
    </div>
  );
}

방법 4: 즉시 실행 함수 (IIFE)

JSX 안에서 복잡한 조건을 처리하고 싶을 때 사용한다. (덜 일반적)

function ComplexCondition() {
  const score = 85;

  return (
    <div>
      <h1>성적표</h1>
      {(() => {
        if (score >= 90) return <span>🏆 A학점</span>;
        if (score >= 80) return <span>👍 B학점</span>;
        if (score >= 70) return <span>😊 C학점</span>;
        return <span>😢 재수강 필요</span>;
      })()}
    </div>
  );
}
[실습 4] 조건부 렌더링 연습
회원 등급에 따라 다른 배지를 보여주는 컴포넌트를 만들어보자:

function UserBadge() {
  const user = {
    name: "이영희",
    level: "gold",  // bronze, silver, gold, platinum
    points: 1500
  };

  // 레벨별 스타일
  const badgeStyles = {
    bronze: { backgroundColor: '#cd7f32', color: 'white' },
    silver: { backgroundColor: '#c0c0c0', color: 'black' },
    gold: { backgroundColor: '#ffd700', color: 'black' },
    platinum: { backgroundColor: '#e5e4e2', color: 'black' }
  };

  // 레벨별 아이콘
  const getIcon = (level) => {
    if (level === 'bronze') return '🥉';
    if (level === 'silver') return '🥈';
    if (level === 'gold') return '🥇';
    if (level === 'platinum') return '💎';
    return '⭐';
  };

  return (
    <div style={{ padding: '20px' }}>
      <div style={{
        ...badgeStyles[user.level],
        padding: '10px 20px',
        borderRadius: '20px',
        display: 'inline-block',
        fontWeight: 'bold'
      }}>
        {getIcon(user.level)} {user.level.toUpperCase()} 회원
      </div>

      <h2>{user.name}님</h2>
      <p>포인트: {user.points}P</p>

      {/* 다음 레벨까지 안내 */}
      {user.level === 'bronze' && user.points >= 500 && (
        <p style={{ color: 'green' }}>
          ✨ Silver 등급까지 {1000 - user.points}P 남았습니다!
        </p>
      )}

      {user.level === 'platinum' ? (
        <p>🎉 최고 등급입니다!</p>
      ) : (
        <button>등급 업그레이드 방법 보기</button>
      )}
    </div>
  );
}

export default UserBadge;

조건부 렌더링 정리

방법 사용 시기 예시
삼항 연산자 참/거짓 둘 다 처리 {isOn ? "켜짐" : "꺼짐"}
AND 연산자 참일 때만 표시 {hasError && "오류!"}
변수 저장 복잡한 조건 if/else로 처리 후 변수에 저장
즉시 실행 함수 JSX 안에서 복잡한 로직 {(() => { /* 로직 */ })()}