본문으로 건너뛰기

© 2026 Molayo

Smashing헤드라인2026. 05. 20. 01:31

CSS @scope: 명명 규칙 및 무거운 추상화의 대안

요약

CSS @scope 규칙이 기존의 BEM 명명 규칙이나 Tailwind, CSS-in-JS와 같은 무거운 추상화 방식의 대안이 될 수 있는지 탐구합니다. 스타일 유출 문제를 해결하기 위해 개발자들이 선택해온 복잡한 명명 규칙과 격리 도구들의 한계를 분석합니다.

핵심 포인트

  • CSS 스타일 유출 문제는 선택자 우선순위 경쟁과 전역 스타일 오염을 유발함
  • BEM은 구조적 해결책을 제시하지만, 유지보수 비용이 높고 HTML 구조 변화에 취약함
  • Tailwind나 CSS-in-JS는 스타일 격리를 제공하지만, 캐스케이드를 포기하거나 무거운 빌드 설정이 필요함
  • CSS @scope는 명명 규칙의 부담을 줄이면서 현대적인 프론트엔드 개발에 적합한 격리 환경을 제공할 가능성이 있음

@scope

이 규칙은 마침내 개발자들에게 현대적인 프론트엔드 (front ends)에 발맞출 수 있는 CSS를 작성할 수 있다는 확신을 줄 수 있을까요? 기본적인 CSS 원칙을 배울 때, 유지보수성을 보장하기 위해 모듈화되고 재사용 가능하며 서술적인 스타일을 작성하도록 배웁니다. 하지만 개발자들이 실제 애플리케이션에 참여하게 되면, 스타일이 의도하지 않은 영역으로 유출 (leak)되지 않게 하면서 UI 기능을 추가하는 것이 불가능하게 느껴질 때가 많습니다.

이 문제는 종종 자기 충족적 루프 (self-fulfilling loop)로 눈덩이처럼 불어납니다. 이론적으로는 하나의 요소나 클래스에 범위가 지정된 (scoped) 스타일이 엉뚱한 곳에 나타나기 시작합니다. 이는 개발자로 하여금 유출된 스타일을 덮어쓰기 위해 더욱 구체적인 선택자 (selectors)를 만들도록 강요하며, 이는 다시 실수로 전역 스타일 (global styles)을 덮어쓰는 식으로 이어집니다.

BEM과 같은 엄격한 클래스 이름 명명 규칙 (class name conventions)은 이 문제에 대한 하나의 이론적인 해결책입니다. BEM (Block, Element, Modifier) 방법론은 CSS 파일 내에서 재사용성과 구조를 보장하기 위해 CSS 클래스의 이름을 짓는 체계적인 방식입니다. 이러한 명명 규칙은 도메인 언어를 활용하여 요소와 그 상태를 설명함으로써 인지 부하 (cognitive load)를 줄일 수 있으며, 올바르게 구현된다면 대규모 애플리케이션의 스타일을 더 쉽게 유지보수할 수 있게 해줍니다.

하지만 현실 세계에서는 항상 그렇게 흘러가지는 않습니다. 우선순위는 변할 수 있으며, 변화와 함께 구현은 일관성을 잃게 됩니다. HTML 구조의 작은 변경만으로도 많은 CSS 클래스 이름 수정이 필요할 수 있습니다. 상호작용이 매우 활발한 프론트엔드 애플리케이션의 경우, BEM 패턴을 따르는 클래스 이름은 길고 다루기 힘들게 변할 수 있습니다 (예: app-user-overview__status--is-authenticating). 또한 명명 규칙을 완전히 준수하지 않으면 시스템의 구조가 깨지게 되어, 결과적으로 그 이점이 무효화됩니다.

이러한 과제들을 고려할 때, 개발자들이 프레임워크로 눈을 돌리는 것은 놀라운 일이 아닙니다. Tailwind는 가장 인기 있는 CSS 프레임워크입니다. 스타일 간의 이길 수 없는 것 같은 명시도 전쟁 (specificity war)에 맞서 싸우기보다는, CSS 캐스케이드 (Cascade)를 포기하고 완전한 격리 (isolation)를 보장하는 도구를 사용하는 것이 더 쉽기 때문입니다.

개발자들은 유틸리티에 더 의존합니다

일부 개발자들이 캐스케이드 스타일 (cascaded styles)을 피하는 데 열성적이라는 것을 어떻게 알 수 있을까요? 그것은 바로 그러한 목적을 위해 특별히 설계된 CSS-in-JS 프레임워크와 같은 "현대적인" 프론트엔드 툴링 (tooling)의 부상입니다. 특정 컴포넌트에 엄격하게 범위가 지정된 (scoped) 격리된 스타일을 사용하는 것은 마치 신선한 공기처럼 느껴질 수 있습니다. 이는 여전히 프론트엔드 작업 중 가장 싫어하고 시간이 많이 걸리는 작업 중 하나인 "이름 짓기"의 필요성을 제거하며, 개발자들이 CSS 상속 (inheritance)의 이점을 완전히 이해하거나 활용하지 않고도 생산성을 높일 수 있게 해줍니다.

하지만 CSS 캐스케이드 (Cascade)를 버리는 것에는 그 나름의 문제점이 따릅니다. 예를 들어, JavaScript에서 스타일을 구성하려면 무거운 빌드 설정 (build configurations)이 필요하며, 종종 스타일이 컴포넌트 마크업이나 HTML과 어색하게 뒤섞이는 결과를 초래합니다. 신중하게 고려된 명명 규칙 (naming conventions) 대신, 우리는 빌드 도구가 우리를 대신해 셀렉터 (selectors)와 식별자 (identifiers)를 자동 생성하도록 허용합니다 (예: .jsx-3130221066). 이는 개발자들이 그 자체로 또 다른 유사 언어 (pseudo-language)를 계속 따라가야 함을 의미합니다. (마치 컴포넌트의 모든 useEffect가 무엇을 하는지 이해하는 인지 부하 (cognitive load)만으로도 이미 충분하지 않은 것처럼 말이죠!)

클래스 이름을 짓는 작업을 툴링으로 더 추상화한다는 것은, 기본적인 디버깅 (debugging)이 개발용으로 컴파일된 특정 애플리케이션 버전에 국한되는 경우가 많음을 의미하며, 개발자 도구 (Developer Tools)와 같이 라이브 디버깅을 지원하는 브라우저의 네이티브 기능을 활용하기 어려워짐을 뜻합니다.

다행히도, 현대적인 CSS 기능들은 표준 CSS를 작성하는 것을 더 유연하게 만들 뿐만 아니라, 우리와 같은 개발자들에게 캐스케이드를 관리하고 이를 우리에게 유리하게 작동시킬 수 있는 훨씬 더 큰 권한을 부여합니다. CSS 캐스케이드 레이어 (CSS Cascade Layers)가 좋은 예시이지만, 놀라울 정도로 관심을 받지 못하는 또 다른 기능이 있습니다. 비록 최근에 **Baseline 호환 (Baseline compatible)**이 되면서 상황이 변하고 있기는 하지만 말입니다.

CSS @scope

At-Rule

저는 CSS @scope at-rule이 우리가 다루었던 스타일 누출 (style-leak)로 인한 불안감을 해결할 잠재적인 치료제가 될 수 있다고 생각합니다. 이는 추상화나 추가적인 빌드 도구(build tooling)를 위해 웹 네이티브 (native web)의 장점을 타협하도록 강요하지 않는 방식입니다.

@scope CSS at-rule을 사용하면 특정 DOM 서브트리 (subtrees) 내의 요소를 선택할 수 있습니다. 이를 통해 재정의하기 어려운 지나치게 구체적인 선택자 (selectors)를 작성하지 않고도, 선택자를 DOM 구조에 너무 밀접하게 결합하지 않으면서 요소를 정밀하게 타겟팅할 수 있습니다.”

— MDN

다시 말해, 우리는 프론트엔드 개발의 오랜 지침이었던 상속 (inheritance), 캐스케이딩 (cascading), 또는 기본적인 관심사 분리 (separation of concerns)를 희생하지 않으면서도 특정 인스턴스 내에서 격리된 스타일을 다룰 수 있습니다.

게다가 브라우저 지원 범위도 매우 훌륭합니다. 실제로 Firefox 146이 지난 12월 @scope 지원을 추가하면서 처음으로 Baseline 호환 (Baseline compatible)이 되었습니다. 다음은 BEM 패턴을 사용하는 버튼과 @scope 규칙을 사용하는 버튼의 간단한 비교입니다.

<!-- BEM -->
<button class="button button--primary">
<span class="button__text">Click me</span
...
<!-- @scope -->
<button class="primary-button">
<span>Click me</span>
...

@scope 규칙은 더 적은 복잡성으로 정밀함을 제공합니다. 개발자는 더 이상 클래스 이름 (class names)을 사용하여 경계를 만들 필요가 없으며, 이는 결과적으로 네이티브 HTML 요소에 기반하여 선택자를 작성할 수 있게 하여 규정적인 CSS 클래스 이름 패턴의 필요성을 제거합니다. 단순히 클래스 이름 관리의 필요성을 없애는 것만으로도, @scope는 대규모 프로젝트에서 CSS와 관련된 두려움을 완화할 수 있습니다.

기본 사용법 (Basic Usage)

시작하려면 CSS에 @scope 규칙을 추가하고 스타일이 적용될 범위의 루트 선택자 (root selector)를 삽입하세요.

@scope (<selector>) {
/* <selector>에 범위가 지정된 스타일 */
}

예를 들어, 스타일의 범위를 <nav> 요소로 지정한다면 다음과 같은 모습이 될 것입니다.

@scope (nav) {
a { /* nav 범위 내의 링크 스타일 */ }
a:active { /* 활성화된 링크 스타일 */ }
...

이것 자체만으로는 획기적인 기능은 아닙니다. 하지만, scope에 두 번째 인자를 추가하여 **하한선 (lower boundary)**을 생성함으로써, scope의 시작점과 끝점을 효과적으로 정의할 수 있습니다.

/* `ul` 내부의 어떤 `a` 요소에도 스타일이 적용되지 않습니다 */
@scope (nav) to (ul) {
a {
...

이러한 방식을 **도넛 스코핑 (donut scoping)**이라고 부르며, DOM 구조에 밀접하게 결합된 일련의 유사하고 매우 구체적인 선택자(selectors)를 사용하거나, :not 의사 선택자 (pseudo-selector)를 사용하거나, 혹은 서로 다른 CSS를 처리하기 위해 <nav> 내의 <a> 요소에 특정 클래스 이름을 할당하는 등 여러 가지 접근 방식을 사용할 수 있습니다.

이러한 다른 접근 방식들과 상관없이, @scope 방식은 훨씬 더 간결합니다. 더 중요한 점은, 클래스 이름이 변경되거나 오용되는 경우, 또는 HTML 구조가 수정되는 경우에도 스타일이 깨질 위험을 방지한다는 것입니다. 이제 @scope가 Baseline 호환이 되었으므로, 더 이상 우회 방법 (workarounds)이 필요하지 않습니다!

우리는 여러 개의 끝 경계 (end boundaries)를 사용하여 "스타일 8자 모양 (style figure eight)"을 만드는 방식으로 이 아이디어를 더 발전시킬 수 있습니다:

/* <aside> 또는 <nav> 내부의 어떤 <a> 또는 <p> 요소에도 스타일이 적용되지 않습니다 */
@scope (main) to (aside, nav) {
a {
...

이를 @scope 규칙 없이 처리하여 개발자가 스타일을 기본값으로 "초기화 (reset)"해야 하는 버전과 비교해 보십시오:

main a {
font-size: 14px;
}
...

다음 예시를 확인해 보세요. 특정 중첩 선택자 (nested selectors)를 타겟팅하면서 다른 요소들은 제외하는 것이 얼마나 간단한지 눈에 보이시나요?

웹 컴포넌트 (web components) 내의 슬롯된 콘텐츠 (slotted content)에 고유한 스타일을 적용해야 하는 시나리오를 가정해 봅시다. 콘텐츠를 웹 컴포넌트에 슬롯팅할 때, 해당 콘텐츠는 Shadow DOM의 일부가 되지만 여전히 부모 문서로부터 스타일을 상속받습니다. 개발자는 콘텐츠가 어떤 웹 컴포넌트에 슬롯팅되었는지에 따라 서로 다른 스타일을 구현하고 싶을 수 있습니다:

<!-- 동일한 <user-card> 콘텐츠, 서로 다른 컨텍스트 -->
<product-showcase>
<user-card slot="reviewer">
...

이 예시에서 개발자는 <user-card>

<team-roster> 내부에 렌더링될 때만 별도의 스타일을 갖도록 설정할 수 있습니다.

:

@scope (team-roster) {
user-card {
display: inline-flex;
...

추가적인 이점 (More Benefits)

@scope는 유틸리티 (utilities)나 JavaScript로 생성된 클래스 이름 (class names)에 의존하지 않고도 클래스 관리의 필요성을 제거할 수 있는 추가적인 방법들을 제공합니다. 예를 들어, @scope는 단순히 클래스 이름뿐만 아니라 어떠한 선택자의 하위 요소 (descendants)도 쉽게 타겟팅할 수 있는 가능성을 열어줍니다:

/* 직계 자식으로 button을 가진 div 요소만 루트 스코프 (root scope)에 포함됩니다 */
@scope (div:has(> button)) {
p {
...

또한, 스코프 안에 스코프를 생성하는 **중첩 (nested)**이 가능합니다:

@scope (main) {
p {
font-size: 16px;
...

게다가, @scope 규칙 내에서 루트 스코프 (root scope)를 쉽게 참조할 수 있습니다:

/* `main`의 직계 자식인 `section` 요소 내부의 요소들에 적용되지만, 해당 섹션들의 직계 자식인 `aside`를 만나면 중단됩니다 */
@scope (main > section) to (:scope > aside) {
p {
...

@scope 어트리뷰트 규칙 (at-rule)은 CSS 명시도 (specificity) 결정 방식에 새로운 근접성 (proximity) 차원을 도입합니다. 전통적인 CSS에서는 두 선택자가 동일한 요소와 일치할 때, 명시도 (specificity)가 더 높은 선택자가 승리합니다. @scope를 사용하면, 두 요소의 명시도가 동일할 때 매칭된 요소에 스코프 루트 (scope root)가 더 가까운 쪽이 승리합니다. 이는 내부 컴포넌트가 자연스럽게 외부 요소의 스타일보다 우선하게 되므로, 요소의 명시도를 수동으로 높여 부모 스타일을 덮어쓸 필요를 없애줍니다.

<style>
@scope (.container) {
.title { color: green; }
...

결론 (Conclusion)

Tailwind와 같은 유틸리티 우선 (Utility-first) CSS 프레임워크는 프로토타이핑과 소규모 프로젝트에는 효과적입니다. 하지만 개발자가 여러 명 참여하는 대규모 프로젝트에서 사용할 때는 그 이점이 빠르게 감소합니다.

프론트엔드 개발은 지난 몇 년간 점점 더 복잡해졌으며, CSS도 예외는 아닙니다. @scope가 [문장 미완성]

rule이 만능 해결책은 아니지만, 복잡한 툴링 (tooling)의 필요성을 줄여줄 수 있습니다. 전략적인 클래스 명명 (class naming)을 대신하거나 이와 병행하여 사용할 때, @scope는 유지보수가 용이한 CSS를 작성하는 것을 더 쉽고 즐겁게 만들어 줄 수 있습니다.

추가 읽을거리

  • CSS @scope (MDN) - “CSS @scope
  • Juan Diego Rodríguez (CSS-Tricks)
  • Firefox 146 Release Notes (Firefox)
  • Browser Support (CanIUse)
  • Popular CSS Frameworks (State of CSS 2024)
  • “The “C” in CSS: Cascade”, Thomas Yip (CSS-Tricks)
  • BEM Introduction (Get BEM)

AI 자동 생성 콘텐츠

본 콘텐츠는 Smashing Magazine의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0