posts

SCSS 호출 및 적용 컨벤션에 대한 이해

Apr 23, 2026 updated Apr 23, 2026 cssnextjsreactscss

에이치나인의 리액트 프로젝트의 CSS 컨벤션을 이해하기 위한 문서이며,

css-in-js는 다루지않고 css-module ( scss )만을 정리합니다.

  • CSS moudule

많은 개발자들이 css next 또는 css modules을 사용하고 있습니다. 그러나 리액트에서 css modules을 classname 속성과 함께 사용하는것은 조금 어려울수도 있습니다. 근본적으로 css modules은 name mangling으로 구현되어있습니다.

사람이 읽을 수 있는 텍스트로 작성된 것을 유니크한 id로 바꿔주며, 이러한 작업이 유니크한 값을 보장하기 때문에 더이상 개발하면서 걱정해야했던 선택자 충돌같은 문제를 고려하지 않아도 됩니다.

일반적으로 대부분은 프로젝트에서 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'): 하이픈이 있는 경우

한가지 단점으로는 자동완성이 안되기때문에 약간의 불편함이 있습니다 ( 편집기마다 차이가 있을수도 있습니다 )

관련 문서