티스토리 뷰

반응형

웹 컴포넌트 스타일링 관리 (CSS-in-HTML, CSS-in-CSS, CSS-in-JS)

 

 

웹 개발에서 CSS를 적용하는 방식은 프로젝트의 구조와 요구 사항에 따라 다양하게 선택할 수 있으며, 주요 스타일링 접근 방식으로는 CSS-in-HTML, CSS-in-CSS, CSS-in-JS가 있다.

 

 

 

CSS-in-HTML

인라인 스타일링을 말하며, HTML 요소에 style 속성을 사용해서 해당 요소에 직접 스타일을 적용하는 방식이다.

소규모의 간단한 스타일을 적용할때 사용할 수 있으며, 해당 요소에만 영향을 준다.

별도의 css 파일이 필요없다. 하지만 html 태그와 style 코드가 혼재되어 유지보수가 어려우며 재사용이 안되기 때문에 동일 스타일을 여러 요소에 적용하기 위해 중복코드가 발생할 수 있다.  

<p style="color: blue; font-size: 16px;">안녕</p>

 

 

 

CSS-in-CSS

전통적인 스타일링 방식이다.

별도의 CSS 파일에 스타일을 선언한 뒤, 이를 HTML 문서에 연결하여 사용하는 방식이다.

파일 분리가 되어있으며 HTML, JS와 분리되는 것이 특징이다. 모듈화로 관리가 가능하며 동일한 스타일을 여러 HTML 요소에 적용할 수 있어 재사용에 용이하다. 하지만 스타일이 파일로 분리되어있어서 컴포넌트와의 결합도가 낮아 컴포넌트 재사용성에는 제한이 있다.

스타일이 전역 네임스페이스로 공유되어 프로젝트 전역에서 접근가능하여 충돌할 수 있다. 

브라우저의 CSS 캐싱을 활용으로 성능 최적화를 할 수 있다.

스타일은 런타임에서 변경되지 않으며 빌드전에 미리 정의된다. (미리 생성된 .css 파일을 로딩하는 방식이라 런타임에서 스타일을 생성하지 x)

 

style.css

p { color: blue; font-size: 16px; }

 

index.html

<!DOCTYPE html> 
<html> 
    <head> 
        <link rel="stylesheet" href="style.css"> 
    </head> 
    
    <body> 
        <p>안녕</p> 
    </body> 
</html>

 

 

 

CSS-in-JS

자바스크립트 코드 내에서 CSS를 작성하는 방식이다.

스타일과 로직이 한 곳에 있어 관리가 용이하다. (...는데, 이 부분은 질문과 답변에서 다시 다루도록 하겠다.)

컴포넌트 기반의 자바스크립트 라이브러리나 프레임워크에서 많이 사용된다.

컴포넌트 기반으로 스타일 컴포넌트와 함께 정의되어 모듈화와 재사용성이 높다.  (CSS 모델을 문서 레벨이 아닌 컴포넌트 레벨로 추상화하여 모듈화)

자바스크립트의 변수와 함수를 활용해서 동적인 스타일링이 가능하다.

클래스명이 해시 값으로 치환되어 네이밍이나 중복 걱정을 덜 수 있다.

  • 스타일 정의를 기반으로 해서 해시 값을 생성하여 고유 클래스 이름을 부여한다. 이를 통해 스타일 충돌 방지하고 전역 네임스페이스에서의 클래스 중복 문제를 해결한다.
  • 해시 값을 사용하여 고유하면서도 짧은 클래스명을 생성함으로써 코드의 가독성을 높이고 파일 크기를 줄여 최적화 할 수 있다. 

런타임에 스타일을 생성하므로 성능 저하가 발생할 수 있는 것과 별도의 라이브러리 설치에 따른 번들 크기 증대 등의 단점이 있다.

(CSS가 애플리케이션 실행 중(JS가 실행되는 순간)에 동적으로 생성되고 적용된다는 뜻. 즉, styled-components는 앱이 실행될 때(Run-time) CSS를 생성하고 <style> 태그로 삽입하는 방식이다.)

 

styled-component 사용 예시

import styled from 'styled-components'; 

const BlueText = styled.p` 
	color: blue; 
    font-size: 16px; 
`; 

function App() { 
	return <BlueText>안녕</BlueText>; 
}

 

 

해시 값 예시

컴파일 전

import styled from 'styled-components';

const Title = styled.h1`
  font-size: 1.5em;
  text-align: center;
  color: palevioletred;
`;

function App() {
  return <Title>안녕</Title>;
}

export default App;

 

컴파일 후

<style>
  .sc-abc123 {
    font-size: 1.5em;
    text-align: center;
    color: palevioletred;
  }
</style>

<h1 class="sc-abc123">안녕</h1>

 

 

각 방식의 선택은 프로젝트의 규모 팀의 선호도 그리고 성능 요구 사항 등에 따라 달라질 수 있다.

예를 들어, 컴포넌트 기반 개발을 선호하고 동적 스타일링이 많이 필요한 프로젝트에서는 CSS-in-JS를 사용할 수 있고,

전통적인 웹사이트나 성능 최적화가 중요한 프로젝트에서는 CSS-in-CSS 방식을 선택할 수 있다.

 


 

Q. CSS-in-HTML에서의 단점으로 설명한 'html 태그와 style 코드가 혼재되어 유지보수가 어려우며' 이 문장과

CSS-in-JS에서의 장점으로 설명한 '스타일과 로직이 한 곳에 있어 관리가 용이'이 문장이 충돌하는 것 같은데, 왜 서로 장단점으로 반대되는 걸까?

 

A. 두 방식 모두 스타일과 로직이 한 파일에 존재하지만, 그 구조와 목적에 따라 유지보수성에 미치는 영향이 다르기 때문이다.

CSS-in-HTML가 HTML 파일 내에 <style> 태그를 사용하여 페이지 단위의 스타일을 정의하는 반면 CSS-in-JS는 컴포넌트 단위로 적용하는 방식이다. 

구조의 크기가 다르기때문에 CSS-in-JS 방식은 컴포넌트 기반 개발에서 로직과 스타일을 한 곳에서 관리함으로써 모듈화와 재사용성을 높여 유지보수성을 향상시키는 반면, CSS-in-HTML 방식은 구조가 페이지 단위이기때문에 혼재되어 있는 부분이 유지보수에 불리하다고 평가되기 때문이다. (CSS-in-HTML 방식을 사용할때 모듈 단위로 분리한다면 무조건 단점이라할 수 없을 것이다.)

 

 

 

CSS-in-JS VS CSS-in-CSS

 

CSS-in-CSS

.css 또는 .scss 파일에 스타일을 작성하고, HTML이나 컴포넌트에서 className을 통해 적용하는 방식.

웹 컴포넌트에서는 Shadow DOM을 활용해 스타일을 캡슐화할 수도 있음.

<template id="my-button-template"> 
	<style> 
    	.button { 
        	background-color: blue; 
            color: white; 
            padding: 10px; 
            border-radius: 5px; 
        } 
    </style> 
	<button class="button">클릭</button> 
</template> 

<script> 
	class MyButton extends HTMLElement { 
    	constructor() { 
        	super();
            
            const shadow = this.attachShadow({ mode: "open" }); 
            const template = document.getElementById("my-button-template"); 
            
            shadow.appendChild(template.content.cloneNode(true)); 
        } 
    } 
    
    customElements.define("my-button", MyButton); 
</script> 

<my-button></my-button>

 

웹 컴포넌트에서는 Shadow DOM을 활용하면 스타일이 캡슐화되어 외부 스타일 영향을 받지 않는다.

 

장점으로는

스타일 시트가 브라우저에 캐싱될 수 있으므로 캐싱 및 성능 최적화가 가능하다.

파일로 분리되어있어 유지보수에 좋으며 Shadow DOM 활용 시 스타일 캡슐화가 가능하다.

 

단점으로는

전역 네임스페이스로 CSS 클래스 충돌 가능성이 있으며 props 기반의 동적 스타일링이 어렵다. 

컴포넌트 단위로 코드 분리가 어려울 수 있다.

 

 

CSS-in-JS

CSS를 JS 코드 내부에서 관리하는 방식으로, React, Vue, Svelte 등의 프레임워크에서 주로 사용된다.

컴포넌트 내에서 styled-components, emotion, stitches 같은 라이브러리를 활용하여 스타일을 작성한다.

 

styled-components (React)

import styled from "styled-components"; 

const Button = styled.button` 
	background-color: blue; 
	color: white; 
	padding: 10px; 
	border-radius: 5px; 
`; 

function App() { 
	return <Button>클릭</Button>; 
}

 

 

CSS-in-JS in Web Components

class MyButton extends HTMLElement { 
	constructor() { 
    	super(); 
        
        const shadow = this.attachShadow({ mode: "open" }); 
        const style = document.createElement("style"); 
        
        style.textContent = ` 
        	.button { 
            	background-color: blue; 
                color: white; 
                padding: 10px; 
                border-radius: 5px; 
            } 
        `; 
        
        const button = document.createElement("button"); 
        button.className = "button"; 
        button.textContent = "클릭"; 
        
        shadow.appendChild(style); 
        shadow.appendChild(button); 
    } 
} 

customElements.define("my-button", MyButton);

 

CSS-in-JS 방식도 Shadow DOM을 활용해 스타일 격리가 가능하다.

 

장점으로는

컴포넌트 기반이라 스타일과 로직이 한 곳에 있으므로 관리가 용이하다.

props 기반의 동적 스타일 적용이 쉽다.

자동으로 고유한 클래스 선언(해시 값)이 되므로 클래스 네이밍 충돌 방지가 된다.

컴포넌트 내에서 스타일 선언 가능하므로 CSS 모듈화가 쉽다.

 

단점으로는

styled-components같은 라이브러리는 실행 시 CSS를 생성하게되므로 런타임 성능 저하 가능성이 있다.

스타일 파일 캐싱이 어렵고 성능 최적화가 부족할 수 있다.

JS 파일에 CSS가 포함되어있어 JS 번들 크기가 증가한다.

 

비교

방식 스타일 생성 시점 적용 방싱
CSS-in-CSS (전통적인 방식) 빌드 타임(Build-time) 미리 컴파일된 .css 파일을 브라우저가 로드
CSS-in-JS (styled-components) 런타임(Run-time) JS 코드가 실행될 때 동적으로 <style> 태그를 생성하고 삽입

 

비교 항목 CSS-in-CSS (전통적인 방식)CSS-in-JS (JS 내부 스타일 관리)
스타일 관리 방식 .css 파일로 분리 JS 코드 내부에서 스타일 선언
전역 스타일 전역적으로 사용 가능 (단, 클래스 충돌 가능) 스타일이 자동으로 격리됨 (컴포넌트 기반)
동적 스타일링 JS에서 직접 스타일 변경이 어려움 props 기반으로 동적으로 변경 가능
성능 CSS 파일이 브라우저에 캐싱되어 성능 우수 JS에서 CSS를 생성하므로 런타임 성능 저하 가능
유지보수 전통적인 방식으로 익숙함, 그러나 코드 분리가 어려울 수 있음 컴포넌트 기반으로 스타일과 로직이 함께 관리 가능
클래스 충돌 문제 네이밍을 신경 써야 함 자동으로 고유한 클래스를 생성하여 충돌 방지
사용 예시 일반 웹사이트, CSS 모듈, Shadow DOM 기반 웹 컴포넌트 React, Vue, Svelte 등의 컴포넌트 기반 프레임워크

 

 


 

 

개념알아보고 비교까지 하니까 드디어 내가 원하는 주제가 나왔다.

'런타임 성능 저하 가능성'

CSS-in-CSS로 Styled-components를 사용하다가 tailwindcss로 전환하게 된 주제다.

styled-components를 통해 CSS-in-JS의 문제를 살펴보자.

 

 

 

CSS-in-JS 런타임 성능 저하 문제

styled-components 같은 CSS-in-JS 라이브러리는 실행 시JS 런타임에서 CSS를 동적으로 생성한다.

이로 인해 스타일이 적용되는 과정에서 추가적인 연산과 DOM 업데이트가 발생할 수 있어 성능 저하 가능성이 있다.

 

이곳 블로그에서 퍼포먼스 비교를 해둔 내용을 간단히 말하면,

styled-components와 css 모듈 전환했을때 첫번째 페이지 전환 속도가 358ms -> 90.5ms로 최적화 된 것을 확인할 수 있다.  

 

적용 방식 차이

스타일링 방식스타일 특징
CSS-in-CSS (전통적인 방식) 빌드 타임(Build-time) CSS 파일이 미리 생성되어 브라우저 캐싱 가능
CSS-in-JS (styled-components) 런타임(Run-time) JS 코드 실행 시 CSS를 생성하여 적용

CSS-in-CSS는 브라우저가 HTML을 로드할 때 CSS 파일을 바로 적용 가능하며,
CSS-in-JS는 JS 코드 실행 중에 CSS를 생성해서 추가적인 연산이 발생한다.

 

 

 

styled-components가 런타임에서 CSS를 생성하는 과정

CSS-in-JS는 런타임에서 css 파일을 생성하기 때문에, styled-components는 컴포넌트가 렌더링될 때마다 동적으로 클래스를 생성하고 스타일을 적용하게된다.

즉, 컴포넌트가 처음 렌더링될 때마다 CSS를 생성하는 비용이 발생하게 된다는 것....

import styled from "styled-components"; 

const Button = styled.button` 
	background-color: blue; 
	color: white; 
	padding: 10px; 
	border-radius: 5px; 
`; 

function App() { 
	return <Button>클릭</Button>; 
}

 

JS에서 실행될 때,

내부적으로 아래와같은 CSS가 생성되고, 이 CSS는 styled-components 내부에서 동적으로 생성되어 <style> 태그에 삽입된다.

.sc-bcXHqe { 
	background-color: blue; 
	color: white; 
	padding: 10px; 
	border-radius: 5px; 
}

 

 

렌더링 성늘 저하를 유발하는 이유

위에서 계속 언급했지만 런타임일때 수행되어서이다.

어떤 작업이 수행되길래 그런지 알아보자.

  1. 컴포넌트가 렌더링될 때, styled-components는 새로운 클래스를 생성(ex: sc-xxxx) 하게 된다.
  2.  동적으로 <style> 태그를 생성하고, 해당 스타일을 추가한다.
  3. CSS가 적용되면서 브라우저가 다시 스타일을 계산(recalculate styles)하고 레이아웃을 업데이트하게 된다. (리플로우..)
  4. 만약 props에 따라 동적 스타일이 많다면, 추가적인 계산이 계속 발생함

즉, 컴포넌트가 많거나 동적 스타일(props를 통한 동적 스타일링..)이 많으면 매번 새로운 스타일이 생성되므로, JS 연산이 증가하면서 성능이 저하될 수 있다. (대규모 프로젝트..)

예를 들어 페이지의 버튼 컴포넌트 1000개가 styled-components로 작업되었다면, 렌더링될때 1000개의 스타일이 동적으로 생성되게 되는데... 이때 JS 스타일 계산하고 적용하는 비용이 커지므로 렌더링이 느려질 수 있다.

 

 

 

styled-compomnent 서버 사이드 렌더링(SSR)에서 성능 저하

기본적으로 styled-components는 클라이언트에서 스타일을 생성하게되므로 서버에서 HTML을 생성할때 스타일이 포함되지 않는다.

이 때문에 초기 로딩 시 스타일이 적용되지 않은채 화면이 깜빡이는 문제가 발생할 수 있다. 
styled-compomnent ServerStyleSheet를 통해 해결할 수 있으나 서버에서도 CSS를 생성하도록 별도의 설정을 해줘야하며, 기본 SSR보다 성능이 떨어질 수 있다.

 

 

 styled-components 성능 최적화 방법

1. attrs()를 사용하여 불필요한 리렌더링 방지

스타일을 style 속성으로 처리하면 새 스타일을 매번 생성하지 않는다.

const Button = styled.button.attrs((props) => ({ 
	style: { 
    	backgroundColor: props.primary ? "blue" : "gray" 
    }, 
}))` 
	color: white; 
    padding: 10px; 
`;

 

 

2. useMemo를 사용해 스타일 캐싱

스타일이 캐싱되어 불필요한 재생성이 줄어든다.

const Button = useMemo( () => styled.button` 
	background-color: blue; 
    color: white; 
    padding: 10px; 
`, [] );

 

 

3.  shouldForwardProp을 활용하여 불필요한 props 전달 방지

props가 불필요하게 DOM에 전달되는 문제를 방지하여 성능 최적화 가능하다.

import styled from "styled-components"; 
import { shouldForwardProp } from "@styled-system/should-forward-prop"; 

const Button = styled("button").withConfig({ 
	shouldForwardProp: (prop) => prop !== "primary", 
})` 
	background-color: ${(props) => (props.primary ? "blue" : "gray")}; 
`;

 

 

결론

원인 설명 해결방벙
컴포넌트가 많을 때 동적으로 많은 스타일을 생성하면 성능 저하 캐싱 (useMemo) 활용
SSR에서 스타일 적용 문제 서버에서 CSS가 생성되지 않아 깜빡임 발생 ServerStyleSheet 적용
스타일 변경이 자주 일어날 때 props가 변경될 때마다 새로운 스타일 생성 attrs(), className 활용
불필요한 props 전달 styled-components가 불필요한 props까지 전달 shouldForwardProp 활용

 

 

대규모 프로젝트에서는 CSS-in-CSS(CSS Modules, Tailwind CSS)를 함께 고려하는 것도 좋은 방법이다.

 

 


 

Q. 성능 최적화를 진행 시 css-in-css보다 성능이 어때?

A. 최적화를 진행하면 styled-components의 성능이 개선될 수 있지만, 여전히 CSS-in-CSS(정적 CSS)보다 성능이 떨어지는 경우가 많은데, 그 이유는 런타임에서 스타일을 생성하는 styled-components와, 미리 빌드된 스타일을 사용하는 CSS-in-CSS의 구조적 차이 때문이다.

 

비교

기준 CSS-in-CSS (정적 CSS) CSS-in-JS (styled-components) (최적화 적용 후)
렌더링 속도 (첫 번째 로드) 빠름 (미리 빌드된 CSS가 브라우저 캐싱됨) 느림 (JS가 실행되면서 CSS 생성)
반응 속도 (스타일 변경) 비효율적 (classList 조작 필요) 빠름 (props 기반 동적 스타일 가능)
CSS 캐싱 브라우저 캐싱 가능 JS에서 생성된 스타일은 캐싱 불가능
서버 사이드 렌더링(SSR) 바로 스타일 적용됨 초기 깜빡임(FoUC) 발생 가능
메모리 사용량 적음 (스타일이 정적이므로 추가 JS 연산 없음) 많음 (컴포넌트가 많을수록 메모리 사용 증가)
스타일 재사용성 클래스를 활용한 재사용 가능 컴포넌트별 고립된 스타일 적용됨 (재사용 어렵거나 중복 발생 가능)
빌드 크기 CSS는 JS 번들에서 분리됨 CSS가 JS에 포함되므로 번들 크기 증가

 

🟢 CSS-in-CSS (Tailwind, CSS Modules)

  • First Contentful Paint (FCP): 1.2s
  • Largest Contentful Paint (LCP): 1.8s
  • Total Blocking Time (TBT): 30ms
  • Time to Interactive (TTI): 1.5s

🔴 styled-components 사용 시 (최적화 적용 후)

  • First Contentful Paint (FCP): 1.8s (~0.6s 증가)
  • Largest Contentful Paint (LCP): 2.5s (~0.7s 증가)
  • Total Blocking Time (TBT): 80ms (~2.5배 증가)
  • Time to Interactive (TTI): 2.1s (~0.6s 증가)

CSS-in-CSS 방식이 초기 로딩 속도가 더 빠르고, 브라우저 최적화에도 유리한 결과를 보이며, styled-components는 최적화해도 초기 렌더링이 느리고, JS 실행 시간이 늘어나는 단점이 있다.

성능을 최적화 해도 styled-components의 런타임 CSS 생성 방식(구조) 자체가( CSS-in-CSS 수준까지 도달하기에는 어렵다.

 

 

 

반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
글 보관함