프론트엔드 CSS에 대해

이희제·2024년 7월 20일
post-thumbnail

업무를 진행하면서 주로 Sass(SCSS)를 사용하면서 개발을 했었는데 이번 프로젝트를 진행하면서 emotion(CSS-in-JS)를 도입했다.

기존 Sass(SCSS)를 사용하면서 파일이 분리가 되어 있다보니 파일 이동이 잦아 불편했다. 내부 관리자 서비스 개발이기 때문에 성능이 크게 중요하지 않다. 번들 사이즈의 증가와 런타임 오버헤드를 어느정도 감수할 수 있는 상황이다.

그리고 Colocation 방식을 개발할 때 적용하고자 하는데 이런 측면에서 컴포넌트 파일 내 스타일을 정의할 수 있다는 점이 적합하다고 생각했다. 한 파일 내에서 개발을 하기 때문에 생산성 또한 증가될 것이라 판단했다.

도입할 때 여러 라이브러리와 비교를 하면서 검토가 필요한데 각각 라이브러리 특징과 장단점에 대해 알아보고자 한다.

스타일 모듈 관련 공부를 하면서 각각 라이브러리에 대해서 검토하고 CSS-in-JS, CSS-in-CSS라는 개념에 대해서 정리해보고자 한다.

내가 알아본 것은 다음과 같다. 기존 사용하던 Sass도 공부할겸 넣었다.

1. Sass(SCSS)
2. tailwindcss
3. styled-components
4. emotion


각 라이브러리에 대해 보기 전에 CSS-in-JS와 CSS-in-CSS의 개념부터 살펴보자.

CSS-in-JS와 CSS-in-CSS

1. CSS-in-JS란?

CSS-in-JS 방식은 단어 그대로 JavaScript 안에서 css를 작성하는 방식이다.

브라우저에서 JavaScript를 실행하여 동적으로 style 태그를 생성하고, 이 태그에 스타일을 속성들을 추가하는 것이다.

장/단점에 대해 보자.

  • 장점

    • 컴포넌트 기반 구조: 스타일을 컴포넌트 파일 내에서 함께 작성할 수 있어, 스타일과 컴포넌트를 함께 관리할 수 있다. 이는 유지보수성과 가독성을 높여준다. (colocation)
    • 동적 스타일링: 자바스크립트의 힘을 빌려 스타일을 동적으로 정의하고 변경할 수 있다. 이를 통해 보다 유연한 스타일링이 가능하다.
    • 스코프 격리: 각 컴포넌트의 스타일이 고유하게 관리되므로, 글로벌 네임스페이스 오염 문제를 방지할 수 있다.
  • 단점

    • 런타임 성능 오버헤드: 스타일을 런타임에 생성하므로, 성능에 영향을 줄 수 있다. 이는 특히 많은 스타일을 동적으로 생성하는 경우 문제가 될 수 있다.
    • 번들 사이즈 증가: 자바스크립트 번들에 스타일이 포함되므로, 번들 크기가 증가할 수 있다. 이는 초기 로딩 시간을 늘릴 수 있다.

2. CSS-in-CSS란?

전통적인 CSS 적용 방식으로 CSS 파일 내에서 스타일을 작성하는 방식이다.

스타일 시트가 별도로 로드되고 이를 기반으로 브라우저가 CSSOM 생성한다.


CSS 전처리기/라이브러리/프레임워크 살펴보기

1. Sass(SCSS)

Sass는 CSS Preprocessor(전처리기)이다. 스타일 시트 언어로서 css로 컴파일 된다.

그렇다면 SCSS는 뭘까?

SCSS는 Sass의 3버전에서 새롭게 등장한 CSS 구문과 완전히 호환되도록 새로운 구문을 도입해 만든 Sass의 모든 기능을 지원하는 CSS의 Superset이다.

공식 문서를 통해 .scss를 사용했을 때와 .sass를 사용했을 때의 문법적인 차이를 볼 수 있다. (;{}의 유무 차이를 보인다)

scss는 아래 코드를 보면 알 수 있듯이 기존 css 문법과 동일하다. 따라서 css를 알고 있다면 사용하는 데 어려움이 없을 것이다.


// scss
.alert, .warning {
  ul, p {
    margin-right: 0;
    margin-left: 0;
    padding-bottom: 0;
  }
}

//sass
.alert, .warning
  ul, p
    margin-right: 0
    margin-left: 0
    padding-bottom: 0

vite, webpack 환경에서 어떻게 적용할 수 있는지 살펴보자.

1. vite

vite에서는 별다른 설정을 하지 않아도 바로 사용할 수 있다. sass만 설치해주고 사용하자. (참고)

2. webpack

webpack에서 번들링할 때 sass/scss를 css로 컴파일하기 위해 sass-loader가 필요하다.

sass-loader로 일단 css로 컴파일을 하고 css를 로드해오기(import/require 해석) 위해 css-loader가 필요하다.

그리고 로드한 css를 <style> 태그에 주입하기 위해 style-loader가 필요하다.

css-loader와 style-loader는 한 쌍으로 생각하자. 추가로 mini-css-extract-plugin을 통해 css를 줄이고 로드할 수 있다. (mini-css-extract-plugin는 style-loader와 같이 사용하지 말자)

공식 문서에 개발 환경에서는 style-loader를 적용하고 실제 서비스 배포용으로 번들링할 때는 css 파일을 아예 독립적으로 생성해주는 mini-css-extract-plugin를 사용하는 것을 추천하고 있다.(참고)

module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        loader: "esbuild-loader",
        options: {
          target: "es2015",
        },
      },
      {
        test: /\.(sa|sc|c)ss$/,
        use: [
          devMode ? "style-loader" : MiniCssExtractPlugin.loader,
          "css-loader",
          "sass-loader",
        ],
      },
    ],
  },

style-loader를 적용하면 html의 head내 style 태그가 삽입된 것을 확인할 수 있다.

반면 mini-css-extract-plugin를 사용하면 개별 css 파일이 생성되고 html 내에서 이를 link 태그를 사용해서 적용하고 있다.

정리하자면 다음과 같다.

Loader/Plugin역할
sass-loaderSass/SCSS 파일을 CSS로 컴파일
css-loaderCSS 파일을 모듈로 변환하고, JavaScript에서 CSS를 import 할 수 있게 한다.
style-loaderJavaScript로 변환된 CSS를 <style> 태그로 HTML 문서의 <head>에 삽입(실질적으로 서비스에 적용되는 것이다)
mini-css-extract-pluginCSS를 별도의 파일로 추출하여, HTML 문서에 <link> 태그로 삽입합니다. 주로 프로덕션 환경에서 사용된다.

2. tailwindcss

tailwindcss는 Utility-First 컨셉을 내세우는 CSS 프레임워크이다. 기존 정의된 유틸리티 클래스를 활용하는 방식이다.

장점은 다음과 같다.

  • 따로 스타일 파일이 필요가 없고 기존 정의된 클래스를 가져와서 사용하면 되기 때문에 클래스명 고민도 없어진다.
  • 클래스를 바로 가지고 와서 사용하기 때문에 개발 속도가 빨라질 수 있다.
  • 추가로 쉽게 커스텀도 가능하다.(참고)

위의 장점과 반대로 내가 느꼈던 단점은 다음과 같다.

  • 컴포넌트에 스타일링을 할 때 코드량이 증가해 코드가 복잡해보일 수 있다.
  • 원활하게 사용하기 위해서는 어느정도 기존 정의된 클래스 규칙에 대해 학습이 필요하다.(즉, 클래스 명을 어느정도 숙지하고 있어야 개발 속도가 빨라질 것 같다.)

또한 prettier도 적용할 수 있고 extension의 도움을 받으면 어느정도 단점이 상쇄될 것 같다.


간단하게 어떻게 사용하는지 보자

먼저, css 파일을 하나 생성해주자.(ex.index.css)

그리고 tailwindcss의 기본 스타일과 유틸리티를 포함하도록 CSS 파일을 다음과 같이 설정해주고 해당 css 파일을 최상단 파일에서 import 하면 된다.

@tailwind base;
@tailwind components;
@tailwind utilities;

(@tailwind 지시문은 PostCSS의 기능을 활용하여 Tailwind CSS의 기본 스타일, 컴포넌트, 유틸리티 클래스들을 CSS 파일에 포함시키는 역할)

아래와 같이 클래스 이름을 붙여주면 정상적으로 스타일이 적용되는 것을 확인할 수 있다.

const PlayGround = () => {
  return <button className="hover:bg-sky-700">Click me</button>;
};

export default PlayGround;

추가로 vite, webpack 환경에서 특별히 추가로 적용을 위해 작업을 해줘야 할 것이 있을까?

확인해보자.

1. vite

역시나 vite에서는 별다른 설정을 하지 않아도 바로 사용할 수 있다. 공식 문서의 가이드 그대로 따라하면 바로 적용된다.

2. webpack

앞서 봤듯이 PostCSS의 기능을 활용하여 Tailwind CSS의 기본 스타일, 컴포넌트, 유틸리티 클래스를 가지고 온다.

따라서 webpack에서 postcss-loader를 설정해줘야 정상적으로 적용된다.

{
        test: /\.(sa|sc|c)ss$/,
        include: path.resolve(__dirname, "src"),
        use: [
          devMode ? "style-loader" : MiniCssExtractPlugin.loader,
          "css-loader",
          "postcss-loader",
        ],
}

3. styled-components, emotion

styled-components, emotion은 runtime CSS-in-JS로 비슷한 성격을 지닌 라이브러리이다.

styled-components

먼저 styled-components부터 보자. 공식 문서의 예시를 확인하면 어떻게 사용하는지 알 수 있다.

예시)

const Title = styled.h1`
  font-size: 1.5em;
  text-align: center;
  color: #BF4F74;
`;

// Create a Wrapper component that'll render a <section> tag with some styles
const Wrapper = styled.section`
  padding: 4em;
  background: papayawhip;
`;

// Use Title and Wrapper like any other React component – except they're styled!
render(
  <Wrapper>
    <Title>
      Hello World!
    </Title>
  </Wrapper>
);

위 코드에서 알 수 있듯이 styled를 통해 하나의 html element를 생성하고 그 안에서 스타일을 할 수 있다.

조건부로 스타일링을 위해 props도 넘겨줄 수 있다. (참고)

import { styled } from 'styled-components';

const Button = styled.button<{ $fontSize?: number; $primary?: boolean }>`
  background: ${(props) => (props.$primary ? '#BF4F74' : 'white')};
  color: ${(props) => (props.$primary ? 'white' : '#BF4F74')};
  font-size: ${(props) => props.$fontSize}px;
`;

const PlayGround = () => {
  return (
    <div>
      <Button $fontSize={40}>Normal</Button>
      <Button $primary>Primary</Button>
    </div>
  );
};

export default PlayGround;

emotion(@emotion/react)

emotion은 css props를 통해 인라인 형태로 바로 스타일링을 할 수 있고 따로 변수화해서 스타일을 하고 css props에 넘겨줄 수도 있었다. (css props 적용 방법)

css props는 class로 변환되어 스타일링이 적용된다.

예시)

import { css } from '@emotion/react'

const color = 'darkgreen'

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

const color = 'darkgreen'

const cssStyle = css`
  background-color: hotpink;
  &:hover {
  	color: ${color};
  }
}`

render(
  <div
    css={cssStyle}
  >
    This has a hotpink background.
  </div>
)

emotion 공식 문서에 따르면 따로 객체 스타일을 빼서 명시하는 것을 추천하고 있다.(https://emotion.sh/docs/best-practices#recommendations)

이렇게 하면 타입 체킹도 가능하기 때문에 스타일 관련된 버그도 줄일 수 있다.

const myCss = css({
  color: 'blue',
  grid: 1 // Error: Type 'number' is not assignable to type 'Grid | Grid[] | undefined'
})

그리고 emotion 스타일 객체를 컴포넌트 외부에 선언을 해서 사용하는 것이 좋다.(참고)

추가로 emotion에서 @emotion/styled를 통해 styled-components와 거의 동일하게 사용할 수도 있다.

개인적으로는 css props를 사용하는 것이 더 편하고 emotion을 사용하는데 styled-components 방식을 사용할 필요가 없어 보인다.

import styled from '@emotion/styled'

const Button = styled.button`
  color: turquoise;
`

render(<Button>This my button component.</Button>)

styled-components는 webpack, vite에서 크게 설정을 해줘야할 것은 없다.

vite에서 emotion의 경우 css props를 사용하기 위해 다음과 같이 작업을 해줘야 한다.

  1. 타입스크립트와 같이 사용 시에 tsconfig.json 내 compilerOptions 다음과 같이 설정 이렇게 설정해야 css props를 사용할 때 타입 오류가 발생하지 않는다.
"jsx": "react-jsx",
"jsxImportSource": "@emotion/react"
  1. vite.config.js 내 react plugin 설정
plugins: [
  react({
    jsxImportSource: '@emotion/react',
  }),
],

결론

결론적으로 emotion을 채택하였는데 이유는 다음과 같다.

  1. 단일 컴포넌트 파일 내에서 컴포넌트와 스타일 모두할 수 있어 효율성 증대

  2. styled-components보다 작은 번들 사이즈 (확인 사이트)

  3. emotion의 SSR 지원

    • 추후 SSR이 필요한 서비스를 개발할 일이 있을 것으로 예상되는데 그때 추가 학습 비용 없이 바로 emotion을 적용할 수 있을 것이다.

    • styled-components와 다르게 SSR 환경에서 별도 설정 없이 적용할 수 있다.

  4. emotion의 css props의 유연성

    • css props를 바로 컴포넌트를 구성하는 html tag에 쓸 수 있어 편리하게 동적 스타일링을 할 수 있다.

    • 즉, styled-components에 비해 emotion의 css props를 통해 스타일을 분기 처리하는 방식이 더 간단해 좀 더 개발 친화적이라고 느꼈다.

    • css props의 결합 가능

      공식 문서에서는 emotion css 객체를 컴포넌트 외부로 빼고 동적 스타일인 경우 style prop을 이용해서 처리하는 것을 권장하고 있다. (참고)

  5. 스타일 타입 체크 가능


최근에는 vanilla-extract 같은 zero-runtime CSS-in-JS에 대해 알게 되었는데 조만간 공부를 해봐야겠다.


참고
https://medium.com/styled-components/how-styled-components-works-618a69970421
https://fe-developers.kakaoent.com/2022/220210-css-in-kakaowebtoon/
https://medium.com/catchtable/how-to-use-vanilla-extract-in-catchtable-b2c-6c4e712c471f
https://jgjgill-blog.netlify.app/post/css-in-ts-vanilla-extract/
https://www.daleseo.com/emotion/
https://github.com/jsjoeio/styled-components-vs-emotion

profile
그냥 하자

0개의 댓글