๐Ÿ’ป์šฉ๋‡ฝ ๊ฐœ๋ฐœ ๋…ธํŠธ๐Ÿ’ป
article thumbnail
๋ฐ˜์‘ํ˜•

[Next.js] styled-components์—์„œ vanilla-extract๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ง„ํ–‰ (app router)

๐Ÿ“– ๋“ค์–ด๊ฐ€๋ฉฐ

๊ธฐ์กด ํ”„๋กœ์ ํŠธ์—์„œ Next.js 14๋ฒ„์ „์˜ ์•ฑ ๋ผ์šฐํ„ฐ์—์„œ styled-components๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

์ด์ „์— Next.js 11๋ฒ„์ „์—์„œ 14๋ฒ„์ „์œผ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ง„ํ–‰ํ•˜๋ฉด์„œ ๋‚จ์•„์žˆ๋˜ ๋ถ€๋ถ„์ด์—ˆ์Šต๋‹ˆ๋‹ค.

 

์•ฑ ๋ผ์šฐํ„ฐ์—์„œ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง€์›ํ•˜๊ธฐ ์‹œ์ž‘ํ•˜๋ฉด์„œ, ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ํ™œ์šฉ ํ•ด๋ณด๊ณ ์ž ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

๊ทธ๋Ÿผ ๋จผ์ € ์™œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ง„ํ–‰ํ–ˆ๊ณ , ์™œ vanilla-extract๋ฅผ ์„ ํƒํ–ˆ๋Š”์ง€ ๊ฐ„๋žตํžˆ ์„ค๋ช…ํ•ด ๋ณผ๊ฒŒ์š”.

๐Ÿ” ์•ฑ ๋ผ์šฐํ„ฐ(app router)์—์„œ styled-components์˜ ๋ฌธ์ œ์ 

styled-components๋Š” ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์—, styled-components๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด 'use client'๋ฅผ ๋ช…์‹œํ•จ์œผ๋กœ์จ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ฐ•์ œ์ ์œผ๋กœ ์‚ฌ์šฉํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ, ๋Ÿฐํƒ€์ž„ ํ™˜๊ฒฝ์—์„œ ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์— ์ œ๋กœ ๋Ÿฐํƒ€์ž„ ํ™˜๊ฒฝ์—์„œ ๋™์ž‘ํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ์ƒ๋Œ€์ ์œผ๋กœ ๋ Œ๋”๋ง์ด ๋Šฆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โœ”๏ธ vanilla-extract ์„ ํƒ ์ด์œ 

์™œ ์ˆ˜๋งŽ์€ ์Šคํƒ€์ผ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ค‘์—์„œ vanilla-extarct๋ฅผ ์„ ํƒํ–ˆ์„๊นŒ์š”?

 

์ฒ˜์Œ์—๋Š” Next.js ๊ณต์‹๋ฌธ์„œ์—์„œ ์ž์ฒด์ ์œผ๋กœ ์ถ”์ฒœํ•ด ์ฃผ๋Š” Tailwind Css๋ฅผ ๊ณ ๋ คํ•ด ๋ดค์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ Tailwind css๋Š” ์ถ”๊ฐ€ ๋ฌธ๋ฒ•์„ ์˜๋„์น˜ ์•Š๊ฒŒ ์™ธ์›Œ์•ผ ํ•˜๋Š” ์ƒํ™ฉ์„ ๊ณ ๋ คํ•˜๊ฒŒ ๋˜์—ˆ๊ณ ,

๊ฐœ์ธ์ ์œผ๋กœ CSS-in-JS ํ˜•์‹์ด ๊ฐ€๋…์„ฑ ๋ถ€๋ถ„์—์„œ ๋” ์ข‹๋‹ค๊ณ  ์ƒ๊ฐํ•˜์—ฌ ์„ ํ˜ธํ•˜๋Š” ํŽธ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

 

styled-components์˜ CSS-in-JS๋ฅผ ๋”ฐ๋ผ๊ฐ€๋ฉด์„œ ์ œ๋กœ ๋Ÿฐํƒ€์ž„ ํ™˜๊ฒฝ์—์„œ ๋™์ž‘ํ•˜๋ฉฐ ์„œ๋ฒ„์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง€์›ํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ค‘์—

panda css์™€ vanilla-extract ์ค‘์—์„œ ๊ณ ๋ฏผ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

ํ˜„์žฌ ์ œ ํ”„๋กœ์ ํŠธ๋Š” Typescript ํ™˜๊ฒฝ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

 

vanilla-extract๋Š” Typescript๋กœ css๋ฅผ ์ž‘์„ฑํ•˜๋ฉฐ ์ •ํ™•ํžˆ CSS-in-JS๊ฐ€ ์•„๋‹Œ CSS-in-TS๋ผ๊ณ  ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ๋ฉด์—์„œ ๊ฐ•๋ ฅํ•œ ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ์ง€์›์ด ๋œ๋‹ค๊ณ  ํŒ๋‹จํ•˜์—ฌ ๋” ๋Œ๋ฆฌ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

 

๋˜ํ•œ, npm trends๋ฅผ ํ™•์ธํ•ด ๋ดค์„ ๋•Œ vanilla-extract๊ฐ€ ์ƒ๋Œ€์ ์œผ๋กœ ๋งŽ์ด ์‚ฌ์šฉ๋ ๋ฟ๋”๋Ÿฌ, pandacss๊ฐ€ vanilla-extract์— ์˜๊ฐ์„ ๋ฐ›์•˜๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

vanilla-extract vs pandcss npm trends

 

์ด๋Ÿฌํ•œ ์ด์œ ๋กœ vanilla-extarct๋ฅผ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.

โœ”๏ธ vanilla-extract์„ ์„ ํƒํ•จ์œผ๋กœ์จ ๊ธฐ๋Œ€ํ•˜๋Š” ๋ถ€๋ถ„

  • ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ํ™œ์šฉ์œผ๋กœ ์ธํ•œ ๋ฒˆ๋“ค ํฌ๊ธฐ ๊ฐ์†Œ ๋ฐ ๋„คํŠธ์›Œํฌ ๋น„์šฉ ๊ฐ์†Œ ๊ธฐ๋Œ€
  • ๋Ÿฐํƒ€์ž„์— CSS๋ฅผ ์ƒ์„ฑํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋” ๋น ๋ฅธ ํŽ˜์ด์ง€ ๋กœ๋“œ ๊ธฐ๋Œ€ (์ œ๋กœ ๋Ÿฐํƒ€์ž„)

โœ๏ธ ์ง„ํ–‰ ๊ณผ์ •

๋จผ์ € vanilla-extract๊ฐ€ ์ž˜ ์ ์šฉ๋˜๋Š”์ง€ ํ™•์ธํ•˜๋ฉด์„œ ์ž‘์—…ํ•˜๊ธฐ ์œ„ํ•ด ์ž‘์€ ์ปดํฌ๋„ŒํŠธ๋ถ€ํ„ฐ ์ ์ง„์ ์œผ๋กœ ์ ์šฉํ•˜๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค.

โœ”๏ธ ์„ค์น˜ ๋ฐ ์ ์šฉ

https://vanilla-extract.style/documentation/integrations/next/

 

Next.js — vanilla-extract

Zero-runtime Stylesheets-in-TypeScript.

vanilla-extract.style

์œ„ ๊ณต์‹๋ฌธ์„œ๋ฅผ ๋ณด๋ฉฐ ์„ค์น˜๋ฅผ ํ•˜๊ณ  ์ ์šฉํ•˜๋Š” ๊ณผ์ •์„ ๊ฑฐ์ณค์Šต๋‹ˆ๋‹ค.

 

next.config.js์— ํ•ด๋‹น ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

next.config.js

๋‹น์‹œ ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด styled-componetns๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ์ฝ”๋“œ๊ฐ€ ๊ณต์กดํ•˜๊ณ  ์žˆ๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โœ”๏ธ theme ์ •์˜

๊ธฐ์กด styled-comopnents์—์„œ๋Š” GlobalStyles.ts์—์„œ ์ „์—ญ ์Šคํƒ€์ผ์„ ์ •์˜ํ•ด์„œ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ theme์— ์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” ์Šคํƒ€์ผ ์ฝ”๋“œ๋„ ์ •์˜ํ•˜์—ฌ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

์•„๋ž˜๋Š” styled-comopnents๋ฅผ ์‚ฌ์šฉํ•  ๋‹น์‹œ ์ฝ”๋“œ์˜ ์ผ๋ถ€์ž…๋‹ˆ๋‹ค.

// theme.ts
const margin = {
  small: '.5rem',
  base: '1rem',
  medium: '1.5rem',
  large: '2rem',
  xLarge: '3rem',
};

const padding = {
  small: '.5rem',
  base: '1rem',
  large: '2rem',
  xLarge: '3rem',
};
...

// GlobalStyle.ts
...
const GlobalStyle = styled.createGlobalStyle`
  ${reset};
  * {
    box-sizing: border-box;
  }
  ${({ theme }) => {
    return styled.css`
      body {
        background-color: ${theme.color.mainBackground};
        font-family: ${notoSansKr.style.fontFamily};
        font-size: ${theme.font.size.small};
        color: ${theme.color.mainFontColor};
        user-select: none;
        transition: all 0.25s linear;
      }
    `;
  }}
  a {
    text-decoration: none;
    color: inherit;
  }
`;

export default GlobalStyle;

 

vanilla-extract ๋˜ํ•œ ๊ฐ•๋ ฅํ•œ theme ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด ์ฃผ๊ธฐ ๋•Œ๋ฌธ์— ๋งค์šฐ ์ข‹์•˜์Šต๋‹ˆ๋‹ค.

// globalTheme.css.ts

...
export const global = createGlobalTheme(':root', {
  fontFamily: {
    notoSansKR: `var(--font-noto-sans-kr)`,
  },
  fontSize: {
    xLarge: '48px',
    large: '36px',
    medium: '28px',
    regular: '18px',
    small: '16px',
    micro: '14px',
  },
..
  margin: {
    small: '.5rem',
    base: '1rem',
    medium: '1.5rem',
    large: '2rem',
    xLarge: '3rem',
  },
  padding: {
    small: '.5rem',
    base: '1rem',
    large: '2rem',
    xLarge: '3rem',
  },
  ...
});

globalStyle('*', {
  boxSizing: 'border-box',
});

globalStyle('body', {
  fontSize: global.fontSize.small,
 ...
  userSelect: 'none',
  transition: 'all 0.25s linear',
});

globalStyle('a', {
  textDecoration: 'none',
  color: 'inherit',
});

createGlobalTheme์„ ํ†ตํ•ด ๊ธ€๋กœ๋ฒŒ ์Šคํƒ€์ผ๋กœ css ๋ณ€์ˆ˜๋ฅผ ์‰ฝ๊ฒŒ ์ง€์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ, globalStyle์„ ํ†ตํ•ด ์ „์—ญ ์Šคํƒ€์ผ์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

๋ฌผ๋ก  ๋‹คํฌ๋ชจ๋“œ ๊ด€๋ จํ•œ ์ฝ”๋“œ๋„ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

vanilla-extarct ๋‹คํฌ๋ชจ๋“œ ๊ด€๋ จํ•ด์„œ๋Š” ๋‹ค์Œ ํฌ์ŠคํŒ…์—์„œ ๋‹ค๋ฃฐ ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.

โœ”๏ธ reset css

๊ธฐ์กด์— styled-components๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋Š” styled-reset ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ค์น˜ํ•˜์—ฌ GlobalStyles.ts์— ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

 

ํ•˜์ง€๋งŒ vanilla-extract์—์„œ๋Š” ๋””ํดํŠธ๋กœ reset CSS๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์—, ์ง์ ‘ reset.css ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ  reset ์Šคํƒ€์ผ์„ ์ ์šฉํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

reset.css๋ฅผ layout ์ปดํฌ๋„ŒํŠธ์—์„œ import ํ•˜์—ฌ ์ ์šฉํ–ˆ๊ณ , ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋†’์ด๊ธฐ ์œ„ํ•ด globalTheme.css๋ณด๋‹ค ์œ„์— ์„ ์–ธํ–ˆ์Šต๋‹ˆ๋‹ค.

import 'src/styles/reset.css';
import 'src/styles/globalTheme.css';

https://meyerweb.com/eric/tools/css/reset/

 

CSS Tools: Reset CSS

CSS Tools: Reset CSS The goal of a reset stylesheet is to reduce browser inconsistencies in things like default line heights, margins and font sizes of headings, and so on. The general reasoning behind this was discussed in a May 2007 post, if you're inter

meyerweb.com

โœ”๏ธ ์ ์ง„์  ์ ์šฉ

๋จผ์ € layout ์ปดํฌ๋„ŒํŠธ๋ถ€ํ„ฐ ์ ์ง„์ ์œผ๋กœ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

// layout.css.ts
import { style } from '@vanilla-extract/css';
import { global } from 'styles/globalTheme.css';

export const container = style({
  width: '100%',
  maxWidth: global.device.mobile,
  minHeight: '100vh',
  margin: 'auto',
  padding: global.padding.base,
});

CSS-In-TS ํ˜•์‹์œผ๋กœ ๊ธฐ์กด styled-components ์ฝ”๋“œ์™€ ๋ฌธ๋ฒ•์ ์œผ๋กœ ํฐ ์ฐจ์ด๊ฐ€ ์—†์–ด์„œ ๋งค์šฐ ํŽธํ–ˆ์Šต๋‹ˆ๋‹ค.

 

๋Œ€์‹  ํŒŒ์ผ ์„ ์–ธ์€ 'ํŒŒ์ผ์ด๋ฆ„.css.ts'๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

 

์ดํ›„ ์Šคํƒ€์ผ ์ ์šฉ์€ ์•„๋ž˜์™€ ๊ฐ™์ด ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import * as style from './layout.css';

<div className={style.container}>
 <DarkModeBtn />
 {children}
 <Footer />
</div>

์ ์šฉ ํ›„ ๋ธŒ๋ผ์šฐ์ €์—์„œ ํ™•์ธํ•œ ๊ฒฐ๊ณผ ์•„๋ž˜์™€ ๊ฐ™์ด ์ž˜ ์ ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.


vanilla-extarct๋„ ๊ฐ class๋งˆ๋‹ค ๊ณ ์œ ํ•œ class๋ฅผ ์ƒ์„ฑํ•ด์„œ ๋ถ€์—ฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— class๊ฐ€ ์ค‘๋ณต๋˜๋Š” ๋ถ€๋ถ„์„ ๋”ฐ๋กœ ๊ณ ๋ คํ•˜์ง€ ์•Š์•„๋„ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

styled-compoents์™€ ๋‹ค๋ฅด๊ฒŒ css ๋ณ€์ˆ˜๋กœ ์ž˜ ์ ์šฉ์ด ๋˜์–ด ์žˆ๋Š” ๋ชจ์Šต์ž…๋‹ˆ๋‹ค.

 

๊ทธ๋ฆฌ๊ณ  ๋ธŒ๋ผ์šฐ์ € ์ฝ˜์†”๋กœ head๋ฅผ ํ™•์ธํ•ด ๋ณด๋ฉด layout.css์— ๋Œ€ํ•œ stylesheet๊ฐ€ ๋ถˆ๋Ÿฌ์˜ค๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค!

์ด๊ฒƒ์ด ๋ฐ”๋กœ ๋นŒ๋“œ ์‹œ์ ์— CSS ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜์—ฌ ์ž˜ ์ ์šฉํ•˜๊ณ  ์žˆ๋‹ค๋Š” ์˜๋ฏธ์ž…๋‹ˆ๋‹ค.

 

๋˜ํ•œ, ์ด์ „์— ์ ์šฉํ–ˆ๋˜ ๊ธ€๋กœ๋ฒŒ ๋ณ€์ˆ˜๋„ ์•„๋ž˜์™€ ๊ฐ™์ด ์ž˜ ์ƒ์„ฑ๋˜์–ด ์‚ฌ์šฉ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

โœ”๏ธ ๋™์  ์Šคํƒ€์ผ๋ง

์ œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋™์  ์Šคํƒ€์ผ๋ง์„ ํ†ตํ•ด ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ Button ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒ์„ฑํ•ด์„œ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

๋จผ์ € ๊ธฐ์กด styled-components๋ฅผ ์‚ฌ์šฉํ•œ Button ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค.

// Button.tsx
'use client'

import styled, { css } from 'styled-components';

type Color =
  | 'contentBackground'
  | 'white'
  | 'whiteYellow'
  | 'yellow'
  | 'whiteBlue'
  | 'bluePuple'
  | 'lightBlue'
  | 'lightGray'
  | 'whiteGray'
  | 'darkGray'
  | 'laime'
  | 'lightRed'
  | 'pink'
  | 'red'
  | 'black';

const SButton = styled.button<{
  bgColor: Color;
  fontColor: Color;
  borderColor: Color;
}>`
  ${({ theme, bgColor, fontColor, borderColor }) => {
    return css`
      width: 100%;
      padding: ${theme.padding.small} ${theme.padding.base};
      margin-bottom: ${theme.margin.medium};
      border: 2px solid ${theme.color[borderColor]};
      border-radius: 4px;
      color: ${theme.color[fontColor]};
      font-size: ${theme.font.size.small};
      font-family: ${theme.font.family.base};
      background-color: ${theme.color[bgColor]};
      cursor: pointer;
    `;
  }}
`;

interface IButtonProps {
  children: React.ReactNode;
  bgColor?: Color;
  fontColor?: Color;
  borderColor?: Color;
  onClick?: (event: React.MouseEvent<HTMLElement>) => any;
  name?: string;
}

const Button = ({
  children,
  onClick,
  bgColor = 'contentBackground',
  fontColor = 'lightBlue',
  borderColor = 'whiteBlue',
  name,
}: IButtonProps) => (
  <SButton onClick={onClick} {...rest}>
    {children}
  </SButton>
);

export default Button;

 

๋™์  ์Šคํƒ€์ผ๋ง์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์ถ”๊ฐ€๋กœ vanilla-extract/dynamic์„ ์„ค์น˜ํ•ด ์คฌ์Šต๋‹ˆ๋‹ค.

https://vanilla-extract.style/documentation/packages/dynamic/

 

Dynamic — vanilla-extract

Zero-runtime Stylesheets-in-TypeScript.

vanilla-extract.style

๋™์ ์œผ๋กœ css variables๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•ด @vanilla-extract/dynamic์—์„œ๋Š” assignInlineVars api๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

๋Ÿฐํƒ€์ž„์— ์ƒ์„ฑํ•˜์ง€๋งŒ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ž์ฒด๋Š” ๋งค์šฐ ๊ฐ€๋ฒผ์šฐ๋ฉฐ ์„ฑ๋Šฅ ์ €ํ•˜๋ฅผ ์ผ์œผํ‚ค์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

 

createVar์™€ assignInlineVars๋ฅผ ํ™œ์šฉํ•ด์„œ ๋™์  ์Šคํƒ€์ผ๋ง์ด ๊ฐ€๋Šฅํ•˜๋„๋ก ํ•ด์คฌ์Šต๋‹ˆ๋‹ค.

// button.css.ts
import { style, createVar } from '@vanilla-extract/css';
import { global } from 'src/styles/globalTheme.css';

export const backgroundColor = createVar();
export const fontColor = createVar();
export const borderColor = createVar();

export const button = style({
  display: 'flex',
  justifyContent: 'center',
  alignItems: 'center',
  width: '100%',
  padding: `${global.padding.small} ${global.padding.base}`,
  marginBottom: global.margin.medium,
  border: `2px solid ${borderColor}`,
  borderRadius: 4,
  color: fontColor,
  fontSize: global.fontSize.small,
  fontFamily: global.fontFamily.notoSansKR,
  backgroundColor,
  cursor: 'pointer',
});
// Button.tsx
import { global, vars } from 'src/styles/globalTheme.css';
import { assignInlineVars } from '@vanilla-extract/dynamic';
import * as style from './button.css';

...
const Button = ({
  children,
  onClick,
  bgColor,
  fontColor = 'lightBlue',
  borderColor = 'whiteBlue',
  name,
}: IButtonProps) => (
  <button
    className={style.button}
    style={assignInlineVars({
      [style.backgroundColor]: bgColor
        ? global.color[bgColor]
        : `hsl(${vars.themeColor.color.contentBackground})`,
      [style.fontColor]: global.color[fontColor],
      [style.borderColor]: global.color[borderColor],
    })}
    onClick={onClick}
    name={name}
  >
    {children}
  </button>
);

export default Button;

๋˜ํ•œ, 'use client'๋ฅผ ์ œ๊ฑฐํ•จ์œผ๋กœ์จ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋กœ ๋™์ž‘ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

 

์ด๋Ÿฌํ•œ ๋ฐฉ์‹์œผ๋กœ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ ์ ์ง„์ ์œผ๋กœ vanilla-extarct๋ฅผ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

โœ”๏ธ next/font ์ ์šฉ ๋ฌธ์ œ

next/font๋ฅผ ๊ธ€๋กœ๋ฒŒ ๋ณ€์ˆ˜๋กœ ์ ์šฉํ•˜๋Š” ๋ฐ ์žˆ์–ด์„œ ๋ฌธ์ œ๋ฅผ ๋งž์ดํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ์กด styled-components์—์„œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ์„ ์–ธ ํ›„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

// GlobalStyle.ts
import { Noto_Sans_KR } from 'next/font/google';
import * as styled from 'styled-components';
import reset from 'styled-reset';

export const notoSansKr = Noto_Sans_KR({
  weight: ['300', '400', '500'],
  subsets: ['latin'],
});
const GlobalStyle = styled.createGlobalStyle`
  ..
  ${({ theme }) => {
    return styled.css`
      body {
      ..
        font-family: ${notoSansKr.style.fontFamily};
..
      }
    `;
  }}
 ..
`;

export default GlobalStyle;

 

๊ทธ๋ž˜์„œ ํ•ด๋‹น ํฐํŠธ๋ฅผ ์ ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‹œ๋„ํ•ด ๋ดค์Šต๋‹ˆ๋‹ค.

layout ์ปดํฌ๋„ŒํŠธ์—์„œ next/font๋ฅผ ์„ ์–ธ ํ›„ globalTheme.css.ts์—์„œ import ํ•œ ํ›„ ๋™์ผํ•˜๊ฒŒ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š”์ง€..?

 

๋„๋ฌด์ง€ ๊ฐ์ด ์žกํžˆ์ง€ ์•Š์•„์„œ ๊ตฌ๊ธ€๋ง ํ•ด ๋ณธ ๊ฒฐ๊ณผ ๊ฐ™์€ ๋ฌธ์ œ๋ฅผ ๊ฒช๊ณ  ์žˆ๋Š” issue๋ฅผ ํ†ตํ•ด ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

https://github.com/vanilla-extract-css/vanilla-extract/discussions/1019#discussioncomment-6776002

 

Global font-family with `next/font` ? · vanilla-extract-css vanilla-extract · Discussion #1019

Hi, I'm trying to use the custom font in my Next app using next/font method. Here's my file having global styles: import { Inter } from "next/font/google"; const inter = Inter({ weight: "variable",...

github.com

 

๋จผ์ € layout ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉํ•  next/font๋ฅผ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

export const notoSansKr = Noto_Sans_KR({
  display: 'swap',
  weight: ['300', '400', '500'],
  subsets: ['latin'],
  variable: '--font-noto-sans-kr',
});

์—ฌ๊ธฐ์„œ variable์„ ๊ผญ ๋ช…์‹œํ•ด์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ด๊ฒŒ css์—์„œ ์‚ฌ์šฉํ•  ๋ณ€์ˆ˜๋ช…์ด ๋ฉ๋‹ˆ๋‹ค.

 

๊ทธ๋ฆฌ๊ณ  ๋จผ์ € body์— ํ•ด๋‹น ํฐํŠธ๋ฅผ ์ ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์•„๋ž˜์™€ ๊ฐ™์ด className์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ko" suppressHydrationWarning>
      <body className={`${notoSansKr.className}`}>
      ...
      </body>
      ..
    </html>
  );
}

 

์ดํ›„ ํ•ด๋‹น ๋ณ€์ˆ˜๋ช…์„ ์‚ฌ์šฉํ•ด ์ค๋‹ˆ๋‹ค.

export const global = createGlobalTheme(':root', {
  fontFamily: {
    notoSansKR: `var(--font-noto-sans-kr)`,
  },

๊ทธ๋ฆฌ๊ณ  ์‚ฌ์šฉํ•˜๋ ค๋Š” css ํŒŒ์ผ์—์„œ ์•„๋ž˜์™€ ๊ฐ™์ด ํฐํŠธ๋ฅผ ์ ์šฉํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

import { style, createVar } from '@vanilla-extract/css';
import { global } from 'src/styles/globalTheme.css';


export const button = style({
 ...
  fontFamily: global.fontFamily.notoSansKR,
 ...
});

โœ”๏ธ styled-components ๊ด€๋ จ ์ฝ”๋“œ ๋ฐ ์ข…์†์„ฑ ์ œ๊ฑฐ

๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์— ์ •์ƒ์ ์œผ๋กœ vanilla-extract๋ฅผ ์ ์šฉํ•œ ํ›„ ๋“œ๋””์–ด styled-compnents๋ฅผ ๊ฑท์–ด๋‚ผ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ styled-components ๋•Œ๋ฌธ์— ๊ฐ•์ œ์ ์œผ๋กœ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋กœ ์‚ฌ์šฉํ•ด์•ผ ํ–ˆ๋˜ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค.

 

์ด์ œ ์ •๋ง๋กœ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋กœ๋งŒ ๋™์ž‘ํ•ด์•ผ ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๋งŒ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋กœ ์ž‘๋™ํ•˜๊ณ , ๋‚˜๋จธ์ง€๋Š” ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋กœ ์ž‘๋™ํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿง ๊ฒฐ๊ณผ

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ง„ํ–‰ํ•œ ๊ฒฐ๊ณผ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋กœ ์ž‘๋™์ด ์ž˜ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๋„คํฌ์›Œํฌ ํƒญ

์ด๋กœ ์ธํ•ด์„œ ์ดˆ๊ธฐ์— ๋‹ค์šด๋กœ๋“œํ•˜๋Š” ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํŒŒ์ผ๊ณผ ํฌ๊ธฐ๊ฐ€ ์ค„์–ด๋“œ๋Š” ๊ฐœ์„  ํšจ๊ณผ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

styled-comopnent (๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ด์ „) vanilla-extract (๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ดํ›„)
   

 

vanilla-extract๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ง„ํ–‰ํ•œ ๊ฒƒ์€ ์ตœ๊ณ ์˜ ์„ ํƒ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

ํ”„๋กœ์ ํŠธ์ƒ ๋‹จ์ ์€ ๋”ฑํžˆ ์—†๊ณ  ์žฅ์ ๋งŒ ๋‚จ์€ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด ๋œ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

๋Œ€์‹ ์— ๊ฐœ๋ฐœํ•˜๋ฉด์„œ ๋‹จ์ ๋„ ๋ถ„๋ช… ์กด์žฌํ–ˆ์Šต๋‹ˆ๋‹ค.

์•„๋ฌด๋ž˜๋„ styled-components๋กœ ๊ฐœ๋ฐœํ•  ๋•Œ๋ณด๋‹ค ์ฐธ๊ณ ์ž๋ฃŒ๊ฐ€ ๋ถ€์กฑํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ๊ณต์‹๋ฌธ์„œ์™€ ๊นƒํ—ˆ๋ธŒ issue์— ๋งŽ์ด ์˜์กดํ•˜์—ฌ ๊ฐœ๋ฐœ์„ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

๋Œ€์‹  ๋ฌธ๋ฒ•์ ์œผ๋กœ๋‚˜ ๋ณต์žกํ•œ ๊ธฐ์ˆ ์ด ์š”๊ตฌ๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๊ธฐ์กด์— CSS-in-JS ๋ฐฉ์‹์— ์ต์ˆ™ํ–ˆ๋‹ค๋ฉด ๊ธˆ๋ฐฉ ์ ์‘์ด ๊ฐ€๋Šฅํ•œ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

๋ฐ˜์‘ํ˜•
profile

๐Ÿ’ป์šฉ๋‡ฝ ๊ฐœ๋ฐœ ๋…ธํŠธ๐Ÿ’ป

@์šฉ๋‡ฝ

ํฌ์ŠคํŒ…์ด ์ข‹์•˜๋‹ค๋ฉด "์ข‹์•„์š”โค๏ธ" ๋˜๋Š” "๊ตฌ๋…๐Ÿ‘๐Ÿป" ํ•ด์ฃผ์„ธ์š”!