리액트 스타일링 - CSS-in-JS
CSS-in-JS 개념
CSS-in-JS는 자바스크립트 파일 안에 스타일을 정의하고 적용하는 방식으로 동적 스타일링을 매우 쉽게 처리할 수 있습니다. 이 방식은 컴포넌트 단위로 스타일을 작성하므로 클래스 충돌이 없고 재사용성도 높습니다.
CSS-in-JS 방식은 정통 스타일 방식이였던 html, js, css로 분리하여 스타일을 적용하는 것이 아닌, js 내부에서 css를 정의하는 방식이다. 정통 스타일 방식은 역할에 따라 구조가 분리되었다는 점에서 장점이 있었지만, 컴포넌트 중심의 설계로 웹 개발이 더 세분화 되면서 다음과 같은 문제가 발생했다.
정통 스타일 방식 리액트 적용 문제점
1. 전역 스코프 문제 각 컴포넌트 별로 따로 css가 적용되어야하는데 전역으로 스타일이 적용되면 클래스 이름이 충돌되지 않게 처리해야해서 번거로웠다. 이 문제는 정통 스타일 방식에서도 CSS 모듈을 써서 해결하려 했었다.
** 2. UI 인터렉션에 따른 스타일 변경 힘듦** 이 부분이 가장 큰 문제가 되었던 부분이다. 리액트 개발은 UI 인터렉션, 즉 js 코드에 따라 UI 스타일이 바뀌는 부분이 많은데 CSS 파일이 분리되어있으면 동적 상태 코드에 따라 스타일을 변경하는 것이 번거롭다.
3. 유지보수 힘듦 현재 적용되어있는 스타일이 어떤 것인지, 사용하지 않는 스타일이 어떤 것인지 파악이 힘들고, 그에 따라 유지보수도 어려워진다.
따라서 요즘은 정통 스타일링 방식보다는 CSS-in-JS 방식이 각광받고 있는 추세고, 대표적인 라이브러리로는 styled-components, emotion, vanilla-extract 3가지가 있다.
styled-components
styled-components는 **js의 태그드 템플릿 리터럴(함수+문자열)**을 이용하여 스타일링 된 컴포넌트를 생성한다. props에 조건을 추가해 컴포넌트를 생성할 때마다 동적으로 스타일을 변경하는 것도 가능하다.
다만 styled-components는 25년 3월 유지보수에만 집중하겠다고 발표해 새로운 프로젝트에 사용하는 것은 권장되지는 않는다.
태그드 템플릿 리터럴
태그드 템플릿 리터럴은 템플릿 리터럴 앞에 함수를 붙여 문자열을 처리할 수 있는 자바스크립트 문법입니다.
function tagFunction(strings, ...values) {
console.log(strings);
console.log(values);
}
const name = '엄노니';
const age = 30;
tagFunction`name: ${name} age: ${age}`;
/* 출력
[ 'name: ', ' age: ' ]
[ '엄노니', 30 ]
*/
설치 및 기본 사용법
npm install styled-components
import styled from 'styled-components';
const Button = styled.button`
background: red;
color: white;
`;
export default function App() {
return <Button>버튼</Button>;
HTML 태그 이름에 해당하는 함수(예시에서는 button)를 사용해 스타일 컴포넌트를 만든다.
이 때 스타일 컴포넌트는 반드시 컴포넌트 외부나 별도 파일에 선언하여, 리렌더링 될 때마다 스타일 컴포넌트가 생성되는 것을 막는다.
동적 스타일링
import styled, { css } from 'styled-components';
const Button = styled.button<{ $primary?: boolean }>`
background: red;
color: white;
${(props) => props.$primary && css`background: blue;`}
`;
export default function App() {
return <Button $primary>버튼</Button>;
동적 스타일링을 위해 우선 css 함수를 불러온다. css 함수를 사용하면 단순 문자열을 사용하는 것과 달리 구문 강조와 인텔리센스 활용이 가능하다.
그리고 styled.button을 사용하여 버튼을 생성할 때, props로 boolean 타입의 $primary 변수를 추가한다.
$primary처럼 변수 앞에 $ 기호를 붙여 전달하는 방식을 트랜지언트 props(transient props)라고 합니다.
트랜지언트 props는 styled-components에서 제공하는 전용 기능으로, 컴포넌트가 스타일링 props를 계속 하위로 전달하는 것을 막기 위해 도입하였다.
$primary props는 styled-components 라이브러리 내부에서 스타일링에만 쓰고 최종 DOM에는 전달되지 않는다.
styled-components는 컴포넌트가 렌더링될 때 ${(props) => ...} 함수를 실행하여 현재 전달된 props를 인자로 넘겨준다.
컴포넌트에서는 <Button $primary>처럼 속성 이름만 적어도 $primary={true}와 동일하게 처리한다.
따라서 위 예시에서는 배경색이 파랗게 적용된다.
emotion
emotion은 **js의 태그드 템플릿 리터럴(함수+문자열)**을 이용하여 JSX의 className을 생성하고 스타일을 적용한다. 속성에 조건을 넣어 동적으로 스타일을 변경하는 것도 가능하다.
설치 및 기본 사용법
npm i @emotion/css
import { css } from '@emotion/css';
export default function App() {
return(
<button
className={css`
background: red;
color: white;
&:hover { background: yellow; }
`}
>버튼</button>
);
}
<button>태그는 className 속성을 통해 css 함수를 호출하고, css 함수는 자동으로 클래스를 생성해 스타일을 적용한다.
런타임에서 확인하면 클래스 이름이 동적으로 생성되어 추가된 것을 확인할 수 있다.
동적 스타일링
import { css } from '@emotion/css';
export default function App() {
const isActive = true;
return(
<button
className={css`
background: ${isActive? blue : red};
color: white;
&:hover { background: yellow; }
`}
>버튼</button>
);
}
emotion은 템플릿 문자열 안에 삼항 연산자를 사용하여 간단하게 동적 스타일을 구현할 수 있다.
vanilla-extract
vanilla-extract은 **별도의 파일(.css.ts)**에 클래스 별 CSS를 작성하고, JSX의 className으로 import하여 스타일을 적용한다.
.css.ts 확장자를 가진 파일에 css를 작성한 후 import하여 사용하는데, 개인적으로는 문자열이 아닌 객체 기반이라 IDE의 인텔리센스가 바로 적용되어 사용하기에는 가장 편했다.
스타일을 정의할 때 오타가 나면 빌드 시점에 에러가 난다.
Q. 정통 방식 처럼 파일이 따로 분리된다는 점에서 코드 파악이나 유지보수 측면에서 불편함이 없을까?
A. ‘CSS-in-JS의 철학’과 ‘빌드 성능’ 사이의 절충안이라고 볼 수 있다.
완전히 한 파일에 넣으면 빌드 도구(Vite, Webpack 등)가 스타일만 따로 추출해내기 까다로워진다.
.css.ts로 분리하면 빌드 타임에 리액트 코드와 스타일 코드를 독립적으로 처리할 수 있어 최적화가 쉬워진다고 한다.

설치 및 기본 사용법
$ npm install @vanilla-extract/css $npm install —save-dev @vanilla-extract/vite-plugin
vite.config.ts에 플러그인 추가
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react'
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
export default defineConfig({
plugins: [react(), vanillaExtractPlugin()],
});
// App.css.ts
import { style } from '@vanilla-extract/css';
export const button = style({
background: 'red',
color: 'white'
':hover': {
background: 'yellow',
},
});
// App.tsx
import { button } from './App.css.ts';
export default function App() {
return (
<button className={button}>버튼</button>
);
}
동적 스타일링
// App.css.ts
import { style } from '@vanilla-extract/css';
export const button = style({
background: 'red',
color: 'white',
':hover': {
background: 'yellow',
},
});
export const active = style({
background: 'blue'
});
// App.tsx
import { button, active } from './App.css.ts';
export default function App() {
const isActive = true;
return (
<button className={`${button} ${isActive && active}`}>버튼</button>
);
}
vanilla-extract은 런타임에서 동적으로 css를 생성하지 않고, 미리 생성한 클래스를 조건에 따라 적용하는 방식으로 동적 스타일링을 한다.