6 분 소요

CSS-only VS Javascript

컴포넌트 및 기능을 구현함에 있어 javascript를 사용하는 대신 pure css를 사용해서 구현해보고 차이점을 알아본다.

why css-only?

생각보다 html과 css의 기능이 강력하여 javascript 없어도 기본적인 기능들은 구현이 가능한 경우가 많다.
따라서 해석이 더 필요한 js보다 html과 css로만 구현하면 성능적인 이점을 가져갈 수 있다.

좋은 기능의 html 및 css의 예를 들면,

  1. heading section 이동

    a 태그와 href 이용하여 구성할 수 있다.

    focus되어 있을 때 stying까지 가능하여 복잡한 js를 추가하지 않아도 더 빠르고 쉽게 오히려 성능이 좋게 만들 수 있다.

    <a href="#section1">Jump to the Section1</a>
    
    <h1 id="section1">Section1</p>
    
  2. input datalist (autocomplete)

    input의 검색 기록 및 마우스로 검색어 이동 등을 담당하는 컴포넌트는 구현이 까다로우나
    html input 프로퍼티의 list와 태그인 datalist를 이용해서 구현할 수 있다.

    <label for="browser">Choose your browser from the list:</label>
    <input list="browsers" name="browser" id="browser" />
    
    <datalist id="browsers">
      <option value="Edge"></option>
      <option value="Firefox"></option>
      <option value="Chrome"></option>
      <option value="Opera"></option>
      <option value="Safari"></option>
    </datalist>
    

    https://www.w3schools.com/tags/tryit.asp?filename=tryhtml5_datalist

이렇게 추가적인 javascript 로직없이 구현하게 되면 개발 공수도 덜 들고, 성능도 훨씬 좋다.

그래서 이번에는 css는 javascript를 대체할 수 있을까에 초점을 맞추어서 기능 구현을 해보려고 한다.

Pure CSS로 구현했을 때 더 좋은 점

  • 빠른 렌더
  • 간결한 코드
  • 실패 코드의 프로덕트 영향

빠른 렌더

css는 javascript와 다르게 html과 병렬적으로 로드가 되고, 브라우저 입장에서 추가적인 해석이 필요하지도 않다.
따라서 parsing이 더 빠르고 렌더도 더 빨리된다.

css가 변경되면 리플로우, 리패인팅을 거쳐서 렌더링이 된다.
(물론 어떤 속성을 변경했느냐에 따라서 리플로우, 리페인팅 둘 다할 수 있고, 리페인팅만 할 수도 있지만)

js로 했을 경우는 html 자체가 새로 생겼다가 없어졌다가 하므로 DOM / CSSOM 생성부터 다시 될 것이다. (element 유/무)
그렇기 때문에 보통의 경우에 css로 구현했을 때 더 빠를 수 밖에 없게 된다!

간결한 코드

코드도 간결하여 굳이 상태를 저장하거나 추가적인 javascript logic을 구성하지 않아도 된다.

실패 코드가 결과물에 미치는 영향

오류가 나더라도 위험성이 덜하다.

javascript로 구현할 때 잘못 구현하거나 버그가 날 경우 에러들이 발생하게 되고, 심한 경우에는 프로덕트가 터지는 등 문제가 생길 수 있다.
그러나 css로 구현했을 때 semicolon을 안하거나 잘못 구현했다하더라도 브라우저는 속성을 무시한다.

따라서 이상한 에러를 뿜거나 예측 불가능한 행동을 하지는 않는다.

Pure CSS with React!

react로 구현을 하려고 한다.

css와 javascript를 비교한다해놓고 css-in-js를 쓰는 건 어불성설이라고 생각이 되어서, css를 사용해서 구현할 예정이다.

(사실 css in js도 성능에 대한 사람들의 회의가 꽤 있어왔고, 실제로도 체감할 수 있는 정도의 차이라고 하니 여기서 더 깊게 알아보지는 않겠지만 styled-components를 사용하지 않고 css를 사용해보았다.)

구현:

  • Toggle button toggle-button

    클릭 시 스위치와 같이 왼쪽 오른쪽으로 이동하며 on-off를 나타내주는 컴포넌트

    js:

    function Toggle() {
      return (
        <div className="toggle-container">
          <input type="checkbox" id="button" />
          <label className="switch-rail" htmlFor="button" />
        </div>
      );
    }
    
    export default Toggle;
    

    css:

    .toggle-container {
      display: flex;
      margin-top: 50px;
    }
    
    .switch-rail {
      padding: 0;
      width: 90px;
      height: 45px;
      border-radius: 22.5px;
      position: relative;
      transform: translateX(0);
      background-color: gray;
      cursor: pointer;
    }
    
    .switch-rail::after {
      content: "";
      width: 48px;
      height: 48px;
      position: absolute;
      top: -2px;
      left: -2px;
      background-color: white;
      border-radius: 50%;
      border: 1px solid purple;
      transition: all 0.5s linear;
      z-index: 1;
      display: block;
    }
    
    /*  실제 input은 보이지 않게 */
    input[type="checkbox"] {
      display: none;
    }
    
    /* click 되었을 때(checkbox가 check 되었을 때) Switch */
    input[type="checkbox"]:checked ~ .switch-rail::after {
      /* switch에서 동그란 버튼 위치 오른쪽으로 옮김 */
      left: 50px;
    }
    
  • Tooltip

    특정 컴포넌트 (여기서는 button) hover 시 설명을 나타내주는 컴포넌트

    js:

    function Tooltip() {
      return (
        <div className="tooltip-container">
          <button className="unknown-button" aria-label="아무버튼도 아니지">
            무슨 버튼일까
          </button>
        </div>
      );
    }
    
    export default Tooltip;
    

    css:

    .tooltip-container {
      margin-bottom: 100px;
    }
    
    .unknown-button {
      position: relative;
    }
    
    .unknown-button::before {
      content: attr(aria-label);
      opacity: 0;
      position: absolute;
      top: 30px;
      right: -90px;
      font-size: 14px;
      width: 100px;
      padding: 10px;
      color: #fff;
      background-color: #555;
      border-radius: 3px;
      pointer-events: none;
      line-height: 1.4em;
    }
    
    .unknown-button:hover::before {
      opacity: 1;
    }
    
  • Tab

    메뉴를 클릭하면 그에 따른 컨텐츠를 보여주는 Tab 컴포넌트

    js:

    const menu = [
      { menu: "Tab1", contents: "Tab 1 Contents" },
      { menu: "Tab2", contents: "Tab 2 Contents" },
      { menu: "Tab3", contents: "Tab 3 Contents" },
    ];
    
    function Tabs() {
      return (
        <div className="tab-container" role="tablist">
          <div className="tab-menu-wrapper">
            {menu.map((el, index) => (
              <div className="menu" key={el.menu} role="tab">
                <input
                  type="radio"
                  name="radioTab"
                  id={el.menu}
                  onChange={(e) => console.log(e.target)}
                />
                <label htmlFor={el.menu}>{el.menu}</label>
    
                <div className="tab-contents-wrapper" tabIndex={index + 1}>
                  {el.contents}
                </div>
              </div>
            ))}
          </div>
        </div>
      );
    }
    

    css:

    .tab-container {
      width: 100%;
      max-width: 640px;
      margin-bottom: 100px;
    }
    
    .tab-menu-wrapper {
      display: flex;
      width: 100%;
      font-weight: bold;
    }
    
    .menu {
      display: flex;
      flex-direction: column;
    
      width: calc(100% / 3);
      background-color: #e3e3e3;
      color: #bcbcbc;
      padding: 15px 10px;
      cursor: pointer;
      transition: all 0.3s linear;
    }
    
    .tab-contents-wrapper {
      position: absolute;
      left: 0;
      top: 5rem;
      padding: 1em 1.2em;
      color: #333;
      z-index: -1;
      opacity: 0;
    
      z-index: -1;
      opacity: 0; /* hides the tab content by default */
    }
    
    label {
      padding: 1em;
      border: none;
      border-radius: 6px 6px 0 0;
    
      background-color: #0094a7;
    
      color: #fff;
      font-size: 1em;
    
      cursor: pointer;
    }
    
    /* hide the radio buttons visually*/
    [type="radio"] {
      position: absolute;
      height: 0;
      width: 0;
      overflow: hidden;
      clip: rect(0, 0, 0, 0);
    }
    
    /* change color of active tab */
    [type="radio"]:checked ~ label {
      background: #007584;
    }
    
    /* makes the active tab's content visible */
    [type="radio"]:checked ~ .tab-contents-wrapper {
      opacity: 1;
      z-index: 1; /* increase the z-index so the content is in focus*/
    }
    
    [type="radio"]:focus + label {
      outline: 2px dotted black;
    }
    
    /* trick to avoid focus line on click*/
    [type="radio"]:focus:not(:focus-visible) + label {
      outline: 2px dotted transparent;
    }
    

이외에도:

sidebar menu open/close

accordion

image loader

심지어 modal 까지도 구현 가능하다.

보통 input radio를 통해서 구현하게 된다.

Pure CSS로만 구현하면 안 좋은 점

  • 오히려 JS가 더 빠를 수도 있음
  • 가독성 및 유지보수
  • 확장성
  • 상태 유지
  • 웹 접근성

등 꽤 많은 단점을 보유하고 계시겠다.

오히려 JS 로직으로 구현하는 것이 더 빠른 경우도 있다

Initial rendering은 구현 기능과 초기 상태값에 따라서 오히려 js로 구현하는 것이 더 빠른 경우가 있을 수 있다.

예를 들면,

어떤 특정 요소를 show / hide 하는 기능의 컴포넌트를 구현한다고 했을 때,
show hide를 css로 했을 때와 js로 상태값으로 했을 때 대충 아래와 같이 될 수 있다.

<div>
  <Component className="hide" />
</div>
<div>{showComponent && <Component />}</div>

여기서도 기본적으로는 css가 더 빠른 것은 맞다.

그런데 만약에 showComponent의 initial state가 false라면..?

그렇다면 렌더링될 때 Component 자체가 그려지지 않으므로 오히려 js로 구현했을 때가 더 빨라지게 되는 것이다.

가독성 및 유지보수

가독성과 다른 개발자가 보았을 때 이해 가능한 코드, 읽기 좋은 코드일까?

css로 구현을 하는데, 아무리 깔끔하게 classname을 부여하고, 여러 방법의 css 방법론들을 도입을 한다고해서 JS 로직보다 직관적으로 이해하기 쉬울까는 아닐 것 같다.

이렇게 되면 유지보수 또한 어려워지겠다.

확장성

토글 기능 정도면 확장성을 굳이 따져야 하나 싶지만, 구현은 확장될 가능성을 항상 열어두어야 할텐데

css로만 구현하게 된다면 그 이상의 기능들은 구현하기 어려워진다.

상태 유지

state를 저장하지 않으니 상태가 유지되거나 해야하는 상황에서는 사용하기 힘들다.

조금은 치명적인 웹 접근성 웹 접근성은 좋을까?

hover해서 tab menu를 보여주는 동작의 경우 css로 구현하면 더 빠를 수는 있겠으나 tab으로 했을 때는 hover를 css알 수 없기 때문에 키보드만 사용하는 유저에게는 메뉴를 보여주지 못하는 대참사가 발생할 수 있다.

따라서 웹 접근성 관점에서는 javascript를 사용하는 것이 더 좋다.

Pure CSS로만 구현하면 아예 안되나?

그래서 언제 사용하면 좋을까라는 의문이 든다.

칼처럼 딱 잘라서 이때는 css만 사용하는 것이 옳고, 이 때는 js만 사용하는 것이 옳은 상황은 없다.

아티클에 따르면

Javascript를 웬만해서는 사용하는 것이 좋을 때:

유저와 interaction이 있는 경우 특히 hovering, focusing, clicking 같은 경우는 javascript를 사용하는 것이 좋다고 한다.

이유는 위와 같이 접근성 문제.

CSS를 사용하면 좋을 때:

animation이나 visibility 혹은 외형적으로 변형이 있는 경우는 css를 이용하는 편이 좋겠다.

컴포넌트의 움직임이나 show/hide 같은 경우는 css를 변경하는 것이 렌더 성능에 더 좋으므로 css를 이용하는 편이 좋다.

그러나 둘을 잘 보고 사용하기 괜찮은 경우는 반대의 경우에도 사용해도 좋다!

이를 테면 tooltip 같은 경우는 hover할 때와 focus될 때 시각적으로 보여주고, 스크린 리더기에서는 aria label 등 aria tag를 이용해서 오히려 접근성과 성능 둘 다 잡을 수도 있겠다.

참고 아티클

https://gomakethings.com/when-to-use-css-vs.-javascript/

https://blog.logrocket.com/css-only-components/

https://www.youtube.com/watch?v=tKj3xsXy9KM


댓글남기기