>웹 프론트엔드 >CSS 튜토리얼 >JS에서 CSS를 다시 생각하다

JS에서 CSS를 다시 생각하다

王林
王林원래의
2024-09-12 16:18:551186검색

0. 소개

웹 개발 세계에서 CSS는 사용자 인터페이스를 아름답고 기능적으로 만드는 핵심 요소입니다.

그러나 웹 애플리케이션의 복잡성이 증가함에 따라 CSS 관리가 점점 더 어려운 작업이 되었습니다. 스타일 충돌, 성능 저하, 유지 관리의 어려움은 많은 개발자의 관심사입니다.

이러한 문제가 프로젝트 진행을 방해하고 있나요? (이미지 출처)

Rethinking CSS in JS

이 기사에서는 이러한 문제를 해결하기 위한 새로운 접근 방식, 특히 JS의 CSS에 대해 자세히 설명합니다.
CSS의 역사적 배경을 시작으로 현대적인 스타일링 방법부터 미래의 디자인 시스템까지 광범위한 주제를 다루고 있습니다.

글의 구성은 다음과 같습니다.

  1. JS에서 CSS의 정의와 배경
    • 1. JS에서 CSS란 무엇인가요?
    • 2. JS에서 CSS의 배경
  2. CSS와 디자인의 역사적 맥락
    • 3. CSS의 배경
    • 4. 디자인의 배경
    • 5. 디자인시스템의 배경
  3. 스타일 관리 방법 분석 및 새로운 제안
    • 6. 스타일 관리는 어떻게 이루어졌나요?
    • 7. 스타일은 어떻게 관리해야 하나요?
  4. JS에서 CSS의 구체적인 구현 계획
    • 8. 왜 JS에서 CSS를 사용하나요?
    • 9. 프로젝트 민초를 소개합니다
    • 10. JS의 CSS 친화적 CSS
    • 11. JS에서 확장 가능한 CSS
  5. 디자인 시스템과의 통합
    • 12. 디자인 시스템을 위한 JS의 CSS

특히, 본 글에서는 SCALE CSS 방법론과 StyleStack이라는 새로운 개념을 소개하고, 이를 기반으로 한 민초 프로젝트를 제안합니다. CSS 친화적이고 확장 가능한 CSS를 JS로 구현하는 것을 목표로 합니다.

이 글의 궁극적인 목적은 개발자, 디자이너 및 기타 웹 프로젝트 이해관계자에게 더 나은 스타일링 솔루션의 가능성을 제시하는 것입니다.

이제 본문에서 JS의 CSS 세계를 더 깊이 파헤쳐 보겠습니다. 긴 여정이 되겠지만, 새로운 감동과 도전의 기회를 선사하시길 바랍니다.

1. JS에서 CSS란 무엇인가요?

JS의 CSS는 JavaScript(또는 TypeScript) 코드 내에서 CSS 스타일을 직접 작성할 수 있는 기술입니다.
별도의 CSS 파일을 만드는 대신 JavaScript 파일의 구성 요소와 함께 스타일을 정의할 수 있습니다.

/** @jsxImportSource @emotion/react */
import { css } from "@emotion/react";

const buttonStyles = (primary) => css({
  backgroundColor: primary ? "blue" : "white",
  color: primary ? "white" : "black",
  fontSize: "1em",
  padding: "0.25em 1em",
  border: "2px solid blue",
  borderRadius: "3px",
  cursor: "pointer",
});

function Button({ primary, children }) {
  return (
    <button css={buttonStyles(primary)}>
      {children}
    </button>
  );
}

function App() {
  return (
    <div>
      <Button>Normal Button</Button>
      <Button primary>Primary Button</Button>
    </div>
  );
}

JavaScript에 통합할 수 있으니 확실히 편리해 보이죠?

2. JS에서 CSS의 배경

CSS in JS는 페이스북 개발자 Vjeux의 'React: CSS in JS – NationJS' 프레젠테이션에서 소개되었습니다.

CSS-in-JS가 해결하고자 했던 문제는 다음과 같습니다.
Rethinking CSS in JS

더 구체적으로 어떤 문제가 있나요?
JS의 CSS는 이를 어떻게 해결하나요?

다음 표에 정리했습니다.

Problem Solution
Global namespace Need unique class names that are not duplicated as all styles are declared globally Use Local values as default
- Creating unique class names
- Dynamic styling
Implicit Dependencies The difficulty of managing dependencies between CSS and JS
- Side effect: CSS is applied globally, so it still works if another file is already using that CSS
- Difficulty in call automation: It's not easy to statically analyze and automate CSS file calls, so developers have to manage them directly
Using the module system of JS
Dead Code Elimination Difficulty in removing unnecessary CSS during the process of adding, changing, or deleting features Utilize the optimization features of the bundler
Minification Dependencies should be identified and reduced As dependencies are identified, it becomes easier
to reduce them.
Sharing Constants Unable to share JS code and state values Use JS values as they are, or utilize CSS Variables
Non-deterministic Resolution Style priority varies depending on the CSS loading order - Specificity is automatically calculated and applied
- Compose and use the final value
Breaking Isolation Difficulty in managing external modifications to CSS (encapsulation) - Encapsulation based on components
- Styling based on state
- Prevent styles that break encapsulation, such as .selector > *

But it's not a silverbullet, and it has its drawbacks.

  1. CSS-in-JS adds runtime overhead.
  2. CSS-in-JS increases your bundle size.
  3. CSS-in-JS clutters the React DevTools.
  4. Frequently inserting CSS rules forces the browser to do a lot of extra work.
  5. With CSS-in-JS, there's a lot more that can go wrong, especially when using SSR and/or component libraries.

Aside from the DevTools issue, it appears to be mostly a performance issue.
Of course, there are CSS in JS, which overcomes these issues by extracting the CSS and making it zero runtime, but there are some tradeoffs.
Here are two examples.

  1. Co‑location: To support co-location while removing as much runtime as possible, the module graph and AST should be analyzed and build times will increase. Alternatively, there is a method of abandoning co-location and isolating on a file-by-file basis, similar to Vanilla Extract.
  2. Dynamic styling restrictions: The combination of build issues and the use of CSS Variables forces us to support only some representations, like Styling based on props in Pigment CSS, or learn to do things differently, like Coming from Emotion or styled-components. Dynamicity is also one of the main metrics that can be used to distinguish between CSS in JS.

Therefore, pursuing zero(or near-zero) runtime in CSS-in-JS implementation methods creates a significant difference in terms of expressiveness and API.

3. The background of CSS

3.1 The Beginning of CSS

Where did CSS come from?
Early web pages were composed only of HTML, with very limited styling options.

<p><font color="red">This text is red.</font></p>
<p>This is <strong>emphasized</strong> text.</p>
<p>This is <em>italicized</em> text.</p>
<p>This is <u>underlined</u> text.</p>
<p>This is <strike>strikethrough</strike> text.</p>
<p>This is <big>big</big> text, and this is <small>small</small> text.</p>
<p>H<sub>2</sub>O is the chemical formula for water.</p>
<p>2<sup>3</sup> is 8.</p>

For example, the font tag could change color and size, but it couldn't adjust letter spacing, line height, margins, and so on.

You might think, "Why not just extend HTML tags?" However, it's difficult to create tags for all styling options, and when changing designs, you'd have to modify the HTML structure itself.
This deviates from HTML's original purpose as a document markup language and also means that it's hard to style dynamically.

If you want to change an underline to a strikethrough at runtime, you'd have to create a strike element, clone the inner elements, and then replace them.

const strikeElement = document.createElement("strike");
strikeElement.innerHTML = uElement.innerHTML;
uElement.parentNode.replaceChild(strikeElement, uElement);

When separated by style, you only need to change the attributes.

element.style.textDecoration = "line-through";

If you convert to inline style, it would be as follows:

<p style="color: red;">This text is red.</p>
<p>This is <span style="font-weight: bold;">bold</span> text.</p>
<p>This is <span style="font-style: italic;">italic</span> text.</p>
<p>This is <span style="text-decoration: underline;">underlined</span> text.</p>
<p>This is <span style="text-decoration: line-through;">strikethrough</span> text.</p>
<p>This is <span style="font-size: larger;">large</span> text, and this is <span style="font-size: smaller;">small</span> text.</p>
<p>H<span style="vertical-align: sub; font-size: smaller;">2</span>O is the chemical formula for water.</p>
<p>2<span style="vertical-align: super; font-size: smaller;">3</span> is 8.</p>

However, inline style must be written repeatedly every time.
That's why CSS, which styles using selectors and declarations, was introduced.

<p>This is the <strong>important part</strong> of this sentence.</p>
<p>Hello! I want to <strong>emphasize this in red</strong></p>
<p>In a new sentence, there is still an <strong>important part</strong>.</p>

<style>
strong { color: red; text-decoration: underline; }
</style>

Since CSS is a method that applies multiple styles collectively, rules are needed to determine which style should take precedence when the target and style of CSS Rulesets overlap.

CSS was created with a feature called Cascade to address this issue. Cascade is a method of layering styles, starting with the simple ones and moving on to the more specific ones later. The idea was that it would be good to create a system where basic styles are first applied to the whole, and then increasingly specific styles are applied, in order to reduce repetitive work.

Therefore, CSS was designed to apply priorities differently according to the inherent specificity of CSS Rules, rather than the order in which they were written.

/* The following four codes produce the same result even if their order is changed. */
#id { color: red; }
.class { color: green; }
h1 { color: blue; }
[href] { color: yellow; }

/* Even if the order is changed, the result is the same as the above code. */
h1 { color: blue; }
#id { color: red; }
[href] { color: yellow; }
.class { color: green; }

However, as CSS became more scalable, a problem arose..

3.2 Scalable CSS

Despite the advancements in CSS, issues related to scalability in CSS are still being discussed.
In addition to the issues raised by CSS in JS, several other obstacles exist in CSS.

  1. Code duplication: When writing media queries, pseudo-classes, and pseudo-elements, a lot of duplication occurs if logic is required.
  2. Specificity wars: As a workaround for name collisions and non-deterministic ordering, specificity keeps raising the specificity to override the style. You can have fun reading Specificity Battle!
  3. Lack of type-safety: CSS does not work type-safely with TypeScript or Flow.

These issues can be addressed as follows:

  1. Code duplication: Use nesting in CSS preprocessors, etc.
  2. Specificity wars: Atomic CSS is defined for each property separately, so it has the same specificity except for the loading order and !important.
  3. Lack of type-safety: Just use CSS in JS with type-safe support.

Expressing layout is another hurdle in CSS, made more complex by the interactions between various properties.
Rethinking CSS in JS

CSS might seem simple on the surface, it's not easy to master. It is well known that many people struggle even with simple center alignment(1, 2). The apparent simplicity of CSS can be deceptive, as its depth and nuances make it more challenging than it initially appears.

For example, display in CSS has different layout models: block, inline, table, flex, and grid.
Imagine the complexity when the following properties are used in combination: box model,Responsive design, Floats, Positioning, transform, writing-mode, mask, etc.

As project scale increases, it becomes even more challenging due to side effects related to DOM positioning, cascading, and specificity.

Layout issues should be addressed through well-designed CSS frameworks, and as previously mentioned, using CSS in JS to isolate styles can mitigate side effects.

However, this approach does not completely solve all problems. Style isolation may lead to new side effects, such as increased file sizes due to duplicate style declarations in each component, or difficulties in maintaining consistency of common styles across the application.
This directly conflicts with the design combinatorial explosion and consistency issues that will be introduced next.

For now, we can delegate layout concerns to frameworks like Bootstrap or Bulma, and focus more on management aspects.

4. The background of Design

At its core, CSS is a powerful tool for expressing and implementing design in web development.

There are many factors to consider when creating a UI/UX, and the following elements are crucial to represent in your design:
Rethinking CSS in JS

  1. Visual Design
    • Layout: Determines the structure of the screen and the placement of elements. Consider spacing, alignment, and hierarchy between elements.
    • Color: Select a color palette that considers brand identity and user experience. Understanding of color theory is necessary.
    • Typography: Choose fonts and text styles that match readability and brand image.
    • Icons and Graphic Elements: Design intuitive and consistent icons and graphics.
  2. Interaction Design
    • Design the behavior of UI elements such as buttons, sliders, and scrollbars.
    • Provide a natural user experience through animations and transition effects.
    • Consider responsive design that adapts to various screen sizes.
  3. Information Architecture
    • Design the structure and hierarchy of content to allow users to easily find and understand information.
    • Design navigation systems to enable users to easily move to desired locations.
  4. Accessibility and Usability
    • Pursue inclusive design that considers diverse users. i18n can also be included.
    • Create intuitive and easy-to-use interfaces to reduce users' cognitive load.
  5. Consistency and Style Guide
    • Create a style guide to maintain consistent design language throughout the application.
    • Develop reusable components and patterns to increase efficiency.

Accurately expressing various design elements across diverse conditions presents a significant challenge.
Consider that you need to take into account devices (phones, tablets, laptops, monitors, TVs), input devices (keyboard, mouse, touch, voice), landscape/portrait modes, dark/light themes, high contrast mode, internationalization (language, LTR/RTL), and more.
Moreover, different UIs may need to be displayed based on user settings.

Therefore, Combinatorial Explosion is inevitable, and it's impossible to implement them one by one manually. (image source)

Rethinking CSS in JS

As a representative example, see the definition and compilation of the tab bar layout in my Firefox theme.
Despite only considering the OS and user options, a file of about 360 lines produces a compilation result reaching approximately 1400 lines.

The conclusion is that effective design implementation needs to be inherently scalable, typically managed either programmatically or through well-defined rulesets.
The result is a design system for consistent management at scale.

5. The background of Design System

Design systems serve as a single source of truth, covering all aspects of design and development from visual styles to UI patterns and code implementation.

Rethinking CSS in JS

According to Nielsen Norman Group, a design system includes the following:

  • Style Guides: Documentation that provides style guidance on specific style needs, including brand, content, and visual design.
  • Component library: These specify reusable individual UI elements, for example buttons. For each UI element, specific design and implementation details are provided, including information like attributes that can be customized(size, copy, etc), different states(enabled, hover, focus, disabled), and the reusable clean, tight code for each element.
  • Pattern library: These specify reusable patterns, or groups of individual UI elements taken from the component library. For example, you might see a pattern for a page header, which could be made up of a title, a breadcrumb, a search, and a primary and secondary button.
  • Design resources: For designers to actually use and design with the components and libraries, a design file is required (usually in Figma). Resources such as logos, typefaces and fonts, and icons are usually also included for designers and developers to use.

Design systems should function as a crossroads for designers and developers, supporting functionality, form, accessibility, and customization.
But designers and developers think differently and have different perspectives.

Let's use components as a lens to recognize the differences between designers' and developers' perspectives!!

5.1 Component Structure

The designer should also decide which icon will be used for the checkbox control.
Rethinking CSS in JS

Designers tend to focus on form, while developers tend to focus on function.
For designers, a button is a button if it looks inviting to press, while for developers, it's a button as long as it can be pressed.

If the component is more complex, the gap between designers and developers could widen even further.

Rethinking CSS in JS

5.2 Designer considerations

  • Visual options: The appearance changes according to the set options such as Primary, Accent, Outlined, Text-only, etc.
    Rethinking CSS in JS

  • State options: The appearance changes depending on the state and context
    Rethinking CSS in JS

  • Design Decision: Determining values with Component Structure, Visual/State Options, Visual Attributes(Color, Typography, Icon, etc) and more.

5.3 Developer considerations

  • Option: Configurable initial values. Visual options are also included. Ex) Outlined, Size
  • State: Changes based on user interaction. Ex) Hover, Pressed, Focused, Selected(Checked)
  • Event: Actions that trigger a change in state. Ex) HoverEvent, PressEvent, FocusEvent, ClickEvent
  • Context: Conditions injected from code that affect behavior. Ex) Readonly, Disabled

The final form is a combination of Option, State, and Context, which results in the combinatorial explosion mentioned above.

Of these, Option aligns with the designer's perspective, while State and Context do not.
Perform state compression considering Parallel states, Hierarchies, Guards, etc to return to the designer perspective.
Rethinking CSS in JS

  • Enabled: Disabled OFF, Pressed OFF, Hovered OFF, Focused OFF
  • Hovered: Disabled OFF, Pressed OFF, Hovered ON
  • Focused: Disabled OFF, Pressed OFF, Focused ON
  • Pressed: Disabled OFF, Pressed ON
  • Disabled: Disabled ON

6. How were styles being managed?

As you may have realized by now, creating and maintaining a high-quality UI is hard work.

So the various states are covered by the state management library, but how were styles being managed?
While methodologies, libraries, and frameworks continue to emerge because the solution has not yet been established, there are three main paradigms.
Rethinking CSS in JS

  1. Semantic CSS: Assign class based on the purpose or meaning of the element.
  2. Atomic CSS: Create one class for each style(visual) attribute.
  3. CSS in JS: Write in JavaScript and isolate CSS for each component unit.

Among these, CSS in JS feels like a paradigm that uses a fundamentally different approach to expressing and managing styles.
This is because CSS in JS is like mechanisms, while and Semantic CSS and Atomic CSS are like policies.
Due to this difference, CSS in JS needs to be explained separately from the other two approaches. (image source)

Rethinking CSS in JS

When discussing the CSS in JS mechanism, CSS pre/post processors may come to mind.
Similarly, when talking about policies, 'CSS methodologies' may come to mind.

Therefore, I will introduce style management methods in the following order: CSS in JS, processors, Semantic CSS and Atomic CSS, and other Style methodologies.

6.1 CSS in JS

Then, what is the true identity of CSS in JS?
The answer lies in the definition above.

Write in JavaScript and isolate CSS for each component unit.

  1. CSS written in JavaScript
  2. CSS isolation at the component level

Among these, CSS isolation can be sufficiently applied to existing CSS to solve Global namespace and Breaking Isolation issues.
This is CSS Modules.

Rethinking CSS in JS

Based on the link to the CSS in JS analysis article mentioned above, I have categorized the features as follows.
Each feature has trade-offs, and these are important factors when creating CSS in JS.

6.1.1 Integration

Particularly noteworthy content would be SSR(Server Side Rendering) and RSC(React Server Component).
These are the directions that React and NEXT, which represent the frontend, are aiming for, and they are important because they have a significant impact on implementation.

  • IDE: Syntax highlighting and code completion
  • TypeScript: Whether it's typesafe
  • Framework
    • Agnostic: Framework independent, libraries like StyledComponent are designed specifically for React.
    • SSR: Extract styles as strings when rendering on the server and support hydration
    • RSC: RSC runs only on the server, so it cannot use client-side APIs.

Server-side rendering creates HTML on the server and sends it to the client, so it needs to be extracted as a string, and a response to streaming is necessary. As in the example of Styled Component, additional settings may be required. (image source)

Rethinking CSS in JS

  1. Server-side style extraction
    • Should be able to extract styles as strings when rendering on the server
    • Insert extracted styles inline into HTML or create separate stylesheets
  2. Unique class name generation
    • Need a mechanism to generate unique class names to prevent class name conflicts between server and client
  3. Hydration support
    • The client should be able to recognize and reuse styles generated on the server
  4. Asynchronous rendering support
    • Should be able to apply accurate styles even in asynchronous rendering situations due to data fetching, etc.

Server components have more limitations. [1, 2]
Server and client components are separated, and dynamic styling based on props, state, and context is not possible in server components.
It should be able to extract .css files as mentioned below.

Rethinking CSS in JS

  1. Static CSS generation
    • Should be able to generate static CSS at build time
    • Should be able to apply styles without executing JavaScript at runtime
  2. Server component compatibility
    • Should be able to define styles within server components
    • Should not depend on client-side APIs
  3. Style synchronization between client and server
    • Styles generated on the server must be accurately transmitted to the client

6.1.2 Style Writing

As these are widely known issues, I will not make any further mention of them.

  • Co-location: Styles within the same file as the component?
  • Theming: Design token feature supports
  • Definition: Plain CSS string vs Style Objects
  • Nesting
    • Contextual: Utilize parent selectors using &
    • Abitrary: Whether arbitrary deep nesting is possible

6.1.3 Style Output and Apply

The notable point in the CSS output is Atomic CSS.
Styles are split and output according to each CSS property.

Rethinking CSS in JS

  • Style Ouput
    • .css file: Extraction as CSS files