웹/배경지식

css-in-js 에서 tailwind로 옮겨가려는 이유

한땀코딩 2023. 7. 15. 12:00

css-in-js 와의 첫 만남

프론트엔드 개발에 입문했던 시기에 리액트와 함께 모두가 사용하던 것이 있었으니 바로 styled-components였습니다. 솔직하게 고백하자면 그때는 쓰는지를 크게 고민하지 않고 모두가 사용한다는 점과, JS 파일에 같이 스타일을 작성해서 관리한다는 점이 좋아 보여서 사용했던 기억이 있습니다. 그리고 막연하게 이 좋은 걸 두고 왜 굳이 힘들게 css 파일을 따로 만들고 클래스 형태로 적용하는지 이해를 못 했습니다.

그러다 일을 시작하고 2년 정도 이런저런 형태를 많이 보다 보니 css-in-js가 마냥 정답은 아니라는 생각이 들었습니다. 특히 제가 속한 팀에서는 scss modules를 많이 사용하고 있는데요, 사용해 보니 의외로 관리가 편하고 마크업, 퍼블리싱 쪽과 협업하기도 수월해서 라이브러리를 무조건 가져오는 것이 능사는 아니라고 느꼈습니다.

그리고 최근에 Why We're Breaking Up with CSS-in-JS 라는 충격적인(?) 글을 발견하면서 다시금 css 전략에 대한 고민을 시작해 보았습니다.

  • 아티클 요약
  • 작성자는 Spot에서 근무하는 emotion의 메인테이너 중 한 명인데, 본인은 정작 css-in-js를 최대한 걷어내려고 하는 중이라고 합니다. 성능 면에서도 당연히 문제가 발생하지만, 최근 React가 점점 서버 컴포넌트 등과 같은 기술을 추가하면서 css-in-js 가 적용되기 위해 고려되어야 하는 사항이 많아지고 관리하기가 여러워지기 때문이라고 말합니다.

TL;DR

어떤 기술을 쓸지는 당연히 정답은 없고, 프로젝트의 구조나 성질에 따라서 매우 달라집니다. 그래도 저는 가급적이면 마크업에 의존하지 않는다면 tailwind CSS 같은 utility-first css 전략을 취하고 웬만하면 css modules 로 해결할 거 같습니다.

css-in-js?

이름 그대로 css-in-js 는 JavaScript 코드 내에 css를 생성하는 로직을 담는 방식입니다. 미리 모든 스타일을 정의해 두는 대신, 클라이언트에서 페이지가 로드될 때 동적으로 필요한 스타일을 생성해 내 페이지에 주입하게 됩니다. 한 마디로, 기존에는 HTML과 CSS 파일만으로 바로 적용되던 단계에서 JS 코드가 로드되고 컴포넌트가 생성될 때 다시 한번 동적으로 계산하여 스타일을 주입하게 됩니다. 왜 이런 오버헤드가 발생하는데 사용할까요? 장단이 확실하기 때문입니다.

(참고로 중복 방지나 전역 스타일 관련된 것도 장단점에 많이 언급되는데, 모듈화 되는 관점에서 보면 css modules나 css-in-js나 비슷하다고 보기 때문에 이 부분은 제외합니다.)

장점

  • ✅ JavaScript 변수에 접근할 수 있어 런타임 환경에서도 동적으로 스타일을 생성 및 주입할 수 있습니다.
    • 리액트 컴포넌트로 생각해 보면, props를 이용해 스타일을 자유로이 변경할 수 있습니다. 단순히 클래스를 바꿔 끼우는 차원이 아니라 동적으로 새로운 css를 JS 변수를 가지고 만들어 주입할 수도 있는 것이죠.
  • 리액트 기준으로는 하나의 파일 안에서 스타일을 함께 관리하기 때문에 DX 면에서도 편하고 semantic 한 구조를 만들 수 있습니다. (스타일 된 태그에 이름을 새로 부여할 수 있기 때문)

단점

장점이 동시에 단점이 됩니다 😇

  • JS 코드에 의존적이기에 매번 계산을 해서 주입하기 때문에 로딩 속도 면에서 불리
  • 런타임 중 스타일로 변환해 주기 위해 css-in-js 라이브러리가 필요하기 때문에 번들 사이즈 증가 (빌드 시에 스타일이 생성될 수가 없기에 추가적인 JS 코드가 필요합니다)
  • 클래스에 비해 재사용이 직관적이지 않다
  • devTools 가 지저분해지는 이슈
    • 이건 생각을 못했었는데, 컴포넌트마다 css를 주입하기 위해 새로운 컴포넌트가 생겨나게 됩니다. 로직과는 무관한 컴포넌트가 반복적으로 생겨나는 셈입니다.

그리고 이런 단점이 불러오는 큰 문제 중 최근 대두되는 것이 React 18과 Next에서 필요로 하는 server component 와의 호환성입니다. 번들 사이즈를 축소하고 더욱 효율적이고 빠른 소스 파일 빌드를 위해 제공되는 기술인데, css-in-js는 브라우저 환경과 런타임에서의 JS 코드 로딩에 의존하기 때문에 사실상 이 기술의 혜택을 크게 받지 못하고 있습니다. 이에 대한 대응도 기존 버전과의 하위호환을 유지하면서 추가되어야 하는데, 당분간은 시행착오를 겪을 것 같습니다. 실제로도 현재 Next 13의 app 디렉터리를 이용한 새로운 구조에서는 emotion은 지원되지 않습니다.

왜 CSS module과 utility-first를 선호하는가?

JS 변수에 접근하지 못하는 것이 얼핏 보면 생각보다 큰 단점처럼 보일 수 있습니다. 리액트를 사용한다면 props에 의존하여 변경되는 스타일이 많기 때문이죠. 특히 테마 변경 등은 객체로 관리하면 커스텀 테마를 훨씬 유연하게 적용할 수는 있어 보입니다.

특히 제가 머리가 아팠던 부분은 active, hover 같은 pseudo-class에 동적으로 스타일을 부여하는 과정이었습니다. 예를 들어 어떤 버튼이 있고, 이 버튼의 hover 색상을 전적으로 사용자가 임의로 정한다고 하면, css-in-js 없이는 런타임 환경에서는 적용하기 어렵습니다. inline style 또한 이런 pseudo-class 접근은 지원하지 않기 때문에, 이렇게 자유도 높은 작업에서는 css-in-js가 유리하겠죠?

이 문제는 사실 서비스 차원에서 어떤 걸 사용할지 보다는, 커스텀 UI 라이브러리를 만드는 과정에서가 더 고민이라고 생각합니다. 실제 서비스에서는 디자인 시스템이 어느 정도 갖춰져 있고, 타입스크립트를 사용한다면, 더더욱이나 props로 들어오는 것들을 ‘any’로 두지는 않을 거라서 이런 지나친 자유도를 위한 접근을 고민할 필요가 없을 겁니다. 그러나 사용자가 튜닝을 해서 사용할 수 있게끔 해주기 위해 만드는 UI 라이브러리에서는 자유도의 범위도 큰 고민이었습니다. 특히, css modules에서 css 변수를 활용하여 테마 같은 것을 생성한다고 치면, 이를 튜닝하기 위해선 변수를 override 하는 등의 접근이 필요한데, 아무래도 번거롭기도 하고 intellisense 나 typescript의 도움을 받기도 어렵습니다.

tailwind는 코드와 config 파일을 참고하여 필요로 하는 모든 클래스를 사전 빌드하여 제공하는 전략을 취하고 있습니다. 그래서 코드 내에서 사용이 예상되는 모든 클래스는 가지고 있지만, css-in-js 처럼 런타임 환경에서 정말 동적으로 클래스를 생성해내지는 않습니다. 사용자가 추가적으로 커스텀 하고 싶은 것이 있다면, config파일에 미리 모두 정의해야 합니다.

결국, 제가 느끼기에 css-in-js 가 갖는 유일한 장점은 사전에 빌드하지 않아도 되기 때문에 개발 환경에서 자유도 높게 css를 수정할 수 있고 DX면에서 편리하다 정도입니다. css modules 라도 결국 조건에 따라 styles를 불러와서 스위치 해주는 작업은 필요한데 css-in-js는 이러한 과정에서 자유롭고 JS 변수가 정확히 스타일의 어느 부분에서 쓰이는지도 명확합니다. (css modules를 쓰게 되면 예를 들어 변수 값이 red 여서 red라는 클래스를 적용하고자 할 때, 불러오는 scss 파일 안에 red라는 클래스가 있는지 보장할 수가 없습니다.) 다만, 장기적으로 server component의 범위가 확대되고, 타입스크립트에 의한 엄격한 코드 관리가 가능해지다 보니, 사전에 정의하는 과정이 크게 발목을 잡는다고 생각이 들지는 않고, 성능 면에서도 역시 클래스를 사용하는 것이 무시할 수 없다고 생각합니다.

마무리

깔끔하게 정리하기가 참 어려워서 의식의 흐름대로 작성했는데요, 여전히 정답은 없다고 생각합니다. 리액트의 서버 컴포넌트와 concurrent 기능을 좀 더 공부해야겠다는 좋은 방향성은 얻었습니다.

최근 이 공부를 하면서 UI 라이브러리도 많이 참고하고 있었는데, Chakra(emotion 기반이기 때문에 공식적으로 Next 13에서는 클라이언트 컴포넌트에서만 사용하라고 가이드하고 있습니다)나 MUI는 의외로 내부 로직에서 css-in-js에 많이 의존하고 있는 것으로 보이는데, 이런 라이브러리들은 Next와 같은 프레임워크와의 호환성을 위해 추후 어떤 전략을 취해나갈지 궁금합니다.