Material UI Sync plugin ๐Ÿงช

Material UI Sync plugin ๐Ÿงช

Sync๋Š” ๋””์ž์ธ(design)์—์„œ ๋ฐ”๋กœ ์ฝ”๋“œ๋กœ Material UI ํ…Œ๋งˆ๋ฅผ ์ƒ์„ฑํ•ด์ฃผ๋Š” Figma ํ”Œ๋Ÿฌ๊ทธ์ธ์ด์—์š”.

:::error Material UI Sync ํ”Œ๋Ÿฌ๊ทธ์ธ์˜ ๊ฐœ๋ฐœ์€ 2024๋…„์— ์ค‘๋‹จ๋˜์—ˆ์–ด์š”. ์•ž์œผ๋กœ ์ด ํ”„๋กœ์ ํŠธ์— ์—…๋ฐ์ดํŠธ๊ฐ€ ์žˆ์„ ๊ฒƒ์œผ๋กœ ๊ธฐ๋Œ€ํ•ด์„œ๋Š” ์•ˆ ๋ฉ๋‹ˆ๋‹ค. :::

์ถœ์ฒ˜: ๋ฌธ์„œ

๋ณธ๋ฌธ

Introduction (์†Œ๊ฐœ)

Material UI Sync๋Š” Material UI for Figma Design Kit์—์„œ ํ…Œ๋งˆ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” Figma ํ”Œ๋Ÿฌ๊ทธ์ธ์ด์—์š”.

Customizing the Material UI Switch component in Figma with the Sync plugin running.

Sync๋Š” Material UI for Figma Design Kit v5.16.0 ์ด์ƒ๊ณผ ํ•จ๊ป˜ ๋™์ž‘ํ•ด์š”.

Running the plugin (ํ”Œ๋Ÿฌ๊ทธ์ธ ์‹คํ–‰ํ•˜๊ธฐ)

Material UI for Figma Design Kit์˜ ์™„์ „ํ•œ ์ตœ์‹  ๋ฒ„์ „์„ ์„ค์น˜ํ•˜์ง€ ์•Š์•˜๋‹ค๋ฉด, ๋Œ€์‹  Community ๋ฒ„์ „์„ ์‚ฌ์šฉํ•ด์„œ ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์–ด์š”.

์„ค์น˜ํ•˜๊ณ  Figma์—์„œ ์—ด์—ˆ๋‹ค๋ฉด, Community ํƒญ์˜ Material UI Sync ํ”Œ๋Ÿฌ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•ด์„œ **Open inโ€ฆ**์„ ํด๋ฆญํ•˜๊ณ  Material UI for Figma Design Kit์„ ์„ ํƒํ•ด์š”.

Accessing Material UI Sync via the Resources menu in Figma.

Customizing design tokens (๋””์ž์ธ ํ† ํฐ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•)

๋””์ž์ธ ํ† ํฐ์€ Design Kit์˜ ๋กœ์ปฌ ๋ณ€์ˆ˜ ์ปฌ๋ ‰์…˜(local variable collections)์— ์ •์˜๋˜์–ด ์žˆ์œผ๋ฉฐ, ์ƒ‰์ƒ ํŒ”๋ ˆํŠธ, ๋ธŒ๋ ˆ์ดํฌํฌ์ธํŠธ, ๋ชจ์–‘(shape), ๊ฐ„๊ฒฉ(spacing) ํ† ํฐ์„ ํฌํ•จํ•ด์š”. ํƒ€์ดํฌ๊ทธ๋ž˜ํ”ผ์™€ ๊ทธ๋ฆผ์ž ๊ด€๋ จ ํ† ํฐ์€ ๋กœ์ปฌ ์Šคํƒ€์ผ ์ปฌ๋ ‰์…˜(local styles collection)์—์„œ ์ฐพ์„ ์ˆ˜ ์žˆ์–ด์š”.

Altering existing tokens (๊ธฐ์กด ํ† ํฐ ์ˆ˜์ •ํ•˜๊ธฐ)

Design Kit๋Š” Material UI React ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ๊ธฐ๋ณธ ํ…Œ๋งˆ์— ๋งคํ•‘๋˜๋Š” ๋””์ž์ธ ํ† ํฐ์ด ๊ฐ€๋“ ๋‹ด๊ฒจ ์žˆ์–ด์š”.

๊ธฐ์กด ํ† ํฐ์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ฆˆํ•˜๋ ค๋ฉด, ์•„๋ž˜ ๊ทธ๋ฆผ์ฒ˜๋Ÿผ ํ•„ํ„ฐ ์•„์ด์ฝ˜์„ ํด๋ฆญํ•ด์„œ ๋กœ์ปฌ ๋ณ€์ˆ˜ ๋ชจ๋‹ฌ์„ ์—ด์–ด์š”. ์ปฌ๋ ‰์…˜์— ์žˆ๋Š” ๋ณ€์ˆ˜๋“ค(ํŒ”๋ ˆํŠธ, ๋ธŒ๋ ˆ์ดํฌํฌ์ธํŠธ, ๋ชจ์–‘, ๊ฐ„๊ฒฉ ๋“ฑ)์„ ์›ํ•˜๋Š” ๋Œ€๋กœ ์กฐ์ •ํ•ด์š”.

The Local variables menu in Figma, where all design tokens are stored and new ones can be added.

๊ทธ๋Ÿฐ ๋‹ค์Œ Material UI Sync ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์—ด๊ณ  Generate theme์„ ํด๋ฆญํ•ด์š”.

The Generate theme button in the Material UI Sync plugin UI.

์ˆ˜์ •๋œ ํ† ํฐ์ด ํฌํ•จ๋œ ํ…Œ๋งˆ๊ฐ€ ์ƒ์„ฑ๋˜์–ด ํ”Œ๋Ÿฌ๊ทธ์ธ์˜ Theme ํƒญ์— ํ‘œ์‹œ๋ผ์š”.

The generated theme displayed in the Material UI Sync plugin UI.

Storybook ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํƒญ์œผ๋กœ ์ด๋™ํ•˜๋ฉด ์ƒ์„ฑ๋œ ํ…Œ๋งˆ์™€ ์ปค์Šคํ„ฐ๋งˆ์ด์ฆˆ๋œ ํ† ํฐ์„ ๋ฏธ๋ฆฌ ๋ณผ ์ˆ˜๋„ ์žˆ์–ด์š”.

The generated theme previewed in Storybook in the Material UI Sync plugin UI.

Adding new tokens (์ƒˆ ํ† ํฐ ์ถ”๊ฐ€ํ•˜๊ธฐ)

๊ธฐ์กด ๋กœ์ปฌ ๋ณ€์ˆ˜ ์ปฌ๋ ‰์…˜์— ์ƒˆ ๋ณ€์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜, ๋กœ์ปฌ ์Šคํƒ€์ผ ์ปฌ๋ ‰์…˜์— ์ƒˆ elevation ๋ฐ typography ์Šคํƒ€์ผ์„ ์ถ”๊ฐ€ํ•ด์„œ, ๊ธฐ์กด ํ† ํฐ ์ง‘ํ•ฉ์„ ์ž์‹ ๋งŒ์˜ ๊ฒƒ์œผ๋กœ ํ™•์žฅํ•  ์ˆ˜ ์žˆ์–ด์š”. ์ปค์Šคํ…€ ํ† ํฐ์„ ์ถ”๊ฐ€ํ•œ ํ›„์—๋Š” Regenerate theme์„ ํด๋ฆญํ•ด์„œ ์ด ํ† ํฐ๋“ค์„ ํ…Œ๋งˆ์— ํฌํ•จ์‹œ์ผœ์š”.

The Regenerate button in the Connect plugin UI.

Customizing components (์ปดํฌ๋„ŒํŠธ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•)

:::warning ์ด ๊ธฐ๋Šฅ์€ ์‹คํ—˜์ ์ด๋ฉฐ Button, Switch, Typography ์ปดํฌ๋„ŒํŠธ๋กœ ์ œํ•œ๋ผ์š”. :::

Sync ํ”Œ๋Ÿฌ๊ทธ์ธ์€ ์ปค์Šคํ„ฐ๋งˆ์ด์ฆˆ๋œ ์ปดํฌ๋„ŒํŠธ์— ๋Œ€ํ•œ ํ…Œ๋งˆ ์Šคํƒ€์ผ๋„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์–ด์„œ, Figma ์•ˆ์—์„œ ์ปดํฌ๋„ŒํŠธ์˜ ๋ฃฉ์•คํ•„์„ ์™„์ „ํžˆ ๋ฐ”๊พธ๊ณ  ์ž์‹ ๋งŒ์˜ ์ปค์Šคํ…€ ๋””์ž์ธ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค˜์š”.

์˜ˆ๋ฅผ ๋“ค์–ด, Switch ์ปดํฌ๋„ŒํŠธ์˜ ์ฒดํฌ๋œ ์ƒํƒœ, ์ค‘๊ฐ„ ํฌ๊ธฐ, primary ์ƒ‰์ƒ์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ฆˆํ•ด์„œ iOS ๋ฃฉ์•คํ•„์„ ์žฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ๋‹ค์Œ๊ณผ ๊ฐ™์•„์š”:

A specific variant of the Switch component selected in the Design Kit.

:::warning Sync๊ฐ€ ์ปค์Šคํ…€ ์ปดํฌ๋„ŒํŠธ ์Šคํƒ€์ผ์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ถ”์ถœํ•˜๊ณ  ํ…Œ๋งˆ๋ฅผ ์ƒ์„ฑํ•˜๋ ค๋ฉด, Design Kit์˜ ์ปดํฌ๋„ŒํŠธ ๋ ˆ์ด์–ด ๊ณ„์ธต ๊ตฌ์กฐ์™€ ๋ ˆ์ด์–ด ์ด๋ฆ„์ด ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์€ ์ƒํƒœ๋กœ ์œ ์ง€๋˜์–ด์•ผ ํ•ด์š”. :::

Sync ํ”Œ๋Ÿฌ๊ทธ์ธ์€ ์ปค์Šคํ„ฐ๋งˆ์ด์ฆˆ๋œ Switch์— ๋Œ€ํ•ด ๋‹ค์Œ ํ…Œ๋งˆ ์ฝ”๋“œ๋ฅผ ์ƒ์„ฑํ•ด์š”:

{
  components: {
    MuiSwitch: {
      styleOverrides: {
        root: {
          '&.MuiSwitch-sizeMedium:has(.MuiSwitch-colorPrimary)': {
            '&:has(.Mui-checked):not(:has(.Mui-disabled)):not(:has(.Mui-focusVisible))':
              {
                width: '40px',
                height: '21px',
                padding: '0',
                '& .MuiSwitch-switchBase': {
                  transform: 'translateX(19px) translateY(2px)',
                  padding: '0',
                  '& .MuiSwitch-thumb': {
                    width: '17px',
                    height: '17px',
                    background: '#FAFAFA',
                  },
                  '& + .MuiSwitch-track': {
                    width: '38px',
                    height: '21px',
                    background: 'var(--mui-palette-success-light)',
                    opacity: '1',
                  },
                },
              },
          },
        },
      },
    },
  },
}

์ƒ์„ฑ๋œ ํ…Œ๋งˆ๋Š” ์œ„์—์„œ ์ •์˜ํ•œ ํŠน์ • Switch ๊ตฌ์„ฑ์— ํ•ด๋‹นํ•˜๋Š” ํด๋ž˜์Šค๋ฅผ ๋Œ€์ƒ์œผ๋กœ ํ•˜๋ฏ€๋กœ, ์Šคํƒ€์ผ์€ Material UI ์ปดํฌ๋„ŒํŠธ์˜ props์™€ ์ƒํƒœ๊ฐ€ ์ปค์Šคํ„ฐ๋งˆ์ด์ฆˆ๋œ Figma ์ปดํฌ๋„ŒํŠธ์™€ ์ผ์น˜ํ•  ๋•Œ๋งŒ ์ ์šฉ๋ผ์š”.

๋‹ค๋ฅธ ์ƒํƒœ๋ฅผ ์ปค์Šคํ„ฐ๋งˆ์ด์ฆˆํ•˜๋ ค๋ฉด, ๋‹ค์Œ ๋‹จ๊ณ„์— ๋”ฐ๋ผ Figma์—์„œ ๊ฐ ๋ณ€ํ˜•(variant)์— ์›ํ•˜๋Š” ๋””์ž์ธ ๋ณ€๊ฒฝ์„ ์ ์šฉํ•ด์•ผ ํ•ด์š”:

  1. ๋‹จ์ผ "๊ธฐ๋ณธ(base)" ๋ณ€ํ˜•์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ฆˆํ•ด์š”โ€”์˜ˆ๋ฅผ ๋“ค์–ด ์ฒดํฌ๋œ ์ƒํƒœ, ์ค‘๊ฐ„ ํฌ๊ธฐ, primary ์ƒ‰์ƒ์˜ Switch ์ปดํฌ๋„ŒํŠธ์š”.
  2. ์ด ๋ณ€ํ˜•์„ ๋ณต์ œํ•˜๊ณ , ์ปค์Šคํ„ฐ๋งˆ์ด์ฆˆํ•˜๋ ค๋Š” ๋‹ค์Œ ๋ณ€ํ˜•์„ ๋Œ€์ƒ์œผ๋กœ ์ด๋ฆ„์„ ๋ฐ”๊ฟ”์š”โ€”์˜ˆ๋ฅผ ๋“ค์–ด ๋ณต์ œ๋ณธ์˜ ์ด๋ฆ„์„ Checked=True, Size=Medium, Color=Primary, State=Enabled์—์„œ Checked=False, Size=Medium, Color=Primary, State=Enabled๋กœ ๋ฐ”๊ฟ”์š”.
  3. ๊ฐ™์€ ๋ณ€ํ˜•์˜ ์ด์ „ ๋ฒ„์ „์„ ์‚ญ์ œํ•ด์š”.
  4. ์ƒˆ ๋ฒ„์ „์„ ๋ณ€ํ˜• ๊ทธ๋ฆฌ๋“œ์˜ ์˜ฌ๋ฐ”๋ฅธ ์นธ์œผ๋กœ ์ด๋™ํ•ด์š”.
  5. ๋ณ€ํ˜•์˜ ํ•˜์œ„ ๋ ˆ์ด์–ด๋“ค์— ํ•„์š”ํ•œ ์Šคํƒ€์ผ ์กฐ์ •์„ ํ•ด์š”.

์ด ๊ณผ์ •์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ฆˆํ•˜๋ ค๋Š” ๊ฐ ๋ณ€ํ˜•๋งˆ๋‹ค ๋ฐ˜๋ณตํ•ด์š”. ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ชจ์Šต์ด ๋  ์ˆ˜ ์žˆ์–ด์š”:

A fully customized Switch component in the Material UI Design Kit.

์—ฌ๊ธฐ์—์„œ Sync๋ฅผ ์‹คํ–‰ํ•ด ์ƒˆ ํ…Œ๋งˆ๋ฅผ ์ƒ์„ฑํ•˜๋ฉด, ์œ„ ์˜ˆ์ œ์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฒฐ๊ณผ๊ฐ€ ์ƒ์„ฑ๋ผ์š”:

{
  components: {
    MuiSwitch: {
      styleOverrides: {
        root: {
          '&.MuiSwitch-sizeMedium:has(.MuiSwitch-colorPrimary)': {
            width: '40px',
            height: '21px',
            padding: '0',
            '& .MuiSwitch-switchBase': {
              padding: '0',
              '& .MuiSwitch-thumb': {
                width: '17px',
                height: '17px',
                background: '#FAFAFA',
              },
              '& + .MuiSwitch-track': {
                width: '38px',
                height: '21px',
                borderRadius: '100px',
                opacity: '1',
              },
            },
            '&:not(:has(.Mui-checked)):not(:has(.Mui-disabled)):not(:has(.Mui-focusVisible))': {
              '& .MuiSwitch-switchBase': {
                transform: 'translateX(3px) translateY(2px)',
                '& + .MuiSwitch-track': {
                  background: '#BDBDBD',
                },
              },
            },
            '&:not(:has(.Mui-checked)):has(.Mui-disabled):not(:has(.Mui-focusVisible))': {
              '& .MuiSwitch-switchBase': {
                transform: 'translateX(3px) translateY(2px)',
                '& + .MuiSwitch-track': {
                  background: 'rgba(229, 229, 229, 0.99)',
                },
              },
            },
            '&:not(:has(.Mui-checked)):not(:has(.Mui-disabled)):has(.Mui-focusVisible)': {
              '& .MuiSwitch-switchBase': {
                transform: 'translateX(3px) translateY(2px)',
                '& + .MuiSwitch-track': {
                  border: '1px solid #000',
                  background: '#BDBDBD',
                },
              },
            },
            '&:has(.Mui-checked):has(.Mui-disabled):not(:has(.Mui-focusVisible))': {
              '& .MuiSwitch-switchBase': {
                transform: 'translateX(19px) translateY(2px)',
                '& + .MuiSwitch-track': {
                  background: 'rgba(187, 231, 188, 0.99)',
                },
              },
            },
            '&:not(:has(.Mui-checked)):not(:has(.Mui-disabled)):not(:has(.Mui-focusVisible)):hover': {
              '& .MuiSwitch-switchBase': {
                transform: 'translateX(3px) translateY(2px)',
                '& + .MuiSwitch-track': {
                  background: '#616161',
                },
              },
            },
            '&:has(.Mui-checked):not(:has(.Mui-disabled)):not(:has(.Mui-focusVisible))': {
              '& .MuiSwitch-switchBase': {
                transform: 'translateX(19px) translateY(2px)',
                '& + .MuiSwitch-track': {
                  background: 'var(--mui-palette-success-light)',
                },
              },
            },
            '&:has(.Mui-checked):not(:has(.Mui-disabled)):not(:has(.Mui-focusVisible)):hover': {
              '& .MuiSwitch-switchBase': {
                transform: 'translateX(19px) translateY(2px)',
                '& + .MuiSwitch-track': {
                  background: 'var(--mui-palette-success-dark)',
                },
              },
            },
            '&:has(.Mui-checked):not(:has(.Mui-disabled)):has(.Mui-focusVisible)': {
              '& .MuiSwitch-switchBase': {
                transform: 'translateX(19px) translateY(2px)',
                '& + .MuiSwitch-track': {
                  border: '1px solid #000',
                  background: 'var(--mui-palette-success-light)',
                },
              },
            },
          },
        },
      },
    },
  },
}

:::info ์ƒ์„ฑ๋œ ํ…Œ๋งˆ์—๋Š” ํŠน์ • ํ•˜์œ„ ํด๋ž˜์Šค๋ฅผ ๋Œ€์ƒ์œผ๋กœ ํ•˜๋Š” CSS has() ์„ ํƒ์ž๊ฐ€ ํฌํ•จ๋  ์ˆ˜ ์žˆ์–ด์š”. ์ด ์„ ํƒ์ž๋Š” ์˜ˆ์ „์— ๋ธŒ๋ผ์šฐ์ € ์ง€์›์ด ์ œํ•œ์ ์ด์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ฌธ์„œ์˜ ๋‹ค๋ฅธ ํ…Œ๋งˆ ๊ด€๋ จ ์˜ˆ์ œ์—์„œ๋Š” ์‚ฌ์šฉ๋˜์ง€ ์•Š์•˜์–ด์š”. ์ง€๊ธˆ์€ ๋ชจ๋“  ์ตœ์‹  ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ง€์›ํ•ด์š”. :::

Storybook ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ํ™•์ธํ•ด์„œ ์ปดํฌ๋„ŒํŠธ์˜ Material UI ๋ฒ„์ „์„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜๋„ ์žˆ์–ด์š”.

A fully customized Switch component in Storybook.

Using the generated theme (์ƒ์„ฑ๋œ ํ…Œ๋งˆ ์‚ฌ์šฉํ•˜๊ธฐ)

Sync๋กœ ์ƒ์„ฑ๋œ ํ…Œ๋งˆ๋ฅผ ์ฝ”๋“œ๋ฒ ์ด์Šค์— ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ๋ฒ•์˜ ์˜ˆ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์•„์š”:

import { createTheme, ThemeProvider } from '@mui/material/styles';

const theme = createTheme({
  cssVariables: true,
  shape: {
    borderRadiusRound: 999,
  },
  components: {
    MuiSwitch: {
      styleOverrides: {
        root: {
          '&.MuiSwitch-sizeMedium:has(.MuiSwitch-colorPrimary)': {
            '&:has(.Mui-checked):not(:has(.Mui-disabled)):not(:has(.Mui-focusVisible))':
              {
                width: '40px',
                height: '21px',
                padding: '0',
                '& .MuiSwitch-switchBase': {
                  transform: 'translateX(19px) translateY(2px)',
                  padding: '0',
                  '& .MuiSwitch-thumb': {
                    width: '17px',
                    height: '17px',
                    background: '#FAFAFA',
                  },
                  '& + .MuiSwitch-track': {
                    width: '38px',
                    height: '21px',
                    background: 'var(--mui-palette-success-light)',
                    borderRadius: 'var(--mui-shape-borderRadiusRound)',
                    opacity: '1',
                  },
                },
              },
          },
        },
      },
    },
  },
});

export default function MyApp(props) {
  const { Component, pageProps } = props;

  return (
    <ThemeProvider theme={theme}>
      <Component {...pageProps} />
    </ThemeProvider>
  );
}

Feedback and bug reports (ํ”ผ๋“œ๋ฐฑ๊ณผ ๋ฒ„๊ทธ ๋ฆฌํฌํŠธ)

ํ”ผ๋“œ๋ฐฑ์ด ์žˆ๋‹ค๋ฉด ์˜๊ฒฌ์„ ๋“ค๋ ค์ฃผ์„ธ์š”.

๋” ์•Œ์•„๋ณด๊ธฐ (Learn more)