⚽️ 목표
CSS in JS를 씹고 뜯고 맛보고 즐겨보자
•
과거의 나를 반성하며 CSS in JS를 고민하고 생각하여 선택하자
•
고민없이 또는 필요성을 느끼지 못한 기술 스택 선택은 잘못된 접근법 이다.
TL;DR
새로 프로젝트를 시작하면 zero runtime CSS in JS라는 선택지도 존재한다.
기존에 문제없이 프로덕트에 사용중인 라이브러리들을 굳.이. 바꿔야할 필요가 있을까?
•
저는 vanilla-extract가 좋아보이던데요?
•
퍼포먼스가 항상 최우선 선택 목표는 아니다.
들어가기 전에
1. 왜 CSS in JS를 쓰게 됐어?
CSS in JS
•
스타일(CSS) 관련 코드를 JS, TS 파일에 바로 작성할 수 있게 해주는 라이브러리 → 우리 모두가 알다시피 styled components, Emotion 이 두개가 제일 유명하다.
•
css해보지 않고솔직하게 CSS in JS로 시작한 사람 손들자
→ 그럴 수 있다고 생각한다 대부분의 강좌(어떤 플랫폼이건)는 (runtime)CSS in JS가 CSS in JS의 전부인양 그것만 사용하고 “사용법”만 가르쳐주니.. 그럴 수 밖에…
•
정작 나또한 styled components, emotion만 사용해봤다. 위의 이유와 같이 “강좌에선 그렇게 알려주니” 정답이고 최고의 답이라 생각했다.
•
지금에서야 내가 깨달은 사실은 여기서 여러가지의 오류가 존재했다.(과거 완료형입니다 여러분)
1.
남들이 다쓰니까, 강좌에서 말해주니까 맞다고 생각했던것
2.
CSS(SCSS도 포함해서 생각하자, 그냥 포함해서 생각하자)라는 기본 기술이 있는데 사용해보지 않고 바로 CSS in JS라는 “보완기술”을 사용한것
•
이걸 왜 사용하게 됐는지 모르니 당연한줄알고
•
그전에 어떤 문제점을 해결하려 나온건지 모르니 제일 기본적인 CSS의 문제점도 모르고
•
runtime, zero-runtime, near-zero-runtime의 차이도 모르고 “아! styled component, Emotion이 곧 CSS in JS구나” 라는 안일한 마인드로 새로운 프로젝트 시작할 때마다 Emotion만 설치하고…무한반복
runtime CSS in JS
•
해당 라이브러리들은 runtime CSS in JS → 간단히 말해 웹 애플리케이션의 런타임 때 스타일을 정의하고 적용하는 CSS in JS
•
컴포넌트가 렌더링 될 때 마다 스타일 생성 및 적용 하므로 동적인 스타일을 줄 수 있으나 더 높은 번들용량, 느린 초기 속도가 단점이다.(아래에서 확인해보겠지만 정확히 말하면 더 나뉘어지긴 한다)
•
내가 지금까지 사용해온 CSS in JS는 곧 runtime CSS in JS다. → Emotion, sytled component
뭐! Emotion maintainer가 CSS-in-JS랑 헤어진다구??
•
naver에서 발행하는 이전 fe-news를 보던 도중 신선한(자극적인)글을 보게됐다. 뭐? Emotion maintainer가 CSS-in-JS를 떠난다고? 아! 이런 자극적인 제목! 너무 좋아! 그냥 지나칠 수 없지.
•
본인은 사실 다들 Styled Components 쓰고 Emotion 쓰길래 흠.. 써야하나보구나 하고 둘중 하나 비교하여 썼었지 CSS in JS의 진짜 장단점을 알고 쓰진 못했음. → 퍼블리셔 경력을 가지고 있는 동료가 “아.. emotion 써보고 나니까 scss가 불지옥이였다는걸 알게됐어요..”라는 반응만 기억하고 있다.
◦
실제로 사용해보고는 으아아악!
2. 두개 반이 오리라 - CSS in JS의 종류
두개 반이 오리라는 대체 무슨소리야;;; 정신차려;;
runtime css in js
CSS 스타일을 JS 코드를 이용 해 브라우저 런타임에 동적으로 작성 - “이 포스트의 공격 대상”
•
emotion, styled-components
•
DOM을 직접 업데이트 하는게 아닌 자바스크립트를 이용해 CSSOM을 직접 조작한다.
•
SSR시 HTML 파일에 <style> 태그로 필요한 스타일 주입한다.
zero runtime css in js
브라우저 런타임이 아닌 빌드타임에 미리 CSS 스타일을 작성
•
vanilla-extract, Linaria
•
빌드 시 .css 파일을 결과물로 extract → SSR에서도 똑같이 작동한다.
near zero runtime css in js - (반)
최소한의 runtime을 가지는 css in js
•
Stitches
•
runtime CSS in JS가 갖고있는 퍼포먼스 오버헤드와 zero runtime CSS in JS가 갖고있는 제약의 적절한 해결법 → JS를 이용한 동적인 스타일 변화가 가능하지만 사전에 정의해둔 스타일로만 변경 가능하다.
3. runtime CSS in JS의 단점
번들 용량 증가
•
런타임에 스타일을 생성 하므로 해당 작업을 하는 CSS in JS 라이브러리가 브라우저에 다운로드 되어야한다.
•
결과적으로 이용자들이 서비스를 이용하기위해 다운받아야할 번들의 용량 증가.
style-serialization
•
emotion이 css 문자열이나 객체를 HTML 문서에 사용 가능한 plain CSS로 변환하는 프로세스(css-125el2)
// 퍼포먼스 이슈가 존재하는 컴포넌트
export const BadPerformanceComponent = () => (
<div
css={{
backgroundColor: "blue",
width: 100,
height: 100,
}}
/>
);
TypeScript
복사
•
해당 컴포넌트는 리렌더링 시 마다 style-serialization이 일어난다 → 리렌더링 될 때 마다 문자열 또는 객체로 존재하는 스타일을 다시 plain css로 적용
// style.ts
const container = css`
width: 100%;
margin-right: 1rem;
`;
// betterPerformanceComponent.tsx
export const BetterPerformanceComponent = () => {
return (
<div css={container}>
<p> this is component</p>
</div>
);
};
TypeScript
복사
•
스타일 객체를 emotion css 함수를 이용해 컴포넌트 바깥에 선언 후 적용 시 문제는 해결된다 → 모듈이 로드 될때만 serialization이 일어난다.
SSR 시 두번 일한다
•
서버에서 initial render 후 브라우저로 보내고 브라우저에서 hydration 중 JS 로직을 이용하여 다시 initial render를 수행 → 3-2. 비교 - SSR 시 <style> vs CSS Extraction에서 정확히 설명하겠다.
3-1. 단점 - 퍼포먼스 오버헤드
대체적으로 zero runtime CSS in JS(plain CSS)가 더 빠르며 몇몇 항목에서는 크게 차이가 난다.
•
runtime CSS in JS(Styled Component) vs plain CSS(Linaria)
◦
본문 글 자체는 Real world CSS를 Linaria로 비교 → Linaria는 plain CSS 파일을 컴파일 타임에 생성하는 CSS in JS 라이브러리
◦
Linaria도 CSS in JS아냐? → zero runtime CSS in JS
•
runtime CSS in JS는 plain CSS 보다 느릴 수 밖에 없다 → JS에 존재하는 style을 serialize 하고 CSSOM을 manipulate 하는 것 자체가 performance consuming job
•
Total request file size, FCP, SI, LCP, TTI, TBR, Execution Time, Total Blocking Time 등등 모든 면에서 plain CSS가 우세하다.
◦
몇몇 항목에서는 초 단위로 차이까지 난다. → 초 단위 차이면 당연하게도 UX에 영향을 끼친다.
•
물론 그런 케이스도 있고 아닌 케이스도 있다, 맹신하면 안되지만 대부분의 케이스, 자료에서 plain CSS가 더 빠르다는 벤치마크가 대다수 이다.
3-2. 단점은 아니지만 비교 - SSR 시 <style> vs CSS Extraction
•
SSR 시 스타일 적용에는 두가지 방식 존재한다. 이 부분에선 일장일단이 존재한다. 명확한 결론은 존재하진 않다.
•
캐싱 유무에대한 차이도 존재하나…
HTML 내부에 <style> 태그로 존재하는 방식
대부분의 runtime CSS in JS(near zero runtime인 stitches도 같은 방식)
•
SSR 시 첫 그리기에 필요한 스타일이 HTML에 삽입된 채로 전달됨 → FCP, LCP가 비교적 빨라 질 수 있다
◦
외부 CSS 파일을 추가 요청할 필요가 없으며 대부분의 runtime CSS in JS에서 지원하는 기능인 Critical CSS extraction을 이용해 딱 첫 그리기에 필요한 스타일만 삽입됨
◦
추가 .css 파일을 요청d이 필요없다 → 스타일을 위한 요소들이 HTML 내부에 있기 때문에 render blocking time이 당연히 상대적으로 짧다!
•
しかし、(하-지-만)
◦
서버로부터 런타임 라이브러리를 추가로 받아와야 한다.
◦
SSR 시 <style> 태그에 이미 존재하는 스타일들 또한 JS 파일에 들어가있는 채로 또 받아온다. → 중복되는 데이터 소모
◦
SSR로 이미 그려져 있는 상태지만, 필요한 모든 파일을 받아온 후 Hydration을 거쳐 이미 보이는 스타일을 다시 입힌다 → TTI가 비교적 늦어짐, Hydration이 진행되는 도중에는 인터랙션이 불가능한 껍데기만 있는 상태
외부 .css 파일
•
최초 그리기에 필요한 style 뿐만 아니라 해당 페이지에 모든 css 파일을 가져온다. → 클라이언트가 가져올 번들의 용량이 많아진다.
◦
마이너한 downside이다, 실제 용량차이가 그렇게 크진 않다.
•
CSSOM 생성 자체가 rendering blocking resource이므로 결과적으로 HTML 문서 내부에 <style> 태그에 존재하여 fetch 필요가 없는 방식보다 CSSOM 생성에 비교적 시간이 더 걸린다. → FCP, LCP가 비교적 더 느리다.
•
しかし、(하-지-만)
◦
runtime CSS in JS 라이브러리를 추가로 fetch 해올 필요가 없다.
◦
Hydration 시 style을 다시 입히지 않아도 됨 → 스타일을 다시 입히는 로직도 불필요하며 시간이 줄어든다. → 비교적 빠른 TTI
4. zero runtime 준결승 전
•
지금 까지 runtime CSS in JS를 신나게 욕만했다. 이쯤 까지 읽었으면 “아 runtime CSS in JS 못쓰겠네~” 싶을수도 있다고 생각한다. 조금만… 아주 조금만 더 읽어주길 간곡히 요청한다.
•
일단 zero runtime CSS in JS 중 하나로 선택했다.
•
지금 사용중인 “이모션만큼 편하지만 더 빠른” 선수를 찾아보자
◦
runtime CSS in JS는 선택하지 않았음
Linaria
•
zero runtime CSS in JS
•
•
•
vanilla-extract
•
zero runtime CSS in JS
•
결론 - vanilla-extract
•
“공존성 + styled-components 처럼 스타일링을 하고싶어” → Linaria
•
“위의 두개는 굳이 필요없어” → vanilla-extract(저는 이걸로 갈게요)
◦
개인적인 코딩 스타일로는 스타일과 비즈니스 로직의 분리를 선호한다. 그러므로 스타일 코드의 공존성은 개인적으로 필요하지 않다.
개인 프로젝트 예시사진
5. vanilla-extract vs stitches
•
간단한 (내가 필요한) 기능 비교만 할 예정이다.
•
스코프 제어, Nested Selectors, TS 지원 및 코드 자동완성, 동적 스타일링,SSR은 모두 가능
둘다 제공하는 기능들
Zero runtime
•
Zero runtime 선호 → “runtime CSS in JS 쓸거면 그냥 Emotion 썼다.”
◦
런타임에서 돌아가는 CSS in JS에서 벗어나기위해 새로운 라이브러리를 선택하는건데… 또 다른 runtime CSS in JS를 선택하기엔 가슴이 너무 아프다.
stitches 
•
반(near) zero runtime
vanilla-extract 
•
완전 zero-runtime으로 빌드 시 결과물 생성
SSR시 CSS 방식
•
static css(.css) 파일로 출력되는 방식을 선호 - 추가적인 runtime library가 필요 없으므로
◦
runtime 에서의 런타임 스타일 삽입 및 출력으로 인한 퍼포먼스 오버헤드를 개선 불가
stitches 
•
HTML <style> 태그 내부에 삽입
vanilla-extract 
•
빌드타임에 생성된 외부 css 파일 사용
정돈된 개발자 도구
•
필수는 아니지만 고려대상 중 하나 → DX를 위해
stitches 
•
styled API 사용 시 여전히 더러워짐
vanilla-extract 
•
결과적으로 CSS 파일을 만드므로 리액트 개발자 도구는 깔끔한 상태로 남는다.
스타일 코드 공존성
stitches 
•
공존 가능
vanilla-extract 
•
*.css.ts 파일이름으로 항상 나뉘어져야 한다.
결과물 용량
stitches 
•
near zero runtime 이므로 상대적으로 더 높음
vanilla-extract 
•
runtime 라이브러리 존재 안함
•
dynamic 스타일링을 사용하더라도 1kb 이하로 추가(99?byte)
결론 1. - stitches는 사망하였는가?
Stitches 살아있니?
•
“우리 살아있어! 살아있다고~!” 하는 강력한 느낌적인 느낌을 강하게 받는다.
•
과연 관리가 되고있을까?(앞으로 관리가 될까?)에 대한 두려움이 서늘하게 나를 뒤덮는다.
stitches의 사망(2023/11/07 - update)
•
stitches는 사망했다. 더 이상 업데이트 되지 않는다. → 공식 홈페이지에도 해당사항이 추가됐다.
•
리액트 18의 당장 사용은 문제가 없으나 미래를 봤을 때는 정적 css 파일 extraction이 되야 한다고한다. 이는, 즉 스타일 엔진을 전체를 재작성해야 하기 때문에 위와같은 결정을 내렸다고한다.
결론 2-1. 그래서?
기술 선택에는 정답이 없지만 현재 상황에 따른 더 나은 선택은 존재한다.
CSS in JS를 파해치며..
•
“Runtime CSS in JS가 나쁘다!”가 아니라 “runtime CSS in JS는 plain CSS에 비해 퍼포먼스가 안좋다"가 내가 생각하는 결론이다.
•
무턱대고 “남들이 emotion / styled-components 쓰니까 나도 쓰자”로 접근하지말고 정확히 내가 사용하는 기술에 대한 pros, cons를 알고 기술적 한계점, 내가 극복할 수 없는점을 정확히 파악하고 사용해야한다. 라는 방법론적인, 이론적인 결론이 도출된다. 누구나 깊은 맘속으론 알고 있으나 시간에 쫓겨, 사람에 쫓겨 생각하지못하고 간단한 구글 검색 후 TLDR 부분만 빠르게 읽고 “이게 좋다는데?” 방식의 TDD(Test Driven Development가 아닌TLDR Driven Development의 약자)는 결과적으로 언젠간 나에게 무조건 언젠간 다시 돌아온다. 장담한다.
•
비단 지금 이 CSS in JS 뿐 아니라 개발의 모든 면에서 적용해야할 접근법이다. 적지않은 개발자들과 이야기를 해봤지만 “리액트 왜써?”라는 질문에 명확한 대답을 하는 개발자는 매우 소수였다. → 물론 “리액트 안쓰면 이직 못해요”라는 치트키를 발동한 개발자들도 적지않았다. 틀린답은 아니라고 생각한다. 정작 나만해도 저 말을 할거 같다.
교과서 적이지만 내가 생각하는 리액트를 사용하는 이유는 크게 두가지다.
•
무조건 퍼포먼스만을 위해(빌드 타임때 CSS 생성을 시 사용되는 시간 조차 performance consuming이다!라는 극한의 Performance Orineted Developer의 경우 이렇게 생각 할 수 있다고 생각한다.) plain CSS 또는 SCSS를 이용해 개발하는건 오히려 개발의 속도가 늦어지고 코드에 예기치 못한 에러가 더 늘어날 가능성이 높다.
마지막 팩트 정리
•
plain CSS로 개발하는건 DX를 고려했을땐 안좋은 선택인가? → 
◦
안그래도 정적 코드검사가(거의) 불가능한 plain CSS인데 TS도 없고 불완전한 auto complete와 개발하는건 불지옥이다.
◦
거기에 미사용 코드 제거는 직접 일일이 확인해가며 지워야한다. 가능하겠는가?
•
zero runtime CSS in JS는 좋은 선택지 인가? → 
◦
퍼포먼스적으로 runtime CSS in JS보다 더 낮은 런타임 오버헤드 + 적은 번들 용량으로 결과물을 만드는건 사실이다.
◦
zero runtime CSS in JS는 DX를 고려했을때 runtime CSS in JS에 절대 뒤쳐지지 않는다. 뒤쳐지는게 아니라 다른점으로 작용한다고 생각한다.
•
runtime CSS in JS가 DX면에서 안좋은 영향을 끼치는가? → 


◦
DX만 고려했을땐 runtime CSS in JS가 최고의 옵션이 될 수 있다.
•
현재 이미 개발 후 운용중인 프로젝트에서 무조건적으로 최대한 빨리 runtime CSS in JS를 벗어나야하는가? → 


◦
최우선 목표일 이유 전혀 없다. 퍼포먼스가 UX의 전부가 아니다. 프로덕트의 개발 + 기능 추가도 너무나도 중요한 부분이다.
◦
C Level에게 “우리 프로덕트에서 어떤 기술(runtime CSS in JS)를 사용하고 있는데 프로덕트 성능저하의 주범이다. 언젠가는 걷어내고싶다~”로 대화를 시작해보는건 어떨까? 프로덕트 개선하겠다는데 싫다는 사람이 어디있겠는가.
•
새로 시작해야 하는 프로젝트가 있다. zero runtime CSS in JS를 고려해야할까? → Maybe 
◦
zero runtime CSS in JS는 확실히 좋은 선택이 될 수 있다. 새로 시작하는거면 Why not? Maybe?가 제일 좋은 답이라 생각한다.