attribute function
프론트엔드 개발자들 모인 자리에 가서 '어떤 스타일링 라이브러리 좋아하세요?'하고 물어보면 못해도 30분은 할 이야기가 생긴다. 내 경우에는 여러번 밝힌 바와 같이 TailwindCSS와 ModuleCSS를 선호한다. 하지만 내 선호를 밝히면 늘 돌아오는 질문이 있다. '그것들은 동적 스타일링하기 불편하지 않아요?'하고.
요즘 사용자를 늘려가고 있는 Vanila-extract나 이제는 죽어버린 StyledComponent와 비교하면 앞서 언급한 두 라이브러리는 확실히 동적 스타일링에 취약하다. 특히 TailwindCSS는 빌드 과정에서 사용된 클래스 이름만 추출해서 최종 CSS를 생성하는 'content scanning' 과정이 필요한데, 이 때문에 아래와 같이 동적으로 클래스 이름을 조합하거나 조건부로 생성하는 패턴은 제대로 처리되지 않을 수 있다.
// ✅ okay
예를 들어 className={isActive ? 'bg-blue-500' : 'bg-gray-300'}
// ❌ wrong
className={'bg-' + color}
ModuleCSS의 경우에는 상황이 조금 나은 편이다. CSS Variable을 사용하면 컴포넌트에 동적인 값을 제공할 수 있고, CSS가 이를 런타임에서도 해석할 수 있기 때문이다. 예를 들어, 테마 색상이나 애니메이션 지속 시간 같은 값을 props를 통해 전달하고, 해당 값을 style 속성으로 변수에 바인딩하면, CSS 모듈은 이를 유연하게 처리할 수 있다.
이런 접근 방식은 정적인 클래스 이름을 기반으로 동작하는 Tailwind보다 확실히 더 동적 스타일링에 유리하다. 물론 스타일 로직이 복잡해지면 유지보수가 어려워질 수 있고, CSS Variable의 범위 관리에도 신경 써야 하지만, 적절히 활용한다면 기능과 유지보수 사이의 균형을 잡을 수 있는 현실적인 대안이 된다.
export default function Page() {
const [size, setSize] = useState(100);
// CSS 변수로 사이즈 전달
const style = {
"--img-size": `${size}px`
} as React.CSSProperties;
return (
<div>
<div className={styles.box} style={style}>
박스 영역
</div>
<button onClick={() => setSize(size + 10)}>사이즈 키우기</button>
</div>
)
}
/* Box.module.css */
.image {
width: var(--img-size);
height: var(--img-size);
background-color: #eee;
display: flex;
align-items: center;
justify-content: center;
}
그런데 CSS Variable을 사용하지 않고도 동적 스타일링을 할 수 있도록 하는 CSS의 새로운 기능이 최근에 발표되었다. attr() 함수는 태그에 존재하는 속성 값을 CSS에서 직접 읽어와 사용할 수 있게 한다. 스타일을 구성하는 로직을 JS가 아닌 HTML 속성에 위임하면서도 CSS가 그것을 동적으로 해석할 수 있게 해주며, 이를 통해 모듈 CSS나 전통적인 CSS에서도 동적 스타일링을 보다 선언적으로 구현할 수 있는 가능성을 제공한다.
attr() 함수는 아래와 같은 구조로 작성되며 데이터타입과 기본값은 선택사항이다. 다만, 데이터타입을 제공하지 않을 시 속성으로 넘겨받는 값은 문자열 취급이며, 따라서 숫자를 받아야 하는 width나 height 혹은 hex 값으로 색상을 넘겨받아야 하는 background-color 등에서는 반드시 데이터타입을 제공해야 한다.
property: attr(속성명 데이터타입, 기본값);
위에서 CSS Variable를 통해 작성했던 코드를 attr() 함수로 변경하면 아래와 같다. 큰 틀에서는 같지만 훨씬 짧고 간단하게 작성할 수 있다는 점이 좋다.
export default function Page() {
const [size, setSize] = useState(100);
return (
<div>
<div className={styles.box} data-size={size}>
박스 영역
</div>
<button onClick={() => setSize(size + 10)}>사이즈 키우기</button>
</div>
)
}
/* Box.module.css */
.image {
width: attr(data-size rem, 1rem);
height: attr(data-size rem, 1rem);
background-color: #eee;
display: flex;
align-items: center;
justify-content: center;
}
어떻게 봐도 attr() 함수의 등장은 반갑기 그지없지만, 사실 여기에는 두 가지 함정이 존재한다. 하나는 큰 함정이고, 다른 하나는 작은 함정이다. 우선 작은 함정부터 살펴보자. HTML 혹은 JSX와 CSS 사이에는 명시적인 연결고리가 없기 때문에, 태그에 제공한 속성이 CSS에서 실제로 참조되는 속성인지 알기 어렵다. 누군가 실수로 data-size 대신 data-length라는 이름으로 속성을 바꿔버리면, CSS는 여전히 attr(data-size)를 참조하고 있기 때문에 원하는 스타일이 전혀 적용되지 않는다.
문제는 이 오류가 컴파일 타임에 감지되지 않고, 런타임에서도 별다른 에러 메시지를 남기지 않아 디버깅이 쉽지 않다는 점이다. 특히 프로젝트 규모가 커지고 협업이 이루어질수록, 이러한 암묵적 연결은 코드의 신뢰성을 저하시킬 수 있다. attr() 함수는 선언적으로 보이지만, HTML과 CSS 사이에 명확한 계약(contract)이 존재하지 않기 때문에 타입 안정성과 도구 지원 측면에서는 여전히 한계가 있다. 작은 함정이라고 표현했지만, 실제로는 충분히 치명적인 문제로 이어질 수 있다.
하지만 큰 함정은 작은 함정을 충분히 작게 보이게 만든다. 바로 브라우저 지원의 한계다. 앵커 포지셔닝 때와 마찬가지로 attr() 함수는 현재 대부분의 브라우저에서 속성 값을 콘텐츠(content) 속성 안에서만 사용할 수 있도록 제한되어 있으며, color, width, margin과 같은 일반적인 스타일 속성에 자유롭게 사용할 수 있는 기능은 아직 정식으로 구현되지 않았거나 실험적인 단계에 머물러 있다. 다시 말해, 지금 이 기능을 활용하고자 해도 실제로 적용 가능한 브라우저는 소수에 불과하며, 이조차도 플래그를 활성화해야 하거나 불안정한 동작을 보일 수 있다.
따라서 이런 상황에서 attr()을 주요한 스타일링 수단으로 채택하는 것은 매우 위험하며, 현실적으로는 아직 프로덕션에 투입할 수 없는 기술이라고 보는 것이 정확하다. 결국 attr() 함수의 등장은 미래의 가능성을 보여주는 흥미로운 시도이지만, 지금 당장은 호기심 이상의 실용성을 제공하진 못한다.
그러나 일단 이런 게 존재한다는 건 알고 있으니, 나중에 정식 출시되면 누구보다 빠르게 남들과는 다르게 동적 스타일링을 처리할 수 있지 않을까. 나는 그것이 기대된다 :)
블로그의 정보
Ayden's journal
Beard Weard Ayden