웹 개발 세계에서 CSS는 사용자 인터페이스를 아름답고 기능적으로 만드는 핵심 요소입니다.
그러나 웹 애플리케이션의 복잡성이 증가함에 따라 CSS 관리가 점점 더 어려운 작업이 되었습니다. 스타일 충돌, 성능 저하, 유지 관리의 어려움은 많은 개발자의 관심사입니다.
이러한 문제가 프로젝트 진행을 방해하고 있나요? (이미지 출처)
이 기사에서는 이러한 문제를 해결하기 위한 새로운 접근 방식, 특히 JS의 CSS에 대해 자세히 설명합니다.
CSS의 역사적 배경을 시작으로 현대적인 스타일링 방법부터 미래의 디자인 시스템까지 광범위한 주제를 다루고 있습니다.
글의 구성은 다음과 같습니다.
특히, 본 글에서는 SCALE CSS 방법론과 StyleStack이라는 새로운 개념을 소개하고, 이를 기반으로 한 민초 프로젝트를 제안합니다. CSS 친화적이고 확장 가능한 CSS를 JS로 구현하는 것을 목표로 합니다.
이 글의 궁극적인 목적은 개발자, 디자이너 및 기타 웹 프로젝트 이해관계자에게 더 나은 스타일링 솔루션의 가능성을 제시하는 것입니다.
이제 본문에서 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에 통합할 수 있으니 확실히 편리해 보이죠?
CSS in JS는 페이스북 개발자 Vjeux의 'React: CSS in JS – NationJS' 프레젠테이션에서 소개되었습니다.
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.
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.
Therefore, pursuing zero(or near-zero) runtime in CSS-in-JS implementation methods creates a significant difference in terms of expressiveness and API.
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..
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.
These issues can be addressed as follows:
Expressing layout is another hurdle in CSS, made more complex by the interactions between various properties.
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.
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:
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)
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.
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.
According to Nielsen Norman Group, a design system includes the following:
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!!
The designer should also decide which icon will be used for the checkbox control.
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.
Visual options: The appearance changes according to the set options such as Primary, Accent, Outlined, Text-only, etc.
State options: The appearance changes depending on the state and context
Design Decision: Determining values with Component Structure, Visual/State Options, Visual Attributes(Color, Typography, Icon, etc) and more.
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.
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.
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)
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.
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.
Among these, CSS isolation can be sufficiently applied to existing CSS to solve Global namespace and Breaking Isolation issues.
This is CSS Modules.
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.
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.
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)
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.
As these are widely known issues, I will not make any further mention of them.
The notable point in the CSS output is Atomic CSS.
Styles are split and output according to each CSS property.