PandaCSS์™€ ํ•จ๊ป˜ CSS-in-JS์˜ ๋ฏธ๋ž˜๋กœ

taehee
  • #PandaCSS
  • #CSS-in-JS
  • #styled-component

์•ˆ๋…•ํ•˜์„ธ์š”. ํ”„๋กฌ ์Šคํ† ์–ด ํŒ€์—์„œ ํ…Œ์ŠคํŠธ์™€ ์ด๊ฒƒ์ €๊ฒƒ ์ „๋„ํ•˜๋Š” ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž ๊น€ํƒœํฌ์ž…๋‹ˆ๋‹ค.

์ œ๊ฐ€ ํ”„๋กฌ ํŒ€์— ํ•ฉ๋ฅ˜ํ•œ์ง€ 9๊ฐœ์›”์ด ๋˜์–ด๊ฐ‘๋‹ˆ๋‹ค. 5์›”์— ์˜คํ”ˆํ•œ ํ”„๋กฌ ์Šคํ† ์–ด๋„ ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์ด ์ถ”๊ฐ€๋˜๋ฉฐ ์ปค์ ธ์™”์Šต๋‹ˆ๋‹ค. ์›น๋ทฐ๋‚˜ B2B ์„œ๋น„์Šค๋ฅผ ํฌํ•จํ•ด ๋‹ค์–‘ํ•œ ํ”„๋กœ์ ํŠธ์™€ ๊ธฐ์ˆ ์Šคํƒ๋„ ๋„์ž…๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ค๋Š˜์€ ์ €ํฌ ํŒ€์ด ์ƒˆ๋กœ์šด ํ”„๋กœ์ ํŠธ๋“ค์— PandaCSS๋ฅผ ๋„์ž…ํ•˜๊ฒŒ ๋œ ๋ฐฐ๊ฒฝ๊ณผ ์ด์ , ์ฃผ์˜ํ•ด์•ผํ•  ์ , ์ œ์•ฝ ๋“ฑ์„ ์†Œ๊ฐœํ•ด๋ณด๋ ค ํ•ฉ๋‹ˆ๋‹ค. CSS in JS ์˜ ๋Ÿฐํƒ€์ž„ ์šฉ๋Ÿ‰์ด๋‚˜ ์„ฑ๋Šฅ ์ด์Šˆ, ๋””์ž์ธ ์‹œ์Šคํ…œ ๋“ฑ์— ๊ด€์‹ฌ์ด ์žˆ๋Š” ๋ถ„์ด๋ผ๋ฉด ํŠนํžˆ ์œ ์šฉํ•˜๊ฒŒ ๋Š๊ปด์ง€์‹œ๋ฆฌ๋ผ ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

styled component์™€์˜ ์• ์ฆ์˜ ์‹œ๊ฐ„

styled component๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ถ„๋“ค์—๊ฒŒ ์–ด๋–ค ์ ์ด ์ข‹์•˜๋Š”์ง€ ๋ฌผ์–ด๋ณด๋ฉด ๋น„์Šทํ•œ ์ด์•ผ๊ธฐ๋ฅผ ํ•ด์ฃผ์‹œ๊ณ ๋Š” ํ•ฉ๋‹ˆ๋‹ค. Component๋กœ ๋งŒ๋“ค๋ฉด์„œ jsx ๋งˆํฌ์—…์ด ๊น”๋”ํ•ด์ง„๋‹ค๊ฑฐ๋‚˜. css์˜ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๊ธฐ ์‰ฝ๋‹ค๋Š” ์‹์œผ๋กœ ๋ง์ด์ฃ . ์ €๋„ ๋™์˜ํ•˜๊ณ  ์ €ํฌ ํŒ€์—์„œ๋„ styled component๋ฅผ ์“ฐ๋ฉด์„œ css์— ๋Œ€ํ•œ ์ดํ•ด๊ฐ€ ๊นŠ์–ด์ง€๊ณ , ๋ฉ‹์ง€๊ฒŒ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ๋˜ ๊ฒฝ์šฐ๋„ ๋งŽ์ง€ ์•Š์•˜๋‚˜ ์‹ถ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ํ•ด์™ธ ์‚ฌ์šฉ์ž ๋ถ„๋“ค์ด ๋น ๋ฅด๊ฒŒ ํŽ˜์ด์ง€๋ฅผ ๋ณด์‹ค ์ˆ˜ ์žˆ๋„๋ก ์ตœ์ ํ™”ํ•˜๋ ค ๊ณ ๋ฏผํ•˜๋˜ ์ค‘, ๋ฒˆ๋“ค ์šฉ๋Ÿ‰์ด ์ปค์ง„๋‹ค๊ฑฐ๋‚˜, ๋Ÿฐํƒ€์ž„ ์†๋„๊ฐ€ ๋Š๋ ค์ง€๋Š” ๋“ฑ์˜ ์ด์Šˆ๋ฅผ ๋ฐœ๊ฒฌํ•˜๊ธฐ ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค.

์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” CSS๊ฐ€ ๋ฒˆ๋“ค์— ํฌํ•จ๋จ

์ด๋ฏธ์ง€๋‚˜ ํฐํŠธ ๋“ฑ์„ ๊ฒฝ๋Ÿ‰ํ™”ํ•˜๊ณ  ๋‚˜๋‹ˆ, ์‚ฌ์ดํŠธ ์„ฑ๋Šฅ์€ JS ๋ฒˆ๋“ค์„ ๋‹ค์šด ๋ฐ›๊ณ  hydrationํ•  ๋•Œ๊นŒ์ง€์˜ ์‹œ๊ฐ„์œผ๋กœ ๊ฒฐ์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํŠนํžˆ code spliting์„ ์ ์šฉํ•˜๋ฉด ๊ฐ ํŽ˜์ด์ง€๋งˆ๋‹ค ๋˜ ๋ฒˆ๋“ค์„ ๋‹ค์šด ๋ฐ›์•„์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฒˆ๋“ค ํฌ๊ธฐ๋ฅผ ์ค„์ด๋Š” ๊ฒŒ ์ค‘์š”ํ–ˆ์Šต๋‹ˆ๋‹ค.

JS ๋ฒˆ๋“ค ํฌ๊ธฐ๋ฅผ ์ค„์ด๋ผ๊ณ  ๊ถŒ๊ณ ํ•˜๋Š” ๋ผ์ดํŠธํ•˜์šฐ์Šค

๊ทธ๋Ÿฌ๋ฉด JS ๋ฒˆ๋“ค ํฌ๊ธฐ์™€ CSS๊ฐ€ ๋ฌด์Šจ ์ƒ๊ด€์ผ๊นŒ์š”? CSS in JS๋ผ๋Š” ๋ง์ฒ˜๋Ÿผ styled component์˜ ์Šคํƒ€์ผ์€ js์ด๊ณ  ๋ฒˆ๋“ค์— ํฌํ•จ๋˜๋Š” ๊ฒŒ ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค!

๋‹ค์Œ์€ ์ €ํฌ ๊ณต์šฉ ์ปดํฌ๋„ŒํŠธ์˜ ๋ฒˆ๋“ค ํฌ๊ธฐ๋ฅผ @next/bundle-analyzer ๋กœ ๋ถ„์„ํ•œ ๋ชจ์Šต์ธ๋ฐ์š”. ์ €ํฌ๋Š” ์Šคํƒ€์ผ์„ ๊ฐ ์ปดํฌ๋„ŒํŠธ๋งˆ๋‹ค styles.ts๋ผ๋Š” ๋ณ„๋„ ํŒŒ์ผ์— ์ƒ์„ฑํ•˜๋Š”๋ฐ, ๋Œ€๋ถ€๋ถ„์„ style์ด ์ฐจ์ง€ํ•˜๊ณ  ์žˆ๋Š” ๊ฑธ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

styles.ts ๊ฐ€ ๋ฒˆ๋“ค์˜ 80% ์ด์ƒ์„ ์ฐจ์ง€ํ•˜๋Š” ๋ชจ์Šต

์™œ ๊ทธ๋Ÿด๊นŒ์š”? ์ €ํฌ ํ”„๋กฌ ๋””์ž์ธ ์‹œ์Šคํ…œ์˜ ์ปดํฌ๋„ŒํŠธ๋“ค๋„ ๋‹ค์–‘ํ•œ variants๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด Button์ด๋ผ ํ•˜๋ฉด primary, secondary, tertiary, outline, quiet ๋“ฑ์ด ์žˆ๋Š”๋ฐ์š”. ์ƒ‰๊น”์— ๋”ฐ๋ผ keycolor, mono, error ๋“ฑ์˜ theme๋„ ์žˆ์–ด์„œ ๊ทธ ๊ฐ€์ง“์ˆ˜๊ฐ€ ๋งค์šฐ ๋งŽ์Šต๋‹ˆ๋‹ค. disabled ๊ฐ™์ด ์ผ๋ฐ˜์ ์ธ ๊ฑด ๊ณ„์‚ฐํ•ด์„œ ํ‰์นœ๋‹ค๊ณ  ํ•ด๋„, keycolor primary, keycolor secondaryโ€ฆ ๋“ฑ๋“ฑ ์กฐํ•ฉ์— ๋”ฐ๋ผ 5 x 3 x n x m โ€ฆ ์œผ๋กœ ๊ฐ€์ง“์ˆ˜๊ฐ€ ๋Š˜์–ด๋‚˜๋Š”๋ฐ์š”. ์ด ๋ชจ๋“  ์Šคํƒ€์ผ์„ ๋„ฃ์œผ๋‹ˆ styles.ts๋งŒ ๋น„๋Œ€ํ•ด์ง€๊ฒŒ ๋œ ๊ฒƒ์ด์ฃ .

๋งค์šฐ ๋‹ค์–‘ํ•œ variants์„ ๊ฐ€์ง„ ๋ฒ„ํŠผ๋“ค

๋ชจ๋“  ํŽ˜์ด์ง€์—์„œ ์ด๋ ‡๊ฒŒ ๋‹ค์–‘ํ•œ variants๋ฅผ ๋ชจ๋‘ ์‚ฌ์šฉํ•˜์ง€๋Š” ์•Š์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด error variants๋งŒ ํ•ด๋„ ๋Œ€๋ถ€๋ถ„์˜ ํŽ˜์ด์ง€์—์„œ ์‚ฌ์šฉํ•˜์ง€ ์•Š์ฃ . ํ•˜์ง€๋งŒ styled component๋Š” ๊ทธ์ € js์•ˆ์— css๋ฅผ ๋ฌธ์ž์—ด๋กœ ๋„ฃ์–ด๋†“์€ ๊ฒƒ์ด๋ผ. ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” CSS๋„ ๋ชจ๋‘ ๋ฒˆ๋“ค์— ํฌํ•จ๋ฉ๋‹ˆ๋‹ค. ํŠนํžˆ ๊ณต์šฉ ์ปดํฌ๋„ŒํŠธ๋ผ๋ฉด ๋ชจ๋“  ํŽ˜์ด์ง€์— ํฌํ•จ๋˜์ฃ ! ๋’ค์—์„œ ์ด์•ผ๊ธฐํ•˜๊ฒ ์ง€๋งŒ tailwind ๋“ฑ์˜ ์†”๋ฃจ์…˜๋“ค์€ ์ด๋ ‡๊ฒŒ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” style์€ ์•Œ์•„์„œ css ์Šคํƒ€์ผ ์‹œํŠธ์— ํฌํ•จ์‹œํ‚ค์ง€ ์•Š๋Š”๋ฐ, ์•„์‰ฌ์šด ์ ์ด ๋งŽ์•˜์Šต๋‹ˆ๋‹ค.

๋ฌผ๋ก  gzip์ด ๋น„์Šทํ•˜๊ฒŒ ์ƒ๊ธด ์ฝ”๋“œ๋Š” ์ค‘๋ณต์ด ๋งŽ๋”๋ผ๋„ ์šฉ๋Ÿ‰์„ ์ค„์—ฌ์ค˜์„œ ๊ฐ ์Šคํƒ€์ผ์€ 1~2kb ๋ฐ–์— ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋Ÿฐ ๊ฒŒ ์ˆ˜ ์‹ญ ์ˆ˜ ๋ฐฑ ๊ฐœ๊ฐ€ ๋˜๋ฉด ์ด์ œ ๋ฌด์‹œํ•  ์ˆ˜ ์—†๋Š” ์ •๋„๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ์ดˆ๊ธฐ์—๋Š” ํฐํŠธ๋‚˜, ์ด๋ฏธ์ง€, ์•„์ด์ฝ˜, ๋น„๋Œ€ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋“ฑ์ด ๋ฌธ์ œ์˜€์ฃ . ํ•˜์ง€๋งŒ ์ด๋Ÿฐ ํฐ ์นœ๊ตฌ๋“ค์„ ์ตœ์ ํ™”ํ•˜๋ฉด ์ตœ์ ํ™”ํ•  ์ˆ˜๋ก, ์ค„์–ด๋“ค์ง€ ์•Š๋Š” ์Šคํƒ€์ผ์˜ ์ž๋ฆฌ๊ฐ€ ์ ์  ์ปค๋ณด์˜€์Šต๋‹ˆ๋‹ค.

๋Ÿฐํƒ€์ž„ ์„ฑ๋Šฅ ์ด์Šˆ

styled component๋Š” ํŽธ๋ฆฌํ•˜๊ฒŒ? ๋ณ€์ˆ˜์— ๋”ฐ๋ผ ์กฐ๊ฑด๋ถ€ ์Šคํƒ€์ผ๋ง์„ ํ•ด์ฃผ๋Š” ๊ธฐ๋Šฅ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ €ํฌ๋„ ๊ธฐ์กด์— ์ด๋Ÿฐ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ์š”. ์‚ฌ์šฉํ•˜๋‹ค๋ณด๋‹ˆ ์‚ฌ์šฉ์ž ์ž…๋ ฅ์ด ๋Š๋ ค์ง€๊ฑฐ๋‚˜ ๋ฒ„๋ฒ…์ด๋Š” ์ด์Šˆ ๋“ฑ์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. rerender๊ฐ€ ํŠนํžˆ ์ž์ฃผ๋˜๋Š” TextField ๊ฐ™์€ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฌธ์ œ์˜€๋Š”๋ฐ์š”.

react๋Š” signal์ด๋‚˜ ์ปดํŒŒ์ผ๋Ÿฌ๋ฅผ ์“ฐ๋Š” ๋‹ค๋ฅธ ํ”„๋ ˆ์ž„์›Œํฌ๋“ค์ฒ˜๋Ÿผ ์ƒํƒœ๊ฐ€ ๋ณ€ํ•˜๋ฉด ๋ณ€๊ฒฝ๋œ ๋ถ€๋ถ„๋งŒ ์ง์ ‘ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ, ์ปดํฌ๋„ŒํŠธ ํ•จ์ˆ˜๋ฅผ ๋‹ค์‹œ ์‹คํ–‰ํ•˜๊ณ  ์ปดํฌ๋„ŒํŠธ์˜ ๊ฐ€์ƒ๋”์„ ๋ชจ๋‘ ๋‹ค์‹œ ๋งŒ๋“  ๋’ค์— ์‹ค์ œ ๋”๊ณผ diffํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‹ค๋ณด๋‹ˆ ๋งค๋ฒˆ styled component๋„ ๋‹ค์‹œ ์‹คํ–‰๋˜๊ณ ์š”. ์Šคํƒ€์ผ์ด ์กฐ๊ธˆ์ด๋ผ๋„ ๋ฐ”๋€Œ๋ฉด classname๋„ ๋‹ค์‹œ ๋งŒ๋“ค์–ด์„œ head์— ์ถ”๊ฐ€ํ•˜๋‹ค๋ณด๋‹ˆ. ๋ธŒ๋ผ์šฐ์ €์—๊ฒŒ๋„ ๋งค๋ฒˆ ์ƒˆ๋กœ์šด css๋ฅผ ํŒŒ์‹ฑํ•˜๊ณ , ํ‰๊ฐ€ํ•˜๊ณ , ๋ Œ๋”ํ•˜๋Š”๋ฐ ๋ณ‘๋ชฉ์ด ์ƒ๊ธฐ๋Š” ๊ฑด ์•„๋‹๊นŒ ์‹ถ์—ˆ์Šต๋‹ˆ๋‹ค.

์ €ํฌ๋Š” ์ด๋Ÿฐ ๋ฌธ์ œ๋“ค์„ ๋™์ ์ธ ๋ถ€๋ถ„์„ css variable์„ ์‚ฌ์šฉํ•œ๋‹ค๊ฑฐ๋‚˜, aria attribute ๋“ฑ์„ ์ด์šฉํ•ด ์ •์ ์œผ๋กœ ์Šคํƒ€์ผ๋ง ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์šฐํšŒํ•ด์™”๋Š”๋ฐ์š”. ์ด์Šˆ๊ฐ€ ์žˆ์„ ๋•Œ๋งˆ๋‹ค styled component๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์“ฐ์ง€ ์•Š๋Š” ์‹์œผ๋กœ ํ•ด๊ฒฐํ•˜๋Š” ๊ฒŒ ๋ฐ˜๋ณต๋˜๊ณ  ๊ทธ๊ฒŒ ๋” ๊ฐ„๊ฒฐํ•˜๊ณ  ์ ‘๊ทผ์„ฑ์ด๋‚˜ ์‹œ๋งจํ‹ฑ์„ ์œ„ํ•ด์„œ๋„ ๋” ๋‚˜์€ ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„์กŒ์Šต๋‹ˆ๋‹ค.

/* before */
${ArrowIcon} {
    ${({ isOpen }) => isOpen && 'transform:rotateZ(180deg);'}
}
/* after */
&[aria-expanded='true'] ${ArrowIcon} {
    transform: rotateZ(180deg);
}

๋ฐ‘๋ฐ”๋‹ฅ๋ถ€ํ„ฐ ๋””์ž์ธ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค๊ธฐ๊ฐ€ ์–ด๋ ต๋‹ค

styled component๋Š” ๋””์ž์ธ ์‹œ์Šคํ…œ์„ ์œ„ํ•œ ๊ธฐ๋Šฅ์„ ๋”ฑํžˆ ์ œ๊ณตํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋””์ž์ธ ์‹œ์Šคํ…œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค๊ณ , props์˜ type์„ ์ง€์ •ํ•˜๋Š” ๋“ฑ์˜ ์ผ์„ ์ง์ ‘ ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํŠนํžˆ ๋ณต์žกํ•œ ์ปดํฌ๋„ŒํŠธ์˜ type์„ ๋งŒ๋“œ๋Š” ๊ฑด ์–ด๋ ค์šด ์ผ์ธ๋ฐ์š”. ๋‹ค์–‘ํ•œ ๊ฒฝ์šฐ์˜ ์ˆ˜๋งˆ๋‹ค ์กฐ๊ฑด๋ถ€ ์Šคํƒ€์ผ์„ ๋‹ฌ๊ธฐ ์œ„ํ•ด ์ฝ๊ธฐ๋„ ์–ด๋ ต๊ณ  ๋ณต์žกํ•œ ์ฝ”๋“œ๊ฐ€ ๋Š˜์–ด๋‚˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

ํŠนํžˆ ์Šคํƒ€์ผ๊ณผ ๊ธฐ๋Šฅ์ด ์ปดํฌ๋„ŒํŠธ์— ๊ฒฐํ•ฉ๋˜๋ฉด์„œ ๊ฐ™์€ ๋ฒ„ํŠผ ์Šคํƒ€์ผ์„ button์—๋„, link์—๋„, ๋•Œ๋กœ๋Š” ๋ฒ„ํŠผ์ฒ˜๋Ÿผ ์ƒ๊ธด radio๋‚˜ option, tab ๋“ฑ์— ์ ์šฉํ•˜๋ ค ํ•˜๋‹ˆ ํ™•์žฅ์„ฑ์ด ๋–จ์–ด์ง€๊ณ , ๋งค๋ฒˆ ๊ณต์šฉ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ณ ์ณ์ค˜์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.


export type ButtonRole = 'button' | 'link' | 'checkbox' | 'radio' | 'option' | 'tab';
export type ButtonThemeT = 'key' | 'mono' | 'error';
export type ButtonVariantT = 'primary' | 'secondary' | 'tertiary' | 'outline' | 'quiet';
export type ButtonSizeT = 'h28' | 'h32' | 'h40' | 'h48' | 'h56';

export interface ButtonProps {
    role?: ButtonRole;
    theme?: ButtonThemeT;
    variant?: ButtonVariantT;
    size?: ButtonSizeT;
    disabled?: boolean;
    // ... 
}

PandaCSS๋ฅผ ์„ ํƒํ•œ ์ด์œ 

PandaCSS๋Š” ChakraUI๋กœ ์œ ๋ช…ํ•œ Sage(@thesegunadebayo)์”จ์˜ ์•ผ์‹ฌ์ž‘์ž…๋‹ˆ๋‹ค. tailwind, stitches, styled-component, vanilla-extract, cva ๋“ฑ ๋‹ค์–‘ํ•œ ์ƒํƒœ๊ณ„์˜ ๊ฒฝํ—˜์— ์ถ•์ ๋œ ๋…ธํ•˜์šฐ๋ฅผ ์ง‘๋Œ€์„ฑํ•ด์„œ, ํ’๋ถ€ํ•˜๊ณ  ๋‹ค์ฑ„๋กœ์šด ๊ธฐ๋Šฅ์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฏธ ๋‹ค์–‘ํ•œ ํšŒ์‚ฌ์˜ ๊ด€์‹ฌ์„ ๋ฐ›๊ณ  ์žˆ์ฃ .

ChakraUI๋Š” ๊ธฐ์กด์— ๋Ÿฐํƒ€์ž„ css in js ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ธ emotion์„ ์‚ฌ์šฉํ•˜๋ฉด์„œ ์—ฌ๋Ÿฌ ์ด์Šˆ๋ฅผ ๊ฒช์–ด์™”์Šต๋‹ˆ๋‹ค. ํŠนํžˆ SSR, Next Server Component ๋“ฑ์„ ์ง€์›ํ•˜๋ฉด์„œ ์ •๋ง ๋‹ค์–‘ํ•œ ์—๋Ÿฌ์™€ ์ด์Šˆ๊ฐ€ ๋งŽ์•˜์Šต๋‹ˆ๋‹ค. ์ €๋„ ChakraUI๋ฅผ ์‚ฌ์šฉํ•ด๋ณธ ์ ์ด ์žˆ๋Š”๋ฐ์š”. ๋‹ค๋ฅธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์Šคํƒ€์ผ๊ณผ ์ถฉ๋Œ์ด ๋‚˜๊ธฐ๋„ ํ•˜๊ณ  ๊ณ ํ†ต์Šค๋Ÿฌ์šด ์ด์Šˆ๋“ค์„ ๊ฒช์—ˆ์Šต๋‹ˆ๋‹ค.

์ž์„ธํ•œ ์ด์•ผ๊ธฐ๋Š” ํŒ๋‹ค๊ฐ€ ์ง์ ‘ ์†Œ๊ฐœํ•˜๊ณ  ์žˆ์œผ๋‹ˆ Why Panda?๋ฅผ ์ฝ์–ด๋ณด์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ(Server Components)์˜ ์ถœ์‹œ์™€ ์„œ๋ฒ„ ์šฐ์„  ํ”„๋ ˆ์ž„์›Œํฌ์˜ ๋“ฑ์žฅ์œผ๋กœ ๋Œ€๋ถ€๋ถ„์˜ ๊ธฐ์กด ๋Ÿฐํƒ€์ž„ CSS-in-JS ์Šคํƒ€์ผ๋ง ์†”๋ฃจ์…˜(์˜ˆ: Emotion, styled-component)์€ ์•ˆ์ •์ ์œผ๋กœ ์ž‘๋™ํ•  ์ˆ˜ ์—†๊ฑฐ๋‚˜ ๋” ์ด์ƒ ์ž‘๋™ํ•  ์ˆ˜ ์—†๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด ์ƒˆ๋กœ์šด ํŒจ๋Ÿฌ๋‹ค์ž„์€ ์„ฑ๋Šฅ, ๊ฐœ๋ฐœ ๋ฐ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์—์„œ ํฐ ์Šน๋ฆฌ์ด์ง€๋งŒ CSS-in-JS ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—๊ฒŒ๋Š” ์ƒˆ๋กœ์šด โ€œํ˜์‹ ํ•˜๊ฑฐ๋‚˜ ์•„๋‹ˆ๋ฉด ์ฃฝ๊ฑฐ๋‚˜โ€์˜ ๊ธฐ๋กœ์— ์„œ๊ฒŒํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋ฉด ์ €ํฌ ํŒ€์—์„œ ์‚ฌ์šฉํ•œ ์‚ฌ๋ก€๋“ค์„ ํ†ตํ•ด, PandaCSS์˜ ๊ธฐ๋Šฅ๊ณผ ์žฅ์ ์„ ์†Œ๊ฐœํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

๋””์ž์ธ ํ† ํฐ

PandaCSS๋Š” ๋””์ž์ด๋„ˆ์™€ ๊ณต์œ ํ•˜๋Š” ๋””์ž์ธ ํ† ํฐ์„ ์‰ฝ๊ฒŒ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ์กด์˜ styled-component๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ์—๋Š” ๋”ฐ๋กœ ๋ณ€์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด์„œ ๋งค๋ฒˆ importํ•˜๊ณ  ${} ์œผ๋กœ ๋„ฃ์–ด์ค˜์•ผ ํ–ˆ๋Š”๋ฐ์š”.

// before
import styled from 'styled-components';
import { BreakPoint, ColorVar, Typography } from 'Styles';

export const SectionTitle = styled.h3`
    ${Typography.Display2_600}
    ${BreakPoint.Tablet} {
        ${Typography.W_Display3_600}
    }
    color: ${ColorVar.text.strong};
    margin-bottom: "1.5rem",

`;

// after
// pandacss ๊ฐ€ ์ƒ์„ฑํ•œ ๋Ÿฐํƒ€์ž„ js๋ฅผ importํ•ฉ๋‹ˆ๋‹ค
import { styled } from '../../styled-system/jsx';

export const SectionTitle = styled.h3({
    base: {
        textStyle: { base: "Display2_600", tablet: "W_Display3_600" },
        color: "text.strong",
        mb: "1.5rem",
    }
});

typescript๋ฅผ ์ด์šฉํ•ด ์ž๋™์™„์„ฑ๋„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ํƒ€์ž…์„ ํ•˜๋‚˜ํ•˜๋‚˜ ์ง์ ‘ ๋‹ฌ์•„์ค„ ํ•„์š”๋„ ์—†์ด์š”!

textStyle: ์— Dis ๊นŒ์ง€๋งŒ ์ž…๋ ฅํ–ˆ๋Š”๋ฐ textStyle ๋ชฉ๋ก์ด ์ž๋™์™„์„ฑ ์ถ”์ฒœ์œผ๋กœ ๋œจ๋Š” ๋ชจ์Šต

์ปดํฌ๋„ŒํŠธ ๋ ˆ์‹œํ”ผ

๋‹ค์–‘ํ•œ ์ปดํฌ๋„ŒํŠธ์˜ variants๋„ recipe์™€ slot recipe๋ฅผ ์ด์šฉํ•ด ์‰ฝ๊ฒŒ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ๋งŒ๋“  recipe๋Š” component์˜ ๊ธฐ๋Šฅ๊ณผ ๋…๋ฆฝ์ ์ด๋ผ, ์–ด๋–ค ์ปดํฌ๋„ŒํŠธ์—๋„ ์‰ฝ๊ฒŒ ๋ถ™์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import { Link } from 'my-router-library';

<Link
    className={button({ size: "h40" })}
    href={page.url.next}
    aria-label="๋‹ค์Œ ํŽ˜์ด์ง€"
>
    &gt;
</Link>

๋ฌผ๋ก  ์ด๋ฅผ styled component์ฒ˜๋Ÿผ jsx component๋กœ ๋งŒ๋“ค์–ด์„œ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์•Œ์•„์„œ prop์œผ๋กœ variant๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๋ฌผ๋ก  ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ์™€ ์—๋””ํ„ฐ ์ง€์›๋„ ์ž˜ ๋˜๊ณ ์š”.

import { Link } from 'my-router-library';
import { styled } from '../../styled-system/jsx';

const LinkButton = styled(Link, button)

<LinkButton
    size="h40"
    href={page.url.next}
    aria-label="๋‹ค์Œ ํŽ˜์ด์ง€"
>
    &gt;
</LinkButton>

์ •์ ์ธ css๋กœ ๋‹ค์–‘ํ•œ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์ž์ฒด ์ง€์›

ํ•œํŽธ styled component๋‚˜ emotion ๋“ฑ์€ vue๋‚˜ svelte, solid ์™€ ๊ฐ™์€ ๋‹ค๋ฅธ ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ ์–ด๋ ค์› ์Šต๋‹ˆ๋‹ค. Sage์”จ๋Š” chakra-ui๋ฅผ vue๋กœ ํฌํŒ…ํ•˜๋ฉด์„œ ์ด ๋ฌธ์ œ๋ฅผ ๊นจ๋‹ฌ์œผ์‹  ๊ฒƒ ๊ฐ™์€๋ฐ์š”.

์ €ํฌ ํŒ€ ์—ญ์‹œ ์ตœ๊ทผ์— qwik์„ ํŒ€์— ๋„์ž…ํ•˜๋ฉด์„œ styled component๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์—ˆ์Šต๋‹ˆ๋‹ค. ์ƒˆ๋กœ์šด ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ๋„์ž…ํ•˜๋Š” ๊ฒƒ๋„ ํฐ ํ•™์Šต ๋น„์šฉ์ด ํ•„์š”ํ•œ๋ฐ, ๋‹ค์–‘ํ•œ ์‹ ๊ทœ ํ”„๋กœ์ ํŠธ๋งˆ๋‹ค ์„œ๋กœ ๋‹ค๋ฅธ ์Šคํƒ€์ผ๋ง ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ํ•™์Šต ๋น„์šฉ์€ ๋ช‡ ๋ฐฐ๋กœ ์ปค์งˆ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์ด ์—ญ์‹œ tailwind๋‚˜ vanilla extract ๋Š” ๋ฌผ๋ก ์ด๊ณ , emotion๋„ framework agnosticํ•œ ์ฝ”์–ด ๋กœ์ง์„ ์ œ๊ณตํ•ด์„œ ์ด๋ฏธ ํ•ด๊ฒฐํ•œ ๋ฌธ์ œ์˜€์Šต๋‹ˆ๋‹ค.

Panda๋Š” tailwind์ฒ˜๋Ÿผ ์ •์ ์œผ๋กœ style sheeet๋ฅผ ์ปดํŒŒ์ผํ•˜๋Š” ์ ‘๊ทผ๋ฒ•์„ ์“ฐ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. js ์ฝ”๋“œ๋Š” ๊ทธ์ € className์„ ์ƒ์„ฑํ•ด์ฃผ๋Š” ์—ญํ• ๋งŒ ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ์œ„์—์„œ ๋ณด์—ฌ๋“œ๋ฆฐ LinkButton์€ ๋‹ค์Œ์ฒ˜๋Ÿผ ๋ณ€ํ•ฉ๋‹ˆ๋‹ค.

<a class="button button--theme_keycolor button--variants_primary button--size_h40 button--shape_square" href="/2" aria-label="๋‹ค์Œ ํŽ˜์ด์ง€">
&gt;
</a>

<style>
@layer recipes {
    .button--size_h40 {padding: 0 .75rem;height: 2.5rem;font-size: .9375rem;font-weight: 500;line-height: 1.25rem}
    .button--theme_keycolor {--primary: var(--colors-keycolor-primary);--secondary: var(--colors-keycolor-secondary);--on_primary: var(--colors-keycolor-on_primary);--tertiary: var(--colors-keycolor-tertiary);--on_secondary_tertiary: var(--colors-keycolor-on_secondary_tertiary)}
    .button--variants_primary {background-color: var(--primary);color: var(--on_primary)}
    .button--shape_square {border-radius: .5rem}

    @layer _base {
        .button {width: 100%;box-sizing: border-box;display: flex;justify-content: center;align-items: center;flex-shrink: 0;--transition-duration: .125s;transition-duration: .125s;--transition-prop: filter outline;transition-property: filter outline;--transition-easing: ease-in-out;transition-timing-function: ease-in-out;position: relative}
        .button:is(:disabled, [disabled], [data-disabled]) {opacity: .24}
        .button _focus-visible {outline: 1px solid;outline-color: var(--colors-selected-on)}
        .button:is(:hover, [data-hover]) {filter: brightness(.95)}
        .button:is(:active, [data-active]) {filter: brightness(.9)}
    }
}
</style>

DaisyUI ๋“ฑ์„ ์‚ฌ์šฉํ•ด๋ณด์‹  ๋ถ„์—๊ฒŒ๋Š” ์ต์ˆ™ํ•œ ๋ชจ์–‘์ด์ฃ . ์ด๋Ÿฌํ•œ ๋ณต์žกํ•œ style์€ stylesheet์—๋งŒ ํฌํ•จ๋˜๊ธฐ ๋•Œ๋ฌธ์—, js ๋ฒˆ๋“ค๋„ ๋œ ์ฐจ์ง€ํ•˜์ฃ . ๋™์ ์œผ๋กœ ์Šคํƒ€์ผ์„ ์ƒ์„ฑํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋‹ˆ ๋Ÿฐํƒ€์ž„ ์„ฑ๋Šฅ ์ด์Šˆ๋„ ์—†์Šต๋‹ˆ๋‹ค. ๋ฌผ๋ก  Next App dir์€ ๋ฌผ๋ก ์ด๊ณ  Qwik์ด๋‚˜ Astro์—๋„ ๋ฌธ์ œ ์—†์ด ํ†ตํ•ฉ๋  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ์ € pure css๋‹ˆ๊นŒ์š”!

condition์„ ์ด์šฉํ•ด aria, dark, ๋ฐ˜์‘ํ˜•๋„ ์‰ฝ๊ฒŒ

์•ž์„œ styled-component์—์„œ aria๋ฅผ ์ด์šฉํ•ด ์กฐ๊ฑด๋ถ€ ์Šคํƒ€์ผ๋ง์„ ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ๋“œ๋ ธ๋Š”๋ฐ์š”. aria attribute๋ฅผ ์ด์šฉํ•˜๋ฉด ํ…Œ์ŠคํŠธ๋„ ์‰ฝ๊ณ , ์„ฑ๋Šฅ์ƒ์œผ๋กœ๋„ ์ข‹์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ css์˜ ๊ธฐ๋ณธ ๋ฌธ๋ฒ•์€ ์ข€ ์žฅํ™ฉํ•˜๊ฒŒ ๋Š๊ปด์งˆ ์ˆ˜๋„ ์žˆ๋Š”๋ฐ์š”. ์˜ˆ๋ฅผ ๋“ค์–ด ํ˜„์žฌ ์ฒดํฌ๋œ ์š”์†Œ์˜ ์ƒ‰์„ ๋ฐ”๊พธ๊ณ  ์‹ถ๋‹ค๋ฉด ๋‹ค์Œ์ฒ˜๋Ÿผ ํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

const LangSelectButton = styled.a`
    color: {ColorVar.text.six};
    &[aria-checked='true'] {
        color: {ColorVar.text.strong};
    }
`

์ด๋ฅผ panda์—์„œ๋Š” ์–ธ๋”๋ฐ”_๋กœ ์‹œ์ž‘ํ•˜๋Š” _checked condition์„ ์จ์„œ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

css({
    color: { base: 'text.six', _checked: 'text.strong' }
})

์ด๋Ÿฌํ•œ ์กฐ๊ฑด์€ ๋‹ค์–‘ํ•œ aria์— ๋Œ€ํ•ด preset์ด ์ •์˜๋˜์–ด ์žˆ๊ณ ์š”. ๋‹คํฌ๋ชจ๋“œ์™€ ๊ฐ™์€ condition์„ ์ง์ ‘ ์ •์˜ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. (ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ์ž๋™์™„์„ฑ๋„ ์•Œ์•„์„œ ๋ฉ๋‹ˆ๋‹ค.)

์˜ˆ๋ฅผ ๋“ค์–ด ๋‹ค์Œ์ฒ˜๋Ÿผ _dark condition์„ panda.config.ts์— ์ •์˜ํ•˜๋ฉดโ€ฆ

export const frommPreset = definePreset({
    conditions: {
        dark: '@media (prefers-color-scheme: dark)'
    },
    // ...
})

์ƒ‰๊น” ๊ฐ™์€ ๋””์ž์ธ ํ† ํฐ๋„ ๋‹คํฌ๋ชจ๋“œ์— ๋”ฐ๋ผ ๋‹ค๋ฅด๊ฒŒ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (๋ผ์ดํŠธ ๋ชจ๋“œ์ผ ๋•Œ ๊ธฐ๋ณธ๊ฐ’์€ base๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค)

export const colors = defineSemanticTokens.colors({
    background: {
        value: { base: '#f9f9fc', _dark: '#121315' }
    },
    keycolor: {
        primary: {
            value: { base: '#fb4866', _dark: '#fa5571' }
        },
        ...
    },
    ...
});

๊ทธ๋Ÿฌ๋ฉด ์ž๋™์œผ๋กœ ์กฐ๊ฑด์— ๋”ฐ๋ผ ํ† ํฐ๋“ค์ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

:where(:root, :host) {
    --colors-background: #f9f9fc;
    --colors-keycolor-primary: #fb4866;
    /* ... */
}
@media (prefers-color-scheme: dark) {
    :where(:root, :host) {
        --colors-background: #121315;
        --colors-keycolor-primary: #fa5571;
        /* ... */
    }
}

PandaCSS๋ฅผ ์“ฐ๋ฉด์„œ ์กฐ์‹ฌํ•ด์•ผํ•  ์ œ์•ฝ, ์ฃผ์˜ํ•  ์ ๋“ค

ํ•˜์ง€๋งŒ Panda์—๋„ ์ œ์•ฝ๊ณผ ํ•œ๊ณ„, ์ฃผ์˜ํ•  ์ ๋“ค์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋Œ€๋ถ€๋ถ„ ์ •์ ์œผ๋กœ ์ปดํŒŒ์ผํ•˜๋Š” ํŠน์„ฑ์—์„œ ์˜ค๋Š” ํ•œ๊ณ„์ž…๋‹ˆ๋‹ค. tailwind์™€ ๊ฐ™์€ ์ •์ ์ธ ํŠน์„ฑ์„ ๊ฐ€์ง„ ์Šคํƒ€์ผ๋ง ์†”๋ฃจ์…˜์„ ์‚ฌ์šฉํ•ด๋ณด์…จ๋‹ค๋ฉด ์ต์ˆ™ํ•œ ์ด์Šˆ์ด์‹ค์ง€๋„ ๋ชจ๋ฅด๊ฒ ๋Š”๋ฐ์š”.

static nature๋กœ ์ธํ•œ tracking ์ด์Šˆ

panda๋Š” ๋Ÿฐํƒ€์ž„์ด ์•„๋‹ˆ๋ผ ๋นŒ๋“œ ํƒ€์ž„์— ์ •์ ์œผ๋กœ ์ฝ”๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ํ† ํฐ์ด๋‚˜ ๋ ˆ์‹œํ”ผ๋ฅผ ์ˆ˜์ •ํ•˜๊ณ  ๋‚˜๋ฉด panda codegen ๋ช…๋ น์–ด๋ฅผ ์จ์„œ, ๋‹ค์‹œ ์ƒ์„ฑ์„ ํ•ด์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฒŒ ๋ฒˆ๊ฑฐ๋กญ๊ณ  ์ž์ฃผ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•˜์‹ ๋‹ค๋ฉด --watch๋ฅผ ์จ์„œ ์ฝ”๋“œ๊ฐ€ ์ˆ˜์ •๋  ๋•Œ๋งˆ๋‹ค ์žฌ์ƒ์„ฑํ•˜๊ฒŒ ํ•˜์‹œ๋Š” ๊ฒƒ๋„ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

tailwind์ฒ˜๋Ÿผ pandacss๋„ ์‚ฌ์šฉํ•˜๋Š” variants์™€ classname๋งŒ ์ถ”์ ํ•ด์„œ, ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฑด ๋ฒˆ๋“ค๊ณผ ์Šคํƒ€์ผ ์‹œํŠธ์— ํฌํ•จ์‹œํ‚ค์ง€ ์•Š์œผ๋ ค ํ•ฉ๋‹ˆ๋‹ค. webpack ๋“ฑ์˜ ๋ฒˆ๋“ค๋Ÿฌ์—์„œ tree shaking์ด ๊ฐ€๋Šฅํ•˜๋„๋ก ๋ชจ๋“  ํ•จ์ˆ˜ ํ˜ธ์ถœ์—๋Š” /*#__PURE__*/ ๊ฐ€ ๋ถ™์–ด์„œ ๋‚˜์˜ต๋‹ˆ๋‹ค.

๋•๋ถ„์— ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ๋Š” ์ž‘์•„์ง€์ง€๋งŒโ€ฆ ๋‚ด๊ฐ€ ๋งŒ๋“  ์ปดํฌ๋„ŒํŠธ์˜ ์Šคํƒ€์ผ์ด ์ ์šฉ๋˜์ง€ ์•Š๋Š” ์ด์Šˆ๊ฐ€ ์ƒ๊ธฐ๊ณ ๋Š” ํ•ฉ๋‹ˆ๋‹ค. ๋ถ„๋ช… css class๋Š” ์ž˜ ๋‹ฌ๋ ค ์žˆ๋Š”๋ฐ style์€ ๋ณด์ด์ง€ ์•Š๋Š” ๊ฒƒ์ด์ฃ !

๊ทธ๋ž˜์„œ pandacss ๋ฌธ์„œ์—์„œ๋„ ์ด๋Ÿฌํ•œ ์ œ์•ฝ์„ ๋ฐ˜๋ณตํ•ด์„œ ๊ฐ•์กฐํ•ฉ๋‹ˆ๋‹ค.

Due to static nature of Panda, itโ€™s not possible to track the usage of the recipes in all cases.

์‹ค๋ฌด์—์„œ ๊ฒช์€ ์‚ฌ๋ก€๋ฅผ ์˜ˆ๋กœ ๋“ค์–ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ์œ„์—์„œ ๋งŒ๋“ค์—ˆ๋˜ LinkButton์„ ๊ธฐ์–ตํ•˜์‹œ๋‚˜์š”? LinkButton์„ ์‹ค์ œ๋กœ ์‚ฌ์šฉํ•ด๋ณด๋ฉด ์Šคํƒ€์ผ์ด ์ ์šฉ๋˜์ง€ ์•Š์•„์„œ ๋‹นํ™ฉํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๋‹ค์Œ์€ ํšŒ์ƒ‰์˜ mono button์˜ ์˜ˆ์‹œ์ธ๋ฐ, ์ƒ‰์ด๋‚˜ ํŒจ๋”ฉ์ด ์ „ํ˜€ ์ ์šฉ๋˜์ง€ ์•Š์•„์„œ ํ…์ŠคํŠธ์™€ ์•„์ด์ฝ˜๋งŒ ๋ณด์ด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

 <LinkButton
    href={Url[props.value].google}
    target='_blank'
    theme='mono'
    variants='tertiary'
    size='h48'
>
    Google Play store
    <ArrowForward />
</LinkButton>

์Šคํƒ€์ผ์ด ์ ์šฉ๋˜์ง€ ์•Š๊ณ  ๊ทธ์ € ๊ฒ€์€ ๊ธ€์ž์— ํฐ๋ฐฐ๊ฒฝ์œผ๋กœ ๋ณด์ด๋Š” ๋ฒ„ํŠผ

์ด๋Ÿฐ ๋‹นํ™ฉ์Šค๋Ÿฌ์šด ๊ฒฝ์šฐ๋ฅผ ๋งŒ๋‚˜์‹œ๋ฉด ์ผ๋‹จ ํŒ๋‹ค ๋ฌธ์„œ์—์„œ tracking ๊ด€๋ จํ•œ ๋ถ€๋ถ„์„ ๊ฒ€์ƒ‰ํ•ด๋ณด์‹œ๊ธฐ๋ฅผ ์ถ”์ฒœ๋“œ๋ฆฝ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ์œ„์˜ ๋ฌธ์ œ๋Š” LinkButton์ด๋ผ๋Š” jsx์˜ prop์„ ์ถ”์ ํ•  ์ˆ˜ ์žˆ๋„๋ก, recipe config์— ์ถ”๊ฐ€ํ•ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

import { defineRecipe } from '@pandacss/dev'
 
const buttonRecipe = defineRecipe({
  className: 'button',
  base: {
    // ...
  },
  variants: {
    // ...
  },
  // JSX ํžŒํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ์ •๊ทœํ‘œํ˜„์‹์„ ์“ธ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
  jsx: ['Button', 'LinkButton']
})

๊ทธ๋Ÿฌ๋ฉด ๋‹ค์Œ์ฒ˜๋Ÿผ ์˜ˆ์˜๊ฒŒ ๋‹ค์‹œ ์Šคํƒ€์ผ์ด ์ ์šฉ๋ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฐ ํžŒํŠธ๊ฐ€ ์žˆ์œผ๋ฉด ์ปดํŒŒ์ผ๋Ÿฌ๋Š” LinkButton์ด ๊ทธ๋ƒฅ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์•„๋‹ˆ๋ผ, panda์˜ recipe๊ฐ€ ์ ์šฉ๋œ ์ปดํฌ๋„ŒํŠธ๋ผ๋Š” ๊ฑธ ์•Œ ์ˆ˜ ์žˆ๊ณ , ์‚ฌ์šฉ๋œ variant๋ฅผ bundle์— ํฌํ•จ์‹œํ‚ฌ ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

ํšŒ์ƒ‰์— textStyle๊ณผ padding๋„ ์ ์šฉ๋œ ์˜ˆ์œ ๋ฒ„ํŠผ

๋‹ค์–‘ํ•œ ๋ฐฉ๋ฒ• ์ค‘์— ํ‘œ์ค€์„ ์ •ํ•˜๊ธฐ

react๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์›Œ๋‚™ ๋‹ค์–‘ํ•ด์„œ ์ €๋Š” ์„œ๋ธŒ์›จ์ด๋ผ๊ณ  ๋ถ€๋ฅด๊ณ ๋Š” ํ•˜๋Š”๋ฐ์š”. pandacss๋„ ๋‹ค์–‘ํ•œ css in js ์†”๋ฃจ์…˜๋“ค์—์„œ ์‰ฝ๊ฒŒ migration ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์„ ๋ชจ๋‘ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ํ•œ ๊ฐ€์ง€๋ฅผ ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์ด ๋„ˆ๋ฌด ๋งŽ์€ ๊ฒŒ ๋ฌธ์ œ์ธ๋ฐ์š”. ์ €ํฌ๋„ ํŒ€ ๋‚ด๋ถ€์—์„œ ์–ด๋–ค ๋ฐฉ์‹์ด ์ข‹์€์ง€๋ฅผ ๋…ผ์˜ํ•˜๊ณ  ํ•ฉ์˜ํ•˜๋Š” ๊ณผ์ •์„ ๊ณ„์†ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

ํŒ๋‹ค๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๋ฐฉ๋ฒ•๋“ค์„ ๋น„๊ตํ•ด๋ณด์‹œ๊ณ  ํ•˜๋‚˜๋ฅผ ๊ณจ๋ผ์„œ ํŒ€์˜ ํ‘œ์ค€์œผ๋กœ ์‚ผ์œผ์‹œ๊ฑฐ๋‚˜, ์–ด๋–ค ๊ฒฝ์šฐ์— ์–ด๋–ค ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•˜์‹ค์ง€ ํ‘œ์ค€์„ ๋ช…ํ™•ํžˆ ์ •ํ•˜์‹œ๋Š” ๊ฒŒ ์ข‹์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด ๋ธ”๋กœ๊ทธ์˜ ์ œ๋ชฉ์„ ์Šคํƒ€์ผ๋ง ํ•˜๊ณ  ์‹ถ๋‹ค๊ณ  ํ•ฉ์‹œ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ๋‹ค์Œ์ฒ˜๋Ÿผ ์ธ๋ผ์ธ ์Šคํƒ€์ผ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฌธ์„œ์—์„œ๋„ ์ด ๋ฐฉ์‹์„ ๊ธฐ๋ณธ์œผ๋กœ ๋ณด์—ฌ์ฃผ์ง€๋งŒ, ์ธ๋ผ์ธ์„ ๋”ฑํžˆ ๊ถŒ์žฅํ•˜๋Š” ๊ฑด ์•„๋‹™๋‹ˆ๋‹ค.

<h3
    className={css({
        textStyle: {
            base: "Heading4_600",
            tablet: "Heading2_600",
        },
    })}
>
    {post.data.title}
</h3>

ํ•˜์ง€๋งŒ tailwind์™€ ๋‹ฌ๋ฆฌ ๋„ˆ๋ฌด ์žฅํ™ฉํ•˜๊ณ , ๋งˆํฌ์—…๋„ ๋šฑ๋šฑํ•ด์ง€๊ณ  ๋ถˆํŽธํ•ด๋ณด์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํŠนํžˆ ์ค‘๋ณต๋˜๋Š” ๊ฒฝ์šฐ๋„ ๋งŽ๋‹ค๋ฉด css ๋ณ€์ˆ˜๋ฅผ ๋ฐ”๊นฅ์œผ๋กœ ๋นผ๋‘˜ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

const titleClass = css({
    textStyle: {
        base: "Heading4_600",
        tablet: "Heading2_600",
    },
})

<h3 className={titleClass}>
    {post.data.title}
</h3>

ํ•œํŽธ ์ธ๋ผ์ธ์€ ์ทจํ–ฅ์ด์ง€๋งŒ ๋” ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์“ฐ๊ณ  ์‹ถ๋‹ค๋ฉด panda๋Š” chakra๋‚˜ ์—ฌ๋Ÿฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ฒ˜๋Ÿผ attribute๋กœ style์„ ์ ์„ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

import { styled } from '../../styled-system/jsx';

<styled.h3
    textStyle={{
        base: "Heading4_600",
        tablet: "Heading2_600",
    }}
>
    {post.data.title}
</styled.h3>

ํ•˜์ง€๋งŒ ์—ฌ์ „ํžˆ inline style์ด ์‹ซ๊ณ , styled component ์ฒ˜๋Ÿผ ์“ฐ๊ณ  ์‹ถ๋‹ค๋ฉด, styled component๋กœ ๊ฐ์‹ธ์„œ ๊น”๋”ํ•˜๊ฒŒ ๋งŒ๋“ค ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

import { styled } from '../../styled-system/jsx';

const Title = styled('h3', {
    base: {
        textStyle: {
            base: "Heading4_600",
            tablet: "Heading2_600",
        }
    }
})

<Title>
    {post.data.title}
</Title>

tailwind๋ฅผ ์˜ค๋ž˜ ์จ์˜ค์‹  ํŒ€์ด๋ผ๋ฉด utility class๊ฐ€ ์–‘๋‚ ์˜ ๊ฒ€์ด๋ผ๋Š” ๊ฑธ ๋ชจ๋‘ ๊ณต๊ฐํ•˜์‹œ๋ฆฌ๋ผ ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. ๋งˆํฌ์—…๊ณผ ์Šคํƒ€์ผ์„ ๊ฐ™์ด ๋‘๋Š” ๊ฑด ํŽธ๋ฆฌํ•˜๊ธฐ๋„ ํ•˜์ง€๋งŒ, ์œ„ํ—˜ํ•˜๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค. ์ปดํฌ๋„ŒํŠธ์™€ ๊ฐ™์€ ์ถ”์ƒํ™”๋ฅผ ์ž˜ ์‚ฌ์šฉํ•˜์‹œ๊ณ  tailwind ๋“ฑ์˜ ์œ ์ง€๋ณด์ˆ˜๋ฅผ ์œ„ํ•œ ๊ถŒ์žฅ์‚ฌํ•ญ๋„ ์‚ดํŽด๋ณด์„ธ์š”.

ํŠนํžˆ ๋ฐ˜๋ณต๋˜๋Š” pattern์ด ์žˆ๊ณ  ์—ญํ• ๋„ ๋ช…ํ™•ํ•˜๋‹ค๋ฉด, ์šฐ๋ฆฌ ํŒ€๋งŒ์˜ ํŒจํ„ด์„ ๋งŒ๋“ค๊ณ  ๋””์ž์ด๋„ˆ์™€๋„ ์•ฝ์†ํ•ด์„œ ์“ฐ์‹œ๋ฉด ์ข‹์Šต๋‹ˆ๋‹ค. ๋‹ค๋งŒ, ๋””์ž์ด๋„ˆ์™€ ์•ฝ์†๋œ ๊ฒŒ ์•„๋‹Œ ์ถ”์ƒํ™”๋Š” ํ•ญ์ƒ ๊นจ์ง€๊ฒŒ ๋˜์–ด ์žˆ๋‹ค๋Š” ๊ฑธ ๊ธฐ์–ตํ•˜์„ธ์š”!

๋ชจ๋˜ CSS๋กœ ๊ตฌํ˜• ๋ธŒ๋ผ์šฐ์ € ์ง€์›ํ•˜๊ธฐ

panda๋Š” where์ด๋‚˜ layer์™€ ๊ฐ™์€ ๋ชจ๋˜ CSS๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋•๋ถ„์— css ์šฉ๋Ÿ‰๋„ ์ค„์–ด๋“ค๊ณ  ์ง€๋ช…๋„ ๊ฐ™์€ ์ด์Šˆ๋„ ์ค„์–ด๋“œ๋Š” ๊ฑด ์ข‹์€ ์ผ์ž…๋‹ˆ๋‹ค๋งŒ. ๊ตฌํ˜• ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์ง€์›ํ•ด์•ผ ํ•˜๋Š” ํŒ€์—์„œ๋Š” ๋„์ž…ํ•˜๊ธฐ ์–ด๋ ต์ง€ ์•Š์„๊นŒ ์‹ถ์œผ์‹ค์ง€๋„ ๋ชจ๋ฅด๊ฒ ์Šต๋‹ˆ๋‹ค.

์‹ค์ œ๋กœ panda์˜ ๋ธŒ๋ผ์šฐ์ € ์ง€์› ๋ฌธ์„œ๋ฅผ ๋ณด๋ฉด, ํฌ๋กฌ 99, ์‚ฌํŒŒ๋ฆฌ15.4 ์ด์ƒ์„ ์ง€์›ํ•œ๋‹ค๊ณ  ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์ €ํฌ ํ”„๋กฌ ์Šคํ† ์–ด๋„ ์‚ฌํŒŒ๋ฆฌ 14์™€ ์‚ผ์„ฑ ์ธํ„ฐ๋„ท์„ ์ง€์›ํ•˜๊ณ  ์žˆ์—ˆ๊ณ , ์ตœ๊ทผ์— ์ค‘๊ตญ์–ด ์ง€์›์„ ์‹œ์ž‘ํ•˜๋ฉด์„œ ์ค‘๊ตญ์—์„œ ๋งŽ์ด ์“ฐ์ด๋Š” 360 ๋“ฑ์˜ ๋ธŒ๋ผ์šฐ์ €๋„ ์ง€์›ํ•˜๊ฒŒ ๋˜์—ˆ๋Š”๋ฐ์š”. ์‹ค์ œ๋กœ ์ด๋Ÿฐ ๊ตฌํ˜• ๋ธŒ๋ผ์šฐ์ €์—์„œ ํŽ˜์ด์ง€๋ฅผ ์—ด์–ด๋ณด๋‹ˆ ์ „ํ˜€ style์ด ์ ์šฉ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

๊ตฌํ˜• ๋ธŒ๋ผ์šฐ์ €์—์„œ ํฐ ํ™”๋ฉด์— css๊ฐ€ ์ ์šฉ๋˜์ง€ ์•Š์€ html๋งŒ ๋ณด์ด๋Š” ๋ชจ์Šต

๊ทธ๋Ÿฌ๋ฉด ์ด๋Ÿฐ ๊ฒฝ์šฐ์—๋Š” ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ•˜๋ฉด ์ข‹์„๊นŒ์š”?

panda๋Š” css์˜ babel์ธ postcss๋ฅผ ์ด์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์ ์ ˆํ•œ postcss ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์ด์šฉํ•˜๋ฉด ๊ตฌํ˜• ๋ธŒ๋ผ์šฐ์ €์—์„œ๋„ ๋Œ์•„๊ฐ€๋Š” css๋กœ polyfill์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ €ํฌ๋Š” ๊ณต์‹ ๋ฌธ์„œ์—์„œ ์ถ”์ฒœํ•˜๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ ์™ธ์—๋„ ๋ช‡ ๊ฐ€์ง€๋ฅผ ๋” ์ถ”๊ฐ€ํ•ด์„œ ์“ฐ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

// postcss.config.js
module.exports = {
  plugins: [
    '@pandacss/dev/postcss',
    'autoprefixer',
    '@csstools/postcss-cascade-layers',
    '@csstools/postcss-is-pseudo-class': {},
    '@csstools/postcss-media-minmax': {}
  ]
}

์ด๋ ‡๊ฒŒ postcss ์„ค์ •์„ ํ•ด์ฃผ๋ฉด ๋‹ค์Œ์ฒ˜๋Ÿผ ์Šคํƒ€์ผ์ด ์ ์šฉ๋œ ๋ชจ์Šต์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์Šคํƒ€์ผ์ด ์ ์šฉ๋˜์—ˆ์ง€๋งŒ ์ƒ‰๊น”์ด ์ œ๋Œ€๋กœ ๋“ค์–ด๊ฐ€์ง€ ์•Š๋Š” ๋ชจ์Šต

ํ•˜์ง€๋งŒ ์—ฌ์ „ํžˆ ์ƒ‰์ด ์ œ๋Œ€๋กœ ๋“ค์–ด๊ฐ€์ง€ ์•Š๋Š”๋ฐ์š”. ์ด๋Š” design token์„ ์ •์˜ํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” :where ์ด postcss๋กœ polyfill์ด ๋ถˆ๊ฐ€๋Šฅํ•œ css์ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. :where์€ design token์„ css variable๋กœ ์ •์˜ํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š”๋ฐ์š”. token์€ ์–ด์ฐจํ”ผ ์ž์ฃผ ๋ฐ”๋€Œ๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ์„œ ์ €ํฌ๋Š” build ์‹œ ์ƒ์„ฑ๋œ css variable์„ :where์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๋ฐฉ์‹์œผ๋กœ ์ง์ ‘ ์Šคํƒ€์ผ์‹œํŠธ๋ฅผ ๋งŒ๋“ค์–ด์„œ html์— inlineํ•ด์ฃผ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

/* before */
:where(:root, :host):not(#\#):not(#\#) {
--shadows-elevation_01: 0px 1px 2px 0px rgba(0, 0, 0, .1);
--shadows-elevation_02: 0px 2px 4px 0px rgba(0, 0, 0, .1);
/* ... */
}

/* after  */
:root {
--shadows-elevation_01: 0px 1px 2px 0px #0000001a;
--shadows-elevation_02: 0px 2px 4px 0px #0000001a;
/* ... */
}

์ด์ œ ๊ฑฐ์˜? ์ •์ƒ์ ์œผ๋กœ ํŽ˜์ด์ง€๊ฐ€ ๋ณด์ด๋Š” ๊ฑธ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์•ฑ์„ ๋‹ค์šด๋กœ๋“œํ•  ์ˆ˜ ์žˆ๋Š” ํŽ˜์ด์ง€์˜ ๋ชจ์Šต

ํ•˜์ง€๋งŒ ์—ฌ์ „ํžˆ ํ•ด๊ฒฐ๋˜์ง€ ์•Š๋Š” ๋ฌธ์ œ๋„ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. flex gap ์˜ polyfill์€ ์ด์ƒํ•œ ์—๋Ÿฌ๊ฐ€ ๋งŽ์•„์„œ, ๊ฒฐ๊ตญ margin์„ ์ง์ ‘ ์ฃผ๋Š” ๋ฐฉ์‹์œผ๋กœ ํ•ด๊ฒฐํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค. postcss plugin์„ ์ด์šฉํ•ด ์˜› css๋กœ ๋ณ€ํ™˜๋˜๋ฉด์„œ ์Šคํƒ€์ผ ์‹œํŠธ์˜ ์šฉ๋Ÿ‰์ด ์ปค์ง€๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค.

์ƒˆ๋กœ์šด ๊ธฐ์ˆ ์—๋„ ํ•œ๊ณ„๊ฐ€ ํ•ญ์ƒ ์žˆ๊ธฐ ๋งˆ๋ จ์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋ฌธ์„œ๋ฅผ ์ž˜ ์ฝ๊ณ  ํ•ด๊ฒฐ์ฑ…์„ ํƒ์ƒ‰ํ•ด์„œ ๋ชจ๋˜ํ•œ ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜์„ ๋ณด์กดํ•˜๋ฉด์„œ๋„, ๊ตฌํ˜• ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์ง€์›ํ•˜๋Š” ์š”๊ตฌ์‚ฌํ•ญ๋„ ๋งŒ์กฑ์‹œํ‚ฌ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์•ž์œผ๋กœ๋„ ์ด ํ•œ๊ณ„๋ฅผ ๋„˜์–ด ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ๋Š” ๋” ์ข‹์€ ๋ฐฉ๋ฒ•์„ ์ฐพ์•„๋ณด๋ ค ํ•ฉ๋‹ˆ๋‹ค.

PandaCSS์™€ CSS-in-JS์˜ ๋ฏธ๋ž˜

PandaCSS๋Š” 2023๋…„ 11์›” 30์ผ ํ˜„์žฌ 0.19.0 ๋ฒ„์ „์ด์ง€๋งŒ, ์˜ค๋Š” 12์›”์— 1.0์„ ๋ฐœํ‘œํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค. ์ถœ์‹œ๋œ์ง€๋Š” ์–ผ๋งˆ๋˜์ง€ ์•Š์•˜์ง€๋งŒ, ๊ธฐ์กด ์ƒํƒœ๊ณ„์˜ ์‹œํ–‰์ฐฉ์˜ค์™€ ๊ฒฝํ—˜์—์„œ ๋ฐฐ์šด ๊ตํ›ˆ์„ ํ† ๋Œ€๋กœ ๋งŒ๋“ค์–ด์กŒ๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค์–‘ํ•œ ํ™˜๊ฒฝ๊ณผ ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ ๋ฌธ์ œ ์—†์ด ๋Œ์•„๊ฐ‘๋‹ˆ๋‹ค.

์•ž์„œ ์ด์•ผ๊ธฐ ๋“œ๋ฆฐ ์ œ์•ฝ์ด๋‚˜ ์ฃผ์˜ํ•  ์ ๋„ ์ž˜ ๋ฌธ์„œํ™”๊ฐ€ ๋˜์–ด ์žˆ๊ณ  ์‹ฌ์ง€์–ด ๊ณต์‹ ์˜์ƒ ๊ฐ•์˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ์ž˜ ์ดํ•ดํ•˜๊ณ  ์‹ ์ค‘ํ•˜๊ฒŒ ๋„์ž…ํ•˜์‹ ๋‹ค๋ฉด, ์‹œํ–‰์ฐฉ์˜ค๋Š” ์ค„์ด๊ณ  ์ด๋“์€ ์ปค์ง€์ง€ ์•Š์„๊นŒ ์‹ถ์Šต๋‹ˆ๋‹ค. (์ €ํฌ ๋ธ”๋กœ๊ทธ๋„ Astro์™€ PandaCSS๋กœ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.)

์ €ํฌ๊ฐ€ ๊ฒช์€ ์‹œํ–‰์ฐฉ์˜ค๊ฐ€ PandaCSS ๋„์ž…์„ ๊ณ ๋ฏผํ•˜๋Š” ๋ถ„๋“ค์—๊ฒŒ ๋„์›€์ด ๋˜๋ฉด ์ข‹๊ฒ ์Šต๋‹ˆ๋‹ค. ํŠนํžˆ ํšŒ์‚ฌ์—์„œ ๋””์ž์ธ ํ† ํฐ๊ณผ ๋””์ž์ธ ์‹œ์Šคํ…œ, ํ˜น์€ ๊ณต์šฉ ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ด๋ฏธ ์ •๋ฆฝํ•ด์„œ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๊ฑฐ๋‚˜, ๋งŒ๋“ค์–ด๋‚˜๊ฐ€๋Š” ์ค‘์ด์‹œ๋ผ๋ฉด ๋”์šฑ ๊ทธ๋ ‡์Šต๋‹ˆ๋‹ค.

Sage ์”จ๋Š” Framework agnosticํ•œ headless component ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ธ Ark UI์™€ Zag.js๋„ ๋งŒ๋“ค๊ณ  ์žˆ๋Š”๋ฐ์š”. ์ด๋“ค์„ ์กฐํ•ฉํ•ด์„œ ์‚ฌ์šฉํ•˜์‹ ๋‹ค๋ฉด React๋ฅผ ์“ฐ์‹œ๋Š” ๋ถ„๋„ ๋‹ค๋ฅธ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์“ฐ๋Š” ํŒ€๋„ ๋ชจ๋“  ์‚ฌ์šฉ์ž๊ฐ€ ์†์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์‚ฌ์ดํŠธ๋ฅผ ๋น ๋ฅด๊ฒŒ ๋งŒ๋“ค์–ด๋‚ด์‹ค ์ˆ˜ ์žˆ์„ ๊ฒ๋‹ˆ๋‹ค.

๋Š˜ ์“ฐ๊ณ  ์‹ถ์€ ๊ธ€์€ ๋งŽ์€๋ฐ, ์‹œ๊ฐ„์€ ๋ถ€์กฑํ•˜๋„ค์š”. ๋” ๋‹ค์–‘ํ•œ ์ฃผ์ œ๋กœ ๋‹ค์‹œ ์ฐพ์•„๋ต™๊ฒ ์Šต๋‹ˆ๋‹ค.

โ† ๋ชฉ๋ก์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ

Art Changes Life

๋…ธ๋จธ์Šค์™€ ํ•จ๊ป˜ ์—”ํ„ฐํ…Œํฌ ์‚ฐ์—…์„ ํ˜์‹ ํ•ด๋‚˜๊ฐˆ ๋ฉค๋ฒ„๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค.

์ฑ„์šฉ ์ค‘์ธ ๊ณต๊ณ  ๋ณด๊ธฐ