React 컴포넌트는 일반 CSS로 스타일을 줍니다. className prop으로 요소에 클래스를 주고 스타일시트에 규칙을 작성하며, 자바스크립트에서 오는 값에는 style prop을 쓰고, 프로젝트가 커지면 CSS Modules나 Tailwind를 사용합니다. 아래 예제는 이 페이지에서 실행할 수 있도록 CSS를 <style> 요소 안에 정의합니다. 프로젝트에서라면 .css 파일에 둘 것입니다.
.btn 규칙의 #2563eb를 #16a34a로 바꾸면 버튼이 초록색이 됩니다. 선택자, 미디어 쿼리, hover 상태, 애니메이션 등 CSS에 대해 알고 있는 모든 것이 그대로 적용됩니다.
class가 아니라 className
JSX는 자바스크립트로 컴파일되고 자바스크립트에서 class는 예약어이므로, React는 DOM 자체가 그 속성에 쓰는 이름인 className(element.className)을 사용합니다. 브라우저는 여전히 일반 class 속성을 받습니다. 라벨의 for 대신 htmlFor를 쓰는 것도 같은 이유입니다. React 19는 개발 환경에서 경고와 함께 class를 받아 주지만, 올바른 이름이자 모든 코드베이스가 쓰는 이름은 className입니다.
className은 문자열을 받으므로 여러 클래스는 공백으로 구분합니다: className="btn btn-primary". 값이 자바스크립트이므로 계산할 수도 있습니다.
조건부 클래스
특정 경우에만 클래스를 추가하려면 삼항 연산자나 템플릿 리터럴로 문자열을 만드세요. 선택된 탭을 강조하는 탭 바가 대표적인 예입니다.
Lessons를 클릭하면 이제 그 버튼의 문자열에만 active가 들어 있으므로 밑줄이 이동합니다. Reviews는 disabled 클래스와 disabled 속성을 받으므로 회색이 되고 클릭을 무시합니다. 템플릿 리터럴은 조건이 false일 때 여분의 공백을 남기지만 브라우저는 이를 무시합니다. 조건부 클래스가 많은 컴포넌트라면 배열이 더 깔끔합니다.
const classes = ['tab', isActive && 'active', isDisabled && 'disabled']
.filter(Boolean)
.join(' ');
많은 프로젝트에서 같은 일에 아주 작은 clsx 패키지를 사용합니다: clsx('tab', { active: isActive, disabled: isDisabled }).
style prop
style prop은 문자열이 아니라 자바스크립트 객체를 받습니다. 그래서 이중 중괄호가 됩니다. 바깥 중괄호는 자바스크립트 표현식을 열고, 안쪽 중괄호는 객체 리터럴입니다. 속성 이름은 camelCase(backgroundColor, fontSize, borderRadius)이고, 값은 문자열이나 숫자입니다. 길이 속성의 숫자는 픽셀이 되므로 padding: 12는 12px을 뜻합니다.
인라인 스타일은 진행 표시줄의 너비처럼 렌더링 시점에 계산되는 값에 빛을 발합니다.
막대가 가득 찰 때까지 Add 10%를 클릭하면 초록색으로 바뀝니다. width: percent + '%'를 보세요. 숫자만 쓰면 픽셀이 되므로 px 이외의 단위는 문자열에 넣습니다. 단위가 없는 속성은 opacity: 0.8, zIndex: 2, flexGrow: 1, lineHeight: 1.5(픽셀이 아니라 배수)처럼 일반 숫자로 둡니다.
인라인 스타일에는 한계가 있습니다. :hover, :focus, 미디어 쿼리, 키프레임 애니메이션을 표현할 수 없고, 요소마다 규칙의 사본을 따로 가집니다. 동적인 값에만 쓰고 나머지는 CSS에 두세요.
props에서 오는 CSS 변수
좋은 절충안은 style에서 CSS 사용자 정의 속성을 설정하고 스타일시트에서 그것을 사용하는 것입니다. hover 상태를 포함한 규칙은 CSS에 남고, 값만 자바스크립트에서 옵니다.
새 색을 고르면 두 버튼이 모두 따라가고, hover 채우기도 일반 CSS이므로 여전히 동작합니다. 사용자 정의 속성 이름은 style 객체에서도 대시를 유지하므로 따옴표 안에 씁니다: { '--accent': accent }.
CSS 파일
실제 프로젝트에서는 .css 파일에 스타일을 작성하고, 필요한 컴포넌트에서 import합니다. Vite에서는 import만으로 충분합니다. 빌드 도구가 import './Button.css'를 보고, 개발 중에는 스타일시트를 페이지에 추가하고, 프로덕션에서는 CSS 파일로 번들링합니다. 사용할 export는 없습니다. 이 import는 부수 효과만을 위해 존재합니다.
// Button.jsx
import './Button.css';
export default function Button({ children }) {
return <button className="button">{children}</button>;
}
/* Button.css */
.button {
background: #2563eb;
color: white;
border-radius: 6px;
}
문제는 import한 모든 스타일시트가 전역이라는 점입니다. Button.css의 .button 규칙은 앱 어디에 있든 그 클래스를 가진 모든 요소에 스타일을 주고, 둘 다 .title을 정의한 두 파일은 서로를 덮어씁니다. 팀에서는 BEM(.card__title) 같은 이름 규칙이나 CSS Modules로 이 문제를 다룹니다.
CSS Modules
.module.css로 끝나는 파일은 CSS Module입니다. 이것을 import하면 작성한 각 클래스 이름을 고유하게 생성된 이름에 연결한 객체를 얻으므로, 클래스는 사용한 곳에만 적용됩니다. Vite와 Next.js는 별도 설정 없이 이를 지원합니다.
// Card.jsx
import styles from './Card.module.css';
export default function Card({ title, children, featured }) {
return (
<section className={featured ? `${styles.card} ${styles.featured}` : styles.card}>
<h3 className={styles.title}>{title}</h3>
{children}
</section>
);
}
/* Card.module.css */
.card { border: 1px solid #ddd; border-radius: 10px; padding: 16px; }
.featured { border-color: #2563eb; }
.title { margin: 0; }
브라우저에서 클래스는 _card_1x2ab_1 같은 모습으로 나옵니다. 다른 컴포넌트가 자기만의 .title을 정의해도 둘은 절대 충돌하지 않습니다. hover 규칙과 미디어 쿼리를 포함해 일반 CSS를 그대로 작성하므로, CSS Modules는 컴포넌트 스타일의 흔한 기본 선택입니다.
React와 Tailwind CSS
Tailwind는 다른 접근 방식을 취합니다. 규칙을 작성하는 대신 작은 유틸리티 클래스를 className에 직접 조합합니다. px-4는 가로 패딩을 설정하고, rounded-lg는 모서리를 둥글게 하며, hover:bg-blue-700은 hover 시 배경을 바꿉니다. Tailwind는 파일을 스캔해서 사용한 클래스에 대한 CSS만 생성합니다.
Vite React 앱에 설정하는 것(Tailwind v4)은 세 단계입니다.
npm install tailwindcss @tailwindcss/vite
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [react(), tailwindcss()],
});
/* src/index.css, imported from main.jsx */
@import "tailwindcss";
그다음 클래스로 컴포넌트에 스타일을 줍니다.
function UpgradeCard({ active }) {
return (
<div className="max-w-xs rounded-xl border border-gray-200 p-4">
<h3 className="mb-2 text-lg font-semibold">Pro plan</h3>
<button
className={`rounded-md px-4 py-2 text-white ${active ? 'bg-green-600' : 'bg-blue-600 hover:bg-blue-700'}`}
>
{active ? 'Active' : 'Upgrade'}
</button>
</div>
);
}
이 페이지의 에디터에서는 Tailwind를 쓸 수 없으므로 정적 코드로 보여 줍니다. 조건부 클래스는 위의 탭 예제와 똑같이 동작하지만, 각 클래스 이름은 완전하게 쓰세요. Tailwind는 소스 텍스트를 스캔해서 클래스를 찾으므로, `bg-${color}-600`처럼 런타임에 조립한 이름은 절대 생성되지 않습니다.
언제 무엇을 쓸까
- 일반 CSS 파일은 작은 앱과 전역 스타일(리셋, 폰트, 페이지 레이아웃)에 적합합니다. 앱이 커지면 클래스 이름 충돌을 조심하세요.
- CSS Modules는 대부분의 컴포넌트 스타일에 적합합니다. 일반 CSS이면서 자동으로 범위가 지정되고, 추가 라이브러리가 필요 없습니다.
- Tailwind는 마크업 안에서 스타일을 주는 것을 선호하고 간격과 색상에 공유 디자인 스케일을 원하는 팀에 적합합니다.
styleprop은 너비, 위치, 사용자가 고른 색처럼 렌더링 시점에 계산되는 값을 위한 것입니다. CSS 변수와 함께 쓰면 hover와 미디어 규칙을 CSS에 둘 수 있습니다.
여러 방법을 섞어 써도 됩니다. 일반적인 앱에는 전역 스타일시트 하나, 컴포넌트용 CSS Modules나 Tailwind, 그리고 동적인 값을 위한 인라인 스타일 몇 개가 있습니다. styled-components와 Emotion 같은 CSS-in-JS 라이브러리도 오래된 코드베이스에서 보게 될 또 다른 선택지입니다. React 설치 페이지에서 이 모든 방법이 동작하는 Vite 프로젝트를 시작하는 법을 보여 줍니다.
자주 묻는 질문
React는 왜 class 대신 className을 쓰나요?
JSX는 자바스크립트로 컴파일되는데, 자바스크립트에서 class는 예약어입니다. React는 DOM 속성 이름인 className을 사용하며, 이는 일반 자바스크립트에서 element.className이라고 부르는 이름이기도 합니다. HTML에서는 여전히 일반 class 속성이 됩니다.
React에서 인라인 스타일은 어떻게 작성하나요?
style prop에 camelCase 속성 이름을 가진 객체를 넘기세요: style={{ backgroundColor: 'teal', fontSize: 18 }}. 바깥 중괄호는 자바스크립트 표현식을 열고, 안쪽 중괄호가 객체입니다. 길이를 받는 속성에서 숫자는 픽셀이 됩니다.
React에서 조건에 따라 클래스를 추가하려면 어떻게 하나요?
자바스크립트로 문자열을 만드세요: className={isActive ? 'tab active' : 'tab'} 또는 className={`tab ${isActive ? 'active' : ''}`} 같은 템플릿 리터럴입니다. 조건이 많다면 clsx 패키지 같은 작은 도우미가 코드를 읽기 쉽게 해 줍니다.
React에서 CSS 파일은 전역인가요?
네. import './App.css'는 스타일시트를 페이지 전체에 추가하므로 클래스 이름이 다른 파일과 충돌할 수 있습니다. CSS Modules(App.module.css)는 각 클래스 이름을 고유한 이름으로 바꿔서 import한 곳에만 적용되게 합니다.
React에서 Tailwind는 어떻게 사용하나요?
Vite 프로젝트에서 tailwindcss와 @tailwindcss/vite를 설치하고, 플러그인을 vite.config.js에 추가한 다음, 메인 CSS 파일 맨 위에 @import "tailwindcss";를 넣으세요. 그다음 className="px-4 py-2 rounded bg-blue-600 text-white"처럼 className에 유틸리티 클래스를 작성합니다.