CSS가 브라우저에 적용되는 3가지 방법
- Inline CSS
- Internal CSS
- External CSS 순으로 발전함
<link>
- 브라우저는 HTML을 읽을 때
<head>
의 <link>
태그를 만나면 href에 걸려있는 주소로 파일을 비동기적으로 다운로드 받는다
- CSS 파일을 다운로드할 때 HTML 파싱(= DOM 생성)은 블로킹되지 않는다.
- 다만 렌더 트리 구성에 필요한 CSSOM을 만들려면 CSS 코드가 필요하므로 자연스럽게 렌더링이 막힐 뿐이다
<script>
<script>
태그는 동기적으로 다운로드됨. 즉 브라우저는 스크립트 다운로드가 끝날 때까지 HTML 파싱마저 중단함
- 그래서
async
, defer
등의 속성을 사용해 비동기적으로 스크립트를 로드할 수 있다. 이미지 출처
async
: 다운로드 끝나면 바로 스크립트 실행. 스크립트 간 의존성이 있다면 부적합. GA나 광고같은 독립적인 스크립트에 유용
defer
: 스크립트가 작성된 순서대로 실행됨, 스크립트 내 CSS 혹은 타 스크립트 간 의존성이 있는 경우 적합
Critical CSS
- First Contentful Paint(above the fold)에 영향을 미치는 첫페이지 스크롤 안 화면의 CSS
- 이 영역의 CSS는 보통 Internal CSS로 HTML
<style>
태그 안에 작성해두는 경우가 많다
- Critical CSS 이외의 영역은 External CSS 방법으로(비동기로) 파일을 가져온다
- 앞서 살펴봤듯 CSS 다운로드는 렌더링을 블로킹한다. below the fold CSS 로딩을 FCP에 영향 주지 않고 비동기로 가져오려면
<link>
의 rel="preload"
, as="style"
속성을 쓰자
우선순위
- inline CSS(
<div style="color: blue;" />
)
- id > class > element 참고
Sass
- ~~Super ass~~ CSS 전처리기. 반복해야하고 변수 못쓰는 CSS를 화려한 문법과 변수를 활용해 쓸 수 있게 하고 빌드 타임에
.sass
/.scss
파일을 .css
파일로 트랜스파일링 시켜준다
PostCSS
- CSS 후처리기. CSS를 자바스크립트를 써서 바벨처럼 모던 문법(Autoprefixer)이나 브라우저 폴리필(postcss-preset-env)같은 사람이 놓치기 쉬운 것들을 자동으로 달아주는 처리기.
- 전처리기가 먼저 순수 CSS 파일이 아닌
.scss
와 같은 파일을 .css
파일로 변환하고, 그 파일을 다시 최종적으로 브라우저에 넘겨줄 파일로 다듬고 번역하는 과정이 후처리 과정이다. 참고
CSS-in-JS
- 웹개발이 분화되고 프론트엔드가 생겨났으며 더이상 jQuery같은 HTML/CSS 명령적 프로그래밍이 아닌 React발 컴포넌트 개발이 자리잡게 되면서 CSS 작성도 '컴포넌트처럼 작성'하고자 하는 욕망(colocation)이 생겼으니 그 실현판이 CSS-in-JS
- 자바스크립트 파일에 CSS를 작성한다. 이
.js
파일은 빌드타임에 .css
로 바뀌지 않고 같이 번들링되어 소스코드에 포함된다. 브라우저는 맨처음 허허벌판 HTML을 받고, 스크립트를 다운받아 그제서야 StyleSheet를 파싱하게 된다
- 코딩 편리함을 취하고 성능을 내줬다
CSS 카테고리
- 현대 CSS의 키포인트는 관리포인트를 일원화시키려는 것.
CSS-in-JS 브라우저 적용 방법
- 런타임 스타일시트(Runtime Stylesheets)
- 정적 CSS 추출(Static CSS extraction 또는 zero-runtime)
런타임 스타일시트
<style>
태그를 <head>
에 추가하는 방법
- HTML을 건드는 방식. DOM+CSSOM 파싱부터 렌더트리 합성, 레이아웃 + 페인트까지 다시 해야한다
- CSSStyleSheet.insertRule()로 CSSOM에 직접 추가하는 방법
- Critical Rendering Path를 공부한 우리는 1번이 얼마나 성능적으로 좋지 않은지 직감할 수 있다
- 그래서 CSS-in-JS 라이브러리들은 dev환경에선 1번, prod환경에선 2번을 택했다(온몸비틀기)
- 그렇지만 여전히 번들 크기가 크고, SSR 환경에서 Critical CSS를 위해 똑같은 스타일시트가 두 번 생성되는 문제는 해결되지 못했다 이미지 출처
정적 CSS 추출 혹은 제로 런타임
- SSR 방식이 대세가 되면서 다시 CSS를 서버에서 빌드하고자 하는 움직임이 생겼다
- 올드스쿨 클래식 HTML, CSS, JS를 웹 서버에서 브라우저로 서빙하는 방식을 표방한다. 다만 예전과 다른 점은 '웹 서버'의 유무고 과거엔 서버가, 현대에선 React Server같은 웹서버가 이 역할을 맡고 있다는 점이다
- (번역) 우리가 CSS-in-JS와 헤어지는 이유 글에 따르면, Emotion의 메인테이너는 런타임 스타일시트 방식의 런타임 환경에서의 오버헤드와 번들 크기의 증가 등의 성능 문제로 Sass 또는 tailwindCSS(utility-first CSS)로 회귀한다고 말하고 있다.
제로 런타임에서의 동적 변수 스타일링
- 딱 하나 포기하기 어려운게 있다면 바로 상태 변화를 스타일에 동적으로 적용시킬 수 있는 기능
- 상태를 추적하고 바뀔 때마다 스타일을 변경시키려면 필히 런타임 환경이 필요했다
- 제로 런타임이면서 동적 스타일링도 할 수는 있음
- 정적인 변수(사전에 정의할 수 있는 변수)는 내부적으로 CSS 변수를 활용해 처리함
.accent_blue__l3kgsb1 {
--accentVar__l3kgsb0: blue;
}
.accent_pink__l3kgsb2 {
--accentVar__l3kgsb0: pink;
}
- 정의할 수 없는 변수는? CSS 변수를 생성해두고 런타임에서 계산한 뒤 inline CSS로 먹인다 (
@vanilla-extract/dynamic
의 assignInlineVars()
)
- 할 수는 있는데 DX가 좋아보이진 않는다
- 엄밀히 말하면 제로 런타임이지만 런타임이 존재한다
Atomic CSS
- SSR도 그렇고, CSS도 결국엔 올드스쿨로 회귀한다
- 올드스쿨은 inline styling, semantic한 class 이름, 컨텐츠와 스타일의 관심사 분리를 추구했다. 이를 타파하던 것이 CSS-in-JS. 여기서 올드스쿨이 과연 진짜 틀린걸까? 의문을 제기하는 자들이 있었으니, 그가 바로
tailwindcss
이미지 출처
- Atomic CSS는 인라인으로 작성하고, 더이상 컨텐츠와 스타일을 함께 컴포넌트화하지 않으며, 따라서 semantic한 이름을 짓지 않는다(의미없는 방법론에서 해방)
- 이름 그대로 원자적인 class 템플릿을 담은 스타일시트를 제공하고, 레고 조립하듯이 스타일을 만든다
- 초기의 tailwindcss는 그냥 다 만들어놨다가, Just In Time 컴파일러를 붙여 빌드 타임에 정말로 쓰는 클래스들만 추출할 수 있게 되었다
- 그래서 매우 가볍고 개발 속도도 매우 빨라졌다.
- 동적 변수를 정식으로 지원하지 않는다. tailwind스럽게 해결하는 방법은 다양한 Config 뭉치를 만들고 변수에 따라 정해둔 단위의 클래스를 적용시키기, 인라인 스타일로 꽂기 등이 있음
- 굳이 값을 동적으로 주입해줘야할까 에 대한 의문을 가질 필요도 있어보인다