[Daily morning study] CSS-in-JS vs CSS Modules ๋น„๊ต

#daily morning study

Image


CSS ์Šคํƒ€์ผ๋ง ๋ฐฉ์‹์˜ ๋ฐฐ๊ฒฝ

์ „ํ†ต์ ์ธ CSS๋Š” ์ „์—ญ ์Šค์ฝ”ํ”„๋กœ ๋™์ž‘ํ•œ๋‹ค. ํด๋ž˜์Šค ์ด๋ฆ„์ด ํ”„๋กœ์ ํŠธ ์–ด๋””์„œ๋“  ์ถฉ๋Œํ•  ์ˆ˜ ์žˆ๊ณ , ์–ด๋–ค ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์–ด๋–ค ์Šคํƒ€์ผ์„ ์“ฐ๋Š”์ง€ ์ถ”์ ํ•˜๊ธฐ ์–ด๋ ต๋‹ค. ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด CSS Modules์™€ CSS-in-JS ๋‘ ๊ฐ€์ง€ ์ ‘๊ทผ๋ฒ•์ด ๋“ฑ์žฅํ–ˆ๋‹ค.


CSS Modules

๊ฐœ๋…

CSS ํŒŒ์ผ์„ ๋ชจ๋“ˆ์ฒ˜๋Ÿผ importํ•ด์„œ ํด๋ž˜์Šค ์ด๋ฆ„์„ ๋กœ์ปฌ ์Šค์ฝ”ํ”„๋กœ ๋งŒ๋“œ๋Š” ๋ฐฉ์‹์ด๋‹ค. Webpack, Vite ๋“ฑ ์ฃผ์š” ๋นŒ๋“œ ๋„๊ตฌ์—์„œ ๊ธฐ๋ณธ ์ง€์›ํ•˜๋ฉฐ, ๋ณ„๋„ ๋Ÿฐํƒ€์ž„์ด ํ•„์š” ์—†๋‹ค.

๋™์ž‘ ๋ฐฉ์‹

๋นŒ๋“œ ํƒ€์ž„์— ๊ฐ ํด๋ž˜์Šค๋ช…์„ ๊ณ ์œ ํ•œ ํ•ด์‹œ๊ฐ€ ํฌํ•จ๋œ ์ด๋ฆ„์œผ๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค.

/* Button.module.css */
.button {
  background-color: blue;
  color: white;
  padding: 8px 16px;
}
// Button.jsx
import styles from './Button.module.css';

function Button({ children }) {
  return <button className={styles.button}>{children}</button>;
}

๋นŒ๋“œ ํ›„ ์‹ค์ œ HTML์—๋Š” .Button_button__abc12 ๊ฐ™์€ ๊ณ ์œ  ํด๋ž˜์Šค๋ช…์ด ์‚ฝ์ž…๋œ๋‹ค. ์ถฉ๋Œ์ด ๊ตฌ์กฐ์ ์œผ๋กœ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค.

์žฅ์ 

  • ๋นŒ๋“œ ํƒ€์ž„ ์ฒ˜๋ฆฌ โ†’ ๋Ÿฐํƒ€์ž„ ์˜ค๋ฒ„ํ—ค๋“œ ์—†์Œ
  • ๊ธฐ์กด CSS ๋ฌธ๋ฒ• ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
  • Sass, Less ๋“ฑ ์ „์ฒ˜๋ฆฌ๊ธฐ์™€ ์กฐํ•ฉ ๊ฐ€๋Šฅ
  • React Server Components(RSC)์™€ ์™„์ „ ํ˜ธํ™˜

๋‹จ์ 

  • ๋™์  ์Šคํƒ€์ผ๋ง์ด ๋ฒˆ๊ฑฐ๋กญ๋‹ค. props์— ๋”ฐ๋ผ ์Šคํƒ€์ผ์„ ๋ฐ”๊พธ๋ ค๋ฉด className์„ ์กฐ๊ฑด๋ถ€๋กœ ์กฐํ•ฉํ•ด์•ผ ํ•œ๋‹ค.
  • ์Šคํƒ€์ผ ํŒŒ์ผ๊ณผ ์ปดํฌ๋„ŒํŠธ ํŒŒ์ผ์ด ๋ถ„๋ฆฌ๋˜์–ด ์žˆ์–ด ํŒŒ์ผ ์ˆ˜๊ฐ€ ๋Š˜์–ด๋‚œ๋‹ค.
// ๋™์  ์Šคํƒ€์ผ๋ง โ€” ๋ถˆํŽธํ•˜์ง€๋งŒ ๊ฐ€๋Šฅ
import styles from './Button.module.css';
import cx from 'classnames';

function Button({ primary, children }) {
  return (
    <button className={cx(styles.button, { [styles.primary]: primary })}>
      {children}
    </button>
  );
}

CSS-in-JS

๊ฐœ๋…

์Šคํƒ€์ผ์„ JS/TSX ํŒŒ์ผ ๋‚ด๋ถ€์—์„œ ์ง์ ‘ ์„ ์–ธํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค. ๋Œ€ํ‘œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ styled-components, Emotion์ด ์žˆ๋‹ค.

๋™์ž‘ ๋ฐฉ์‹

์ปดํฌ๋„ŒํŠธ ๋‹จ์œ„๋กœ ์Šคํƒ€์ผ์„ ์„ ์–ธํ•˜๊ณ , ๋Ÿฐํƒ€์ž„์— ๋™์ ์œผ๋กœ CSS ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•ด์„œ <style> ํƒœ๊ทธ์— ์ฃผ์ž…ํ•œ๋‹ค.

// styled-components ์˜ˆ์‹œ
import styled from 'styled-components';

const Button = styled.button`
  background-color: ${(props) => (props.$primary ? 'blue' : 'white')};
  color: ${(props) => (props.$primary ? 'white' : 'black')};
  padding: 8px 16px;
`;

function App() {
  return <Button $primary>Click me</Button>;
}

Emotion์€ css prop ๋ฐฉ์‹๋„ ์ œ๊ณตํ•œ๋‹ค.

/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';

const buttonStyle = (primary) => css`
  background-color: ${primary ? 'blue' : 'white'};
  color: ${primary ? 'white' : 'black'};
  padding: 8px 16px;
`;

function Button({ primary, children }) {
  return <button css={buttonStyle(primary)}>{children}</button>;
}

์žฅ์ 

  • JS ๋ณ€์ˆ˜, props, ์ƒํƒœ ๊ฐ’์„ ์Šคํƒ€์ผ์— ์ง์ ‘ ํ™œ์šฉ โ†’ ๋™์  ์Šคํƒ€์ผ๋ง์ด ์ž์—ฐ์Šค๋Ÿฝ๋‹ค
  • ์Šคํƒ€์ผ๊ณผ ์ปดํฌ๋„ŒํŠธ ๋กœ์ง์ด ํ•œ ํŒŒ์ผ์— ์žˆ์–ด ์‘์ง‘๋„๊ฐ€ ๋†’๋‹ค
  • ํด๋ž˜์Šค๋ช… ์ถฉ๋Œ์ด ์ž๋™์œผ๋กœ ๋ฐฉ์ง€๋œ๋‹ค

๋‹จ์ 

  • ๋Ÿฐํƒ€์ž„ ์˜ค๋ฒ„ํ—ค๋“œ: ์Šคํƒ€์ผ์„ ๋Ÿฐํƒ€์ž„์— ์ƒ์„ฑยท์ฃผ์ž…ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์„ฑ๋Šฅ ๋น„์šฉ์ด ์žˆ๋‹ค
  • ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ž์ฒด์˜ ๋ฒˆ๋“ค ํฌ๊ธฐ ์ถ”๊ฐ€ (styled-components ~30KB gzip ๊ธฐ์ค€)
  • React Server Components(RSC)์—์„œ ๋™์ž‘ํ•˜์ง€ ์•Š๋Š”๋‹ค. ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋Š” ๋Ÿฐํƒ€์ž„ JS๋ฅผ ์‹คํ–‰ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
  • SSR ํ™˜๊ฒฝ์—์„œ ๋ณ„๋„ ์„ค์ •์ด ํ•„์š”ํ•˜๋‹ค

๋น„๊ต ์š”์•ฝ

ํ•ญ๋ชฉCSS ModulesCSS-in-JS (styled-components, Emotion)
์Šคํƒ€์ผ ์œ„์น˜๋ณ„๋„ .module.css ํŒŒ์ผJS/TSX ํŒŒ์ผ ๋‚ด๋ถ€
์ฒ˜๋ฆฌ ์‹œ์ ๋นŒ๋“œ ํƒ€์ž„๋Ÿฐํƒ€์ž„
๋™์  ์Šคํƒ€์ผ๋ง๋ถˆํŽธ (className ์กฐํ•ฉ ํ•„์š”)์ž์—ฐ์Šค๋Ÿฝ๊ณ  ๊ฐ„๊ฒฐํ•จ
๋Ÿฐํƒ€์ž„ ์„ฑ๋Šฅ์šฐ์ˆ˜ (์˜ค๋ฒ„ํ—ค๋“œ ์—†์Œ)์ƒ๋Œ€์ ์œผ๋กœ ๋А๋ฆผ
RSC ํ˜ธํ™˜์„ฑ๊ฐ€๋Šฅ๋Œ€๋ถ€๋ถ„ ๋ถˆ๊ฐ€
๋ฒˆ๋“ค ํฌ๊ธฐ์˜ํ–ฅ ์—†์Œ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋งŒํผ ์ฆ๊ฐ€
CSS ์ „์ฒ˜๋ฆฌ๊ธฐ ์ง€์›๊ฐ€๋Šฅ๋ณ„๋„ ์„ค์ • ํ•„์š”

Zero-runtime CSS-in-JS

๋Ÿฐํƒ€์ž„ ๋น„์šฉ ์—†์ด CSS-in-JS์˜ ํŽธ์˜์„ฑ์„ ์–ป์œผ๋ ค๋Š” ์‹œ๋„๋กœ Zero-runtime ๋ฐฉ์‹์ด ๋“ฑ์žฅํ–ˆ๋‹ค. ๋Œ€ํ‘œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ: vanilla-extract, Panda CSS, Linaria

์ด ๋„๊ตฌ๋“ค์€ JS์—์„œ ์Šคํƒ€์ผ์„ ์ž‘์„ฑํ•˜์ง€๋งŒ, ๋นŒ๋“œ ํƒ€์ž„์— ์ •์  CSS ํŒŒ์ผ์„ ์ƒ์„ฑํ•œ๋‹ค.

// vanilla-extract ์˜ˆ์‹œ
import { style } from '@vanilla-extract/css';

export const button = style({
  backgroundColor: 'blue',
  color: 'white',
  padding: '8px 16px',
});
// ๋™์  ์Šคํƒ€์ผ์€ styleVariants๋กœ ์ฒ˜๋ฆฌ
import { styleVariants } from '@vanilla-extract/css';

export const buttonVariant = styleVariants({
  primary: { backgroundColor: 'blue', color: 'white' },
  secondary: { backgroundColor: 'white', color: 'black' },
});

๋นŒ๋“œ ํ›„ .css ํŒŒ์ผ์ด ์ƒ์„ฑ๋˜๋ฉฐ, ๋Ÿฐํƒ€์ž„์—๋Š” ํด๋ž˜์Šค๋ช…๋งŒ ์ฐธ์กฐํ•œ๋‹ค. RSC์™€๋„ ํ˜ธํ™˜๋˜๊ณ  ์„ฑ๋Šฅ์€ CSS Modules ์ˆ˜์ค€์ด๋‹ค.


์„ ํƒ ๊ธฐ์ค€

  • ์ •์  ์Šคํƒ€์ผ ์œ„์ฃผ, ์„ฑ๋Šฅ ์ค‘์š”, Next.js App Router ์‚ฌ์šฉ โ†’ CSS Modules ๋˜๋Š” vanilla-extract
  • ๋™์  ์Šคํƒ€์ผ๋ง ๋นˆ๋ฒˆ, ๊ฐœ๋ฐœ ํŽธ์˜์„ฑ ์šฐ์„ , Pages Router ๋˜๋Š” CRA โ†’ Emotion, styled-components
  • ํƒ€์ž… ์•ˆ์ „์„ฑ๊ณผ Zero-runtime ๋‘˜ ๋‹ค ์›ํ•จ โ†’ vanilla-extract

Next.js 13+ App Router์—์„œ ๋Ÿฐํƒ€์ž„ CSS-in-JS๋ฅผ ์“ฐ๋ฉด ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ๋Š” ์Šคํƒ€์ผ์ด ์ ์šฉ๋˜์ง€ ์•Š๊ณ , ๋ฌด์กฐ๊ฑด ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ('use client')๋กœ ์„ ์–ธํ•ด์•ผ ํ•œ๋‹ค. ์ด ์ œ์•ฝ ๋•Œ๋ฌธ์— ์ตœ๊ทผ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” CSS Modules๋‚˜ Tailwind CSS๋กœ ์ „ํ™˜ํ•˜๋Š” ์‚ฌ๋ก€๊ฐ€ ๋Š˜๊ณ  ์žˆ๋‹ค.