css in js vs css 비교 1차

jiimy·2023년 1월 31일
0

react-style

목록 보기
1/1
post-thumbnail

지금부터 제가 전달할 내용은 react, 그리고 next에서 사용한 css in js(styled-component, emotion)와 css(module css, scss)의 비교, 장단점에 대해 경험한 내용입니다.

이 내용들이 새로 프로젝트를 시작할 때 어떤 스타일링 설계 방법을 선택해야 하는지, 지금 우리 프로젝트는 좋은 설계 방법인지에 대해 조금의 도움이라도 되는 마음으로 내용을 시작하겠습니다.

react에서 주로 사용되는 스타일링 방식은 scss와 css in js인 styled-components가 대표적입니다.

사용량, 만족도

styled-component 와 css module의 사용량과 만족도는 다음과 같습니다.

사용량
styled-component

출처: state of css 2022

emotion

출처: state of css 2022

css module

출처: state of css 2022

사용량에서 styled-component와 module이 상위권 인데에 반해

만족도
styled-component

출처: state of css 2022

emotion

출처: state of css 2022

css module

출처: state of css 2022

만족도부분에서 styled-component와 emotion은 하락세에 있습니다.

국내 기업의 사용 빈도 입니다.

styled-component 국내 기업들 사용 빈도

출처: 코드너리
emotion 국내 기업들 사용 빈도

출처: 코드너리

248개의 기업중 186개의 국내 기업이 사용중입니다.

이렇듯 css in js와 css는 가장 많은 곳에서 사용되고 있습니다.

css in js 의 장점

중복되지 않는 class명

사용시에 정의가 되어서 class명이 중복되진 않을까 하는 고민이 덜게 됩니다.

독립적인 ui

css는 html의 선택자입니다. css만 정의되어있다고 해서 dom에 스타일을 줄 수가 없지만,
css in js는 그 자체로도 dom을 생성하며, 스타일이 정의됩니다.
정의된 스타일 만으로 dom의 구조를 직접 만들 수가 있습니다.

const ButtonWrap = styled.div``
const Button = styled.button``

<ButtonWrap>
  <Button></Button>
<ButtonWrap>

유동적인 변화에 대응

props로 값을 받게 되어 여러 상태에 대한 대응이 용이합니다.

css in js 의 단점

해시처리된 class명

[해시처리된 이미지]
렌더 과정에서 class명이 hash처리되어 추적이 불가능합니다. 새로운 사람이 와서 유지보수를 하거나 다른 작업자가 있을 경우 이 부분이 어디인지 추적을 하려면, 그 안에 있는 content를 찾아서 code내에서 검색하여 추적해야 합니다.

단점 개선 방안

해시 처리된 class명으로 인해 추적이 불편한부분을 추적용으로 classname을 추가해줍니다. 이 classname은 직접적인 style이 들어가지않고 단순 추적용입니다.

import, export

선언된 스타일이 많을 경우, 스타일부분만 별도로 파일을 분리합니다.
하지만 분리할경우 사용하는 선언된것들을 export 하고 import 해야하는 수고스러움이 있습니다.

ui 상태에 대한 확인의 불편함

여러 class방법론들과 상태를 변경하는 class들과의 조합이 좋지 않습니다.
특정 ui가 특정 상태를 가지게 되어 ui가 변경되었을때 smascss의 is-active처럼 ui변경용 클래스를 넣게 되는데, js단게에서 렌더링 되다보니 개발자모드에서 class를 넣다뺏다 하며 ui변경에 대한 확인이 어렵습니다.

테스트를 하려면 실제 코드에 첨부 후 확인해야 됩니다.

단점 개선 방안

ui가 변경되는 상태를 관리해주는 별도의 스위치용 ui를 만들어서 사용합니다.

낮은 확장성

미디어쿼리를 포함한 반복되는 코드에 대한 확장성이 낮습니다.
미디어쿼리는 대부분 const 로 정의하여 사용하지만 reduce와 같은 함수를 사용하지 않으면 미디어쿼리 선언 코드가 길어지고, 특정 해상도에서만 수정이 필요할 경우 scss보다 더 많은 코드작성이 필요합니다.

css의 장점

render에만 집중

scss에서 내장된 for문이나 if문들을 통해 반복된 스타일에 대한 정의, 함수처리 등 여러 기능을 사용할 수 있습니다.

간편한 사용

위의 render에만 집중한다와 비슷한 내용이지만 css in js에서의 미디어쿼리는 자동완성이 되지 않고, 긴 코드를 작성해야 합니다.

css의 단점

중복된 class명 발생

class명을 여러 파일에서 썼을시 비슷한 부분의 class명이 중복되어 오버라이드 될 가능성이 있습니다. 만약 오버라이드가 발생한다면 기존 class명, 이후에 오버라이드된 class명 둘중 하나를 수정해야하거나, class명의 depth가 길어 질 수 있습니다.

단점 개선 방안

module 을 사용하여 해쉬처리된 값을 뒤에 붙여줄 수 있습니다.

다양한 상태에 대한 대처

상태를 props로 받는 css in js와는 달리 여러 상태에 대한 대처가 유동적이지 않습니다. 상태가 추가 될때마다 상태에 대한 스타일정의를 새로 작성해야 합니다.

단점 개선 방안

사용자 지정 css로 일부 개선할 수 있습니다.

공통된 단점

변수명 및 class명에 대한 고민

css in js나 css로 작성시 변수명이나 class명에 대한 고민이 필요한것은 같습니다.

const **Input** = styled.input``
.**input** {
  // 속성
}

위 코드들의 bold체가 된 부분은 ai나 코파일럿이 짜주지 않습니다.
결국 선택자가 들어갈 변수는 코드를 직접 짜는 사람이 짜야 하는데, input 컴포넌트를 만드는데 const를 bb로 해야될지, InputWrap으로 해야될지, input으로 해야될지는 직접 짜는 사람이 고민을 해야 됩니다.

선택자 관리

선택자, 즉 dom과 매치되는 속성들이 많을 수록 더울 더 관리가 필요한것은 마찬가지입니다.
A라는 사람이 특정 반복되는 부분을 줄이기 위해서 a 라는 코드를 만들었습니다. 하지만 글로벌로 사용하지 않고 로컬파일에서만 사용하고 있는데,
B라는 사람도 비슷한 코드를 만들게됩니다.

이 경우 비슷한 코드들이 우후죽순 생겨납니다.

사용되어 지지 않는 코드도 발생될 확률도 여전히 있습니다.

코드 비교

이 부분의 styled component는 typescript 와 emotion환경입니다.

이미지 루프

// 1. 순회 바로 하기 
// ⓐ 이미지 변수명의 map 객체 생성
$class-img : (
 receipe,
 hospital
);

// ⓑ 실제 적용 
li {
	@for $i from 1 through length($class-img) {
  &.nth-of-type($i) {
		.img {
				display: inline-block;
				vertical-align: middle;
				width: 23px;
				height: 23px;
				background: map-get($class-img, $i);
				background-size: auto 23px;
				}
	   }
	}
}
// 2. 함수로 사용
// ⓐ 이미지 변수명의 map 객체 생성
$class-img : (
 receipe,
 hospital
);

// ⓑ 맵 순회 함수 
@mixin array ($map-name) {
  @for $i from 1 through length($map-name) {
    &:nth-of-type(#{$i}) {
      .img {
				display: inline-block;
				vertical-align: middle;
				width: 23px;
				height: 23px;
				background: map-get($class-img, $i);
				background-size: auto 23px;
				}
     }
  } 
}

// ⓒ 실제 적용
li {
	@include array($class-img);
}

css in js

// ⓐ 이미지 파일을 import 
import {
receipe,
hospital
} from './icon';

// ⓑ 불러온 파일을 배열 처리
const menuimgurl: string[] = [receipe, hospital];

// ⓒ 렌더해주는 부분
function loopRender(i: number) {
	return `
		&:nth-of-type(${i + 1}) {
			.img {
				display: inline-block;
				vertical-align: middle;
				width: 23px;
				height: 23px;
				background: url( ${menuimgurl[i]} ) no-repeat center;
				background-size: auto 23px;
			}
		}
	`;
}

// ⓓ 배열을 순회하는 함수
function arrayloop() {
	let str = "";
	for (let index = 0; index < menuimgurl.length; index += 1) {
		str += loopRender(index);
	}
	return str;
}

// ⓔ 스타일드 컴포넌트에 적용
export const Menulist = styled.li`
	${arrayloop()}
	.img {
		position: relative;
	}
`;

정해져있는 상태 받기 ( true, false 의 경우 )

이 부분에서는 탭 부분에서 탭컨텐츠가 다르게 보일경우, 또는 테마형식으로 레이아웃등이 비슷하나 일부가 다른 경우 사용하는 부분입니다. 이미 테마의 갯수나 상태가 정해져있는 경우

@mixin List($type) {
	@if( $type == 'true') {
		color: red;
	}
	@if ($type == 'false') {
		color: blue;
	}
}

li {
	&.is-true {
		@include List('true');
}
	&.is-false {
		@include List('false');
	}
}

테마(바뀔 부분)가 '확정' 적으로 정해져있다면 스타일드컴포넌트를 만들시에 타입을 지정하지만..

// ⓐ 넘겨줄 props 설정
export type ListProps = {
	eventstate? : string;
}

const List = styled.li<ListProps>`
  ${props => props.eventState == 'true'
	? 
    `color: red;
		`
	: 
		`color :blue;
		`
  }
`

<List eventstate='true'></List>
<List eventstate='false'></List>

정해져있지 않은 상태 받기

기존에 상태가 없었고 비슷하나 이전에 없던 다른 테마나 상태가 추가될 경우

css에선 타입 추가시 유동적으로 대응 가능.

.button {
	background: black;
// or 
	&.is-bg-red {
	}
}

// 클래스를 추가해서 작업
.btn-bg-red {
	background: red;
}

<button class="button"></button> => <button class="button btn-bg-red"></button> 

테마(바뀔 부분)가 '확정' 적으로 정해져있다면 스타일드컴포넌트를 만들시에 타입을 지정하지만..

// ⓐ 넘겨줄 props 설정
export type ListProps = {
	eventstate? : string;
}

const List = styled.li<ListProps>`
  ${props => props.eventState == 'true'
	? 
    `color: red;
		`
	: 
		`color :blue;
		`
  }
`

<List eventstate='true'></List>
<List eventstate='false'></List>

타입이 아에 없는 상태에서 추가를 하게 된다면 기존 스타일드컴포넌트 파일에 타입을 넣는 부분을

수정해야 한다.

// 선언
const btn = styled.button`
	background: black;
`
// 사용
<btn></btn>

// 재선언 및 타입을 추가
type btn = {
	theme: string;	
}
const btn = styled.button<btn>`
	background: black;
  color: ${props => props.theme == 'red' ? 'red' : '';
`

// 타입 연결
<btn theme=""></btn>

키프레임

@keyframes delayShow {
	0% {
		opacity: 0;
		bottom: -10px;
	}
	100% {
		opacity: 1;
		bottom: 0;
	}
}

span {
	animation: delayShow 0.5s ease backwards
}
import { css, keyframes } from "@emotion/core";

const delayShow = keyframes`
	0% {
		opacity: 0;
		bottom: -10px;
	}
	100% {
		opacity: 1;
		bottom: 0;
	}
	`;

<span css={css`
	animation: ${delayShow} 0.5s ease backwards
`}>
</span>

믹스인

@mixin button($width: '20px') {
 // 각종 속성 및 함수들
	width: $width;
}

div {
	@include button();
}
// css in js에서는 mixin 개념이 없고 위쪽의 props를 받아서 쓰는 방식으로 사용합니다. 

extend

한파일내에서 반복되는 부분이 있다면 코드의 라인수를 줄이기 위해 사용될수도 있지만, 테마가 다른 탭같은 경우는 mixin안에 두고 타입을 받아 변동하는것으로 extend를 대체할 수 있습니다.

@extend listbutton{
  display: block;
}

.listbutton {
	%listbutton;
}

스타일드 컴포넌트에서는 자주 쓰이는 편입니다.

const ListbuttonStyle = css`
	display: block;
`
const Listbutton = styled.div`
	${ListbuttonStyle}
`

<Listbutton></Listbutton>

react에서의 스타일링 정답은?

위의 분석 내용처럼 프로젝트의 예상개발범위를 예측하여 성격, 규모에 따 설계방법이 달라질 수 있습니다.

혼합사용

제가 선택한 방법은 혼합사용하는 패턴입니다.

사용하지 않을 때의 문제점

기존 scss로 작업시 단순히 ui만 변경되는 부분일 때 위 코드 부분의 type mixin 을 통하여 받는 변수에 따라 분기를 달리하여 ui를 변경하였는데,

이 방식을 사용할 경우. 만들어진 컴포넌트에 어떤 ui 테마가 있는지 사용하는 개발자는 파악이 어려웠습니다.

그래서 단순히 테마만 있는 ui는 styled component에. 개발자가 신경을 쓰지 않고, 사용하지 않는 ui는 scss에 정리하여 스타일을 분리했습니다.

예시 코드를 보겠습니다.

// ⓐ 
&.shop {
	position: fixed;
	width: calc(100% - 200px);
	min-height: 52px;
	padding: 9px 16px;
	z-index: 12;
}

// ⓑ
&.is-active {
	background: #f6f7ff;
	color: #4d5eff;
}

위 코드에서 ⓐ와 ⓑ는 어떤 차이점이 있을까요?

ⓐ는 어떠한 ‘클래스’가 shop 클래스를 가지고 있을 때에 대한 ‘테마’ 이고, ⓑ는 어떠한 ‘컴포넌트’가 is-active 라는 클래스를 가지고 있을 때에 대한 ‘상태’ 로 구분할 수 있습니다.

여기에서 저는 css인데도 불구하고 ‘테마’와 ‘상태’ 라는 말을 사용하였는데, 좀 더 깊게 설명하자면

‘테마’ : 하나의 레이아웃에 여러 타입이 있는 경우( 헤더 - 상단에 고정된 헤더, 고정되지 않은 헤더, 모바일 헤더 등… )

‘상태’ : 하나의 컴포넌트로 구분될수 있는 모듈에 대한 그 컴포넌트가 가진 상태

테마

테마는 한번 만들어놓으면 거의 수정되거나 변동될 일이 없으므로 정의된 컴포넌트를 불러올때 (ex :

) 이 컴포넌트의 props를 보내 정해진 테마를 사용하고 그 테마는 컴포넌트 내부에 interface로 정의가 되어있으면 개발자가 컴포넌트를 불러올때 어떠한 테마가 지금 정의되 있고, 이 테마를 사용만 하면된다라고만 인식이 되면 ui 개발자는 더이상 관여를 할 필요가 없습니다.

type Theme = 'fixed' | 'main';

interface HeaderProps {
  theme?: Theme;
}

const Headers = styled.div`
  ${(props: any) =>
    props.theme &&
    css`
    background-color: red;
    font-size: 32px;
  `};
`;

const Header = ({
  theme
}: HeaderProps) => {
  return <Headers theme={theme} classname="a">헤더</Headers>;
};

export default Header;	
// headerstyle.ts

테마에 대한 속성들은 코드라인이 길고, 경우의 수는 n개 이상이어서 별도의 js스타일파일로 생성합니다.

상태

상태는 input, button 처럼 최소한의 기능을 가진 작은 유닛에 대한 각자의 고유한 상태를 말하고, 이런 각자의 상태들은 이 작은 유닛들이 적용되는 부분마다 미세하게 다르거나,

button의 여러 타입들과 같이 사용되어 더 많은 타입들이 나올 수도 있습니다. ( small-btn, medium-btn, large-btn * active, disable, default 등 )

단순 hover나 토글같은 경우는 css파일에서 선언하고,
상태에 대한 값들이 3가지 이상이라면 사용자가 추후 상태에 대한 경우의 수를 추가하기 용이하게 컴포넌트 내부에 추가합니다.

혼합 사용의 예

// 기본구조
import React, { useState, useEffect } from "react";
import cn from "classnames";
import styled from "styled-components";
import useScreenSize from "../../hooks/useCssVar";
import "./timerprogress.scss";

const TimerProgress = ({ open = false, timer = 1, count = 10 }) => {
  const [getOpen, setGetOpen] = useState(false);

  useEffect(() => {
    setGetOpen(open);
  }, [open, timer]);

  return (
    <ProgressWrap
      className={cn("timer-progress", { "is-on": getOpen })}
      count={count}
      timer={timer}
    >
      {[...Array(parseInt(count))].map((n, index) => (
        <div
          className="timer-progress-item"
          data-time={index + 1}
        ></div>
      ))}
    </ProgressWrap>
  );
};

export default TimerProgress;
// 스타일드 컴포넌트로만 만들었을 경우
import React, { useState, useEffect } from "react";
import cn from "classnames";
import styled from "styled-components";
import useScreenSize from "../../hooks/useCssVar";
import "./timerprogress.scss";

function loopRender(i, count) {
  let second = i / count;
  return `
		&:nth-child(${i + 1}) {
      background: #ccc;
      transition-delay: ${second}s;
    }
	`;
}

function arrayloop(props) {
  let str = "";
  for (let index = 0; index < props.count; index++) {
    str += loopRender(index, props.count);
  }
  return str;
};

const ProgressWrap = styled.div`
  &.is-on {
    .timer-progress-item {
      ${(props) => arrayloop(props)};
    }
  }
`;

const TimerProgress = ({ open = false, timer = 1, count = 10 }) => {
  const [getOpen, setGetOpen] = useState(false);
  
  useEffect(() => {
    setGetOpen(open);
  }, [open, timer]);

  return (
    <ProgressWrap
      className={cn("timer-progress", { "is-on": getOpen })}
      count={count}
      timer={timer}
    >
      {[...Array(parseInt(count))].map((n, index) => (
        <div
          className="timer-progress-item"
          data-time={index + 1}
        ></div>
      ))}
    </ProgressWrap>
  );
};

export default TimerProgress;
// scss를 사용할 경우
const TimerProgress = ({ open = false, timer = 1, count = 10 }) => {
  const [getOpen, setGetOpen] = useState(false);

  // NOTE: scss로 했을 경우
  useScreenSize(count, "--progressCount");
  useScreenSize(timer, "--progressTimer");
  useEffect(() => {
    setGetOpen(open);
  }, [open, timer]);

  return (
    <ProgressWrap className={cn("timer-progress", { "is-on": getOpen })}>
    {[...Array(parseInt(count))].map((n, index) => (
        <ProgressItem
          className="timer-progress-item"
          data-time={index + 1}
          count={count}
        ></ProgressItem>
      ))}
    </ProgressWrap>
  );
};

export default TimerProgress;
// scss를 사용할 경우
.timer-progress {
  &.is-on {
    .timer-progress-item {
      // NOTE: 사용자 지정 css인 var 사용 불가능
      @for $i from 1 through 10 {
        &:nth-child(#{$i}) {
          background: #ccc;
          &[data-time='#{$i}'] {
            transition-delay: calc(#{$i} * (var(--progressTimer) / var(--progressCount)) * 1s);
          }
        }
      }
    }
  }
}

next

next에서 scss를 사용할 경우 파일명.scss 는 불가하며 파일명.module.scss로 작성해야 합니다.

module.scss로 작성할 경우 아래와 같이 파일명_클래스명__해쉬 값으로 컴파일됩니다.

이렇게 작성될 경우 styled-component의 단점인 추적을 개선하며, 중복된 클래스가 적용되는 css의 문제도 개선되지만,
중복을 막게되어 컴포넌트에서 children으로 받는 dom의 스타일을 줄때 아래와 같은 방식으로 하면 선택이 되지 않습니다.

 // button.jsx
import s from "./toast.module.scss";

const Toast = ({ children }) => {
  return (
    <div className={s.toast}>
      {children}
    </div>
  );
};

export default Toast;
 // main.jsx
 <Toast>
 	<div className="item">aasdfsdf</div>
 </Toast>
// toast.module.scss
.toast {
  position: fixed;
  right: 0;
  left: 0;
  bottom: 20px;
  width: 300px;
  margin: auto;
  .item {
    color: red;
  }
}

이 경우에는 scss의 문법인 global을 사용하시면 적용할 수 있습니다.

// global 사용
.toast {
  position: fixed;
  right: 0;
  left: 0;
  bottom: 20px;
  width: 300px;
  margin: auto;
  :global(.item) {
    color: red;
  }
}

global은 해당 class를 전역으로 사용하게 만들어줍니다.

profile
let me introduce test

0개의 댓글