tailwindcss의 사용법을 어느정도 익힌 후, 새로운 스타일링 라이브러리를 찾아보던 중 Vanilla Extract라는 것을 알게 되었어요.
Vanilla Extract: Zero Runtime Stylesheets-in-TypeScript
Vanilla Extract는 공식 Github에서 "Zero Runtime Stylesheet-in-typescript" 라고 소개합니다. 기본적으로 TypeScript 파일에서 스타일을 지정할 수 있는 CSS-in-JS 방식이지만, styled-components나 Emotion과는 다르게 런타임에 스타일을 생성하지 않고, Sass나 Less처럼 빌드 시에 스타일을 생성한다고 합니다.
CSS-in-JS의 장점과 단점
styled-components와 emotion은 스타일을 Javascript에서 작성하기 때문에 동적 스타일링이 편리합니다. 둘 다 스타일을 변수로 활용할 수 있어 유지보수 측면에서 유리하지만, Runtime 시에 코드를 CSS로 파싱하는 과정에서 성능 저하 문제가 발생할 수 있어요.
TailwindCSS는 유틸리티 클래스를 활용하여 빌드 시에 스타일링을 적용함으로써 zero-runtime을 보장합니다. 이는 성능적으로 뛰어난 장점이 있지만, 기존의 CSS-in-JS 패턴에 익숙한 사용자에게는 다소 낯설고, 가독성 및 재사용성이 낮을 수 있습니다.
Vanilla Extract는 이러한 CSS-in-JS와 TailwindCSS의 장, 단점을 적절히 조합한 스타일링 라이브러리라고 생각됩니다. 기존의 CSS-in-JS 패턴으로 개발할 수 있어 동적 스타일링 및 재사용성과 가독성 측면에서 우수하고, TailwindCSS처럼 Zero-Runtime을 보장합니다. 또한 Typescript를 강력하게 지원하기 때문에 정적 타입 체크가 가능하며, 스타일링 작성시 자동 완성과 타입 오류 감지 기능을 제공하는 강력한 스타일링 솔루션인 것 같습니다.
그래서 저는 혼자 작은 사이드 프로젝트를 진행하면서 이 라이브러리를 적용해 보기로 결심했습니다!
설치 및 기본 설정
1. vanilla-extract 설치
npm install @vanilla-extract/css
위와 같이 vanilla-extract를 설치해준 후, 빌드 환경에 맞게 plugin을 설치해 주고 세팅해 주어야 합니다.
저는 vite로 React+Typescript로 프로젝트를 생성했기 때문에 아래와 같이 plugin을 설치 및 세팅합니다.
2. 빌드 환경에 맞게 plugin 설치
npm install @vanilla-extract/vite-plugin
3. Plugin 추가
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
vanillaExtractPlugin({ // vanilla-extract plugin 추가
identifiers: 'debug',
}),
],
});
Vanilla Extract는 TailwindCSS와 다르게 기본적으로 Reset CSS가 적용이 안되어있습니다. 따라서 Reset CSS를 적용하고 싶으시면 따로 적용을 해줘야 합니다.
관련해서 이것 저것 찾아보다가 발견한 어떤 일본인 개발자 분의 Reset css 적용을 따라해 보겠습니다.
Reset CSS for vanilla-extract Gist
Reset CSS for vanilla-extract.
Reset CSS for vanilla-extract. GitHub Gist: instantly share code, notes, and snippets.
gist.github.com
Reset CSS 적용 단계
1. layers.css.ts 생성
// layers.css.ts
import { layer } from "@vanilla-extract/css";
export const reset = layer("reset");
export const components = layer("components");
layer 함수를 이용해서 reset 레이어와 components 레이어를 생성합니다. 이 함수는 CSS 네임스페이스 관리와 스타일 우선순위 제어를 효과적으로 하기 위해 사용합니다. 각 레이어는 특정 스타일 그룹을 독립적으로 관리할 수 있게 해주고, 충돌을 방지하며, 최종 CSS 출력 시 스타일을 최소한의 중복으로 병합할 수 있는 기능을 제공합니다.
* layer 함수는 @layer 선택자를 지원하는 브라우저에서만 사용할 수 있어요. 아래 지원 브라우저를 확인해 보시고 적용하세요 ㅎㅎ
https://caniuse.com/css-cascade-layers
2. reset.css.ts 생성
// reset.css.ts
import { globalStyle } from '@vanilla-extract/css';
import * as layers from './layers.css.ts';
/**
* 'display' 속성만 제외한 모든 "User-Agent-StyleSheet" 스타일을 제거합니다.
* - "symbol *" 부분은 Firefox에서 발생하는 SVG 스프라이트 버그를 해결하기 위한 것입니다.
* - "html" 요소는 제외되며, 그렇지 않으면 Chrome에서 CSS 하이픈(hyphens) 속성이 망가지는 버그가 발생합니다.
* (관련 문제: https://github.com/elad2412/the-new-css-reset/issues/36).
*
* Remove all the styles of the "User-Agent-Stylesheet", except for the
* 'display' property
*
* - The "symbol *" part is to solve Firefox SVG sprite bug
* - The "html" element is excluded, otherwise a bug in Chrome breaks the CSS
* hyphens property (https://github.com/elad2412/the-new-css-reset/issues/36)
*/
globalStyle(
'*:where(:not(html, iframe, canvas, img, svg, video, audio):not(svg *, symbol *))',
{
'@layer': {
[layers.reset]: {
all: 'unset',
display: 'revert',
},
},
}
);
/**
* 기본 box-sizing border-box로 설정
* Preferred box-sizing value
*/
globalStyle('*, *::before, *::after', {
'@layer': {
[layers.reset]: {
boxSizing: 'border-box',
},
},
});
/**
* 모바일 사파리에서 가로 모드로 전환할 때 글꼴 크기가 자동으로 커지는 현상을 방지
* Fix mobile Safari increase font-size on landscape mode
*/
globalStyle('html', {
'@layer': {
[layers.reset]: {
MozTextSizeAdjust: 'none',
WebkitTextSizeAdjust: 'none',
textSizeAdjust: 'none',
},
},
});
/**
* a 태그와 button 태그에 pointer 재적용
* Reapply the pointer cursor for anchor tags
*/
globalStyle('a, button', {
'@layer': {
[layers.reset]: {
cursor: 'pointer',
},
},
});
/**
* 리스트 스타일 제거 (불릿/넘버)
* Remove list styles (bullets/numbers)
*/
globalStyle('ol, ul, menu, summary', {
'@layer': {
[layers.reset]: {
listStyle: 'none',
},
},
});
/**
* 이미지 요소가 컨테이너의 크기를 넘지 않도록 설정
* For images to not be able to exceed their container
*/
globalStyle('img', {
'@layer': {
[layers.reset]: {
maxInlineSize: '100%',
maxBlockSize: '100%',
},
},
});
/**
* 테이블 셀 사이의 기본 간격을 제거
* Removes spacing between cells in tables
*/
globalStyle('table', {
'@layer': {
[layers.reset]: {
borderCollapse: 'collapse',
},
},
});
/**
* 사파리 브라우저에서 user-select:none을 적용할 때 발생할 수 있는 문제를 방지하고, 텍스트 입력 요소가 정상적으로 동작
* Safari - solving issue when using user-select:none on the <body> text input
* doesn't working
*/
globalStyle('input, textarea', {
'@layer': {
[layers.reset]: {
WebkitUserSelect: 'auto',
},
},
});
/**
* 사파리 브라우저에서 textarea 요소의 white-space 속성을 기본값으로 되돌리기 위해 사용됩니다.
* Revert the 'white-space' property for textarea elements on Safari
*/
globalStyle('textarea', {
'@layer': {
[layers.reset]: {
whiteSpace: 'revert',
},
},
});
/**
* meter 태그 사용을 위한 최소한의 스타일 설정
* Minimum style to allow to style meter element
*/
globalStyle('meter', {
'@layer': {
[layers.reset]: {
WebkitAppearance: 'revert',
appearance: 'revert',
},
},
});
/**
* pre 태그의 브라우저 기본 스타일을 복원, box-sizing border-box 설정
* Preformatted text - use only for this feature
*/
globalStyle(':where(pre)', {
'@layer': {
[layers.reset]: {
all: 'revert',
boxSizing: 'border-box',
},
},
});
/**
* input의 placeholder의 컬러를 지정하지 않음
* Reset default text opacity of input placeholder
*/
globalStyle('::placeholder', {
'@layer': {
[layers.reset]: {
color: 'unset',
},
},
});
/**
* hidden 속성을 가진 요소의 display none을 적용
* Fix the feature of 'hidden' attribute. display:revert; revert to element
* instead of attribute
*/
globalStyle(':where([hidden])', {
'@layer': {
[layers.reset]: {
display: 'none',
},
},
});
/**
* contenteditable 요소의 편집 기능이 제대로 동작하도록 설정
* Revert for bug in Chromium browsers
*
* - Fix for the content editable attribute will work properly.
* - Webkit-user-select: auto; added for Safari in case of using user-select:none
* on wrapper element
*/
globalStyle(':where([contenteditable]:not([contenteditable="false"]))', {
// @ts-expect-error: -webkit-line-break is a non-standard property
'@layer': {
[layers.reset]: {
MozUserModify: 'read-write',
WebkitUserModify: 'read-write',
overflowWrap: 'break-word',
WebkitLineBreak: 'after-white-space',
WebkitUserSelect: 'auto',
},
},
});
/**
* draggable 속성이 있는 요소에서 드래그 기능이 제대로 작동하도록 설정
* Apply back the draggable feature - exist only in Chromium and Safari
*/
globalStyle(':where([draggable="true"])', {
'@layer': {
[layers.reset]: {
// @ts-expect-error: -webkit-user-drag is a non-standard property
WebkitUserDrag: 'element',
},
},
});
/**
* modal의 기본 동작 복원
* Revert Modal native behavior
*/
globalStyle(':where(dialog:modal)', {
'@layer': {
[layers.reset]: {
all: 'revert',
boxSizing: 'border-box',
},
},
});
reset.css.ts 파일을 생성합니다.
Vanilla Extract의 globalStyle 함수를 사용하여 reset css를 적용합니다. globalStyle 함수는 html 및 body와 같은 HTML 문서의 전역 선택자에 스타일을 적용할 수 있게 합니다. 이렇게 하면 모든 요소에 일관된 스타일을 적용할 수 있습니다.
그리고 globalStyle 함수 내부에 @layer 선택자를 사용하여 reset Layer에 스타일링을 적용합니다. 이로써 reset css 설정을 우선 적용할 수 있습니다.
3. global.css.ts 생성
// global.css.ts
import "./layers.css";
import "./reset.css";
global.css.ts를 생성하여 layers.css.ts와 reset.css.ts를 import합니다.
4. global.css.ts 적용
// App.tsx
import './styles/global.css';
function App() {
return <>This is App.tsx</>;
}
export default App;
global.css.ts 파일을 main.tsx나 App.tsx에 import합니다.
이렇게 해서 Vanilla Extract를 이용한 Reset CSS 적용이 완료되었습니다.
'WEB > ETC' 카테고리의 다른 글
Core Web Vitals (웹 핵심 성능 지표) (0) | 2024.10.05 |
---|---|
(후기 및 회고) 코드잇 스프린트 프론트엔드 부트캠프 6기 (7) | 2024.09.08 |
REST, RESTful API (0) | 2024.02.01 |