Skip to content
This page has been auto-translated and may contain errors.View in English

컴포넌트 스타일링

React에는 자체 스타일링 시스템이 없습니다. 일반적인 CSS를 작성하고, JSX가 엘리먼트에 스타일을 붙이는 두 가지 방법을 제공합니다. 클래스 이름(거의 모든 경우를 처리합니다)과 style prop(스타일시트가 미리 알 수 없는 소수의 값들)입니다. 이 장에서는 둘 다와 그 사이의 패턴을 다룹니다. prop이나 상태로부터 클래스 이름을 구성하여 데이터가 변할 때 엘리먼트가 자동으로 다시 스타일링되도록 하는 방식입니다.

JSX의 클래스 이름

JavaScript에서 class는 예약어이므로 JSX는 이 속성을 className이라고 부릅니다. 값은 평문 문자열이고, React는 이를 DOM의 엘리먼트 class 속성으로 설정합니다. 따라서 반대쪽의 CSS는 항상 그래왔던 방식으로 작성됩니다.

jsx
function Avatar() {
  return <img className="avatar avatar--large" src={photo} alt="Ada Lovelace" />
}

한 문자열에 두 개의 클래스 이름이 공백으로 분리되어 있으며, 이는 HTML과 동일합니다. JSX의 camelCase 규칙은 마크업의 속성 이름에 적용됩니다. 클래스 이름 자체는 사용자의 것이므로 kebab-case, BEM 또는 스타일시트에서 이미 사용 중인 모든 규칙을 그대로 사용할 수 있습니다.

스타일시트 임포트하기

컴포넌트 파일은 맨 위의 간단한 import로 필요한 CSS를 가져옵니다.

jsx
import './App.css'

export default function App() {
  return <h1 className="title">React Facts</h1>
}

import의 왼쪽에는 변수가 없고, 컴포넌트는 파일을 읽지 않습니다. 이것은 빌드 도구에 대한 지시입니다. 이 모듈이 그 CSS에 의존하므로 포함하라는 뜻입니다. Vite는 개발 중에 규칙을 페이지에 주입하고 프로덕션 빌드에서 실제 .css 파일을 내보냅니다. React 프로젝트 설정하기는 이미지를 처리하는 동일한 메커니즘을 설명합니다.

import를 어디에 배치할지는 조직의 문제입니다. Button.jsx 맨 위에 Button.css를 import하면 스타일을 그것을 사용하는 컴포넌트 옆에 둘 수 있습니다. 규칙 자체는 전역입니다. .title 선택자는 페이지의 어디서든 그 클래스를 가진 모든 엘리먼트와 일치하며, 어느 파일이 이를 import했는지는 상관없습니다. BEM 같은 네이밍 규칙은 그 공유 네임스페이스를 관리하기 위해 존재하며, CSS Modules과 CSS-in-JS 라이브러리는 앱이 네이밍 규칙만으로는 감당하기 어려워지면 규칙의 범위를 제한합니다.

버전 노트

React 19는 컴포넌트 내부에서 <link rel="stylesheet"><style>을 렌더링하는 지원을 추가했으며, precedence prop으로 React가 이들을 문서 head로 끌어올리고 예측 가능하게 순서를 정할 수 있습니다. 이전 버전들은 react-helmet 같은 라이브러리가 필요했습니다. 위의 빌드 도구 import는 모든 버전에서 동일하게 작동합니다.

데이터에 따라 달라지는 클래스 이름

className은 다른 JSX 표현식처럼 중괄호 안에 있으므로 문자열을 계산할 수 있습니다. 각 주사위가 굴림 사이에 고정될 수 있는 주사위 게임을 생각해봅시다. 주사위가 고정되어 있는지 여부는 상태에 포함되고 prop으로 여기에 도달합니다.

jsx
function Die({ value, isHeld }) {
  return (
    <button className={`die ${isHeld ? 'die--held' : ''}`}>
      {value}
    </button>
  )
}

모든 주사위는 die를 얻습니다. 고정된 주사위는 die--held도 얻습니다. 스타일시트가 실제 색상과 테두리를 가지고 있으며, 컴포넌트의 유일한 역할은 어느 클래스가 적용되는지 결정하는 것입니다. isHeld가 바뀌면 React가 class 속성을 업데이트하고 브라우저가 다시 그립니다. 컴포넌트 내부의 아무것도 "held"가 어떻게 보이는지 알 필요가 없으므로 이 파일을 변경하지 않고도 그 외관을 바꿀 수 있습니다.

한 가지 문제점: isHeld가 거짓이면 템플릿 리터럴이 뒤에 공백이 있는 "die "를 생성합니다. 브라우저는 이를 무시하지만 devtools에서는 보기 흉합니다. 배열이 더 깔끔하게 처리하고 하나의 조건을 넘어 확장합니다.

jsx
const classes = ['die', isHeld && 'die--held'].filter(Boolean).join(' ')

return <button className={classes}>{value}</button>

isHeld && 'die--held'는 조건이 실패할 때 false로 계산되고, filter(Boolean)이 이를 제거하며, join(' ')이 남은 것들 사이에 하나의 공백을 놓습니다.

style prop

style prop은 JavaScript 객체를 받습니다. 속성 이름은 CSS 속성의 camelCase 버전이고, 이중 중괄호는 일반적인 표현식 슬롯에 객체 리터럴이 들어있습니다.

jsx
<div style={{ backgroundColor: 'darkslateblue', paddingTop: 12 }}>
  Styled inline
</div>

background-colorbackgroundColor가 되고 padding-toppaddingTop이 됩니다. 값은 대개 문자열입니다. 숫자를 그대로 전달하면 React가 길이를 받는 속성에 px를 붙이므로 paddingTop: 12padding-top: 12px로 렌더링됩니다. CSS가 단위 없음으로 취급하는 속성들은 원래 숫자를 그대로 둡니다. lineHeight, opacity, flexGrow, zIndexfontWeight는 모두 그대로 통과합니다. 다른 모든 단위는 width: '60%' 또는 margin: '2rem'처럼 문자열로 작성해야 합니다.

대부분의 스타일링은 스타일시트에 속하며, 한 클래스가 수십 개의 선언을 가지고 수십 개의 엘리먼트에서 공유될 수 있습니다. style prop은 값이 런타임에만 알려지고 CSS가 도달할 방법이 없을 때 그 자리를 차지합니다.

jsx
function ProgressBar({ percent }) {
  return (
    <div className="progress">
      <div className="progress__fill" style={{ width: `${percent}%` }} />
    </div>
  )
}

채우기의 색상, 높이, 전환 및 모서리 반경은 모두 .progress__fill에 있습니다. 스타일시트가 알 수 없는 유일한 값은 데이터에서 오므로 너비이고, 그 값만 인라인으로 갑니다. API에서 도착하는 URL을 가진 배경 이미지도 동일한 형태입니다.

jsx
<div className="hero" style={{ backgroundImage: `url(${photo.url})` }} />

크기, 위치 지정 및 오버레이는 .hero에 있고, URL만 prop을 통해 옵니다.

인라인 스타일에도 단단한 한계가 있습니다. 스타일 객체는 하나의 엘리먼트 선언을 설명하므로 호버 상태, 미디어 쿼리 또는 키프레임 애니메이션을 표현할 방법이 없습니다. 나머지 스타일링이 얼마나 동적이든 상관없이 이들은 CSS에 속합니다.

템플릿 리터럴 내의 단일 삼항 연산자는 읽기 좋습니다. 같은 문자열에 쌓인 2개 또는 3개는 백틱과 물음표의 벽이 되어 아무도 편집하고 싶지 않습니다. 컴포넌트가 하나 이상의 조건을 가지면 계산을 return 위로 옮기고 배열 형태가 일하도록 하세요.

jsx
function Button({ variant, size, isDisabled, isLoading, children }) {
  const classes = [
    'btn',
    `btn--${variant}`,
    `btn--${size}`,
    isDisabled && 'btn--disabled',
    isLoading && 'btn--loading',
  ]
    .filter(Boolean)
    .join(' ')

  return (
    <button className={classes} disabled={isDisabled}>
      {children}
    </button>
  )
}

각 줄은 하나의 결정이므로 네 번째 조건을 추가한다는 것은 표현식을 재구성하는 대신 줄을 추가하는 것을 의미하며, JSX는 속성이 단일 식별자이기 때문에 읽을 수 있습니다.

여러 클래스가 상호 배타적일 때 조회 객체는 삼항 연산자 체인을 이깁니다.

jsx
const statusClasses = {
  idle: 'card--idle',
  loading: 'card--loading',
  error: 'card--error',
}

const classes = ['card', statusClasses[status]].filter(Boolean).join(' ')

인식되지 않은 statusundefined를 생성하고, filter(Boolean)이 이를 제거하므로 엘리먼트는 속성에 리터럴 텍스트 "undefined"를 렌더링하는 대신 기본 card 클래스로 폴백됩니다.

모든 컴포넌트에서 이렇게 작성하기 시작하면 clsxclassnames 패키지가 몇 백 바이트로 동일한 작업을 하며, 조건부 부분에 객체 형태를 제공합니다. clsx('btn', { 'btn--disabled': isDisabled })처럼 말입니다. 이것들은 먼저 이해할 가치가 있는 위의 배열 버전에 비해 편의성입니다.

style prop은 객체를 받습니다. 이는 DOM이 스타일링을 노출하는 방식에 직접 매핑되기 때문입니다. 모든 엘리먼트는 style 속성 CSSStyleDeclaration을 가지며, 그 멤버는 camelCase CSS 속성 이름입니다. node.style.backgroundColor처럼 말입니다. React는 이 멤버들을 개별적으로 할당합니다. 문자열이란 cssText를 작성하는 것을 의미하며, 이는 어느 하나라도 변할 때마다 엘리먼트의 모든 선언을 다시 파싱하고 바꿉니다.

속성별로 할당하는 것은 스타일 업데이트를 diffable하게 만드는 것입니다. React는 이전 스타일 객체를 다음 객체와 키별로 비교합니다. 값이 일치하는 키는 그대로 두고, 변경된 키는 작성되며, 사라진 키는 빈 문자열로 재설정됩니다. 실제로 변경된 속성만 DOM을 건드립니다. CSS 문자열은 그 부분 업데이트를 표현할 방법이 없습니다.

vendor-prefixed 속성은 WebkitLineClamp 같이 첫 글자가 대문자인 동일한 camelCase 규칙을 따르며, ms는 예외로 소문자 상태를 유지합니다. (msOverflowStyle). React는 자동으로 prefix를 추가하지 않습니다.

custom property는 키가 CSS가 표기하는 것과 정확히 동일한 경우입니다.

jsx
<div className="card" style={{ '--accent': team.color }}>

React는 앞의 --를 보고 이를 setProperty를 통해 라우팅하며, 값을 그대로 전달합니다. px가 붙지 않으므로 단위는 사용자의 것입니다. 이는 두 접근 방식 간의 유용한 다리입니다. 데이터로부터 하나의 custom property를 인라인으로 설정하고 스타일시트가 수많은 규칙, pseudo-classes 및 미디어 쿼리에서 이를 사용하도록 합니다.

identity 비용은 알 가치가 있습니다. JSX에 직접 작성된 인라인 스타일 객체(위의 예처럼)는 모든 렌더링에서 새로운 객체를 할당합니다. 호스트 엘리먼트의 경우 React가 레퍼런스보다 값을 비교하고 일치할 때 아무것도 작성하지 않으므로 측정 가능한 비용이 없습니다. 객체가 컴포넌트 경계를 넘을 때 중요해집니다. memo로 감싼 자식에 새로 만들어진 스타일 객체를 전달하고, shallow prop 비교가 모든 렌더링에서 새로운 레퍼런스를 보고 memoization을 무효화하면서 어차피 자식을 다시 렌더링합니다. className 문자열은 값으로 비교하므로 그런 문제가 없습니다.

수정은 일반적입니다. 정적 스타일 객체를 모듈 범위로 끌어올려서 레퍼런스가 한 번만 생성되도록 하고, 동적 객체를 그들이 파생되는 값을 키로 하는 useMemo로 감싸세요. (Hooks 참조) 둘 다 프로필이 다시 렌더링 비용이 무언가를 나타내는 경우에만 할 가치가 있으며, 클래스 이름으로 도달하면 질문 자체가 사라집니다. 클래스는 브라우저에서 어차피 더 잘 확장됩니다. 일치된 규칙 세트는 클래스를 가진 모든 엘리먼트에서 공유되는 반면, 인라인 선언은 각 엘리먼트에서 반복되고 server-rendered HTML을 부풀립니다.

Juno먼저 클래스에 도달하고, 그다음 인라인 스타일을 선택하세요 React의 스타일링은 JavaScript에서 class가 예약어이기 때문에 className을 통해 붙인 이미 작성한 CSS입니다. 파일 맨 위에 스타일시트를 import하고, 규칙을 거기에 넣으며, 컴포넌트가 prop이나 상태로부터 문자열을 구성하여 어느 클래스가 적용되는지 결정하도록 하세요. 스타일시트가 미리 알 수 없는 값(데이터에서 오는 너비 같은)에 대해 style prop을 남겨두세요.
Juno먼저 클래스에 도달하고, 그다음 인라인 스타일을 선택하세요className은 표현식 슬롯이므로 템플릿 리터럴은 하나의 조건을 다루고 filter(Boolean).join(' ')이 있는 배열은 나머지를 다룹니다. JSX가 스캔 가능하게 유지되도록 return 위에 계산하고, 클래스가 상호 배타적일 때 조회 객체를 사용하세요. style prop은 camelCase 키를 받고, 길이 속성에 대해 숫자 앞에 px를 붙이며, opacity처럼 단위 없는 것들은 그대로 둡니다. 런타임 값에서 그 자리를 차지하며, 호버 상태나 미디어 쿼리를 전혀 표현할 수 없습니다.
Juno먼저 클래스에 도달하고, 그다음 인라인 스타일을 선택하세요style 객체는 CSSStyleDeclaration으로 매핑되기 때문에 존재하며, React가 cssText 문자열을 다시 파싱하는 대신 개별 속성을 diff하고 작성할 수 있습니다. --로 시작하는 키는 setProperty를 통해 그대로 통과하며, custom property를 런타임 데이터에서 스타일시트로 가장 깔끔한 다리로 만듭니다. 함정은 identity입니다. 매 렌더링마다 새로운 객체 리터럴은 호스트 엘리먼트에서는 보이지 않고 memo 경계에서는 치명적이므로 정적 객체를 모듈 범위로 끌어올리고, 동적 객체를 useMemo로 감싸고, 한 가지가 통할 곳에서 클래스 이름을 선호하세요.

다음: Props, 여기서 컴포넌트가 부모로부터 데이터를 받기 시작합니다.