원문: https://blog.maximeheckel.com/posts/the-power-of-composition-with-css-variables
많은 프런트엔드 개발자들이 CSS가 얼마나 강력한지 간과할 때가 있다. 필자 역시 CSS만으로도 특정 작업을 처리할 수 있다는 것을 떠올리기 전에 항상 다른 것을 먼저 고려하던 사람들 중 한 명이었다.
그리고 몇 주 전 블로그의 테마를 변경하는 동안, CSS 변수 합성의 강력함을 발견했다.
필자가 발견한 패턴은 HSLA 색상에 대한 CSS 일부 값을 변수에 할당하여 합성하는 방법이었다. 덕분에, 블로그에 적합한 테마와 색상 팔레트를 CSS만 사용하여 만들 수 있었다. ThemeProvider도 없으며, CSS-in-JS 도구나 이상한 편법도 없다. 오직 CSS만 있을 뿐이다. 또한 이것은 기존 React/CSS-in-JS 설정과 완벽하게 통합할 수 있었고 많은 스파게티 코드를 해결하는 데 도움이 됐다.
이러한 패턴에 대해 함께 살펴보고 CSS의 멋진 면을 살펴보자.✨
위에서 말한 것처럼, CSS 자체는, 그것이 순수 CSS이든 CSS-in-JS이든 간에, 필자에게는 우선순위가 아니었다. 항상 자바스크립트 측면에서 확장성이 좋은 코드를 만드는 데 노력했었다. 반면에 CSS는 무언가를 아름답게 만드는 것으로만 생각했다. 필자의 CSS 코드는 항상 엉망이었다! 중복된 스타일과 CSS 변수도 사용하지 않고 하드코딩된 색상값들이 널려있었다.
이러한 문제를 해결하기 위해 CSS-in-JS, ThemeProviders, 그리고 interpolation 함수에 매우 의존했다. 하지만 Kent C Dodds의 React Context 대신 CSS 변수을 사용하라를 읽고 나서 다시 생각해보기 시작했다. 자바스크립트 기반의 테마를 제거하고 CSS 변수에 값을 주입하기 위해 CSS 파일만 사용했다.
// interpolation 함수와 자바스크립트 기반 테마를 사용(이전 방식):
const MyStyledComponent = styled('div')`
background-color: ${(p) => p.theme.primaryColor};
color: ${(p) => p.theme.typefaceColor1};
`;
// 글로벌 CSS 파일에 정의된 CSS 변수를 사용(현재 방식):
const MyStyledComponent = styled('div')`
background-color: var(--primaryColor);
color: var(--typefaceColor1);
`;
Josh Comeau는 또한 styled-component의 행복한 길에서 이것에 대한 글을 작성하였다.
정말 좋은 글이니 추천한다.
필자는 HEX와 RGBA 양쪽 모두 수많은 색을 정의하였다...그리고 이는 아주 엉망진창이었다.
--main-blue: #2663f2;
--secondary-blue: #0d4ad9; /* main blue도다 10% 어두운 색상 */
--main-blue-transparent: rgba(
38,
99,
242,
0.1
); /* main blue도다 10% 불투명한 색상 */
사용할 색상을 HEX 형식으로 정의하였고, 이 색상에 약간의 불투명성을 더해야 한다면 RGBA로 재정의해야 한다. 그리고 코드를 읽으면서 서로 유사한 두 가지 색상을 두 가지 다른 형식으로 작성하게 되었음을 알게 되었다.😱
모든 색상을 RGBA로 변경할 수 있었지만, 다른 문제가 있었다. 모든 색상에 맞는 색상 팔레트를 만들고 싶었지만, RGBA에서 파란색상의 10가지 음영을 관리하는 것은 어려운 일이었다. 각각의 파란색 음영은 RGBA로 작성되었을 때 서로 완전히 다른 색상처럼 보였다.
--main-blue: rgba(38, 99, 242, 1);
--secondary-blue: rgba(13, 74, 217, 1); /* main blue but 10% darker */
/*
모든 것을 동일한 형식으로 작성하였지만 --secondary-blue의 값과
--main-blue의 값은 서로 완전히 다른 것처럼 보인다.
*/
--main-blue-transparent: rgba(
38,
99,
242,
0.1
); /* main blue보다 10% 불투명한 색상 */
디자이너 친구들이 이 문제를 듣고 팁을 주었다. 그 팁은 이 글에서 소개할 패턴을 가능하게 하는 첫 번째 방법이었다: HSLA를 사용하라.
HSLA는 색상을 정의하는 데 필요한 네 가지 주요 구성 요소인 Hue(색상) Saturation(채도) Lightness(명도) Alpha(투명도) 를 나타낸다. Hue는 360도의 색상 바퀴에 정의된 팔레트에서 시작된다. Saturation, Lightness 및 Alpha 속성은 각각 백분율(%)로 정의되며 다음을 나타낸다.
위의 코드 조각에서 보았던 파란색의 음영을 HSLA로 다시 작성다면, 이번에는 이러한 색상이 서로 "가까이" 있다는 것을 쉽게 알 수 있다.
--main-blue: hsla(222, 89%, 55%, 100%);
--secondary-blue: hsla(222, 89%, 45%, 100%); /* main blue보다 10% 어두운 색상 */
/*
여기서 --secondary-blue가 더 읽기 쉬운 것 볼 수 있다.
--main-blue와 동일한 색상과 채도를 나타내므로 쉽게 식별할 수 있다.
*/
--main-blue-transparent: hsla(
222,
89%,
55%,
10%
); /* main blue보다 10% 불투명한 색상 */
그러나 HSLA 형태로 전체 색상을 정의하면 유사한 색상들에 대해서는 동일한 색상과 채도를 중복하여 작성해야 한다. 만약 CSS 변수를 통해 색상과 채도의 일부만을 정의하고 그것을 재사용하여 다른 변수를 정의할 수 있다면 어떨까? 이것은 유효한 패턴이었다.
이전에 많은 개발자들이 CSS 변수를 통해 CSS 속성을 작성하였다. 하지만, 그들은 여기서 "완전한 값"이라고 부르는 것을 주어진 변수에 할당하였다.
--spacing-0: 4px;
--spacing-1: 8px;
.myclass {
padding: var(--spacing-0) var(--spacing-1); /* padding: 4px 8px; 과 동일*/
}
값의 일부를 합성할 수 있다는 생각은 이전에는 하지 못했다. 그리고 이 패턴을 우연히 발견했을 때 그것이 좋은 방법인지 궁금하였다. 트위터에서 다른 개발자들에게 이것에 대한 생각을 물어봤을 때, 많은 사람들이 이미 이 패턴을 사용하고 있음을 알게 되었다! [트위터 링크]
또한 이것이 Tailwind CSS의 핵심 패턴 중 하나라는 것을 깨달았다. Adam Wathan은 CSS 변수들을 합성하는 훌륭한 글을 썼는데 거기서 CSS 변수 합성의 훌륭한 사용 사례를 볼 수 있다!
위의 트위터 스레드에서 @martinthieman의 대답은 깨달음을 주었다: "이 [패턴]은 HSLA 색상을 사용할 때 강력해진다."
그의 말이 옳았다! 이제 어떻게 하면 HSLA 색상과 CSS 변수를 합성하여 색상 배율을 구축할 수 있는지 알아보겠다✨.
이 트윗에서 받은 답변 중 하나는 필자의 작업 결과물을 함께 공유하자는 것이었다🚀.
동료 디자이너들은 HSLA를 사용하여 다음과 같은 색상 계층을 만들었다.
이 방법을 통해 주어진 색상의 전체 계층을 얻은 다음 애플리케이션에서 사용할 수 있다.
CSS 변수의 일부 값을 합성하면 그 방법을 CSS에서도 동일하게 수행할 수 있다! 즉, 더 이상 모든 색상을 하드 코딩하지 않아도 된다. 기본 색상만 있으면 나머지 색상도 구할 수 있다.
--base-blue: 222, 89%
). 이 경우 222
, 89
는 일부 부분 값이 된다.--base-blue: 222, 89%; /* 동일한 색상과 채도를 가진 색상들 */
--palette-blue-10: hsla(var(--base-blue), 10%, 100%);
--palette-blue-20: hsla(var(--base-blue), 20%, 100%);
--palette-blue-30: hsla(var(--base-blue), 30%, 100%);
--palette-blue-40: hsla(var(--base-blue), 40%, 100%);
--palette-blue-50: hsla(var(--base-blue), 50%, 100%);
--palette-blue-60: hsla(var(--base-blue), 60%, 100%);
--palette-blue-70: hsla(var(--base-blue), 70%, 100%);
--palette-blue-80: hsla(var(--base-blue), 80%, 100%);
--palette-blue-90: hsla(var(--base-blue), 90%, 100%);
--palette-blue-100: hsla(var(--base-blue), 100%, 100%);
--base-blue
CSS 변수가 핵심이다. 이 값을 다른 CSS 변수의 부분 값으로 사용하였다. CSS 변수 자체는 완전한 CSS 값으로 사용할 수 없지만 합성을 통해 사용할 수 있다.
아래는 이 ✨놀라운✨ 패턴을 보여주는 작은 위젯이다. 하나의 CSS 변수를 조정하면 전체 색 배율을 생성할 수 있다!
이 기술을 통해 단순히 "멋져 보이는" 무작위의 색상을 선택하는 대신 이치에 맞고 이해할 수 있는 색상을 생성할 수 있었다.
모든 색상을 쉽게 정의할 수 있게 되었지만, 아직은 부족하다. 스타일링된 컴포넌트에 어떤 파란색을 사용해야 하는지 어떻게 알 수 있을까? 그리고 만약 내일 테마를 파란색에서 빨간색으로 바꾼다면?
이것은 2가지 CSS 변수 세트의 중요성을 강조한 것이다. 하나는 색상의 "의미"에 대한 것이고 다른 하나는 색상의 실제 값에 대한 것이다. 필자는 기본 버튼 구성 요소가 반드시 파란 배경색이 아니라 "기본" 배경색을 가지기를 원했다. 이 기본 색상이 파란색인지, 빨간색인지, 보라색인지는 컴포넌트 수준에서 중요하지 않다.
서로 다른 불투명도를 가진 색상 변수를 별도로 분리하였다.
카드의 배경색과 강조 표시된 텍스트가 기본 파란색과 같은 파란색 음영이지만 불투명도가 8%
가 되도록 만들고 싶었다. 그 배경색이 미적으로 보기 좋았고 다크 모드와 잘 어울렸기 때문이다.
그래서 필자는 --emphasis
라는 새로운 색을 정의하였다. --primary
색상 변수와 동일한 부분 값으로 구성되었다.
--base-blue: 222, 89%; /* 동일한 색상과 채도를 가진 색상들 */
/*
여기서 색상 팔레트를 선언했다.
각 색상은 다른 CSS 변수의 부분 값으로 사용된다.
*/
--palette-blue-10: var(--base-blue), 10%;
--palette-blue-20: var(--base-blue), 20%;
--palette-blue-30: var(--base-blue), 30%;
--palette-blue-40: var(--base-blue), 40%;
--palette-blue-50: var(--base-blue), 50%;
--palette-blue-60: var(--base-blue), 60%;
--palette-blue-70: var(--base-blue), 70%;
--palette-blue-80: var(--base-blue), 80%;
--palette-blue-90: var(--base-blue), 90%;
--palette-blue-100: var(--base-blue), 100%;
/*
여기서 색상을 의미에 따라 합성한다.
- primary 와 emphasis는 --palette-blue-50을 사용하여 합성한다.
- 이 변수들은 다른 불투명도를 사용하기 때문에 다른 의미를 갖고 있다.
- primary는 버튼 및 Primary CTA에 사용되는 굵은 색상이다
- emphasis는 텍스트를 강조 표시하거나 카드의 배경을 강조하기 위해 사용된다.
*/
--primary: hsla(var(--palette-blue-50), 100%);
--emphasis: hsla(var(--palette-blue-50), 8%);
그렇게 함으로써 두 가지를 할 수 있었다.
--base-blue
)에서 파생된 2개의 다른 CSS 변수를 정의하여 일부 색상 계층을 생성할 수 있다.아직도 이 패턴에 납득이 안간다면, 실제로 보여줄 수 있다. 트위터 테마 선택기를 바로 아래에서 재생할 수 있는 데모로 재구성하였다👇. 이것은 방금 본 색상 변수를 구분하여 합성 패턴을 적용하고 애플리케이션의 테마를 정의하는 것이 얼마나 쉬운지 보여준다.
블로그의 모든 주 색상은 다른 명도 또는 불투명도를 가진 단일 변수에서 상속된다! 따라서 이러한 "기본 색상" 중 하나를 클릭하면 이 기본 색상을 합성하여 전반적인 색상과 채도가 이 값에 따라 업데이트된다.
참고 사항 👉 이 테마 선택기는 이 블로그 사이트를 변경해야 한다. 여기서 onClick 동작은 2개의 CSS 변수를 업데이트하는 자바스크립트 코드 한 줄만 있다. 다른 건 아무 것도 없다! 😄
또한 이 블로그 게시물을 탐색하여 선택한 테마가 적용되었는지 확인할 수 있다. 곧 이 기능을 블로그의 설정으로 추가하여 선택하실 수 있을 것이다.
👉 위의 테마 선택기로 선택한 색상은 접근성을 염두에 두진 않았다!
따라서 필요한 경우 위의 Reset 버튼을 클릭하거나 원하는 경우 언제든지 페이지를 새로 고쳐서 테마를 기본값으로 복원하는 것이 좋다.
하지만 여전히 특정한 패턴을 반복하여 작성하고 있다. 이 것을 좀 더 개선해 보겠다. 한 가지 방법은 선언된 CSS 변수의 수를 최소화하는 것이다. 이상적으로는 기본 색상만 있으면 된다. 거기서 합성를 통해 필요한 모든 색상을 추정할 수 있다. 전체 색상 팔레트를 선언하지 않아도 된다!
👋
지금까지 읽은 것이 마음에 들었으면 좋겠다! 이 부분은 선택 사항이며, 필자가 실험하고 있는 부분이다. 이것의 목적은 CSS 변수만을 사용하여 부분 값의 구성을 어디까지 할 수 있는지를 보여주는 것이다.
이를 추가 사항으로 간주하고 반드시 따라야 할 모범 사례는 아니다. 단지 너무 흥분해서 이를 공유하지 않을 수 없었다.😄
이것의 용도는 무엇인가? 라고 질문해도 된다. 다음 시나리오를 생각해 보자.
--base-primary
변수가 사용한다.--base-primary
과 50%
의 밝기로 합성된 배경색을 가진다./**
이 경우 CSS 변수 집합은 다음과 같다.
--base-primary: 222, 89%;
--primary: hsla(var(--base-primary), 50%, 100%);
--primary-hover: hsla(var(--base-primary), 40%, 100%);
--primary-focus: hsla(var(--base-primary), 30%, 100%);
--text-color: hsla(0, 0%, 100%, 100%);
**/
const StyledButton = styled('button')`
height: 45px;
border-radius: 8px;
box-shadow: none;
border: none;
padding: 8px 16px;
text-align: center;
color: var(--text-color);
background-color: var(--primary);
cursor: pointer;
font-weight: 500;
&:hover {
background-color: var(--primary-hover);
}
&:focus {
background-color: var(--primary-focus);
}
`;
지금까지의 모든 예에서 모든 색상은 50%의 밝기를 기준으로 정의되었다. 위의 예제의 hover 및 focus 색상도 마찬가지이다. 하지만 사용자가 50%
와 다른 밝기의 색상을 선택하면 문제는 복잡해진다.
지금까지 살펴본 바로는, CSS 변수 덩어리를 더 생성하여 쉽게 해결할 수 있다. 하지만, 필자는 그것에 대한 더 나은 해결책이 있는지 알고 싶었다. 그래서 애초에 기본 색상을 합성하는 방식에 대해 다시 생각해 보았다.
--base-primary
를 정의한다.--base-primary-lightness
를 정의한다.아래에서는 구현 예제를 볼 수 있다. 다른 배경색을 위해 CSS 변수 집합 하나만 사용한 "테마가 적용된" 버튼이다. 버튼은 CSS 변수들을 합성하여 만들었고 '기본 밝기'를 기준으로 밝기를 계산한다. 그렇기 때문에 버튼의 색상을 변경하면 색상에 관계없이 각각 10%, 20%씩 더 어둡게 hover와 focus 색상이 동일하게 나타난다. 코드를 직접 확인해보자!
/**
아래의 코드에서는 hover, focus 및 버튼의 나머지 상태나 배경색에 대한 CSS 변수만 전달한다.
**/
const StyledButton = styled('button')`
height: 45px;
border-radius: 8px;
box-shadow: none;
border: none;
padding: 8px 16px;
text-align: center;
color: var(--text-color);
background-color: var(--primary);
cursor: pointer;
font-weight: 500;
&:hover {
background-color: var(--primary-hover);
}
&:focus {
background-color: var(--primary-focus);
}
`;
/**
아래에서 밝기 또는 기본 색상과 채도를 수정할 수 있다.
버튼 hover 및 focus 색상이 조정되는 것을 볼 수 있다.
거의 모든 색상에 대해 가능하다!
**/
const BlueThemeWrapper = styled('div')`
--base-primary: 222, 89%;
--base-primary-lightness: 50%;
--primary: hsla(var(--base-primary), var(--base-primary-lightness), 100%);
--primary-hover: hsla(
var(--base-primary),
calc(var(--base-primary-lightness) - 10%),
/* --primary-hover는 --primary 보다 10% 더 어둡다 */ 100%
);
--primary-focus: hsla(
var(--base-primary),
calc(var(--base-primary-lightness) - 20%),
/* --primary-focus는 --primary 보다 20% 더 어둡다 */ 100%
);
--text-color: hsla(0, 0%, 100%, 100%);
`;
const CyanThemedWrapper = styled('div')`
--base-primary: 185, 75%;
--base-primary-lightness: 60%;
--primary: hsla(var(--base-primary), var(--base-primary-lightness), 100%);
--primary-hover: hsla(
var(--base-primary),
calc(var(--base-primary-lightness) - 10%),
100%
);
--primary-focus: hsla(
var(--base-primary),
calc(var(--base-primary-lightness) - 20%),
100%
);
--text-color: hsla(0, 0%, 100%, 100%);
`;
const RedThemeWrapper = styled('div')`
--base-primary: 327, 80%;
--base-primary-lightness: 40%;
--primary: hsla(var(--base-primary), var(--base-primary-lightness), 100%);
--primary-hover: hsla(
var(--base-primary),
calc(var(--base-primary-lightness) - 10%),
100%
);
--primary-focus: hsla(
var(--base-primary),
calc(var(--base-primary-lightness) - 20%),
100%
);
--text-color: hsla(0, 0%, 100%, 100%);
`;
const ThemedButton = () => {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
height: '175px',
}}
>
<BlueThemeWrapper>
<StyledButton>Primary Button</StyledButton>
</BlueThemeWrapper>
<CyanThemedWrapper>
<StyledButton>Primary Button</StyledButton>
</CyanThemedWrapper>
<RedThemeWrapper>
<StyledButton>Primary Button</StyledButton>
</RedThemeWrapper>
</div>
);
};
render(<ThemedButton />);
[테마가 적용된 버튼]
위의 방법은 멋지지만 한계가 있다.
--base-primary-lightness
를 5%
로 수정하여 사용할 수 있다.--base-primary-lightness
를 95%로 수정하여 사용할 수 있다.필자는 SaaS를 사용함으로써 이러한 문제들을 쉽게 해결할 수 있었지만, 기술 스택을 단순하게 유지하기 위해 가능한 한 CSS에만 의존하려고 노력한다. 지금은 클라이언트 단에서 이러한 것들을 간단히 처리하였다. 특히 CSS-in-JS에서 이 방법은 빛을 발할 것이라 생각한다.
이러한 CSS 합성 기법이 마음에 들길 바라며, 이 패턴이 필자에게 깨달음을 준 것처럼 같은 방식으로 깨달음을 줄 수도 있을 것이다😄. 이 블로그를 포함한 웹 사이트에 이러한 기능을 적용하면서 상당히 개선되었고 기술 스택과 코드를 단순화하는 데 많은 도움이 되었다!
지금까지 이 합성 패턴을 색상에만 적용했다. 하지만 어떤 CSS 속성에도 적용할 수 있을 것이다. 색상과 색상 변수 관리는 필자에게 큰 문제였기 때문에 이 글을 쓸 수 있을 만큼 충분히 큰 활용 사례라고 생각하였다.
이 패턴을 더 발전시키는 방법에 대한 제안이나 아이디어가 있다면 알려달라! 아직도 그것에 대해 실험하고 있는데 당신이 어떻게 생각하는지 듣고 싶다!