DevTools에서 CSS-in-JS 지원

알렉스 루덴코
알렉스 루덴코

이 문서에서는 Chrome 85 이후 출시된 DevTools의 CSS-in-JS 지원 기능과 일반적으로 CSS-in-JS의 의미, DevTools에서 오랫동안 지원해 온 일반 CSS와의 차이점에 대해 설명합니다.

CSS-in-JS란 무엇인가요?

CSS-in-JS의 정의는 다소 모호합니다. 넓은 의미에서 이는 JavaScript를 사용하여 CSS 코드를 관리하는 접근 방식입니다. 예를 들어 CSS 콘텐츠가 JavaScript를 사용하여 정의되고 최종 CSS 출력이 앱에서 즉시 생성된다는 의미일 수 있습니다.

DevTools의 경우 CSS-in-JS는 CSSOM API를 사용하여 CSS 콘텐츠가 페이지에 삽입된다는 의미입니다. 일반 CSS는 <style> 또는 <link> 요소를 사용하여 삽입되며 정적 소스 (예: DOM 노드 또는 네트워크 리소스)를 포함합니다. 반면에 CSS-in-JS에는 정적 소스가 없는 경우가 많습니다. 여기서 특별한 경우는 CSSOM API를 사용하여 <style> 요소의 콘텐츠를 업데이트할 수 있어 소스가 실제 CSS 스타일시트와 동기화되지 않는 것입니다.

CSS-in-JS 라이브러리 (예: styled-component, Emotion, JSS)를 사용하는 경우 라이브러리는 개발 모드와 브라우저에 따라 내부적으로 CSSOM API를 사용하여 스타일을 삽입할 수 있습니다.

CSS-in-JS 라이브러리가 하는 것과 비슷한 CSSOM API를 사용하여 스타일시트를 삽입하는 방법에 대한 몇 가지 예를 살펴보겠습니다.

// Insert new rule to an existing CSS stylesheet
const element = document.querySelector('style');
const stylesheet = element.sheet;
stylesheet.replaceSync('.some { color: blue; }');
stylesheet.insertRule('.some { color: green; }');

완전히 새로운 스타일시트를 만들 수도 있습니다.

// Create a completely new stylesheet
const stylesheet = new CSSStyleSheet();
stylesheet.replaceSync('.some { color: blue; }');
stylesheet.insertRule('.some { color: green; }');

// Apply constructed stylesheet to the document
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];

DevTools의 CSS 지원

DevTools에서 CSS를 처리할 때 가장 일반적으로 사용되는 기능은 스타일 창입니다. 스타일 창에서 특정 요소에 적용되는 규칙을 보고 규칙을 수정하고 페이지의 변경사항을 실시간으로 확인할 수 있습니다.

작년 이전에는 CSSOM API를 사용하여 수정된 CSS 규칙에 대한 지원이 다소 제한적이었습니다. 적용된 규칙을 볼 수만 있고 수정할 수는 없었습니다. 작년에 우리가 달성한 주요 목표는 Styles 창을 사용하여 CSS-in-JS 규칙을 수정할 수 있도록 하는 것이었습니다. CSS-in-JS 스타일을 "configurationed"로 호출하여 웹 API를 사용하여 구성되었음을 나타내기도 합니다.

DevTools에서 스타일 편집이 어떻게 작동하는지 자세히 살펴보겠습니다.

DevTools의 스타일 편집 메커니즘

DevTools의 스타일 편집 메커니즘

DevTools에서 요소를 선택하면 스타일 창이 표시됩니다. 스타일 창은 CSS.getMatchedStylesForNode라는 CDP 명령어를 실행하여 요소에 적용되는 CSS 규칙을 가져옵니다. CDP는 Chrome DevTools Protocol의 약자이며 DevTools 프런트엔드가 검사된 페이지에 관한 추가 정보를 가져올 수 있는 API입니다.

CSS.getMatchedStylesForNode가 호출되면 문서의 모든 스타일시트를 식별하고 브라우저의 CSS 파서를 사용하여 이를 파싱합니다. 그런 다음 모든 CSS 규칙을 스타일시트 소스의 위치와 연결하는 색인이 생성됩니다.

CSS를 다시 파싱해야 하는 이유는 무엇일까요? 여기서 문제는 성능상의 이유로 브라우저 자체는 CSS 규칙의 소스 위치에 신경 쓰지 않으므로 이를 저장하지 않는다는 점입니다. 하지만 DevTools에서 CSS 편집을 지원하려면 소스 위치가 필요합니다. Google은 일반 Chrome 사용자가 성능 저하를 감수하는 것을 원하지 않지만 DevTools 사용자가 소스 위치에 액세스할 수 있도록 하고 싶습니다. 이 재파싱 방식은 두 사용 사례를 모두 해결하며 단점을 최소화합니다.

그런 다음 CSS.getMatchedStylesForNode 구현은 브라우저의 스타일 엔진에 지정된 요소와 일치하는 CSS 규칙을 제공하도록 요청합니다. 마지막으로, 이 메서드는 스타일 엔진이 반환한 규칙을 소스 코드와 연결하고, CSS 규칙에 관한 구조화된 응답을 제공하여 DevTools가 규칙의 어느 부분이 선택기인지 또는 속성인지 알 수 있도록 합니다. 이를 통해 DevTools는 선택기와 속성을 독립적으로 편집할 수 있습니다.

이제 편집에 대해 알아보겠습니다. CSS.getMatchedStylesForNode는 모든 규칙의 소스 위치를 반환한다는 점을 기억하세요? 이는 편집에 매우 중요합니다. 규칙을 변경하면 DevTools가 실제로 페이지를 업데이트하는 또 다른 CDP 명령어를 실행합니다. 명령어에는 업데이트 중인 규칙 프래그먼트의 원래 위치와 프래그먼트를 업데이트해야 하는 새 텍스트가 포함됩니다.

백엔드에서 수정 호출을 처리할 때 DevTools가 대상 스타일시트를 업데이트합니다. 또한 유지관리하는 스타일시트 소스의 사본을 업데이트하고 업데이트된 규칙의 소스 위치를 업데이트합니다. edit 호출에 대한 응답으로 DevTools 프런트엔드가 방금 업데이트된 텍스트 프래그먼트의 업데이트된 위치를 다시 가져옵니다.

따라서 DevTools에서 CSS-in-JS를 편집할 수 없는 이유를 알 수 있습니다. CSS-in-JS에는 실제 소스가 저장되지 않으며 CSS 규칙은 CSSOM 데이터 구조의 브라우저 메모리에 저장됩니다.

CSS-in-JS에 대한 지원을 추가한 방법

따라서 CSS-in-JS 규칙 편집을 지원하기 위해, 우리는 위에서 설명한 기존 메커니즘을 사용하여 수정할 수 있는, 구성된 스타일시트의 소스를 생성하는 것이 최선의 해결책이라고 결정했습니다.

첫 번째 단계는 소스 텍스트를 빌드하는 것입니다. 브라우저의 스타일 엔진은 CSSStyleSheet 클래스에 CSS 규칙을 저장합니다. 이 클래스는 이전에 설명한 대로 JavaScript에서 만들 수 있는 인스턴스를 포함합니다. 소스 텍스트를 빌드하는 코드는 다음과 같습니다.

String InspectorStyleSheet::CollectStyleSheetRules() {
  StringBuilder builder;
  for (unsigned i = 0; i < page_style_sheet_->length(); i++) {
    builder.Append(page_style_sheet_->item(i)->cssText());
    builder.Append('\n');
  }
  return builder.ToString();
}

CSSStyleSheet 인스턴스에서 찾은 규칙을 반복하고 이로부터 단일 문자열을 빌드합니다. 이 메서드는 InspectorStyleSheet 클래스의 인스턴스를 만들 때 호출됩니다. InspectorStyleSheet 클래스는 CSSStyleSheet 인스턴스를 래핑하고 DevTools에 필요한 추가 메타데이터를 추출합니다.

void InspectorStyleSheet::UpdateText() {
  String text;
  bool success = InspectorStyleSheetText(&text);
  if (!success)
    success = InlineStyleSheetText(&text);
  if (!success)
    success = ResourceStyleSheetText(&text);
  if (!success)
    success = CSSOMStyleSheetText(&text);
  if (success)
    InnerSetText(text, false);
}

이 스니펫에서는 내부적으로 CollectStyleSheetRules를 호출하는 CSSOMStyleSheetText를 볼 수 있습니다. 스타일시트가 인라인 또는 리소스 스타일시트가 아닌 경우 CSSOMStyleSheetText가 호출됩니다. 기본적으로 이 두 스니펫에서는 new CSSStyleSheet() 생성자를 사용하여 만든 스타일시트의 기본 수정이 가능합니다.

특별한 경우는 CSSOM API를 사용하여 변경된 <style> 태그와 연결된 스타일시트입니다. 이 경우 스타일시트에는 원본 텍스트와 소스에 없는 추가 규칙이 포함됩니다. 이를 처리하기 위해 이러한 추가 규칙을 원본 텍스트에 병합하는 메서드를 도입합니다. 여기서 순서가 중요한 이유는 CSS 규칙을 원본 소스 텍스트의 중앙에 삽입할 수 있기 때문입니다. 예를 들어 원래 <style> 요소에 다음 텍스트가 포함되어 있다고 가정해 보겠습니다.

/* comment */
.rule1 {}
.rule3 {}

그런 다음 JS API를 사용하여 .rule0, .rule1, .rule2, .rule3, .rule4의 규칙 순서를 생성하는 새 규칙을 삽입했습니다. 병합 작업 후 결과 소스 텍스트는 다음과 같습니다.

.rule0 {}
/* comment */
.rule1 {}
.rule2 {}
.rule3 {}
.rule4 {}

규칙의 원본 텍스트 위치는 정확해야 하므로 수정 과정에서 원본 주석과 들여쓰기를 보존하는 것이 중요합니다.

CSS-in-JS 스타일시트의 또 다른 특별한 점은 언제든지 페이지에서 변경할 수 있다는 점입니다. 실제 CSSOM 규칙이 텍스트 버전과 동기화되지 않으면 수정이 작동하지 않습니다. 이를 위해 소위 프로브를 도입했습니다. 프로브를 사용하면 스타일시트가 변경될 때 브라우저가 DevTools의 백엔드 부분에 알릴 수 있습니다. 변경된 스타일시트는 다음번 CSS.getMatchStylesForNode 호출 시 동기화됩니다.

이 모든 요소를 갖춘 상태에서 CSS-in-JS 편집은 이미 작동하지만 스타일시트가 구성되었는지 여부를 나타내도록 UI를 개선하고자 했습니다. 프런트엔드에서 CSS 규칙의 소스를 올바르게 표시하는 데 사용하는 isConstructed라는 새 속성을 CDP의 CSS.CSSStyleSheetHeader에 추가했습니다.

구성 가능한 스타일시트

결론

여기서의 스토리를 요약하자면, DevTools가 지원하지 않는 CSS-in-JS와 관련된 관련 사용 사례를 살펴보고 이러한 사용 사례를 지원하는 솔루션을 살펴보았습니다. 이 구현에서 흥미로운 점은 CSSOM CSS 규칙에 일반 소스 텍스트가 포함되도록 하여 기존 기능을 활용할 수 있었다는 점입니다. 따라서 DevTools에서 스타일 편집을 완전히 다시 설계할 필요가 없습니다.

자세한 내용은 설계 제안 또는 모든 관련 패치를 참조하는 Chromium 추적 버그를 확인하세요.

미리보기 채널 다운로드

Chrome Canary, 개발자 또는 베타를 기본 개발 브라우저로 사용하는 것이 좋습니다. 이러한 Preview 채널을 통해 최신 DevTools 기능에 액세스하고 최첨단 웹 플랫폼 API를 테스트하며 사용자보다 먼저 사이트에서 문제를 찾을 수 있습니다.

Chrome DevTools 팀에 문의하기

다음 옵션을 사용하여 게시물의 새로운 기능과 변경사항 또는 DevTools와 관련된 다른 모든 것에 대해 논의합니다.

  • crbug.com을 통해 제안이나 의견을 제출해 주세요.
  • DevTools에서 옵션 더보기   더보기   > 도움말 > DevTools 문제 보고를 사용하여 DevTools 문제를 신고합니다.
  • @ChromeDevTools로 트윗을 보냅니다.
  • DevTools의 새로운 기능 YouTube 동영상 또는 DevTools 팁 YouTube 동영상에 의견을 남겨주세요.