블로그

Styled-Components 도입기

등록일
2024-08-14 19:31:43
조회수
361

안녕하세요, softeer 개발자 영지김 입니다.

저는 프론트엔드 개발을 시작한지 얼마 안 된 주니어 개발자 입니다. 최근 대회용 스코어보드 기능을 개발하면서 CSS의 불편함 해결하고자 styled-components를 도입하게 되었습니다. 이 글에서는 그 배경과 도입 과정, 그리고 결과에 대해서 이야기 해보려 합니다.



1. CSS의 문제점


1990년대 초창기 웹은 HTML로 문서를 만들어 브라우저로 공유하는 단순한 구조였습니다. 그러다 디자인에 대한 요구가 생겨 아래와 같이 inline-style로 디자인을 입히게 되었고, 이런 inline-style의 불편함을 개선하고자 CSS가 생기게 되었습니다. CSS는 inline-style의 반복 작성 문제를 선택자와 선언 방식으로 해결하였습니다.





CSS는 선택자에 따라 스타일 적용 우선순위를 계산하는 매커니즘을 가지고 있습니다. 이를 명시도(Specificity)라고 하는데요, 명시도가 높은 규칙이 낮은 규칙을 덮어쓰며 동일한 명시도 값을 가진 경우엔 나중에 선언된 규칙이 우선 적용됩니다. 명시도를 계산하는 방법은 아래와 같이 복잡하기 때문에 어떤 CSS가 적용될지 직관적으로 알기 힘들어졌고, CSS를 덮어쓰기 위해 더 복잡하고 어려운 선택자를 쓰게 되었습니다.



< Specificity 계산 방식 >

  • Inline 스타일 : 1,0,0,0
  • ID 선택자 : 0,1,0,0
  • 클래스 선택자, 속성 선택자, 의사 클래스 : 0,0,1,0
  • 타입 선택자, 의사 요소 : 0,0,0,1


#header .nav li.active a { color: red; } 의 Specificity는 각 선택자 값의 합인 0,1,2,2가 됩니다.



그리고 CSS는 여러 파일에 나누어 작성해도 그 스코프가 Global 하다는 문제점이 있습니다. 그래서 반드시 중복되지 않는 className을 가져야 했고 BEM 방식같이 길고 복잡한 클래스 이름을 작성해야 했습니다. 이 외에도 Facebook FE개발자가 발표한 7가지 유명한 CSS의 태생적인 문제가 아래와 같습니다.



  • Global namespace: 모든 스타일이 global에 선언되어 중복되지 않는 class 이름을 적용해야 하는 문제
  • Dependencies: css와 JS간의 의존관계를 관리하기 힘든 문제
  • Dead Code Elimination: 기능 추가, 변경, 삭제 과정에서 불필요한 CSS를 제거하기 어려운 문제
  • Minification: 클래스 이름의 최소화 문제
  • Sharing Constants: JS 코드와 상태 값을 공유할 수 없는 문제
  • Non-deterministic Resolution: CSS 로드 순서에 따라 스타일 우선 순위가 달라지는 문제(Specificity)
  • Breaking Isolation: CSS의 외부 수정을 관리하기 어려운 문제 (캡슐화)

 


스코어 보드를 개발하면서 가장 불편했던 점은 CSS가 어떻게 적용될지 직관적으로 알 수 없다는 점이었습니다. 길고 복잡한 className과 선택자들이 그 이유였습니다.

때문에 점점 개발할수록 제가 원하는 부분에만 CSS를 적용하는 것이 어려워지고, 수정할 때마다 사이드 이펙트가 얼마나 발생할지 예상할 수 없게 되었습니다. 이를 해결할 새로운 방법이 필요했습니다.

 


2. 문제를 해결하기 위한 다양한 방법


CSS의 태생적인 문제를 해결하기 위한 여러 해결 방안이 굉장히 많았습니다. 크게 아래와 같은 3가지 패러다임이 있는데 Pre/Post-Processors, CSS-Framework, CSS-in-JS가 있습니다.


  • Pre/Post-Processors는 css에서 변수, 중첩, 함수 등을 사용할 수 있게 만든 방법(sass, less)
  • CSS-Framework는 미리 정의된 CSS 스타일과 컴포넌트를 사용하는 방법(Bootstrap, Tailwind)
  • CSS-in-JS는 JS안에서 CSS를 정의하고 컴포넌트 기반으로 스타일을 적용하는 방법 (styled-components)


이 중 Global namespace 문제를 가장 확실하게 해결해주는 CSS-in-JS를 선택했습니다. CSS-in-JS는 컴포넌트에만 국한된 스타일을 작성할 수 있어 전역적인 충돌을 방지할 수 있고 JS 상태 값에 따라 동적으로 CSS를 변경 하기도 쉽다는 장점이 있습니다.

아래는 CSS2023의 CSS-in-JS의 점유율 통계인데 styled-components가 가장 사용량이 많기도 하고, 러닝 커브도 낮아 styled-components를 사용하기로 결정 했습니다. (그런데 styled-components의 만족도 통계가 생각보다 낮아 다음에는 만족도 1위인 CSS Modules를 이용해보고 싶네요)


 


3. styled-componets 적용


styled-components를 사용하는 방법은 정말 쉬웠습니다. 공식문서에 적힌 대로 모듈을 다운 받고 리액트 컴포넌트를 만들 듯이 스타일드 컴포넌트를 만들면 끝이었습니다.


기분 좋게 코드를 작성하고 있었는데 예상치 못한 문제가 생겼습니다.


styled-components를 쓰면 더 이상 네이밍 이슈가 생기지 않으리라 생각했는데 네이밍 이슈가 생겨버렸습니다. Header.tsx를 작성하는 중 스타일드 컴포넌트의 이름과 리액트 컴포넌트의 이름이 모두 Header로 동일해서 에러가 났습니다.


위와 같은 충돌을 피하기 위해 Wrapper혹은 Container같은 Postfix를 뒤에 붙일 수도 있었으나, 저는 깔끔하게 Header라는 이름을 쓰고 싶었습니다. 그래서 S-dot 네이밍 방식을 사용했는데요, S-dot 방식은 스타일드 컴포넌트를 다른 파일으로 분리한 뒤 불러올 때 아래 코드와 같이 import * as S 를 사용하는 방식입니다. 이렇게 하면 스타일드 컴포넌트와 리액트 컴포넌트는 동일한 이름을 가질 수 있고 리액트 로직과 스타일이 분리되어 더욱 깔끔한 코드를 작성하는 효과를 얻을 수 있었습니다.




또 다른 문제도 있었습니다. 스타일이 들어가는 모든 태그를 스타일드 컴포넌트로 만들다 보니 스타일드 컴포넌트가 너무 많아져서 CSS를 쓰는 것과 다른 바 없는 지저분한 코드가 되어 버렸습니다. 이를 해결하기 위해 스타일드 컴포넌트를 적절한 곳에서만 사용하고 className이나 하위 선택자를 사용해서 스타일을 적용했습니다.




추가로 아래와 같은 더 상세한 컨벤션을 만들며 styled-components를 사용하였습니다.


  • 레이아웃을 구성하는 스타일드 컴포넌트는 ~Layout으로 끝난다.
  • 공통적으로 사용하는 변수(컬러코드, 폰트사이즈 등)는 ThemeProvider를 사용한다.
  • 전역적으로 사용되는 스타일은 GlobalStyle에 정의한다.


그리고 styled-components를 사용하면 빌드 과정에서 자동으로 고유한 랜덤 클래스 이름을 생성하게 되는데, 개발 모드에서 더 읽기 쉬운 클래스명을 생성하게 할 수 도 있습니다. babel-plugin-styled-components를 설치하고 설정해 두면 디버깅이 더 쉬워집니다.

 


4. 적용 결과


styled-components를 적용하고 난 후 CSS를 수정하는 것이 훨씬 편해졌습니다.


컴포넌트에만 국한된 CSS를 적용 할 수 있어서 사이드 이펙트를 더 이상 고려하지 않아도 되었고, 복잡한 BEM구조의 클래스 이름과 선택자를 사용하지 않아도 된다는 점도 너무 좋았습니다.


혹시 React 앱을 개발할 일이 생긴다면 꼭 CSS-in-JS 라이브러리를 꼭 사용해보길 추천 드립니다.

최신 블로그