CSS작성 테크닉에 대한 고찰

리액트 환경에서 어떤 디자인 시스템을 적용해야 하는가에 대한 고민을 담았습니다.

배경

그냥 보통 많이 쓰는 Emotion을 쓰다가 리뷰어분이 디자인 시스템에 대한 조언을 해주셨다. 가만…히 생각해보니 “CSS-in-JS가 정말 적합한가?” 하는 생각이 들어서 여러가지 기술들에 대한 조사와 내가 원하는 바를 어떻게 충족시킬것인지에 대해 분석을 해보았다.

시중에 나와있는 기술들

CSS

tag, class, id등의 선택자를 요소에 주고, css파일에서 해당 요소들을 디자인 한다.

다만, 사이즈가 조금만 커져도 다음과 같은 문제들이 발생한다.

  1. Global Namespace : 클래스 이름이 충돌 될 수 있다
  2. Dead Code Elimination : 더이상 필요 없는 코드를 찾아서 지우는게 너무 힘들다. 이걸 지웠을때 다른 곳에서 스타일이 바뀔 수 있기 때문이다.
  3. Non-deterministic Resolution : css로드 순서에 따라서 스타일에 적용되는 우선순위가 달라진다. 다만, 로드 순서가 뒤에 있어도 앞에 로드된 CSS의 가중치(class < id)가 더 높다면 로드 순서에 상관없이 가중치가 높은 스타일이 적용된다 (리뷰 내용)

(vjeux가 설명하는 CSS의 문제점 7가지 중 제가 공감하고 이해하는 문제점만 뽑았습니다.)

Sass

css안에서 변수, if, for, 함수 등을 사용할 수 있다. 이렇게 작성된 sass파일은 전처리기에 의해 css파일로 변환된다. css만 쓸거면 차라리 sass를 쓰는게 훨씬 좋다. 하지만 그렇다고 해서 css가 가지는 3가지 문제점을 해결한것은 아니다. 그냥 css를 더 쉽게 작성할 수 있게 되었을 뿐이다.

SASS + BEM

BEM은 이름 짓는 규칙이다. 예를들면 아래와 같다.

from : https://keepinguptodate.com/pages/2020/05/bem-visually-explained/

이렇게 하면 이름 충돌날 걱정을 안해도 되서 좋다. 다만 이름 짓는게 힘들고 작은 컴포넌트로 나눠서 재사용 하는 관점에서는 별로 유용해 보이지 않는다. 다만 React가 아니라 그냥 웹페이지라면 나는 SCSS + BEM을 선호한다.

Postcss

PostCSS는 js로 만든 플러그인으로 우리가 작성한 css파일을 변환해준다. 예를들어 autoprefixer라는 플러그인을 사용하면 아래와 같은 일을 할 수 있다.

::placeholder {
  color: gray;
}

이렇게 작성하고 PostCSS에 넣으면 다음과 같은 파일을 뱉어준다.

::-moz-placeholder {
  color: gray;
}
:-ms-input-placeholder {
  color: gray;
}
::placeholder {
  color: gray;
}

여기저기 검색해보니 SCSS보다는 PostCSS의 다양한 플러그인을 사용하는 편이 더 낫다고 하는 글들이 많았다. 다양한 기능을 쓸 수 있는건 좋은데 아무래도 플러그인 개발자가 개발을 멈추면 프로젝트에 큰 영향을 끼칠 수 있다는 불안함도 있다.

css-module

import styles from './app.module.css';

function App() {
  return (
    <div>
      <div className={styles.box}>IM BOX</div>
      <div className={styles.container}>IM CONTAINER</div>
    </div>
  );
}
// app.module.css

.box {
  background-color: green;
  color: white;
  width: 500px;
  height: 200px;
}

.container {
  background-color: yellow;
  width: 100%;
  height: 100px;
}

이렇게 하면 결과가 아래처럼 나온다

클래스 이름은 알아서 해쉬값이 들어간다. 이름 충돌날 걱정이 없다!

Atomic css

HTML태그에 css를 바로 냅다 꽂아버리는 방식이다.

.bw-2x {
  border-width: 2px;
}
.bss {
  border-style: solid;
}
.sans {
  font-style: sans-serif;
}
.p-1x {
  padding: 10px;
}
<div class="bss sans p-1x">Hello World</div>

처음에는 진짜 별로라고 생각했는데, 뭔가 여러글들을 읽으면서 사람들이 좋다 좋다 하니까 좀 좋아보인다. 그러니까 핵심은 자꾸 element에 어떤 의미 있는 이름을 부여할려고 하니까 css가 어려워 진다는 것이다. 그냥 “왼쪽에 이미지가 있고 오른쪽에 글이 있는 div” 로 봐야지 이거를 자꾸 “media-object”라 던지 “row-card”라는 이름을 붙이려고 하지 말라는 말이다. 뇌가 피곤하니까! 그냥 포토샵 하듯이 냅다 스타일을 줘버리라는 것이다.

생각해보니 inner, wrapper, container 등등,,, 이런 이름 짓는게 약간 탐탁치 않기는 했다.

이런 식으로 스타일링을 하는 프레임워크가 몇가지 있다.

  1. tailwind css : 가장 유명하다.
  2. windi css : tailwind보다 좀 더 발전되었다고 한다.
  3. unocss : 위의 2개와 달리 입력할때마다 해당 class를 생성해 준다고 한다. 미리 css를 정의해두고 class를 사용하는게 아니라 실시간으로 생성되니 더 편할것 같다. Atomic CSS를 쓴다면 unocss를 사용할 듯 싶다.

그런데 몇가지 단점도 보인다.

<div class="py-8 px-8 max-w-sm mx-auto bg-white rounded-xl shadow-md space-y-2 sm:(py-4 flex items-center space-y-0 space-x-6)">
  <img class="block mx-auto h-24 rounded-full sm:(mx-0 flex-shrink-0)" src="/img/erin-lindford.jpg" alt="Woman's Face" />
  <div class="text-center space-y-2 sm:text-left">
    <div class="space-y-0.5">
      <p class="text-lg text-black font-semibold">Erin Lindford</p>
      <p class="text-gray-500 font-medium">Product Engineer</p>
    </div>
    <button class="px-4 py-1 text-sm text-purple-600 font-semibold rounded-full border border-purple-200 hover:(text-white bg-purple-600 border-transparent) focus:(outline-none ring-2 ring-purple-600 ring-offset-2)">
      Message
    </button>
  </div>
</div>
  1. 지금 필요하지 않은 정보도 읽어야 한다. 예를들면 media-query 같은거. css로 작성했으면 media-query는 다른 파일이나 다른 공간에 작성되었을 것이다.
  2. 이게 가로로 읽어서 그런건가 머릿속에서 해당 엘리먼트가 어던 모습일지 잘 안그려진다. css에서 작성할때는 그래도 가로길이 세로길이 딱 나오고, position나오고 폰트사이즈 나오고 배경색 나오니까 그림이 그려지는데 이거는 읽기가 어렵다.
  3. 너무 많은 class때문에 HTML 구조가 잘 눈에 안들어 온다.

그래도! 네이밍의 고통에서 해방된다는건 참 좋아 보인다.

CSS-in-js (emotion)

js파일 안에서 css를 작성하는 방식이다.

장점

  1. 네이밍 걱정 안해도 된다.
  2. 알아서 vendor prefix가 붙어서 나온다.
  3. 명시적이어서 보기에 좋다 (<Container> vs <div className="container">)
  4. sass 처럼 &(부모 선택자)를 지원한다.
  5. style과 component를 한공간에 둘 수 있다. 스타일 수정하러 다른 파일에 들락날락 하지 않아도 된다. (근데 이거는 창 2개 키면 되는거 아닌가?)
  6. props를 받을 수 있어서 다이나믹한 css를 작성할 수 있다.
  7. typescript를 지원한다. 즉, props로 받는 것들의 타입을 한정 할 수 있다.
  8. style을 적용한 컴포넌트를 쉽게 만들 수 있다.

대략 이런식으로 작성한다.

styled compoennt

const Button = styled.button`
  padding: 32px;
  background-color: hotpink;
  color: black;
  &:hover {
    color: white;
  }
`

styled component with props

const Button = styled.button`
  color: ${props =>
    props.primary ? 'hotpink' : 'turquoise'};
`

// global theme을 사용한 경우
const Button = styled.button`
  color: ${props =>
    props.primary ? props.theme.primaryPink : 'turquoise'};
`
const dynamicStyle = props =>
  css`
    color: ${props.color};
  `

const Container = styled.div`
  ${dynamicStyle};
`
render(
  <Container color="lightgreen">
    This is lightgreen.
  </Container>
)

inline style

const color = 'darkgreen'

render(
  <div
    css={css`
      background-color: hotpink;
      &:hover {
        color: ${color};
      }
    `}
  >
    This has a hotpink background.
  </div>
)

나는 우선 이정도 기능만 사용한다.

단점

  1. js가 실행 될때 스타일이 적용되는 방식이라서 브라우저 에서 지원하는 css캐싱이 적용되지 않는다. 페이지 새로고침 할때마다 다시 읽혀야 하니까 아무래도 느리다.
  2. Stylelint를 사용할 수 없다
  3. scss의 유틸 함수를 사용할 수 없다(그런데 이건 비슷하게 js로 만들 수는 있을것 같다)
  4. hot reload가 비교적 느리다

그런데 이런 생각이 든다. 굳이 css-in-js를 써야하나? css-module로 이름 충돌 해결할 수 있고, dynamic한 스타일은 className을 추가로 줘서 해결할 수 있다.

내가 원하는것

이름 충돌은 안돼 !

CSS, SCSS는 탈락.

BEM은? emotion이나 css-module로 이름 충돌을 피할 수 있기 때문에 굳이 사용할 필요가 없어보인다

고유의 스타일을 가진다

chakra-ui badge
chakra-ui avatar
chakra-ui button

뱃지, 아바타, 버튼들은 각각 고유의 스타일이 있다. 어딜 내놔도 거의 변하지 않는 그런 고유의 스타일이 있다. 나는 이런 고유의 스타일을 줄 수 있어야 한다고 생각한다. 그래야 재사용하기 쉽기 때문이다.

css-in-js

const StyledButton = styled.button`
  font-size: 12px;
`;

쉽게 만들 수 있다.

css-module

.button {
  font-size: 12px;
}
import styles from 'button.module.scss';

function Button() {
  return <button type="button" className={styles.button}>버튼</button>
}

Atomic css

function Button() {
  return <button className="bg-black fs-12">버튼</button>
}

뭐 이런식으로 줄 수 있을것 같다.

고유한 스타일을 가지면서 때로는 변형도 가능하다

CSS-in-js

const dynamicButtonStyle = (props) => css`
  background-color: ${props.isActive ? "yellow" : "white"};
`;

const StyledButton = styled.button`
  font-size: 12px;
  ${dynamicButtonStyle}
`;

function Button() {
  const isActive = true;
  return <StyledButton isActive={isActive}></StyledButton>
}

이렇게 props를 받을 수 있다. 좋다. 깔끔하다.

css-module

.button {
  font-size: 12px;
  background-color: white;
  &.active {
    background-color: yellow;
  }
}
function Button() {
  const isActive = true;
  const cns = cn(styles.button, { [styles.active]: isActive });
  return (
    <button type="button" className={cns}>
      버튼입니다
    </button>
  );
}

이것도 css-in-js보다는 조금 길지만, 낫배드

atomic css

function AmoticCSSButton() {
  const isActive = true;
  const cns = cn('fs-12', 'bg-white', { 'bg-yellow': isActive });
  return (
    <button type="button" className={cns}>
      버튼입니다
    </button>
  );
}

변경 사항이 많은 경우도 깔끔하게 처리 가능하다

Button의 size, variant 이렇게 2가지의 variation이 있다고 가정하고 3가지의 기법을 비교해보자.

css-in-js

const getSize = (size) => {
  switch (size) {
    case "sm":
      return css`
        font-size: 12px;
      `;
    case "md":
      return css`
        font-size: 15px;
      `;
    default:
      return css`
        font-size: 24px;
      `;
  }
};

const getVariant = (variant) => {
  switch (variant) {
    case "primary":
      return css`
        background-color: blue;
        color: white;
      `;
    case "secondary":
      return css`
        background-color: gray;
        color: white;
      `;
    default:
      return css`
        background-color: blue;
        color: white;
      `;
  }
};

const dynamicButtonStyle = ({ size, variant }) => css`
  ${getSize(size)}
  ${getVariant(variant)}
`;

const StyledButton = styled.button`
  font-size: 12px;
  background-color: white;
  ${dynamicButtonStyle}
`;

function Button({ size, variant }) {
  return (
    <StyledButton size="md" variant="secondary">
      버튼입니다
    </StyledButton>
  );
}

나쁘지 않다. 다만 브라우저 inspector에 이렇게 구분이 없이 나오는건 약간 아쉽다.

그래도 css-in-js는 유연하게 코딩이 가능하다. 예를들면, 위처럼 switch로 안돌리고 각 variation에 대한 스타일을 변수에 담아 놓을 수도 있다. 이것이 css-module에 비해 css-in-js가 가지는 최대 장점이 아닐까 싶다.

css-module

// app.module.scss

.button {
  font-size: 12px;
  background-color: white;
}

.sm {
  font-size: 12px;
}

.md {
  font-size: 15px;
}

.lg {
  font-size: 24px;
}

.primary {
  background-color: blue;
  color: white;
}

.secondary {
  background-color: gray;
  color: white;
}
import styles from './app.module.scss';

function Button({ size, variant }) {
  const cns = cn(styles.button, styles[size], styles[variant]);
  return (
    <button type="button" className={cns}>
      버튼
    </button>
  );
}

오 이것도 상당히 깔끔하다. 다만! CSS-in-JS와는 달리 아주 다이나믹한 값들은 넣기 힘들다. 예를들어서 marginButton: 37px같은 것들은 CSS-in-JS라면 props로 받아서 바로 넘겨주면 되는데 class로는 이게 좀 힘들다. 그래도 어지간 하면 CSS-Module로 커버가 될듯 싶다.

그리고 추가적인 장점은 inspector에서 확인 가능하다.

지금은 class명이 좀 난잡하지만, 이거는 webpack.config.js 설정을 바꿔줌으로써 해결 가능하다.

{
  loader: "css-loader",
  options: {
    modules: {
      localIdentName: "[name]__[local]___[hash:base64:5]",
    },
  },
},

그러면 아래와 같이 나온다.

Good

atomic css

function Button({ size, variant }) {
  const btnClasses = {
    base: "fs-12 bg-white",
    size: {
      sm: "fs-12",
      md: "fs-15",
      lg: "fs-24",
    },
    variant: {
      primary: "bg-blue fc-white",
      seconary: "bg-gary fc-white",
    },
  };
  const cns = cn(
    btnClasses.base,
    btnClasses.size[size],
    btnClasses.variant[variant]
  );
  return (
    <button type="button" className={cns}>
      버튼입니다
    </button>
  );
}

여기서 cn말고 다른 더 스마트한 방법을 사용할 수 있겠지만, 대강 이런 모습이다. 지금 느낌으로는 Atomic CSS를 메인으로 쓰기에는 쫌…거시기 하다.

외부에서 추가적인 스타일 조정이 가능하다

자신의 정체성이 확고한 컴포넌트도 특정 상황에 들어가면 바뀔 수 있어야 한다. 예를들면 margin같은것들! 물론 props로 받을 수 도 있겠지만, 이거는 흰 이빨에 낀 고춧가루 같은 느낌이다. 해당 컴포넌트와 상관없는 정체성(속성)까지 props항목에 추가 시키고 싶지는 않다. 자주 사용되는 항목이 아니라면 더욱더!

CSS-in-JS

const StyledButton = styled.button`
  font-size: 12px;
  background-color: white;
`;

function SomeLayout() {
  return (
    <>
      <div>
        <StyledButton
          css={css`
            margin-bottom: 20px;
          `}
        >
          버튼입니다
        </StyledButton>
      </div>
      <div>
        <StyledButton>버튼입니다</StyledButton>
      </div>
    </>
  );
}

emotion에서는 css속성을 지원한다. 그래서 button에 props를 넘겨주지 않아도 추가적인 스타일링이 가능하다.

css-module

// app.module.scss

.button {
  font-size: 12px;
  background-color: white;
}
import styles from './app.module.scss';

function SomeLayout() {
  return (
    <>
      <div>
        <button
          type="button"
          className={styles.button}
          style={{ marginBottom: "20px" }}
        >
          버튼입니다
        </button>
      </div>
      <div>
        <button type="button" className={styles.button}>
          버튼입니다
        </button>
      </div>
    </>
  );
}

inline스타일을 주면 된다. 아니면 unocss랑 같이 써보는건 어떨까?

— 2시간 후 —

webpack plugin이 있긴 한데 사용을 잘 못하겠다. 그래서 우선은 tailwind를 써보았다!

function SomeLayout() {
  return (
    <>
      <div>
        <button type="button" className={cn(styles.button, "mb-30")}>
          버튼입니다
        </button>
      </div>
      <div>
        <button type="button" className={styles.button}>
          버튼입니다
        </button>
      </div>
    </>
  );
}

반응형을 위한 코드를 관리하기 쉬워야 한다

내가 생각하는 관리하기 좋은 반응형 코드

.app {
  @media (max-width: 1280px) {
    .nav {
      display: none;
    }
    .content {
      width: 100%;
    }
    .sidebar {
      display: block;
    }
  }
}

리액트는 컴포넌트를 잘개 쪼갠다. 하지만 반응형의 경우에는 숲에서 나무를 보듯, 이런식으로 하위 Element들을 컨트롤 해야 코드를 읽기가 편하다고 생각한다. 반응형의 핵심은 “관계”라고 생각한다. 즉, 1280px 이하에서 내 자식들이 어떻게 변할지 부모가 결정해 주는게 보기 좋다고 생각한다.

css-in-js

파일구조
├─ content
│  └─ Content.jsx
├─ nav
│  └─ Nav.jsx
├─ page
│  └─ Page.jsx
└─ sidebar
   └─ Sidebar.jsx

page 파일

// Page.jsx

import styled from "@emotion/styled";
import Nav from "../nav/Nav";
import Content from "../content/Content";
import Sidebar from "../sidebar/Sidebar";

function Page() {
  return (
    <StyledPage>
      <Nav className="nav" />
      <Content className="content" />
      <Sidebar className="sidebar" />
    </StyledPage>
  );
}

const StyledPage = styled.div`
  & > .nav {
    width: 100%;
  }
  & > .content {
    display: inline-block;
    width: 80%;
  }
  & > .sidebar {
    display: inline-block;
    width: 20%;
  }

  @media (max-width: 1280px) {
    & > .nav {
      display: none;
    }
    & > .content {
      display: block;
      width: 100%;
    }
    & > .sidebar {
      display: block;
      width: 100%;
    }
  }
`;

export default Page;

nav 파일

// Nav.jsx

import styled from "@emotion/styled";

function Nav({ className }) {
  return (
    <StyledNav className={className}>
      <ul className="menu">
        <li>Item-1</li>
        <li>Item-2</li>
        <li>Item-3</li>
      </ul>
    </StyledNav>
  );
}

const StyledNav = styled.div`
  height: 100px;
  padding: 0 20px;
  background-color: green;
`;

export default Nav;

content 파일

// Content.jsx

import styled from "@emotion/styled";

function Content({ className }) {
  return <StyledContent className={className} />;
}

const StyledContent = styled.div`
  background-color: yellow;
  height: 500px;
`;

export default Content;

sidebar 파일

// Sidebar.jsx

import styled from "@emotion/styled";

function Sidebar({ className }) {
  return <StyledSidebar className={className} />;
}

const StyledSidebar = styled.div`
  height: 500px;
  background-color: aqua;
`;

export default Sidebar;

위 코드를 보다보면 이런 생각이 들수있다. ‘아니 그러면 이름 충돌 생기는거 아니야?’ 가능성이 없는건 아니지만, 모든 코드에서 & > 를 잘 써주면 충돌날 일은 없을것 같다. 왜냐하면 .nav가 global scope에 들어갔지만, emotion이 생성한 .css-jza6vr 라는 고유한 className에 > 로 엮어 있기 때문에 괜찮다. 다른 .nav에 피해 줄일도 없고 피해 받을 일도 없다.

하지만, & > 를 쓰도록 강제하기도 어렵고(실수가 나오기 쉽다), 계속 쓰는것도 여간 귀찮은 일이 아니다.

결과물

실행해보기

css-module

우선 파일 구조는 다음과 같다

├─ content
│  ├─ Content.jsx
│  └─ content.module.scss
├─ nav
│  ├─ Nav.jsx
│  └─ nav.module.scss
├─ page
│  ├─ Page.jsx
│  └─ page.module.scss
└─ sidebar
   ├─ Sidebar.jsx
   └─ sidebar.module.scss

page 파일

// Page.jsx

import Nav from "../nav/Nav";
import Content from "../content/Content";
import Sidebar from "../sidebar/Sidebar";
import styles from "./page.module.scss";

function Page() {
  return (
    <div className={styles.page}>
      <Nav className={styles.nav} />
      <Content className={styles.content} />
      <Sidebar className={styles.sidebar} />
    </div>
  );
}

export default Page;
// page.module.scss

.nav {
  width: 100%;
}
.content {
  display: inline-block;
  width: 80%;
}
.sidebar {
  display: inline-block;
  width: 20%;
}

@media (max-width: 1280px) {
  .nav {
    display: none;
  }
  .content {
    display: block;
    width: 100%;
  }
  .sidebar {
    display: block;
    width: 100%;
  }
}

nav파일

// Nav.jsx

import styles from "./nav.module.scss";

function Nav() {
  return (
    <div className={styles.nav}>
      <ul className="menu">
        <li>Item-1</li>
        <li>Item-2</li>
        <li>Item-3</li>
      </ul>
    </div>
  );
}

export default Nav;
// nav.module.scss

.nav {
  height: 100px;
  padding: 0 20px;
  background-color: green;
}

content파일

// Content.jsx

import styles from "./content.module.scss";

const cn = require("classnames");

function Content({ className }) {
  return <div className={cn(styles.content, className)} />;
}

export default Content;
// content.module.scss

.content {
  background-color: yellow;
  height: 500px;
}

sidebar 파일

// Sidebar.jsx

import styles from "./sidebar.module.scss";

const cn = require("classnames");

function Sidebar({ className }) {
  return <div className={cn(styles.sidebar, className)} />;
}

export default Sidebar;
// sidebar.module.scss

.sidebar {
  height: 500px;
  background-color: aqua;
}

다행히 여기서는 클래스 이름 충돌이 나지 않는다.

결과물

실행해보기

결론

전반적으로 봤을때, 지금 당장은 CSS-in-JS를 굳이 써야 하나 싶다. dynamic한 값들은 tailwind로 해결 가능하고, variation은 오히려 css-module이 깔끔하다.

지금 까지의 분석으로는 css-in-module + atomic css(tailwind) 의 조합이 좋아 보인다.

추가 사항 (2022.05.24)

css-module를 쓰면서 만난 문제들

값이 너무 dynamic할때

padding, margin, color 등이 너~무 dynamic하게 들어오는 경우에는 inline-style을 주었다. 이럴때는 확실히 css-in-js로 받는게 편할것 같다.

css코드를 재사용 하고 싶을때

scss안에서 mixin을 쓰거나 extend하면 된다. 실험 결과 extend를 하니 안쓰는 코드가 생성되었다.

// AComponent.jsx

import styles from "./a-component.module";

function AComponent() {
  return <div className={styles.aComponent} />;
}

export default AComponent;
// a-component.module.scss

@import "./test.module";

.aComponent {
  background-color: yellow;
  @extend .test1;
}
// BComponent.jsx

import styles from "./b-component.module";

function BComponent() {
  return <div className={styles.bComponent} />;
}

export default BComponent;
// b-component.module.scss

@import "./test.module";

.bComponent {
  background-color: green;
  @extend .test2;
}
// test.module.scss

.test1 {
  display: block;
}

.test2 {
  display: none;
}

.test3 {
  position: absolute;
}

빌드 결과물

.a-component-module__test1___s5EI4,
.a-component-module__aComponent___lATvI {
	display:block
}

.a-component-module__test2___zwo4u {
	display:none
}
.a-component-module__test3___DsV23 {
	position:absolute
}
.a-component-module__aComponent___lATvI {
	background-color:yellow
}

.b-component-module__test1___Y07HL {
	display:block
}

.b-component-module__test2___iB7VI,
.b-component-module__bComponent___Oe2cV {
	display:none
}

.b-component-module__test3___bL74O {
	position:absolute
}
.b-component-module__bComponent___Oe2cV {
	background-color:green
}

import하고 test3를 사용하지도 않았지만, .a-component-module__test3___DsV23.b-component-module__test3___bL74O 가 생성되었다.

근데 이건 module이라서가 아니라 그냥 test.scss를 두번 import하면 발생하는것 같다.

.test1,.aComponent{display:block}.test2{display:none}.test3{position:absolute}.aComponent{background-color:yellow}

.test1{display:block}.test2,.bComponent{display:none}.test3{position:absolute}.bComponent{background-color:green}

a-component.module.scss는 AComponent.jsx에 물려있기 때문에, 외부 scss를 쓰려면 어쩔 수 없이 @import를 해야한다. 그러면 위와 같이 중복 코드가 발생한다. 그래서 지금 생각에는 extends대신에 가급적 mixin을 쓰는게 좋을것 같다.

@mixin a-no-style {
  text-decoration: none;
  color: inherit; 
}

@mixin ul-no-style {
  list-style: none;
  padding: 0;
  margin: 0;
  li {
    padding: 0;
    margin: 0;
  }
}

@mixin button-no-style {
  background: none;
  border: none;
  cursor: pointer;
}

이거는 전처리 과정에서 이뤄지기 때문에 빌드 결과물에는 남지 않는다.

다만, scss에 extend키워드가 있다는것은 그만큼 css코드를 재활용 한다는 것이기 때문에 약간 불안하긴 하다. 우선 mixin으로 잘 해결해 봐야겠다.

theme 적용하기

emotion처럼 편하지는 않지만, theme 적용도 된다. 개인적으로는 emotion(css-in-js)에서 theme을 받는것 보다는 깔끔하게 처리된다고 생각한다. 분리가 잘 되기 때문이다.

// App.jsx

import Menu from "./menu/Menu";
import ThemeProvider from "./ThemeContext";

export function App() {
  return (
    <ThemeProvider>
      <Menu />
    </ThemeProvider>
  );
}
// ThemeContext.jsx

import React, { useContext } from "react";

const defaultValues = {
  mode: "dark"
};

const ThemeContext = React.createContext(defaultValues);

export const useThemeContext = () => {
  const context = useContext(ThemeContext);
  return context;
};

export default function ThemeProvider({ children }) {
  return (
    <ThemeContext.Provider value={defaultValues}>
      {children}
    </ThemeContext.Provider>
  );
}

menu

// Menu.jsx

import { useThemeContext } from "../ThemeContext";
import styles from "./menu.module.scss";
import cn from "classnames";

function Menu() {
  const theme = useThemeContext();
  return (
    <div className={cn(styles.menu, { [styles.dark]: theme.mode === "dark" })}>
      <ul>
        <li>Apple</li>
        <li>Banana</li>
        <li>Orange</li>
        <li>Lemon</li>
      </ul>
    </div>
  );
}

export default Menu;
// menu.module.scss

.menu {
  border: 1px solid green;
  ul {
    background-color: white;
    color: black;
  }
}

.dark {
  &.menu {
    border: 1px solid yellow;
    ul {
      background-color: black;
      color: white;
    }
  }
}

결과물

실행해보기

아직까지는 괜춘하다!

4 replies on “CSS작성 테크닉에 대한 고찰”

글보고 제 생각 몇마디 남겨봅니다. 어디까지나 개인적인 생각이니 참고만 해주세요~~

개인적으로 스타일을 숲의 관점에서 보는걸 선호하지 않습니다. 반응형을 예시로 드셨기 때문에 반응형으로 저도 예시를 들어보겠습니다.
반응형으로 버튼을 만든다면 버튼내부에서 미디어쿼리를 적용하면 됩니다. 스토리북으로 ui테스트를 할때도 각각의 컴포넌트마다 미디어쿼리를 적용해야 단위테스트를 할 수 있습니다. 결국 저자분은 숲의 관점에서 바라보고 계시기 때문에 css in js를 선호하지 않는 것 같습니다. 일부는 숲의관점으로 일부는 나무의 관점에서 바라보고 계시네요. 어떤 방법이 무조건적으로 맞다고 생각하지는 않습니다. 그리고 상황에 따라 저자분의 방법이 더 좋을수도 있다고 생각합니다.

그리고 사실 자바스크립트로 css를 관리한다는 점은 굉장히 유용합니다. css, css 전처리기로는 할 수 없는 것들을 편리하게 할 수 있습니다. 이건 개발자의 역량이 올라갈수록 더욱더 강력해집니다. 또한 타입스크립트도 무시할 수 없습니다. 스타일로만 관리를 하게 되면 컴파일이전에 잘못된 것들을 찾기 힘듭니다. 반면에 css in js와 타입스크립트를 사용한다면 컴파일 이전에 에러를 잡아낼 수 있습니다.

또한 서버사이드렌더링을 할 때도 css in js가 유용합니다. 나중에 기회가 된다면 한번 공부해보시면 좋을 것 같아요

맞습니다, 저도 아직 경험이 부족해서 딱 이거다라고 말하기는 어렵습니다. 확실히 전처리기로는 어려운 작업이 있을것 같아요. 더 공부해 보면서 지속적으로 글을 업데이트 해보겠습니다 😀

Leave a Reply

Your email address will not be published.