SCSS 호출 및 적용 컨벤션에 대한 이해
에이치나인의 리액트 프로젝트의 CSS 컨벤션을 이해하기 위한 문서이며,
css-in-js는 다루지않고 css-module ( scss )만을 정리합니다.
- CSS moudule
많은 개발자들이 css next 또는 css modules을 사용하고 있습니다. 그러나 리액트에서 css modules을 classname 속성과 함께 사용하는것은 조금 어려울수도 있습니다. 근본적으로 css modules은 name mangling으로 구현되어있습니다.
사람이 읽을 수 있는 텍스트로 작성된 것을 유니크한 id로 바꿔주며, 이러한 작업이 유니크한 값을 보장하기 때문에 더이상 개발하면서 걱정해야했던 선택자 충돌같은 문제를 고려하지 않아도 됩니다.
classnames
일반적으로 대부분은 프로젝트에서 SCSS를 모듈로 사용하는 방법을 선택합니다.
그 중에서도 간편하게 사용할 수 있는classnames라이브러리를 많이 이용하는데. 이 라이브러리 사용법과 코드 컨벤션을 알아보고 더 나은 컨벤션을 적용할 수 있도록합니다.
스타일 객체의 작성된 클래스네임들은 웹팩에 의하여 유니크한 이름으로 생성되며, classnames이 바인딩하는 형식입니다.
이 문서에서는 classname의 기능들이 어떻게 사용되며 작동하는지에 대해서 알아보도록 하겠습니다.
- 샘플 스타일코드
.something { color: deeppink; } .selected { font-weight: bold; } .something-else { color: goldenrod; } /* remember this! we're laying a trap / .something-global { color: black; } / also make note that we're not adding ".something-local" */
CSS는 여러 방식으로 사용할 수 있습니다. 전역으로 설정하는 경우도 있고 지역으로 설정하는 경우도 있는데,
대부분 지역으로 선언하는것을 선호합니다. 자세한 css scoping의 정보는https://github.com/webpack-contrib/css-loader#scope를 참고해보시면 좋습니다.
- CSS 모듈을 사용하지 않는 경우
과거에 css 모듈을 사용할 때 간편하고 또 대중적으로 많이 사용했던 방식입니다.
어떠한 스타일도 import하지않고 classnames 라이브러리만으로 호출합니다. 이 예제에서는 전역적으로 사용하는 스타일과 이 컴포넌트에만 필요한 스타일들 모두를 포함하고 있습니다.
아래의 코드는 boolean props를 쉽게 사용할 수 있으며. 매우 간결하고 축약된 코드를 작성할 수 있습니다.
import classnames from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; const Something = ({ selected }) => { return ( <div className={classnames('something', { selected })}> Hello <span className="global-style something-else"> World </span> </div> ); }; Something.propTypes = { selected: PropTypes.bool }; export default Something;
중요한것은 selected prop이 어떠한 기본값도 요구하지 않는다는 것입니다.
컴포넌트를 설계할 때 값이 undefined인 경우를 생각해보는것은 상당히 중요합니다.
위의 경우에 undefined가 들어올 경우 classnames는 selected를 찾지 못하고 prop의 부재를 초래합니다.
- CSS 모듈을 사용하지만, classnames를 사용하지않는경우
위의 샘플과 비교할경우 모듈은 사용하지만 클래스네임 라이브러리를 사용하지 않는 경우입니다.
classnames의 helper function을 사용할 수 없기 때문에 문자열을 사용해야합니다.
// <-- not importing classnames import PropTypes from 'prop-types'; import React from 'react'; import styles from './styles.css'; // <-- CSS modules const Something = ({ selected }) => { const selectedClassName = selected ? ` ${styles.selected}` : ''; // <-- captured in a variable because it's too long return ( <div className={`${styles.something}${selectedClassName}`)}> Hello <span className={`global-style ${styles['something-else']}`}> World </span> </div> ); }; Something.propTypes = { selected: PropTypes.bool }; export default Something;
Bad things:
This is pretty hard to read compared to the example above.
Less ugly alternatives, like array joining, add the overhead of creating arrays to create a string.
Good things:
No specialized API to learn.
Very straight-forward. Just plain JS.
- classnames를 사용하지만 binding을 사용하지 않는 경우
다시 돌아와서 classnames를 사용하지만 모듈을 사용하지 않는 경우입니다.
문자열로 호출해야하는 귀찮은 문법에서는 벗어났지만 여러가지 문제가 남아있습니다.
import classnames from 'classnames'; // <-- better than nothing import PropTypes from 'prop-types'; import React from 'react'; import styles from './styles.css'; const Something = ({ selected }) => { return ( <div className={classnames(styles.something, { [styles.selected]: selected })}> Hello <span className={classnames('global-style', styles['something-else'])}> World </span> </div> ); }; Something.propTypes = { selected: PropTypes.bool }; export default Something;
Bad things:
Pressure to use camelCased classnames in css. Considerclassnames(styles['something-else'])versusclassnames(styles.somethingElse)
Verbose when setting boolean classnames. Considerclassnames({ [styles.selected]: selected }
Much more verbose than usingclassnameswithout CSS modules. Rememberclassnames('something-else')from above? Compare that toclassnames(styles.something).
Fails in cases wherestylesis undefined (some edge cases in tests and SSR).
Good things:
Very explicit about where a specific classname is coming from. No potential for confusion between imported CSS module styles and global styles (more on that later).
Decidedly easier to read than not usingclassnamesat all.
- classnames.bind를 사용하는 경우
마지막으로 가장 개선된 사용법인 bind를 사용하는 경우입니다.
import classnames from 'classnames/bind'; // <-- notice bind import PropTypes from 'prop-types'; import React from 'react'; import styles from './styles.css'; const cx = classname.bind(styles); // <-- explicitly bind your styles const Something = ({ selected }) => { return ( <div className={cx('something', { selected })}> Hello <span className={cx('global-style', 'something-else')}> World </span> </div> ); }; Something.propTypes = { selected: PropTypes.bool }; export default Something;
Bad things:
If you seecx('something')you will need to refer to thestyles.cssto be sure you're not accidentally referencing a global class (more on this below).
Good things:
More like usingclassnameswithout CSS Modules
Usingcx('something')is significantly fewer characters thanclassnames(styles.something)
Usingcx({ selected })is significantly less cumbersome/error prone thanclassnames({ [styles.selected]: selected})
In situations wherestylesis undefined (some edge cases in tests and SSR), the boundcxfunction falls back silently to global classes.
Using the boundcxversion makes it less painful to use dash-cased strings, considercx('something-else')versusclassnames(styles['something-else']). It essentially removes the pressure to use camelCased strings for classnames.
위의 설명이 이번 문서를 작성하게된 주요 내용입니다.
.list-item { font-size: 14px; line-height: 20px; display: flex; flex-wrap: wrap; height: 3rem; width: 500px; background: #f0fffd; transition: all ease-out 150ms; }
위의 코드를 컴포넌트에서 불러와 사용하는 경우
import styles from './Task.module.scss'; import classNames from 'classnames/bind'; const cx = classNames.bind(styles); const Task: FC<TaskProps> = props => { ... return ( <div className={cx('list-item')}> <label className={cx('checkbox')} htmlFor="checked" aria-label={`archiveTask-${id}`} > ... </label> ... </div> ); }; export default Task;
하이픈이 들어가는 경우cx(styles.list-item)을 인식하지 못합니다.
cx(styles['list-item']으로 사용해야 인식이 가능하기 때문에 하이픈이 없는 네임 컨벤션과 달라지게됩니다.
네이밍에 따른 차이 발생
cx(styles.something): 하이픈이 없는 경우
cx(styles[something-hypen]): 하이픈이 있는경우
네이밍에 따른 차이가 발생하지 않음
cx('something'): 하이픈이 없는 경우
cx('something-hypen'): 하이픈이 있는 경우
한가지 단점으로는 자동완성이 안되기때문에 약간의 불편함이 있습니다 ( 편집기마다 차이가 있을수도 있습니다 )