인라인 스타일
리액트 jsx 안에 style 프로퍼티로 스타일을 정의하는 것.
장점
- 동적 스타일링 용이 : state를 활용해 스타일을 다르게 주고 싶을 때 쉽게 조정 가능
- 스코핑 자동화 : 인라인 스타일은 해당 컴포넌트에 직접 적용되므로 스타일의 충돌을 걱정할 필요가 없다.
- 추가 설정 불필요 : CSS 파일을 별도로 관리하거나 로드할 필요가 없다.
단점
- 성능 이슈 : external CSS와 달리 별도의 컴파일 및 축소된 CSS 파일을 사용하지 않기 때문에 HTML 문서의 큰 부분을 차지하여 로드 시간이 늘어날 수 있다. 각 컴포넌트 인스턴스마다 스타일이 복제되어 DOM 요소가 많은 페이지에서는 성능 저하를 초래할 수 있다.
- 재사용성 제한 : 같은 스타일을 여러 컴포넌트에서 사용하고 싶은 경우, 각 컴포넌트에 동일한 스타일 규칙을 반복해서 적용해야 한다.
- 기능 제한 : ‘:hover’ 같은 가상 선택자, ‘@media’ 쿼리 등 CSS의 일부 기능을 사용할 수 없어 스타일링 옵션이 제한된다.
런타임 CSS-in-JS
앱이 실행될 때 라이브러리가 스타일을 해석하고 적용하는 방식.
styled-components, Emotion이 이 방식에 속함
장점
- 지역 스코프 스타일 : CSS-in-JS는 기본적으로 스타일을 지역 스코프로 지정하여 의도하지 않은 클래스 이름 충돌 문제를 방지함.
- 코로케이션 : colocation, 유지보수의 용이성을 위해 단일 컴포넌트에 관련된 모든 것을 같은 위치에 두는 것.
일반 CSS를 사용할 때 CSS 파일은 자바스크립트와 별도의 파일로 존재하고, 그 위치와 상관없이 스타일이 전역으로 적용되기 때문에 코로케이션을 구현하기 어려움.
반면 CSS-in-JS를 사용하는 경우 스타일을 사용하는 리액트 컴포넌트 내부에 직접 스타일 작성 가능 ⇒ 앱의 유지보수 편해짐 - 자바스크립트 변수를 스타일 규칙에 사용 가능 : 스타일 규칙 작성 시 자바스크립트 변수를 사용하여 중복을 줄일 수 있음
단점
런타임 오버헤드 : 성능 문제
CSS-in-JS 라이브러리는 컴포넌트가 렌더링될 때 document에 삽입할 수 있는 일반 CSS로 스타일이 직렬화되며 이 과정에서 앱의 성능에 영향을 미침
- 스타일 직렬화 : Emotion이 CSS 문자열 또는 객체 스타일을 가져와 document에 삽입할 수 있는 일반 CSS 문자열로 변환하는 과정
만약 스타일 직렬화가 다음과 같이 리액트 렌더링 주기 내부에서 수행된다면,
function MyComponent() {
return (
<div
css={{
backgroundColor: "blue",
width: 100,
height: 100,
}}
/>
);
}
MyComponent가 렌더링될 때마다 객체 스타일은 다시 직렬화되며, 자주 렌더링될 경우 높은 성능 비용을 초래할 수 있음
만일 컴포넌트 외부로 스타일을 이동해 렌더링이 아닌 모듈이 로드될 때 한 번만 직렬화가 발생하도록 한다면,
const myCss = css({
backgroundColor: "blue",
width: 100,
height: 100,
});
function MyComponent() {
return <div css={myCss} />;
}
성능 문제는 줄어들겠지만, 스타일에서 프로퍼티에 접근할 수 없으므로 CSS-in-JS의 주요 셀링 포인트 중 하나를 놓치게 됨
또한 CSS 규칙을 자주 삽입하면 브라우저에서 더 많은 추가 작업을 수행해야 하는데, CSS-in-JS 라이브러리는 컴포넌트가 렌더링될 때 새로운 스타일 규칙을 삽입해 작동하므로 이는 근본적으로 성능에 좋지 않음
번들 크기 증가
사이트를 방문하는 각 사용자는 CSS-in-JS 라이브러리용 자바스크립트를 다운로드해야 하므로 다운받는 JS 번들 크기가 필연적으로 증가됨
Emotion은 압축되었을 때 7.9KB고, styled-components는 12.7KB임. (react+react-dom은 44.5KB)
React DevTools 어지럽힘
Emotion은 css 프로퍼티를 사용하는 각 요소에 대해 및 컴포넌트를 렌더링함. 이 경우 Emotion의 내부 컴포넌트가 React DevTools를 어지럽힐 수 있음
SSR에서의 오류
CSS-in-JS를 사용하면 SSR 및 컴포넌트 라이브러리를 사용할 때 오류가 많이 발생함. MUI, Mantine, 또 다른 Emotion 기반 컴포넌트 라이브러리를 서버 사이드 렌더링에 사용할 때 제대로 동작하지 않는다는 이슈가 많음
주로 발생하는 문제들의 공통 주제는 다음과 같음
- Emotion의 여러 인스턴스가 한 번에 로드됨 : 여러 인스턴스가 모두 동일한 버전의 Emotion인 경우에도 문제가 발생
- 컴포넌트 라이브러리는 스타일이 삽입되는 순서를 완전히 제어할 수 없는 경우가 많음
- Emotion의 SSR 지원은 리액트 17과 18에서 다르게 작동함
Sass 모듈(Sass + CSS 모듈)
Sass와 CSS Module을 함께 사용한 방법.
- CSS 모듈(css-modules) : CSS의 global scope 문제를 해결하기 위해 등장한 라이브러리. CSS를 scope 단위로 나누어 작성할 수 있게 해준다.
- Sass : 순수한 CSS 파일에 다양한 기능을 추가하기 위해 만들어진 CSS 확장 언어. CSS로 트랜스파일링되는 CSS 전처리기이며, 중첩 구문을 사용할 수 있고 믹스인으로 코드 뭉치를 하나로 사용할 수 있는 등 여러 기능을 지원한다.
Sass의 코드 재활용성과 CSS 모듈의 지역스코프 기능을 함께 누릴 수 있어 보통 함께 사용한다.
.module.scss 확장자를 쓰는 스타일 파일이 Sass 모듈을 사용한 것.
장점
- 지역 스코프 스타일 제공
- 우수한 런타임 성능 : Sass는 CSS 전처리기로서 프로젝트를 빌드하는 시점에 CSS 파일로 변환된다. CSS 모듈 또한 일반 CSS 파일로 컴파일되므로, 런타임 성능 비용이 없다.
- 코드의 재활용성 : 중첩 구문과 믹스인 등 추가적인 기능으로 일반 CSS에 비해 코드를 재활용할 수 있는 개발자 경험을 제공한다.
- 캐싱 및 최적화 : Sass에서 최종적으로 생성된 CSS 파일은 정적 파일로서 효과적으로 캐시되고 최적화될 수 있어 페이지 로드 시간을 단축시킬 수 있다.
단점
- JS 변수 사용 불가 : CSS-in-JS처럼 스타일에서 JS 변수를 사용할 순 없다. 다만 :export 블록을 사용해 Sass 코드의 상수를 JS에서 사용할 수 있도록 할 수 있다.
- 코로케이션 불가 : Sass 모듈을 사용하면 컴포넌트와 스타일 코드는 별도의 파일에 위치하게 된다.
- 컴파일 시간 비용 : Sass는 원시 CSS로 컴파일되어야 하기 때문에 파일을 수정하거나 빌드할 때 컴파일 시간을 소요하며 이는 개발 효율성을 떨어뜨리거나 빌드 시간을 늘릴 수 있다. 또한 복잡한 중첩 및 믹스인을 사용하는 큰 프로젝트에서는 컴파일 시간이 길어진다.
- 불편한 개발자 경험 : 스타일을 적용하기 위해서는 별도의 .module.scss 파일을 생성하고 이를 임포트하며, 클래스명을 짓고 맞추는 등의 사소한 작업들을 필요로 하여 다른 방식에 비해 스타일 적용 과정이 번거로운 편이다.
- Sass 기능에 대한 러닝커브
유틸리티 클래스(Atomic CSS)
웹사이트의 시각적 요소를 작은 단위로 나누고, 단위별로 이름을 지정해 CSS를 작성하는 방법.
일반적으로 한 컴포넌트에 여러 유틸리티 클래스를 결합해 원하는 스타일을 얻는다.
Bootstrap, Tailwind 등의 CSS 프레임워크가 이 방식에 속한다.
장점
- 뛰어난 런타임 성능
- 익숙해질 경우 빠른 개발 가능
- 가벼움 : 미리 정의된 CSS 파일을 className으로 계속 재사용하기 때문에 실제 CSS 코드양이 얼마 되지 않고, 애플리케이션 확장에 따라 커지는 CSS 번들의 크기에 대해 걱정할 필요가 없다.
- 요소를 추가하거나 삭제할 때 별도의 스타일시트를 추가하는 작업 생략 가능
- 스타일 충돌 문제에서 자유로움
단점
- 클래스에 대한 러닝커브
- 좋지 않은 가독성
- 컴포넌트 재사용의 어려움
- 동적 값 계산의 어려움 : class는 동적으로 매칭이 불가능하므로 동적 값을 계산할 때는 style 속성을 사용하거나 clsx 같은 외부 라이브러리를 추가적으로 사용해야 한다.
- prebuilt class 형식의 번거로움 : 라이브러리 내에서 유틸리티 클래스로 정의해 둔 스타일만 적용 가능하기 때문에 제공되지 않는 스타일을 적용하고 싶을 경우 속성마다 클래스를 따로 만들어야 해 번거로움을 초래한다. ex) tailwind에서 text-shadow를 지원하지 않아 적용하려면 플러그인 설정을 통해 클래스를 만들어야 함.
컴파일 타임 CSS-in-JS(zero-runtime CSS-in-JS)
컴파일 타임에 스타일을 일반 CSS로 변환하는 CSS-in-JS 방식.
Compiled, Vanilla Extract, Linaria 라이브러리가 이 방식에 속함
서버에서 빌드될 때 CSS 파일이 생성되어 브라우저에 전달되며, 동적 변수는 CSS 변수 및 특별한 패키지를 이용해 해결한다. (Vanilla Extract의 경우, vanilla-extract/dynamic 패키지에서 동적 변수를 처리. CSS 변수명만 생성해 두고, 런타임에서 값을 계산한 후, inline으로 CSS 변수를 바로 등록해 스타일을 입히는 방식이다.)
장점
- SSR 환경에서 사용 가능 : CSS를 미리 추출하므로 SSR에서도 사용 가능하다.
- 퍼포먼스 향상 : 컴파일 타임에 CSS를 생성하므로 런타임 성능 부담이 없어 더 빠른 페이지 로딩 속도와 사용자 경험을 제공한다.
- 타입스크립트 통합 : Vanilla Extract는 타입스크립트를 활용해 CSS Property에 타입으로 자동완성을 활용할 수 있어 버그를 예방 가능
- 자동 최적화를 통한 가벼움 : Vanilla Extract는 빌드 시 컴파일되며, CSS가 JavaScript에서 분리되어 정적 파일로 생성되는데 이 과정에서 사용되지 않는 스타일을 제거하고, CSS 변수 및 미디어 쿼리를 최적화할 수 있는 기능을 제공한다. 이는 최종 CSS 파일의 크기를 줄여준다.
- 모듈화 및 캡슐화 : CSS를 모듈화하여 컴포넌트 단위 스타일 관리 가능
- 유연한 스타일링 : 변수, 테마, 조건부 스타일 등 고급 CSS 기능을 쉽게 구현 가능
단점
- vanilla extract의 경우 코로케이션을 지원하지 않음
- 러닝커브
- 설정 복잡성 : 프로젝트 설정 시 컴파일러 설정과 빌드 도구와의 통합이 필요할 수 있다. 이는 초기 프로젝트 설정을 복잡하게 만들 수 있으며, 설정 오류로 인한 개발 지연이 발생할 수 있다.
- 범용성 제한 : 일부 기존 CSS 기능들을 사용하는 데 제한이 있을 수 있다.
참고문서
https://borstch.com/blog/styling-in-reactjs-inline-styles-and-limitations
모던 CSS 적용 방법 둘러보기(CSS-in-JS with zero-runtime)
https://github.com/andreipfeiffer/css-in-js/blob/main/README.md