디버깅 StyleX + Vite: ‘Invalid Empty Selector’의 미스터리

발행: (2026년 1월 2일 오전 06:36 GMT+9)
12 min read
원문: Dev.to

Source: Dev.to

방법론적인 여정: 오류 메시지가 전혀 알려주지 않을 때 CSS‑in‑JS 레이스 컨디션 디버깅

TL;DR – Vite와 함께 StyleX에서 Invalid empty selector 오류가 발생하고, [breakpoints.tablet]: '1rem'와 같이 가져온 상수를 계산된 속성 키로 사용하고 있다면, 그것이 문제입니다. "@media (max-width: 768px)": '1rem'와 같은 인라인 문자열로 교체하세요. 아래는 왜 이런 일이 발생하는지와 우리가 어떻게 파악했는지에 대한 설명입니다.

일반 오류 메시지 (검색 용도)

검색을 통해 여기 오셨다면, 다음 오류 중 하나를 보셨을 수 있습니다:

Error: Invalid empty selector
Invalid empty selector
unknown file:528:1
    at lightningTransform (node_modules/@stylexjs/unplugin/lib/es/index.mjs)
LightningCSS error: Invalid empty selector
@stylexjs/unplugin: Invalid empty selector
vite stylex Invalid empty selector

또는 생성된 CSS에서 @media var(--xxx)를 볼 수 있는데, 이것이 실제 오류 원인입니다.


우리에게 아무것도 알려주지 않은 오류

거의 쓸모 없는 정보를 제공하는 오류로 시작되었습니다:

Error: Invalid empty selector
unknown file:528:1
    at lightningTransform (node_modules/@stylexjs/unplugin/lib/es/index.mjs)
    at processCollectedRulesToCSS (node_modules/@stylexjs/unplugin/lib/es/index.mjs)
    at collectCss (node_modules/@stylexjs/unplugin/lib/es/index.mjs)

소스 파일도 없고, 우리 코드의 라인도 없으며, 어떤 스타일이 깨졌는지에 대한 힌트도 없습니다—단지 “unknown file”과 우리가 볼 수 없는 생성된 CSS의 라인 번호만 표시됩니다.

이것이 우리가 근본 원인을 추적해낸 이야기이며, 더 중요한 디버깅 방법론이 여기서 시작됩니다.

왜 이 패턴이 작동해야 하는가

디버깅에 들어가기 전에 이해해야 할 점은 StyleX 문서에 따르면 우리가 잘못된 작업을 하고 있지 않다는 것입니다.

StyleX는 미디어 쿼리와 같은 재사용 가능한 상수를 정의하기 위해 stylex.defineConsts()를 제공합니다. 공식 문서에서는 바로 이 패턴을 보여줍니다:

// This is the documented, recommended approach
import * as stylex from '@stylexjs/stylex';

export const breakpoints = stylex.defineConsts({
  tablet: "@media (max-width: 768px)",
  mobile: "@media (max-width: 640px)",
});

그리고 이러한 상수를 계산된 프로퍼티 키로 사용하는 것은 표준 JavaScript입니다:

import { breakpoints } from './breakpoints.stylex';

const styles = stylex.create({
  container: {
    padding: {
      default: '2rem',
      [breakpoints.tablet]: '1rem', // Standard JS computed property
    },
  },
});

이 패턴은 다음 환경에서 완벽히 작동합니다:

  • Production builds
  • Webpack dev server
  • Next.js

하지만 Vite 개발 모드에서는 깨집니다. 문제는 왜 발생했을까요?

Source:

디버깅 여정

1단계 – 눈에 띄는 첫 시도

# 모든 캐시 삭제
rm -rf node_modules/.vite
rm -rf node_modules/.cache
npm run dev
# 핵심 옵션 – 모든 것을 재설치
rm -rf node_modules
npm install
npm run dev

두 시도 모두 오류를 해결하지 못했으므로 캐시가 원인이 아니었다.

2단계 – 오류 원인 파악

스택 트레이스는 @stylexjs/unpluginlightningTransform을 가리켰다. LightningCSS는 StyleX가 사용하는 CSS 파서/트랜스포머이다. 오류 메시지 *“Invalid empty selector”*는 LightningCSS가 잘못된 CSS를 받았다는 뜻이었다.

핵심 인사이트: 오류는 우리 소스 코드에 있지 않았다 — 생성된 CSS에 있었으며, 우리는 이를 볼 수 없었다.

3단계 – 빌드 파이프라인에 계측 코드 삽입

중간 CSS를 확인할 수 없었기 때문에 node_modules/@stylexjs/unplugin/lib/es/index.mjs에 디버그 훅을 추가했다:

function processCollectedRulesToCSS(rules, options) {
  if (!rules || rules.length === 0) return '';

  const collectedCSS = stylexBabelPlugin.processStylexRules(rules, {
    useCSSLayers: options.useCSSLayers ?? false,
    classNamePrefix: options.classNamePrefix ?? 'x',
  });

  // DEBUG: 항상 CSS를 파일에 기록해 어떤 것이 생성되는지 확인
  const fs = require('fs');
  const lines = collectedCSS.split('\n');
  console.log('[StyleX DEBUG] CSS lines:', lines.length);
  fs.writeFileSync(`stylex-debug-${lines.length}.css`, collectedCSS);

  let code;
  try {
    const result = lightningTransform({
      filename: 'styles.css',
      code: Buffer.from(collectedCSS),
      minify: options.minify ?? false,
    });
    code = result.code;
  } catch (error) {
    // CRITICAL: 실패를 일으킨 CSS를 캡처
    fs.writeFileSync('stylex-debug-FAILED.css', collectedCSS);
    console.log('[StyleX DEBUG] FAILED – check stylex-debug-FAILED.css');
    throw error;
  }

  return code.toString();
}

왜 중요한가: 빌드 도구를 디버깅할 때는 중간 산출물을 직접 볼 수 없는 경우가 많다. 이를 캡처하기 위한 계측 코드를 추가하는 것이 필수적이다.

4단계 – 첫 번째 단서

계측 코드를 넣고 개발 서버를 실행하면 stylex-debug-FAILED.css가 생성되었다. 파일을 열어보니 다음과 같은 내용이 있었다:

@media var(--xgageza) {
  .x1abc123 {
    padding-left: 1rem;
  }
}

@media var(--xgageza)는 유효한 CSS가 아니며, 바로 LightningCSS가 불평했던 부분이다.

5단계 – var(--xgageza)의 출처 추적

생성된 CSS를 검색해 보니 StyleX가 만든 모든 미디어 쿼리가 CSS 변수 참조로 대체되고 있었다. 원인은 Vite 개발 서버가 import된 상수를 참조하는 계산된 프로퍼티 키를 처리하는 방식이었다. 상수들이 StyleX Babel 플러그인이 규칙을 수집한 뒤에 평가되면서, 해결되지 않은 자리 표시자 변수(var(--xgageza))가 남은 것이다.

6단계 – 가설 검증

최소 재현 예제를 만들었다:

// breakpoints.stylex
import * as stylex from '@stylexjs/stylex';
export const bp = stylex.defineConsts({
  tablet: '@media (max-width: 768px)',
});

// component.jsx
import { bp } from './breakpoints.stylex';
export const styles = stylex.create({
  box: {
    [bp.tablet]: { padding: '1rem' },
  },
});
  • Webpack → 정상 빌드.
  • Vite (dev)Invalid empty selector 오류 발생.

인라인 문자열로 바꾸면 오류가 사라졌다:

const styles = stylex.create({
  box: {
    '@media (max-width: 768px)': { padding: '1rem' },
  },
});

따라서 문제는 Vite 개발 모드에서 상수를 런타임에 import하는 방식에 있다.

7단계 – 우회 방법 / 해결책

계산된 프로퍼티 사용을 리터럴 문자열로 교체하거나, 상수를 stylex.defineConsts 없이 일반 객체에 넣고 직접 참조한다:

// breakpoints.js – StyleX API를 사용하지 않은 일반 객체
export const breakpoints = {
  tablet: '@media (max-width: 768px)',
  mobile: '@media (max-width: 640px)',
};

// component.jsx
import { breakpoints } from './breakpoints';
export const styles = stylex.create({
  box: {
    [breakpoints.tablet]: { padding: '1rem' },
  },
});
styles = stylex.create({
  container: {
    [breakpoints.tablet]: { padding: '1rem' },
  },
});

또는 문제를 해결한 최신 버전(≥ 0.5.2)의 @stylexjs/unplugin으로 업그레이드하거나, Vite를 optimizeDeps로 상수들을 사전 번들링하도록 구성하십시오.


주요 내용 및 디버깅 방법론

  1. 분명한 것부터 시작 – 캐시를 비우고, 재설치하고, 재시작합니다.
  2. 스택 트레이스를 읽어라 – 오류를 발생시키는 도구(LightningCSS)를 찾습니다.
  3. 중간 산출물을 캡처 – 빌드 파이프라인에 도구를 삽입해 생성된 CSS를 출력하도록 합니다.
  4. 패턴을 검색 – 잘못된 @media var(--…)가 변환 버그를 가리키고 있었습니다.
  5. 최소 재현 사례 격리 – 오류를 재현하는 가장 작은 예제로 축소합니다.
  6. 가설 테스트 – 상수를 리터럴 문자열로 교체하면 오류가 사라집니다.
  7. 수정 또는 우회 적용 – Vite 개발 모드에서 문제 패턴을 피하거나 플러그인을 업그레이드합니다.

이 단계들을 따라 하면 불투명한 “Invalid empty selector” 오류를 구체적이고 재현 가능한 버그로 전환하고 신속히 해결할 수 있습니다. 디버깅을 즐기세요!

CSS 변수 참조, 미디어 쿼리가 아님!

CSS는 다음과 같아야 합니다:

@media (max-width: 768px) {
  .x1abc123 {
    padding-left: 1rem;
  }
}

스모킹 건

어딘가에서 @media (max-width: 768px)가 있어야 할 곳에 var(--xgageza)가 생성되고 있었습니다.

단계 5 – 잘못된 가설

우리의 첫 번째 가설: stylex.defineConsts()가 깨졌을지도 몰라요. 대신 일반 JavaScript 객체를 사용해 봅시다.”

// Changed from stylex.defineConsts() to plain object
export const breakpoints = {
  tablet: "@media (max-width: 768px)",
  mobile: "@media (max-width: 640px)",
};

캐시를 지우고, 재시작했지만… 여전히 깨졌습니다.

단계 6 – 증거를 따라가기

우리는 실패한 CSS 파일로 돌아가 var(--가 사용된 모든 사례를 검색했습니다. 많은 사례가 있었고, 모두 일정한 패턴을 따랐습니다 — 미디어 쿼리 문자열이 있어야 할 곳에 나타났습니다.

그 다음에는 이 값들이 정의된 방식이 아니라 사용된 방식을 살펴보았습니다:

// In roles.stylex.js
const styles = stylex.create({
  heading: {
    paddingBlock: {
      default: '1.5rem',
      [breakpoints.tablet]: '1.25rem', //  **Have you encountered similar CSS‑in‑JS race conditions?**  
      // > What’s your approach to debugging build‑tool issues? Share your stories in the comments.
    },
  },
});

태그

stylex, vite, css-in-js, debugging, javascript, react, build-tools, lightningcss

Keywords (for search engines)

  • StyleX 잘못된 빈 선택자
  • StyleX Vite 오류
  • @stylexjs/unplugin 오류
  • LightningCSS 잘못된 빈 선택자
  • StyleX stylex.create 미디어 쿼리 오류
  • StyleX 브레이크포인트가 작동하지 않음
  • StyleX defineConsts Vite
  • StyleX 계산된 속성 키
  • Vite CSS‑in‑JS 경쟁 조건
  • StyleX 미디어 쿼리에서 var(--…)
  • @media var CSS 오류
  • StyleX unplugin lightningTransform 오류
  • StyleX Vite 개발 모드 충돌
  • processCollectedRulesToCSS 오류
Back to Blog

관련 글

더 보기 »