styled-components를 처음 사용해본 것은 바야흐로 2021년...
대학생 스타트업 팀의 팀원으로 합류해 프론트엔드 개발을 처음 시작했을 때였다.
그 때의 나는 정말 아무것도 모르는 상태였다.
난 개발계의 도라에몽같았던 학교 선배가 시키는대로 따라갔고,
그 선배가 택했던 CSS 라이브러리인 styled-components를 사용하게 되었다.
그 이후로는 그냥 자연스럽게 계속 styled-components를 사용했다.
내가 그 라이브러리에 너무 익숙한 탓도 있었고, 프로젝트나 인턴을 할 때도 대부분의 사람들이 그 라이브러리를 사용하는 것에 동의했다.
시간이 어느정도 지난 후에는 styled-components 대신 Emotion으로 변경하긴 했지만, 사실 둘은 사용법 측면에서 크게 다르지는 않았으니까.
중간에 잠깐씩 타의로 css module이나 SCSS를 사용했던 적이 있기도 했다.
근데 내가 styled-components에 너무 절여져서 그랬던건지, 그 라이브러리들이 상당히 마음에 들지 않았다.
'styled-components에서는 이랬는데, 여기서는 왜 이러지?' 같은 생각이 들었다.
그냥 요약하자면,
딱히 이유없이 styled-components와 Emotion을 쓰게 됐고,
다른 CSS 라이브러리에 거부감이 느껴졌으며,
관성적으로 styled-components쪽으로 돌아오려고 했다는 것.
현재 나는 넥스터즈 24기에 프론트엔드 개발자로 참여하고 있고
이번 프로젝트에서는 vanilla-extract를 사용하기로 했다.
지금부터 내가 styled-components와 Emotion에서 벗어나 vanilla-extract를 선택하게 된 이유를 설명해볼까 한다.
팀원분과 이번 프로젝트의 기술스택을 정하며, 어떤 CSS 라이브러리를 쓸 지에 관한 이야기를 나누었다.
후보는 tailwind, Emotion, vanilla-extract였다.
내가 개인적으로 tailwind 스타일에 극강의 불호를 느끼고 있어서 tailwind는 후보에서 제외하게 되었다..
vanilla-extract는 팀원분께서 제안해주신 라이브러리였는데, zero-runtime에다가 요즘 기업들이 많이 사용하고 있다고 강력 추천하셨다.
그래서 vanilla-extract와 Emotion을 비교해보게 되었다.
vanilla-extract와 Emotion의 가장 큰 차이점인 것 같다.
vanilla-extract는 zero-runtime CSS이고, Emotion은 runtime CSS이다.
먼저 runtime CSS에서는 Javascript 런타임 중에 CSS를 생성한다.
즉 Javascript 코드에서 스타일을 동적으로 생성하고 적용한다.
zero-runtime CSS에서는 빌드 시점에 CSS를 생성한다.
그래서 런타임에는 추가적인 Javascript 실행 없이 이미 생성된 CSS를 사용하게 된다.
원리는 아래와 같다.
1. 스타일 정의
2. 빌드 시점에 스타일 추출
3. 스타일 사용
즉 스타일 시트를 별도의 CSS 파일로 추출해서, 브라우저가 페이지를 렌더링할 때 CSS를 즉시 사용할 수 있는 것이다.
이 경우 런타임에 추가적인 스타일 계산이나 적용이 없기 때문에 런타임 성능이 개선된다.
위와 같은 이유로 런타임 성능 면에서는 vanilla-extract가 훨씬 우수하다.
Emotion이 아닌 다른 라이브러리에 궁금했던 것 중 하나는, CSS props를 이용한 동적 스타일링 가능 여부였다.
안타깝게도 vanilla-extract는 zero-runtime CSS이기에 CSS props를 이용한 동적 스타일링이 불가능하다.
CSS props 방식은 런타임에 Javascript를 사용하여 스타일을 계산하고 적용하는 방식이다.
하지만 zero-runtime CSS의 경우 빌드 시점에 모든 CSS를 생성하기 때문에 위와 같은 방법을 사용할 수 없다.
대신 zero-runtime CSS에서는 CSS 변수를 사용하여 동적 스타일링을 처리할 수 있다.
근데 찾아보니 코드가 뭔가 좀 구렸다...
쩝...
다행히 vanilla-extract에서는 Sprinkles, Recipes같은 기능들을 제공해줘서 조금 더 깔쌈하게 코드를 작성할 수 있다.
Sprinkles는 vanilla-extract를 위한 zero-runtime atomic CSS 프레임워크이다.
공식문서의 설명에 따르면 Sprinkles는
내 번역의 한계로.. 뭔가 좀 알아듣기 어려운 설명이 되었는데..
아무튼 이걸 사용하면 런타임에 동적으로 스타일을 적용할 수 있다.
물론 미리 정의된 스타일 집합의 조합을 런타임에 변경할 수 있는 것이지, 런타임에 새롭게 스타일을 정의하거나 새로운 스타일을 적용할 수는 없다.
예를 들면 아래와 같다.
import React from 'react';
import { defineProperties } from '@vanilla-extract/sprinkles';
const colors = {
red: '#f44336',
green: '#4caf50',
blue: '#2196f3',
};
const properties = defineProperties({
properties: {
color: colors,
},
});
const sprinkles = properties;
function MyButton() {
return <button className={sprinkles({color: 'blue'})}>Click Me!</button>
}
여기서는 간단하게 예시를 작성했지만, 실제로 defineProperties 내에서 properties 말고도 conditions, defaultCondition, shorthands 등의 속성을 사용하여 더 복잡한 스타일링을 할 수 있다.
또 Recipes라는 것이 있는데,
multi-variant styles를 만드는 것을 도와준다고 한다.
import { createRecipe, style } from '@vanilla-extract/recipes';
const buttonVariants = createRecipe({
base: style({ // base는 기본적인 스타일을 정의
padding: '10px',
border: 'none',
borderRadius: '4px',
fontSize: '1rem',
}),
variants: { // variants는 다양한 style variants를 정의
color: {
primary: style({ backgroundColor: 'blue', color: 'white' }),
secondary: style({ backgroundColor: 'grey', color: 'white' }),
},
},
});
export function MyButton() {
return <button className={buttonVariants({ color: 'primary' })}>Click Me!</button>
}
이런 느낌으로 사용할 수 있다.
vanilla-extract에는 Sprinkles와 Recipes 말고도 Dynamic같은 기능들을 사용해 깔끔한 스타일링을 할 수 있다.
이 글은 vanilla-extract 기능을 설명하는 글은 아니니 이정도에서 넘어가보자.
사실 빌드 시점에 CSS가 생성되는 만큼, CSS 변수를 쓴다고 해도 동적 스타일링에 한계가 있다는 점은 조금 아쉽다.
그래도 vanilla-extract의 여러 기능들을 사용한다면 충분히 보완 가능하다는 생각이 든다.
SSR을 사용하는 환경에서는 Emotion 설정이 더 간단하고, 초기 렌더링 성능이 좋다.
Emotion은 동적으로 CSS를 생성하고, 이를 HTML에 인라인으로 삽입한다.
그래서 클라이언트가 HTML을 처음 받았을 때 이미 필요한 CSS를 모두 가지고 있게 된다.
이로 인해 초기 렌더링 시 깜박임이 발생하지 않는다.
하지만 vanilla-extract는 CSS 파일을 빌드 시점에 생성하기 때문에, SSR 환경에서는 추가적인 설정이 필요하다.
프로젝트에 SSR을 사용한다면 Emotion이 더 적합할 수 있겠지만, 이번에 우리는 CSR을 사용할 예정이다.
Next.js를 사용할 지에 대한 의논을 해보았지만 굳이 쓸 이유가 없다는 결론이 나와서 typescript + React만 사용하기로 했다.
그래서 이 부분에서는 Emotion의 이점이 없다고 판단했다.
새로운 CSS 라이브러리에서 CSS props 외에 궁금했던 점은 스타일 재사용성이었다.
스타일 중복을 최대한 줄일 수 있는 코드를 작성할 수 있는지가 궁금했다.
vanilla-extract에서는 스타일 객체를 배열 형태로 제공한다.
그래서 아래와 같이 중복된 스타일코드를 줄일 수 있다.
import { style } from '@vanilla-extract/css';
const base = style({
padding: '10px',
border: 'none',
borderRadius: '4px',
fontSize: '1rem',
});
const primaryButton = style([base, { backgroundColor: 'blue', color: 'white' }]);
const secondaryButton = style([base, { backgroundColor: 'grey', color: 'white' }]);
스타일 재사용성 부분에서도 Emotion과 비슷하게 사용할 수 있을 것 같다.
vanilla-extract는 Typescript로 작성되었다.
그래서 스타일과 테마 변수에 대한 타입 검사를 제공한다.
1. 스타일 작성 시 타입 검사, 2. 테마 변수에 대한 타입 검사, 3. 스타일 변형에 대한 타입 검사가 이루어진다.
이러한 타입 검사로 개발자가 잘못된 값을 사용하는 것을 막음으로써 버그를 방지할 수 있다.
Emotion은 Typescript와 함께 사용할 경우 1. 스타일 작성 시 타입 검사가 가능하여, vanilla-extract와 마찬가지로 CSS 속성과 값에 대한 타입 검사를 할 수 있다.
하지만 2. 테마 변수와 3. 스타일 변형에 대한 타입 검사의 경우, 개발자가 직접 테마 타입을 정의하고 관리해주어야 한다.
그래서 타입 안정성 측면에서는 vanilla-extract가 더 강력하다.
개발 과정을 안정화시키고 버그를 줄이기 위해서는 vanilla-extract가 더 좋아보인다.
솔직히 말하면 나는 Emotion이 더 편하긴 하다.
개발 속도나 효율을 따졌을 때는 새로운 vanilla-extract보다 익숙한 Emotion이 더 나을 것이다.
또 vanilla-extract보다는 styled-components와 Emotion 자료가 훨씬 더 많기도 하다.
하지만.
이제는 이유없이 익숙한 것만 고집하는 것에서 탈피하고싶다.
사이드 프로젝트인 만큼, 너무 효율만 따지기보다는 새로운 기술을 학습해보고 싶은 마음도 있다.
그래서 이렇게 이것저것 따져보고 vanilla-extract를 사용하기로 결정했다.
이번 프로젝트에서는 새로운 것들을 많이 접해볼 수 있을 것 같다.