ããã«ã¡ã¯ãã¬ã·ãäºæ¥é¨ã®kaorun343ã§ããæã ã®ãã¼ã ã§ã¯レシピサービスのフロントエンドを Next.js と GraphQL のシステムに置き換えている話 - クックパッド開発者ブログã«ã¦ç´¹ä»ããã¨ãããã¬ã·ããµã¼ãã¹ã Next.js ãã¼ã¹ã®æ°ã·ã¹ãã ã¸ã¨ç§»è¡ãã¦ãã¾ããä»åã¯ããã®æ°ã·ã¹ãã ã®CSS in JSãEmotionããã¼ãã©ã³ã¿ã¤ã ã®vanilla-extractã¸å¤æ´ãã話ã§ãã
èæ¯
以忏ãã レシピサービスのフロントエンドに CSS in JS を採用した話 - クックパッド開発者ブログã§ã¯ãCSS in JSã©ã¤ãã©ãªã¨ã㦠Emotionï¼@emotion/reactï¼ãæ¡ç¨ããçµç·¯ã¨éçºç°å¢æ´åãç´¹ä»ãã¾ãããæ¡ç¨çç±ã¨ãã¦ã¯ä»¥ä¸ã®éãã§ããã
- ã»ã¬ã¯ã¿ã«ä¸æãªIDãå²ãæ¯ãããã®ã§ãã¹ã¿ã¤ã«ãé©ç¨ããè¦ç´ ã¨ã¯å¥ã®è¦ç´ ã¸ã®ãæå³ããªãã¹ã¿ã¤ã«é©ç¨ãé²ããã¨ãã§ããã
- ESLintãTypeScriptã³ã³ãã¤ã©ã¨ãã£ãJavaScriptã®éçè§£æãã¼ã«ã®æ©æµãåãããã¨ãã§ããã¿ã¤ããæ©è½å餿ã®å餿¼ãã«æ°ã¥ãããããªãã
- styled-componentsã®ãããªã¹ã¿ã¤ã«ã§ã¯JSXã®ããªã¼ãè¦ãã¨ãã«ãæ©è½ãæã¤ã³ã³ãã¼ãã³ããªã®ãè£ é£¾ãããã³ã³ãã¼ãã³ããªã®ããããããã³ã¼ãã¬ãã¥ã¼ããã«ããã
- é常ã®CSSã®è¨æ³ã«æ £ããã¡ã³ãã¼ãå¤ãã®ã§ãString Stylesãããªãã¡ã¿ã°ä»ããã³ãã¬ã¼ããªãã©ã«ãæ¡ç¨ããã
ãã®ãããªæ¹éã§Emotionã®å°å ¥ã決ããstylelintãeslintãå°å ¥ããå¿ è¦ã«å¿ãã¦ã«ã¹ã¿ã ã«ã¼ã«ã使ãã¦æ©è½éçºãé²ãã¾ããã
ããããªãããEmotionãå°å ¥ãã¦ãã2å¹´ã»ã©çµã£ãçµæã以ä¸ã®ãããªèª²é¡ãæ¸å¿µãæ±ããããã«ãªãã¾ããã
- ãã¼ã¸ãµã¤ãºï¼SSRæã«ã¯åæè¡¨ç¤ºç¨ã®CSSãEmotionãä½ãããã§ããããã®CSS㯠.css ãã¡ã¤ã«ã¨ãã¦ãã©ã¦ã¶ã«å±ãã®ã§ã¯ãªã Next.jsããé ä¿¡ãããHTMLã«åãè¾¼ã¾ããç¶æ ã§ãã©ã¦ã¶ã«å±ãã¾ãããã®ããããã¼ããã©ã³ãµã¼ãééããHTMLã®ãµã¤ãºãå¢å ãã¦ãã¾ãã¾ããCSSã®ãã¼ã¿ãCDNãéããªããããããã©ã¼ãã³ã¹ã®é¢ã§ãã³ã¹ãã®é¢ã§åé¡ã§ããå®éãbackground-imageã«base64ã®ç»åURLãåãè¾¼ãã ã¨ãã«ã¯ããã®å½±é¿ãå¼·ãåºã¦ãã¾ãã¾ããã
- åççæã«ããè¥å¤§åï¼Next.jsã®SSRæã«ãã£ããCSSã®ããªã¨ã¼ã·ã§ã³ãå¢ããã¦ãã¾ãã¨ãNext.jsããã»ã¹ã®ã¡ã¢ãªä½¿ç¨éãå¢å¤§ããã¢ããªã±ã¼ã·ã§ã³ãè½ã¡ã¦ãã¾ãã¾ããããã¯ãEmotionãã¤ã³ã¡ã¢ãªã®ãã£ãã·ã¥æ©æ§ãåãã¦ãããä¸åº¦çæããCSSãã¼ã¿ãä¿æãç¶ããããã§ããéå»ã®äºä¾ã§ã¯ãã¬ã·ããã¨ã«ç°ãªãbackground-imageãè¨å®ããCSSãEmotionã§æ¸ããã¨ãã«ãã®åé¡ãçãã¾ãã*1ã
- ã¯ã©ã¤ã¢ã³ãå´ã®ãªã¼ãã¼ãããï¼CSSçæã®ããã«ãã©ã¦ã¶ä¸ã§JavaScriptãå®è¡ãããããããã¼ã¸ã®ããã©ã¼ãã³ã¹ã¸å½±é¿ãããæ¸å¿µãããã¾ããEmotionã¯CSSã®è¨è¿°å
容ã®è§£æãå¤ããã©ã¦ã¶åãã®è¨è¿°ã®è¿½å ãCSSã®åæãããã¦ã¹ã¿ã¤ã«ã®DOMã¸ã®æ¿å
¥ããã©ã¦ã¶ä¸ã§å®è¡ãã¾ããEmotionã®
css颿°ã使ãã°ä½¿ãã»ã©CSSã«é¢ããå¦çã®å®è¡æéãå¢ãã¦ããã¾ããã¾ãããããã®å¦çããã©ã¦ã¶ä¸ã§å®è¡ããããã®JSã®ã³ã¼ããå¿ è¦ã¨ãªãããããã³ãã«ãµã¤ãºãå¢å ãã¦ãã¾ãã¾ããæ°ã·ã¹ãã ããã¹ããã¦ãããã¼ã¸ã¯ã¹ãã¼ããã©ã³åãã®ãã¼ã¸ã§ãããããã©ã¼ãã³ã¹ããã³ãã«ãµã¤ãºã¯ç¹ã«æ³¨è¦ãã¦ãã¾ãã
ããã§ãEmotionããå¥ã® CSS ç°å¢ã¸ã®ç§»è¡ãæ¤è¨ãã¾ããã
æè¡é¸å®
ä¸è¨ã®èª²é¡ãè¸ã¾ãã以ä¸ã®è¦ä»¶ã§æ°ããCSS ç°å¢ãæ¤è¨ãã¾ããã
- Emotionã«è¿ãéçºä½é¨ï¼Emotionã¨åæ§ã«ãCSS ã¯ã©ã¹åãèªåã§ã¤ããå¿ è¦ããªããã¨
- CDNã®æ´»ç¨ï¼ãã«ãæã« CSS ãã¡ã¤ã«ãçæãã㦠CDN ããéçã«é ä¿¡ã§ãããã¨
- ä½ããªã¼ãã¼ãããï¼ã¼ãã©ã³ã¿ã¤ã ã§ãããã¨ï¼ãã«ãæã«CSSãçæãããã©ã¦ã¶ã«éãããJavaScriptã«ã¯CSSãçæããã³ã¼ããå«ã¾ãªããã¨ï¼
- å°æ¥æ§ï¼Server Componentså°å ¥ãè¦æ®ãã¦ãServer Componentsã«å¯¾å¿ãã¦ããã¨å¬ãã
æ¤è¨ããçµæããããã®è¦ä»¶ãæºããã©ã¤ãã©ãªã¨ãã¦vanilla-extractãæããã¾ããã vanilla-extractã§ã¯CSSãJavaScriptã®ãªãã¸ã§ã¯ãã¨ã㦠.css.[jt]s ã¨ããæ¡å¼µåã®ãã¡ã¤ã«ã«è¨è¿°ãã¾ãããããvanilla-extractã®å種ãã³ãã©ã«å¯¾å¿ãããã©ã°ã¤ã³ãCSSã«å¤æããCSSãã¡ã¤ã«ãçæãã¾ããã¾ããããããã®ã¹ã¿ã¤ã«ã¯ä¸æãªã¯ã©ã¹åãã»ã¬ã¯ã¿ã¨ãã¦ãããEmotionã¨åãããã«æå³ããªãã¹ã¿ã¤ã«é©ç¨ãé²ããã¨ãã§ãã¾ãã
Emotionã¨vanilla-extractã®æ¯è¼ã表ã«ããã¨ä»¥ä¸ã®ããã«ãªããæè¡é¸å®ã§éè¦ããé ç®ãæºããã¦ãã¾ãã
| æ¯è¼é ç® | Emotion | vanilla-extract |
|---|---|---|
| ã¯ã©ã¹åã®èªåä»ä¸ | â | â |
| CDNããé å¸å¯è½ | ï¼HTMLã«åãè¾¼ã¾ããï¼ | â |
| ã¼ãã©ã³ã¿ã¤ã | ï¼ãã©ã¦ã¶ï¼ | â |
| Server Componentså¯¾å¿ | ï¼CSRã®ã¿ï¼ | â |
| ã³ã³ãã¼ãã³ãã¨åããã¡ã¤ã«ã«æ¸ãã | â | ï¼.css.[jt]sã«æ¸ãå¿ è¦ãããï¼ |
| CSSã®æ¸ãæ¹ | String StylesãObject Styles | Object Stylesã®ã¿ |
| Stylelint | â | ï¼æªå¯¾å¿ï¼ |
| ã¹ãããã·ã§ãããã¹ã | â | ï¼ãªãï¼ |
| ãã³ãã¼ãã¬ãã£ã¯ã¹ã®èªåä»ä¸ | â | ï¼ãªãï¼ |
ï¼æ¯è¼å½æã@emotion/reactã¯v11.10.0ã@vanilla-extract/cssã¯v1.9.1ã§ããï¼
çè ãvanilla-extractãææ¡ããéã¯ãè¨è¿°æ¹æ³ã®éãããã«ãææç©ã®å·®ããããããã«ãå®éã®ãã¼ã¸ãæ¸ãæãããã«ãªã¯ã¨ã¹ããä¾ç¤ºãã¾ãããCSSã®ã³ã¼ãéãå°ããOGPç»åçæç¨ã®ãã¼ã¸ã対象ã«ãã¾ããã
vanilla-extractã®ãã¡ãªãã
䏿¹ã§ãvanilla-extractã«ã¯ãã¡ãªãããåå¨ãã¾ãã
CSSã®æ¸ãæ¹
ã¾ããããã¾ã§éãString Stylesã§è¨è¿°ãããã¨ãã§ããªããªãã¾ããããã®ç¹ã«ã¤ãã¦æ¸å¿µç¹ããªãããã¶ã¤ãã¼ã®æ¹ã«ä¼ºã£ãã¨ããããCSSãæ¸ããã°åé¡ãªããã¨ã®ãã¨ã§ãããvanilla-extract㯠.css.[jt]s ã«è¨è¿°ããå¿ è¦ãããã¾ããããã®ç¹ã«ã¤ãã¦ãããã¼ã ã¡ã³ãã¼ããåæããããã¾ããã
Stylelint
å ãã¦è¨æ³ãå¤ãã£ããã¨ã«ããStylelintã§æ¤æ»ã§ããªããªãã¾ãããããããªãããCSSã®ããããã£ãå¤ã®ã¿ã¤ãã»ããããã£ã®éè¤ã¯TypeScriptã§è¦ã¤ãããã¾ãããæã ã®ã¢ããªã±ã¼ã·ã§ã³ã§ã¯è©³ç´°åº¦ã«é¢é£ãã¦å°ããããªæ¸ãæ¹ããã¦ããªãã®ã§ãStylelintã廿¢ãããã¡ãªããã¯å°ããã¨å¤æãã¾ããã
ã¹ãããã·ã§ãããã¹ã
Emotionã§ã¯@emotion/jestãã¹ãããã·ã§ãããã¹ãã«CSSã®è¨è¿°ã表示ããä»çµã¿ãæä¾ãã¦ãã¾ããããããvanilla-extractã§ã¯æä¾ããã¦ããããã¹ãããã·ã§ãããã¹ãã§CSSã®è¨è¿°ã確èªãããã¨ãã§ããªããªãã¾ãããã¹ãããã·ã§ãããã¹ãã«ã¤ãã¦ã¯ãéè¯ããã°ãæ¤ç¥ã§ããã»ã©ã®ã¡ãªãããããªãã¨å¤æãã使ããªããªããã¡ãªããã¯å°ããã¨å¤æãã¾ããã
ãã³ãã¼ãã¬ãã£ã¯ã¹ã®èªåä»ä¸
Emotionã¯ãã³ãã¼ãã¬ãã£ã¯ã¹ãèªåã§ä»ä¸ãã¦ãããã®ã§ãããEmotionã§ã¯ã©ã¤ãã©ãªå©ç¨è ããã©ã¦ã¶ã®ãã¼ã¸ã§ã³ãæå®ã§ããªããããã¯ãã¯ãããã®æ¨å¥¨ç°å¢ããå¤ããã©ã¦ã¶ã対象ã¨ããããããã£ã追å ããã¦ãã¾ãããã¯ãã¯ãããã®æ¨å¥¨ç°å¢ãéã¿ãèªåä»ä¸ããªããªããã¡ãªããã¯å°ããã¨å¤æãã¾ããã
vanilla-extractã¸ã®ç§»è¡
CSSã®è¨è¿°ãvanilla-extractã¸ç§»è¡ãããã¨ã決å®ããå¾ãç§»è¡ä½æ¥ã«ã¨ããããã¾ããã
æåã¯ãã¹ã¦æä½æ¥ã§æ¸ãæãã¦ããã®ã§ãããéä¸ããæ£è¦è¡¨ç¾ã使ã£ãç°¡ç´ ãªå¤æãã¼ã«ãå°å ¥ãã¦ç§»è¡ä½æ¥ãã¹ãã¼ãã¢ãããã¾ããã Emotionã¨vanilla-extractã¯å ±åã§ãããããæã空ãã¦ããã¨ãã«æåãããã¦å°ããã¤ç§»è¡ãã¦ããã¾ãããã¾ããNext.jsã¢ããªã±ã¼ã·ã§ã³æ¬ä½ã ãã§ã¯ãªããå ±éã³ã³ãã¼ãã³ãããã±ã¼ã¸ãããã¦ç¤¾å ã®ãã¶ã¤ã³ã·ã¹ãã ã®Reactã©ã¤ãã©ãªãvanilla-extractã«ç§»è¡ãã¾ããã
// Emotion import { css } from '@emotion/react' const linkStyle = css` flex: 1; box-sizing: border-box; background-color: white; ` const linkDisableStyle = css` ${linkStyle} background-color: gray; ` export const MyComponent = () => { return ( <section> <a href="#" css={linkStyle}> Link </a> <a href="#" css={linkDisableStyle}> Disabled Link </a> </section> ) }
// vanilla-extract // MyComponent.css.js export const linkStyle = style({ flex: 1, boxSizing: 'border-box', backgroundColor: 'white', }) export const linkDisabledStyle = style([ linkStyle, { backgroundColor: 'gray', }, ]) // MyComponent.js import { linkDisabledStyle, linkStyle } from './MyComponent.css.js' export const MyComponent = () => { return ( <section> <a href="#" className={linkStyle}> Link </a> <a href="#" className={linkDisabledStyle}> Disabled Link </a> </section> ) }
åçã«ã¹ã¿ã¤ã«ãçæãã¦ããç®æã«ã¤ãã¦ã¯ã @vanilla-extract/dynamic ã @vanilla-extract/recipesãå©ç¨ãã¦åé¡ãªãç½®ãæãããã¾ããã
// Emotion import { css } from '@emotion/react' const linkStyle = (size) => css` width: ${size}; height: ${size}; ` export const MyComponent = () => { return ( <section> <a href="#" css={linkStyle('100px')}> Link 1 </a> <a href="#" css={linkStyle('200px')}> Link 2 </a> </section> ) }
// vanilla-extract // MyComponent.css.js import { createVar, style } from '@vanilla-extract/css' export const sizeVar = createVar() export const linkStyle = style({ width: sizeVar, height: sizeVar, }) // MyComponent.js import { assignInlineVars } from '@vanilla-extract/dynamic' import { sizeVar, linkStyle } from './MyComponent.css.js' export const MyComponent = () => { return ( <section> <a href="#" className={linkStyle} style={assignInlineVars({ [sizeVar]: '100px' })} > Link 1 </a> <a href="#" className={linkStyle} style={assignInlineVars({ [sizeVar]: '200px' })} > Link 2 </a> </section> ) }
ç§»è¡ããçµæãEmotionã§èª²é¡ãæ¸å¿µã«æãã¦ãããã¨ãè§£æ¶ã§ãã¾ããã
- ãã¼ã¸ãµã¤ãºï¼CSSãã¡ã¤ã«ã«ãã¼ã¸å ¨ä½ã®CSSãå«ã¾ããããã«ãªããCDNããé å¸ã§ããããã«ãªãã¾ãããbackground-imageã¨ãã¦base64ã®ç»åãã¡ã¤ã«ãåãè¾¼ãã å ´åã§ããã¼ããã©ã³ãµã¼ãéãHTMLã®ãµã¤ãºã大ãããªããã¨ã¯ããã¾ããã
- åççæã«ããè¥å¤§åï¼ã¡ã¢ãªä½¿ç¨éãå¢å ãã¦Next.jsããã»ã¹ãè½ã¡ããã¨ã¯ãªããªãã¾ããã
- ã¯ã©ã¤ã¢ã³ãå´ã®ãªã¼ãã¼ãããï¼CSSã®çæã¯ãã«ãæã«ã®ã¿ãããªãããããã«ãªããçæã®ããã®JavaScriptã¯@vanilla-extract/dynamicã@vanilla-extract/recipesã ãã«ãªãã¾ãããã¾ããEmotionã®ã©ã³ã¿ã¤ã åé¤ã«ãããã³ãã«ãµã¤ãºã¯gzipã§10kBå¼±æ¸å°ãã¾ããã
ã¨ã³ã¸ãã¢ããã¶ã¤ãã¼ããããç¹ã«ãã¬ãã£ããªæè¦ã¯åºã¦ãã¾ãããåãã¦vanilla-extractã触ãã¡ã³ãã¼ããåé¡ãªãCSSã夿´ã§ãã¦ãã¾ãã
ãããã«
ä»åã¯ã¬ã·ããµã¼ãã¹ã®æ°ã·ã¹ãã ã«ããã ã¼ãã©ã³ã¿ã¤ã CSS in JS ã®è©±ãç´¹ä»ãã¾ãããã¯ãã¯ãããã§ã¯ãããããã¢ãã³ãªæè¡ã«ããã¬ã·ããµã¼ãã¹ã®å·æ°ãé²ãã¦ããã¾ãã
*1:ãã®ã±ã¼ã¹ã§ã¯style屿§ã«ç´æ¥background-imageãæå®ããCSSãä»ä¸ãã¦åé¡ãåé¿ãã¾ããããéçºè ããã®ç¹æ§ãæ°ã«ãç¶ããã®ã¯é£ããã§ã