본문 바로가기
    개발/React

    리액트 스타일링과 컴포넌트 설계의 모든 것 - fsd 부분 작성중

    by 지미의 생각 2024. 6. 18.

    react의 스타일링 방식들

    대표적으로 기존의 css in js와 css/scss + module, 떠오른 tailwind가 있습니다.

    사용량, 만족도

    styled-component 와 css module의 사용량과 만족도는 다음과 같습니다. (22년 기준)

    https://2023.stateofcss.com/ko-KR/

    사용량
    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

    js에서 css를 정의하여 변수, 객체 처럼 사용할 수 있습니다. 가장 많이 쓰는 종류로 styled-component와 emotion이 있으며, 기본적으로는 같은 사용법이나 emotion에 몇몇의 차이점이 있습니다.

    emotion에서 가능한 것

    • inline으로 css를 넣을 수 있고, inline으로 넣은 css는 inline으로 들어가지 않으며 선택한 div의 속성을 들어가게 된다.
      <div
        className={css`
          background-color: hotpink;
          &:hover {
            color: ${color};
          }
        `}
      >
        This has a hotpink background.
      </div>
    • 스타일링 선언시에 for문과 같은 제어문을 사용할 수 있다.

    css ( scss, module )

    확장자가 css. scss로 끝나는 파일을 별도로 만들어서 사용합니다. spa는 node환경을 지원하기 때문에 일반 css는 사용을 안하고(사용할 경우는 reset이나 base 처럼 기본 설정만 하는 파일) scss나 module.scss로 활용합니다.
    module이 붙은 파일은 class명을 해쉬처리 해줍니다.

    scss

    파일의 확장자를 .scss로 지정하여 scss문법을 사용할 수 있고, 별도의 export 이 없이 import './header.scss 처럼 import 하여 사용합니다.
    만약 경로가 맞지만 연결이 되지 않는다면 node-sass와 같은 별도의 sass npm을 install 해야 합니다.

    module

    header.module.scss와 같이 scss확장자 앞에 module이 추가로 붙는 파일명을 가집니다.

    일반적인 파일들과 같이 import 하여 사용하지만 정의된 클래스를 별로도 연결해줘야 합니다.

    import s from './header.module.scss';
    
    <div className={s.header} />

    만약 classname이 '-'가 들어간다면 s[header-wrap]의 형식으로 사용합니다.

    <div className={s.[header-wrap]} />

    m-n 형식의 class명을 사용하기 위해서 [] 를 넣는 것은 새로운 코드 작성방식이 추가됨과 같아 가독성을 저하시킵니다.

    • classname을 props로 받을경우와 한 dom에 []가 포함된 classname이 들어갈경우 코드의 가독성 저하
    • ui 상태를 classname으로 변경시 복잡한 코드 - props로 변경되는 is-상태 들과 같이 쓸 경우 classnames 같은 묶는 npm과 쓰지 않으면 위와 같이 가독성 저하

    className의 bind를 이용해서 사용할 수 도있습니다.

    import classNames from 'classnames/bind';
    
    const cx = classNames.bind(styles);
    
    <div className={cx('checkbox')}>

    bind를 이용한 방법은 여러개의 classnames 를 이용하기 위함이지만 이렇게 따로 분리하는것보다는 classnames를 이용하는것이 가독이 더 좋습니다.

    tailwind

    css의 여러 방법론 중 유틸리티 클래스명의 관점에 중점을 두고 있는 개념으로 나온 방식입니다.
    js에서 해석이 되며 기본적으로 rem단위를 사용합니다. css를 작성할때 필수적으로 사용하는 emmet의 약어와는 달리 독자적인 약어를 사용하고있고, 별도의 설정, 테마, 기타 유틸리티 npm들을 사용할 수 있습니다.
    css 파일이 아닌 js등 실제 렌더링 하는 코드에 유틸리티 클래스를 넣어서 사용합니다.

    atomic css

    ㅡ 설명 필요 ㅡ

    open-props

    https://open-props.style/
    2023 년자로 새롭게 소개 되었습니다.

    css 기반으로 global css의 개념으로 var로 사용하는 기능입니다.
    이 방식은 기존에도 css 방식일 경우 필요하다면 사용되었던 방식이었어서 크게 뜨진 못할것 같습니다.

    장단점

    css in js

    장점

    상태의 유연함

    js에서 스타일링을 하기 때문에 다양한 상태에 대해 대처가 가능합니다. 예를 들어 슬라이드같은 ui에서 버튼을 드래그했을때 버튼의 위치의 값을 실시간으로 받아서 사용할 수 있고,
    dom의 높이를 실시간으로 받아서 사용할 수 있습니다.

    js 기반이기 때문에 props로 상태를 전달합니다.

    해쉬처리된 클래스명

    선언한 클래스는 실제 렌더링시 해쉬처리된 클래스로 변경됩니다. 그에 따라 클래스명끼리 중복되어 클래스명이 오버라이딩될 위험이 없습니다.

    독립적인 ui

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

    const ButtonWrap = styled.div``
    const Button = styled.button``
    
    <ButtonWrap>
      <Button></Button>
    <ButtonWrap>

    단점

    js 기반

    js 기반이기 때문에 컴파일이 css가 아닌 js에서 먼저 되어 렌더링 측면에서 많은 문제를 야기합니다.

    • css보다 더 느린 렌더링 속도
      [이미지추가]
    • 렌더링 하는 순간에 컴파일이 되기 때문에 개발자모드등에서 상태를 변경하며 확인이 불가능합니다. is_active 등 특정 상태에 대한 ui를 확인하려면 코드에 직접 삽입 후 다시 컴파일을 돌려야 합니다.
    • 사용자의 이벤트에 의한 ui변경이 많을 수록 로딩 속도 저하

    불편한 관리

    파일관리

    import 와 export를 하는 js 파일이기 때문에 한 스타일 파일내에서 정의된 스타일이 많을 수록 export 와 import 하는 양이 많아집니다.

    폴더관리

    스타일 파일은 기본적으로 렌더링 하는 파일과 별도로 두어야 관리가 편합니다. (로직 이나 구조 수정과 동시에 일을 처리할 수 있음.)
    만약 한 컴포넌트 폴더 안에 스타일용 js파일과 렌더링용 js파일이 있다면 그 스타일용 js파일은 그 폴더안에서만 쓴다고 생각할 수 있습니다.
    그래서 그 컴포넌트가 아닌 별도의 layout이나 스타일용 폴더를 만들어 그 폴더안에 스타일용 js를 몰아서 넣는 경우가 많은데요.
    이렇게 되면 하나의 페이지나 컴포넌트를 만들때마다 한 파일내에서 import 하는 경로가 많아집니다.

    해쉬처리된 classname으로 인한 ui 디버깅의 불편함

    한 페이지 안에 리스트 -> 아이템 -> 이미지라는 컴포넌트가 있다고 가정해봅시다.

    • 디자인에서 아이템부분의 ui 상태 추가가 있습니다.
      is_active같은 상태용 classname을 사용해서 상태를 변화하거나 props를 전달해서 상태를 변화할텐데요.
      변화된 환경을 디자인이나 기획에 테스트 요청을 보내기 위해선 배포를 해야만 합니다.(스토리북을 사용하여 컴포넌트 단위로 한다해도 배포가 필요)
      기능에 따른 ui변화 확인에 조건이 들어가있다면(로그인을 해야한다. 특정 조건이 만족되어야 한다) 개발자모드에서 수정하여 확인이 어렵습니다.
    • 디자인에서 아이템부분의 ui 수정이 있습니다.
      수정을 하기 위해서 이 아이템이 실제 코드에서 어떤 파일인지 알아야 하기 위하여 개발자모드를 열면 해쉬처리된 클래스명으로 실제 사용되고 있는 클래스명을 찾을수가 없고, 속성으로 찾기엔 너무 많습니다.
      이 문제는 확장프로그램인 LocatorJS 로 해결할 수 있습니다.미리어쿼리를 포함한 반복적인 코드에 대한 확장성이 낮습니다.
      미디어쿼리는 대부분 const 로 정의하여 사용하지만 reduce와 같은 함수를 사용하지 않으면 미디어쿼리 선언 코드가 길어지고, 특정 해상도에서만 수정이 필요할 경우 scss보다 더 많은 코드작성이 필요합니다.
    • 낮은 확장성

    css/scss

    장점

    사용하기 간편

    베이스가 되는 css에 집중하고 있기 때문에 css in js 에서 불편했던 미디어쿼리나 자동완성등에 강합니다.

    독립적인 관리포인트

    js파일과는 별도로 css파일이 관리되기 때문에 로직이나 렌더링과 동시작업을 진행할 수 있습니다.

    단점

    class명의 중복 가능성

    module이 아닌 css/scss일 경우 다른 파일에 같은 classname을 쓰고 있을 경우 override될 가능성이 높습니다.
    scss자체로는 class명이 중복되는 것을 100% 피할 순 없습니다. 컴포넌트 마다 고유의 class명을 줄 순있으나 중복되지 않는다는 것을 장담할 수가 없습니다.
    다만 module.scss를 쓰게 되면 컴파일 시 '파일명_클래스명_해시처리된 텍스트'로 클래스명이 자동으로 만들어져서 코드의 위치를 찾을때도 더 편리하고, class명이 중복되진 않을까 하는 걱정에서 완전히 벗어나게 됩니다.

    다양한 상태의 대처 어려움

    사용자의 progessbar나 div 의 높이 구분같은 동적인 부분이 필요한 부분을 실시간으로 위치 추적하기가 어렵습니다.
    이 경우에는 사용자 지정 css를 이용한 hooks를 만들어서 사용할 수 있습니다.

    import { useState, useEffect } from "react";
    
    function useScreenSize(property, attr) {
      useEffect(() => {
        document.documentElement.style.setProperty(
          `${property}`,
          `${attr}`
        );
      }, [attr, property]);
      return [];
    }
    
    export default useScreenSize;
    import useScreenSize from './useScreenSize';
    
    const Component = () => {
      useEffect(() => {
        setHeight(wrapRef?.current?.clientHeight);
      }, [wrapRef?.current?.clientHeight]);
        useScreenSize(`"--snackbar", ${height}px`);
    }

    module

    파일명.module.css/scss로 사용할 수 있습니다.
    렌더링시 파일명_클래명_해쉬변수 로 변환됩니다.

    기존 css/scss의 장점들을 다 가지고 있고, 파일명_클래스명으로 수정위치 찾기도 편합니다.
    렌더링시 단점이었던 class명의 중복 가능성이 완벽히 없어집니다.

    하지만 다양한 상태의 대처 어려움은 아직 존재합니다.

    공통된 단점

    네이밍 고민

    css in js와 scss 는 속성을 정의하는 변수가 필요합니다.
    scss는 class명 안으로 속성이 정의되고, css in js는 const 로 선언된 styled안으로 변수가 선언됩니다.

    선택자 관리

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

    이 경우 비슷한 코드들이 우후죽순 생겨납니다.
    사용되지 않는 코드가 발생될 확률도 여전히 있습니다.

    tailwind

    장점

    간단한 사용

    스타일링을 위한 별도의 파일이 필요하지 않고 렌더링 하는 dom 자체에 유틸리티 클래스를 넣습니다. 그에 따라 파일 관리 포인트가 줄어들게 됩니다.
    margin-top: 10px같은 코드를 mt-10처럼 사용하여 코드의 양이 줄고 빠르게 ui를 입힐 수 있습니다.

    단점

    간단하지 않은 사용

    기본 단위가 rem이어서 px을 사용하려면 설정 파일을 이용하여 rem에서 px로 변환해야 합니다.
    기존에 px을 rem으로 바꿔서 사용했다면(body에서 설정하는 방식) 대부분 10px을 10rem으로 사용했을 겁니다. 설정하지 않은 tailwind의 rem은 16px이 기준이고, 그 값을 변경할 수는 없습니다.

    // tailwind.config.ts
    const px0_10 = { ...Array.from(Array(11)).map((_, i) => `${i}px`) };
    const px0_100 = { ...Array.from(Array(101)).map((_, i) => `${i}px`) };
    const px0_1000 = { ...Array.from(Array(1001)).map((_, i) => `${i}px`) };
    const px0_10000 = { ...Array.from(Array(10001)).map((_, i) => `${i}px`) };
    
    module.exports = {
      content: [
        "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
        "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
        "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
      ],
      theme: {
        extend: {
          top: px0_10,
          right: px0_100,
          width: px0_1000,
          maxWidth: px0_10000,
          minWidth: px0_10000,
          maxHeight: px0_10000,
          minHeight: px0_10000,
          height: px0_1000,
          padding: px0_100,
          border: px0_10,
          borderRadius: px0_100,
          spacing: px0_100,
          backgroundColor: {
            dim: "rgba(0, 0, 0, 0.3)",
          },
          fontFamily: {
            noto: ["Noto Sans KR", "sans-serif"],
          },
          fontSize: {
            xs: "12px",
            sm: "14px",
            base: "16px",
            lg: "18px",
            xl: "20px",
            "2xl": "22px",
            "3xl": "24px",
          },
          colors: {
            primary: "#6366f1",
            secondary: "#29303d",
            red: "#ff0000",
          },
          saturate: {
            10: ".1",
            30: ".3",
            50: ".5",
            70: ".7",
          },
        },
      },
      plugins: [],
    };

    css 속성이 적을 경우엔 빠르게 작업 할 수 있으나

    미디어쿼리, hover 등의 css기능들과 말줄임, grid 속성들이 들어간다면 tailwind의 자체적인 문법으로는 복잡한 부분이 있습니다.

    추적 및 수정

    고유한 class명이 없기 때문에 개발자모드로 본 후 추적하여 수정읗 하기엔 css in js 처럼 어려움이 있습니다.

    이 부분은 css in js 와 마찬가지로 locatorJS라는 확장프로그램을 이용하여 추적할 수 있습니다.

    클래스명이 길어질 수 있습니다.

    호버나 미디어쿼리등 특정 조건을 만족하기 위한 상태가 필요하다면 classname이 엄청나게 길어집니다. 이 classname은 @tailwind components 로 묶을 수 있으나 이 코드는 '공통 파일'에만 선언이 가능합니다. 덧붙여 @tailwind components 로 공통으로 관리할 필요가 있는지 판단을 해야 하는 순간부터 관리가 어려워집니다.
    관리자나 협업자가 2명 이상일 경우 '공통 파일'이 git에서 충돌이 나거나 관리 포인트가 꼬일 수 있습니다.

    공통된 부분은 글로벌로 설정된 css에서 @layer components 와 @apply 를 이용해서 묶을 수 있습니다.

    @layer components {
    .flex-center {
        @apply flex justify-center items-center;
      }
    }

    하지만 관리포인트가 한 파일이다 보니 협업시에는 문제가 될 수 있고, 작성된 class명들이 묶을 class명인지 안묶어도 되는 class명인지 구분이 어렵습니다.
    만약 안묶어도 되는 class명이지만 반응형, hover, focus-within 같은 의사클래스가 들어갈수록 배로 길어집니다.

    class명 props전달의 불편

    기존 classnames에 익숙한 사람은 기존방법대로 쓰면 적용이 안됩니다.

    // page.tsx
    <Button className={closeBtn}>버튼</Button>
    
    // Button.tsx
    type buttonType = {
      className?: string;
    }
    
    const Button = ({className}:buttonType) => {
      return (
        <button className={`text-primary ${className}`}>
        </button>
      )
    }

    tailwind가 아닌 다른 2종류에선 받은 classname그대로 사용할 수 있지만, tailwind는 props로 받은 classname이 오버라이딩이 되지 않고, 원본이 적용이 됩니다.

    받은 prop로 오버라딩을 하려면 twMerge 라는 기능을 이용해야 합니다.

    npm i tailwind-merge

    로 설치할 수 있으며,

    import { twMerge } from "tailwind-merge";
    
    const Button = ({className}:buttonType) => {
      return (
        <button className={twMerge(`px-10 py-4 box-border cursor-pointer whitespace-pre rounded-4`, className)}>
        </button>
      )
    }

    로 사용할 수 있습니다.

    다른 방법은

     const Input =
      ({ label, children, className, ...attr }: InputType) => {
        const inputClass = "hidden peer";
        const labelClass = `group hover:bg-gray-50 flex items-center justify-between px-4 py-2 border-2 rounded-lg cursor-pointer text-sm ${className}`;
    
        return (
          <div>
            <label htmlFor={attr.id} className={labelClass}>
              <input className={inputClass} {...attr} />
            </label>
          </div>
        );
      };

    이렇게 쓰는 방법도 있습니다.

    코드 비교

    tailwind는 유틸리티 css이기 때문에 tailwind를 제외한 css in js와 scss의 비교입니다.

    변수 사용

    이미지 loop

    서버에서 받아오는 이미지인 경우 map안에 넣지만, 서버에 넣을 필요가 없는 이미지들(색상만 같은 비슷한 아이콘등) 을 사용할 경우 입니다.

    // ⓐ 이미지 파일을 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;
        }
    `;
    // case 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;
                    }
           }
        }
    }
    // case 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);
    }

    svg 컴포넌트

    위의 이미지처럼 svg 로 된 이미지파일의 내부의 색상을 변경하며 사용하고 싶을 경우

    // imgs/index.ts
    import HeartIcon from './HeartIcon'
    import FillHeartIcon from './FillHeartIcon'
    
    export { SearchIcon, BookmarkIcon, HeartIcon, FillHeartIcon }
    
    // imgs/HeartIcon.tsx
    import React from 'react';
    
    const HeartIcon = ({ width = 24, height = 24, color }: IconType) => {
      return (
        <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width={width} height={height}><g id="SVGRepo_bgCarrier" strokeWidth="0"></g><g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M4.8824 12.9557L10.5021 19.3071C11.2981 20.2067 12.7019 20.2067 13.4979 19.3071L19.1176 12.9557C20.7905 11.0649 21.6596 8.6871 20.4027 6.41967C18.9505 3.79992 16.2895 3.26448 13.9771 5.02375C13.182 5.62861 12.5294 6.31934 12.2107 6.67771C12.1 6.80224 11.9 6.80224 11.7893 6.67771C11.4706 6.31934 10.818 5.62861 10.0229 5.02375C7.71053 3.26448 5.04945 3.79992 3.59728 6.41967C2.3404 8.6871 3.20947 11.0649 4.8824 12.9557Z" stroke={color || '#323232'} strokeWidth="2" strokeLinejoin="round"></path> </g></svg>
      );
    };
    
    export default HeartIcon;
    @function icon($iconName,$color) {
        $colors: '%23#{$color}';
        $iconList: (
            person:"%3Csvg fill='#{$colors}' viewBox='0 0 56 56' xmlns='http://www.w3.org/2000/svg'%3E%3Cg id='SVGRepo_bgCarrier' stroke-width='0'%3E%3C/g%3E%3Cg id='SVGRepo_tracerCarrier' stroke-linecap='round' stroke-linejoin='round'%3E%3C/g%3E%3Cg id='SVGRepo_iconCarrier'%3E%3Cpath d='M 28.0117 27.3672 C 33.0508 27.3672 37.3867 22.8672 37.3867 17.0078 C 37.3867 11.2187 33.0274 6.9297 28.0117 6.9297 C 22.9961 6.9297 18.6367 11.3125 18.6367 17.0547 C 18.6367 22.8672 22.9961 27.3672 28.0117 27.3672 Z M 13.2930 49.0703 L 42.7305 49.0703 C 46.4101 49.0703 47.7226 48.0156 47.7226 45.9531 C 47.7226 39.9062 40.1523 31.5625 28.0117 31.5625 C 15.8477 31.5625 8.2774 39.9062 8.2774 45.9531 C 8.2774 48.0156 9.5898 49.0703 13.2930 49.0703 Z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E"
            );
        $icon: map-get($iconList, $iconName);
    @return url("data:image/svg+xml;utf8,#{$icon}");
    }
    
    div { background:icon(person,'002C5F') no-repeat center center / 24px 24px; }

    keyframe

    import styled, { keyframes } from 'styled-components';
    
    const fadeIn = keyframes`
      from {
        opacity: 0;
      }
      to {
        opacity: 1;
      }
    `;
    
    const AnimatedDiv = styled.div`
      animation: ${fadeIn} 2s ease-in-out;
      width: 100px;
      height: 100px;
      background-color: hotpink;
    `;
    
    <AnimatedDiv />
    @keyframes delayShow {
        0% {
            opacity: 0;
            bottom: -10px;
        }
        100% {
            opacity: 1;
            bottom: 0;
        }
    }
    
    span {
        animation: delayShow 0.5s ease backwards
    }

    mixin

    // css in js에서는 mixin 개념이 없고 위쪽의 props를 받아서 쓰는 방식으로 사용합니다.
    @mixin button($width: '20px') {
     // 각종 속성 및 함수들
        width: $width;
    }
    
    div {
        @include button();
    }

    extend

    const ListbuttonStyle = css`
        display: block;
    `
    const Listbutton = styled.div`
        ${ListbuttonStyle}
    `
    
    <Listbutton></Listbutton>
    @mixin button($width: '20px') {
     // 각종 속성 및 함수들
        width: $width;
    }
    
    div {
        @include button();
    }

    상태변화

    is_active, is_select같은 ui의 상태변화의 경우

    import styled, { css } from 'styled-components';
    
    // 함수를 별도로 만들 경우
    const List = (type) => {
      if (type === 'true') {
        return css`
          color: red;
        `;
      }
      if (type === 'false') {
        return css`
          color: blue;
        `;
      }
    };
    
    const Li = styled.li`
      &.is-true {
        ${List('true')}
      }
      &.is-false {
        ${List('false')}
      }
      &.is_select {
        // 속성을 여기에 추가하세요
        // 예시:
        font-weight: bold;
      }
    `;
    
    // ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
    
    // 별도로 만들지 않는 경우
    const Li = styled.li`
      &.is-true {
        ${({ type }) =>
          type === 'true' &&
          css`
            color: red;
          `}
      }
      &.is-false {
        ${({ type }) =>
          type === 'false' &&
          css`
            color: blue;
          `}
      }
      &.is_select {
       // 속성
      }
    `;
    @mixin List($type) {
        @if( $type == 'true') {
            color: red;
        }
        @if ($type == 'false') {
            color: blue;
        }
    }
    
    li {
        &.is-true {
            @include List('true');
    }
        &.is-false {
            @include List('false');
        }
        // 공통으로 사용되지 않는 경우 
        &.is_select {
            // 속성
        }
    }

    scoped

    파일명.scoped.css / scss 로 사용하는 방법도 있긴 하지만 지금은 거의 사용되지 않습니다.
    module과 비슷하나 class명 자체에 해쉬값이 붙지 않고 data-로 붙게 됩니다.

    참고링크
    https://min9nim.github.io/2020/04/react-scoped-css/
    https://github.com/gaoxiaoliangz/react-scoped-css/tree/main/examples/simple

    혼합 사용

    성격이 다른 css in js와 scss의 장점들을 이용해 혼합하여 사용할 수 있습니다. ( tailwind까지 같이 사용 수 있습니다.)
    혼합사용시 포인트가 더 늘어나게된다. 혼동이 된다. 등의 우려가 있을 수 있지만 각각의 성격대로 사용하게 되면 필요한 영역에 필요한 기술을 사용하는 것이 자연스레 구분이 됩니다.

    장점

    기존 scss로 작업시 단순히 ui만 변경되는 부분일 때 위 코드 부분의 type mixin 을 통하여 받는 변수에 따라 분기를 달리하여 ui를 변경하였는데,
    이 방식을 사용할 경우. 만들어진 컴포넌트에 어떤 ui 테마가 있는지 사용하는 개발자는 파악이 어려웠습니다.
    그래서 단순히 테마만 있는 ui는 styled component에 개발자가 신경을 쓰지 않고, 사용하지 않는 ui는 scss에 정리하여 스타일을 분리했습니다.

    css in js + scss

    변하지 않는 고정적인 속성은 css에서, 변하는 요소가 많은 속성은 css in js에서 사용하는 것입니다.

    예시 코드

    button.tsx

    import React from 'react';
    import styled, { css } from 'styled-components';
    import s from './button.module.scss';
    
    type StyledButtonType = {
      size?: 'small' | "medium";
      children: ReactNode;
      theme?: 'primary' | 'secondry'
    }
    
    // props와 type으로 구분된 애들은 styled로 작성. 
    const Button = styled.button<StyledButtonType>`
      ${props => props.size == 'small'
        && css`
          font-size: 14px;
          height: 60px;
        `
      }
      ${props => props.size == 'medium'
        && css`
          font-size: 16px;
          height: 80px;
        `
      }
    `
    
    const StyledButton = ({ size = 'medium', children, theme = 'primary' }: StyledButtonType) => {
      return (
        <Button size={size} className={s.button}>
          {children}
        </Button>
      );
    };
    
    export default StyledButton;
    // 공통된 애들은 module.scss에 작성
    .button {
      display: inline-flex;
      padding: 0 8px;
    }

    css in js + scss + tailwind

    css in js + scss에 tailwind가 포함된 구조는 기존 css in js + scss에서 한가지만 더 추가됩니다.
    ( 기존에 특정 부분만 margin을 수정한다던지 하는 ) 관리가 필요하지 않은 특정 스타일을 inline으로 어쩔 수 없이 사용했던 부분을 tailwind의 기능으로 사용합니다.

    예시 코드

    Apage.tsx, Bpage.tsx

    Apage에 있는 리스트만 margin-top 20px

    // APage.tsx
    <List className="mt-20" />
    
    // BPage.tsx
    <List className="" />

    기타

    사용되지 않는 css 탐색

    tailwind는 빌드시에 사용되지 않는 class명일 경우 사용되지 않으나 그 외의 css in js는 아래와 같은 방법들로 사용되지 않은 css들을 찾아낼 수 있습니다.

    chrome Coverage

    ![[Pasted image 20240611211552.png]]
    Console왼쪽의 점 3개 버튼을 눌러서 나오는 메뉴에서 Coverage 탭을 추가할 수 있으며, 각 라우트에서 사용되고 있는 css파일의 사용 비율과 용량을 확인할 수 있습니다.

    그 외

    audits, purifycss, vscode 확장프로그램 - Unused CSS Class Finder, uncss

    spa환경에서는 잘되지 않습니다.

    대표적으로 선언된 css 속성이 사용되지 않는 경우는 다음과 같습니다.

    1. 컴포넌트가 사용되지 않는 경우
    2. 컴포넌트가 많은 변경이 있을 경우
    3. 조건부 렌더링

    1 의 경우: 대부분의 경우에 컴포넌트와 스타일파일이 1:1 매칭이기 때문에 컴포넌트가 사용되지 않는다면 연결된 스타일파일도 제거하면 되고,
    만약 그렇지 않다면

    • 여러개의 js파일에 한개의 스타일파일이 연결되는 경우
      ┗ page단위로 스타일파일을 묶어서 사용하는 경우인 scss나 module방식에서 자주 보입니다. 예를 들어 mypage/login.js , mypage/coupon.js가 있다면 mypage.module.scss로 사용하는 경우를 말하는데요. 이 경우 각 컴포넌트 파일은 상단에 컴포넌트를 상징하는 독립적인 class로 선언하기 때문에 연결된 한개의 스타일 파일에서 제거 또는 수정된 class명을 수정하면 됩니다.
      예시) coupon.js 삭제된다면 coupon.js의 첫 div의 classname이 coupon일경우 -> 연결된 mypage.module.scss에서 .coupon 클래스 제거
    • 한개의 js파일에 여러개의 스타일파일이 연결되는 경우
      ┗ css in js 환경에서 많이 보입니다. 여러개의 분리된 스타일 전용 js파일 들을 만들고 만들어진 것들을 가져오기 때문에요. 이 경우에는 수정에 손이 많이 갑니다. ui 컴포넌트 하나를 제거한다면 연결된 모든 스타일 관련 js를 추적해야 합니다.

    2 의 경우:

    • 리뉴얼급으로 다 엎을 경우
    • 한 파일 내부의 구조등이 변경될 경우

    3 의 경우: 조건부 렌더링이 되는 경우는 2가지 방향이 있습니다.

    • 렌더링 이 && 와 같이 조건부인 경우
    • 렌더링은 같으나 is_처럼 상태로 인해 classname이 변경되는 경우

    2와 3의 경우는 설계와 구조에 따라, 1 과 같이 css in js 라면 많은 복잡성을 가지고, scss라면 덜한 복잡성을 가집니다.

    설계 방법과 설계 방법에 따른 활용

    지금은 추천되지 않는 container 패턴은 다루지 않습니다.

    메인 패턴

    1폴더 1 js, 1 스타일

    기본적인 components 폴더 안에 성격이 비슷한 유형끼리 폴더로 묶어 렌더파일과 스타일 파일을 분리하는 방식.

    ├── components
    │   ├── button
    │   │   ├── button.js
    │   │   └── button.스타일
    │   └── form
    │       ├── form.js
    │           │     ├── input.js
    │       └── form.스타일
    • 컴포넌트와 파일명이 매칭되기 때문에 한 파일의 스타일을 수정할때 추적이 용이합니다.
    • 렌더링, 로직 파일과 별도이기 때문에 동시 작업 이 가능합니다.

    page 구조와 동일한 componets 구조 - 도메인기반

    components 폴더 아래로 page폴더나 app폴더와 같은 구조를 만들고, 그 구조에서만 쓰는 컴포넌트가 아닐 경우 common 폴더로 감싸고 그 안에서 위의 설계방식을 사용합니다.

    • page 구조를 계속 따라가야하기 때문에 page가 늘어날수록 component 폴더 내의 구조도 동시적으로 늘어납니다.
    • 앱의 구조가 복잡해질수록 수정되야 하는 파일의 위치를 찾기 어려워집니다.
    • 앱의 구조가 단순할수록 구조 파악에 용이합니다.

    아토믹 패턴

    아토믹 패턴의 개념은 가장 작은 단위(아톰)으로부터 아톰끼리 합치고, 또 합치면서 결국 페이지까지 만들어지는 개념으로 설계된 패턴입니다.

    기본적인 구조는 아래와 같습니다.

    ├── components
    │   ├── button
    │   │   ├── button.js
    │   │   └── button.스타일
    │   └── form
    │       ├── form.js
    │           │     ├── input.js
    │       └── form.스타일
    • 컴포넌트의 최초 폴더 위치 선정에 어려움을 겪습니다.
    • 합성컴포넌트 패턴과는 어울리지 않습니다. (상속된 컴포넌트의 위치)FSD 패턴 ( #작성해야함 )
    ├── app
    ├── processes
    ├── pages
    ├── widgets
    ├── features
    ├── enttities
    │   ├── user
    │   ├── post 
    │   │   ├── ui
    │   │   ├── model
    │   │   └── api
    │   └── comment  
    └── shared

    1 depth = Layers , 2 depth = Slices, 3 Depth = Segments

    분리된 스타일 파일

    스타일을 정의한 파일만 따로 분류하는 방법입니다.

    ├── components
    │   ├── button
    │   │   └── button.js
    │   └── form
    │       ├── input.js
    │       └── form.js
    ├── style
    │   ├── button
    │   │   └── button.스타일
    │   └── form
    │           ├── input.스타일
    │       └── form.스타일
    • 위의 도메인기반과는 다르게 components의 폴더가 하나더 생기는것과 같습니다.
    • 도메인기반의 단점을 다 가지고 있습니다.
    • 도메인기반으로 분류된것도 아니어서 구조파악에 어려움을 겪습니다.서브 패턴메인 패턴 안에 일부분만 적용할 수 있는 패턴들입니다.
      서브 패턴끼리 조합 가능합니다.합성 컴포넌트컴포넌트 끼리 합성하여 종속성을 가지게 되는 개념으로 설계된 패턴입니다.
      특정 상황에서 유용하게 사용할 수 있습니다.

    기본 개념은 관련된 컴포넌트들을 묶어서 사용하여 필요할 시에 정해진 ui를 사용하거나, 내부 ui의 순서 변경등을 용이하게 하기 위한 용도입니다.

    Dialog 컴포넌트가 있고, 이 Dialog에는 title, content영역이 있습니다.
    하지만 어떤 Dialog에는 title이 없을 수도 있고, content가 없을 수도 있습니다. 이럴때 props로 보여줄 ui의 상태를 정의하기보다는 modal을 사용할때 modal 컴포넌트에 종속된 컴포넌트를 만들어서 사용할 수 있습니다.

    단순 렌더링

    // Dialog/Dialog.js
    import DialogMain from "./container/DialogMain";
    import DialogContent from "./item/DialogContent";
    import DialogTitle from "./item/DialogTitle";
    
    export const Dialog = Object.assign(DialogMain, {
      Title: DialogTitle,
      Content: DialogContent
    })

    DialogMain, DialogContent, DialogTitle 는 단순하게 children 으로 받는 부분만 있습니다.

    ![[Pasted image 20240617211617.png]]
    ctrl + space 로 힌트 보기를 하면 연결된 컴포넌트가 나옵니다.

    // page.js
    <Dialog>
        <Dialog.Title>타이틀</Dialog.Title>
        <Dialog.Content>컨텐츠</Dialog.Content>
    </Dialog>

    이벤트 전달

    이벤트 전달은 더 복잡합니다. useContext 를 사용합니다.
    ![[Pasted image 20240617212712.png]]
    위의 구조로 시작해볼까요

    //numberInput/container/Counter.js 
    import { createContext, useState } from "react";
    import './numberInput.scss';
    
    // Context생성
    export const CounterContext = createContext({
      value: 0,
      increment: () => {},
      decrement: () => {},
    });
    
    const Counter = ({ children, initValue = 0, minimum, maximum }) =>{
      const [count, setCount] = useState(initValue);
    
      const increment = () =>
        setCount((prev) => {
          if (maximum === undefined) {
            return prev + 1;
          } else {
            return prev < maximum ? prev + 1 : prev;
          }
        });
    
      const decrement = () =>
        setCount((prev) => {
          if (minimum === undefined) {
            return prev - 1;
          } else {
            return prev > minimum ? prev - 1 : prev;
          }
        });
    
      return (
        <CounterContext.Provider value={{ value: count, increment, decrement }}>
          <div className="number-input">{children}</div>
        </CounterContext.Provider>
      );
    }
    
    export default Counter;

    루트가 되는 Counter 컴포넌트에서 +, - 이벤트를 추가하고 Context값으로 넘깁니다.

    //numberInput/hook/userCounter.js
    import { useContext } from "react";
    import { CounterContext } from "../container/Counter";
    
    function useCounter() {
      const value = useContext(CounterContext);
      if (!value) {
        throw Error("Conuter Context가 없습니다.");
      }
      return value;
    }
    
    export default useCounter;

    생성된 Context를 value로 사용하는 hooks를 만듭니다.

    //numberInput/item/CounterButton.js
    import useCounter from "../hook/useCounter";
    
    const CounterButton = ({ children, type }) =>{
      const { increment, decrement } = useCounter();
      return (
        <button onClick={type === "increment" ? increment : decrement}>
          {children}
        </button>
      );
    }
    
    export default CounterButton;

    만들어진 hooks의 increment, decrement를 버튼에 연결합니다.

    //numberInput/item/CounterStatus.js
    import useCounter from "../hook/useCounter";
    
    const CounterStatus = () => {
      const { value } = useCounter();
      return (
        <div>
          <input value={value} />
        </div>
      );
    };
    
    export default CounterStatus;

    value값을 input에 넣어줍니다.

    import { createContext, useState } from "react";
    import CounterButton from "../item/CounterButton";
    import CounterStatus from "../item/CounterStatus";
    import './numberInput.scss';
    
    export const CounterContext = createContext({
      value: 0,
      increment: () => {},
      decrement: () => {},
    });
    
    const Counter = ({ children, initValue = 0, minimum, maximum }) =>{
      const [count, setCount] = useState(initValue);
    
      const increment = () =>
        setCount((prev) => {
          if (maximum === undefined) {
            return prev + 1;
          } else {
            return prev < maximum ? prev + 1 : prev;
          }
        });
    
      const decrement = () =>
        setCount((prev) => {
          if (minimum === undefined) {
            return prev - 1;
          } else {
            return prev > minimum ? prev - 1 : prev;
          }
        });
    
      return (
        <CounterContext.Provider value={{ value: count, increment, decrement }}>
          <div className="number-input">{children}</div>
        </CounterContext.Provider>
      );
    }
    
    // 여기
    Counter.Button = CounterButton;
    Counter.Status = CounterStatus;
    
    export default Counter;
    

    상속받을 컴포넌트들을 Counter 에 연결해줍니다.

    <Counter initValue={0} minimum={0} maximum={100}>
        <Counter.Button type="decrement">-</Counter.Button>
        <Counter.Status />
        <Counter.Button type="increment">+</Counter.Button>
    </Counter>

    단순히 컴포넌트와 연결된 컴포넌트를 알려주는 힌트 정도로 생각할 수 있지만, 힌트를 알려주는 jsdoc이나 typescript 와는 목적과 사용이 다릅니다.
    jsodc이나 typescript 는 컴포넌틔 타입이나 문법상의 약속을 보여주는것이고, 합성컴포넌트는 컴포넌트의 구조 자체를 알려줍니다.

    VAC 패턴

    View Asset Component의 약자로 목적은 로직과 UI를 분리하자 입니다.

    vac패턴을 도입하면 기존에 한페이지에서 통신하고, props받고 렌더링 하는 부분에서 렌더링 부분만 별도의 파일로 분리됩니다.

    import React from 'react'
    
    const Button = ({ children }) => {
      const click = e => {
        console.log('click', e.target.value)
      }
    
      return <button onClick={(e) => click(e)}>{children}</button>
    }
    
    export default Button
    

    click이벤트만 있는 button 컴포넌트를 vac로 변환해 보겠습니다.

    import React from 'react'
    import ButtonUi from './ButtonUi'
    
    const Button = ({ children }) => {
      const click = e => {
        console.log('click', e.target.value)
      }
      const props = {
        click,
        children
      }
    
      return <ButtonUi {...props} />
    }
    
    export default Button

    렌더링할 컴포넌트를 별도로 생성하고 이벤트를 묶어서 전달합니다.

    import React from 'react'
    
    const ButtonUi = props => {
      return <button onClick={e => props.click(e)}>{props.children}</button>
    }
    
    export default ButtonUi

    렌더링 하는 부분은 받은 이벤트를 연결하고 렌더링만 담당합니다.

    로직과 렌더링은 분리하면서 관심사를 분리하는것은 좋으나 파일의 양이 더 많아지며 복잡성을 더 올릴 수 도 있습니다.
    컴포넌트의 크기에 따라 패턴 사용을 선택 할 수 있습니다.

    팁 1. styled components

    media-query 간편하게 사용하기

    styled components 에서 설정을 하지 않으면 medie-query 작성이 많이 불편합니다.

    import styled from 'styled-components';
    
    const Container = styled.div`
      width: 80%;
      margin: 0 auto;
    
      @media (max-width: 1200px) {
        width: 90%;
      }
    
      @media (max-width: 900px) {
        width: 95%;
      }
    
      @media (max-width: 600px) {
        width: 100%;
      }
    `;
    
    const StyledButton = styled.button`
      background-color: blue;
      color: white;
      padding: 10px;
      border: none;
      border-radius: 5px;
      cursor: pointer;
    
      @media (max-width: 600px) {
        background-color: red;
      }
    `;
    
    const App = () => (
      <Container>
        <StyledButton>Click me</StyledButton>
      </Container>
    );
    
    export default App;

    설정 따로 하지 않은 media-query 입니다.
    @media 이후의 코드를 직접 타이핑해야 합니다.

    설정의 예시를 보여드립니다. 사용자에 따라 다를 수 있습니다.

    // assets/styles/media.ts
    import { css, CSSProp } from 'styled-components';
    
    interface Media {
      desktop: (literals: TemplateStringsArray, ...placeholders: any[]) => CSSProp;
      tablet: (literals: TemplateStringsArray, ...placeholders: any[]) => CSSProp;
      phone: (literals: TemplateStringsArray, ...placeholders: any[]) => CSSProp;
    }
    
    const sizes = {
      desktop: 1024,
      tablet: 768,
      phone: 576,
    };
    
    const media: Media = {
      desktop: (literals, ...placeholders) => css`
        @media (max-width: ${sizes.desktop}px) {
          ${css(literals, ...placeholders)}
        }
      `,
      tablet: (literals, ...placeholders) => css`
        @media (max-width: ${sizes.tablet}px) {
          ${css(literals, ...placeholders)}
        }
      `,
      phone: (literals, ...placeholders) => css`
        @media (max-width: ${sizes.phone}px) {
          ${css(literals, ...placeholders)}
        }
      `,
    };
    
    export default media;

    styled-components의 CSSProp을 이용해서 지정합니다.

    import styled, { css } from 'styled-components';
    import media from '@/styles/media';
    
    const Button = styled.button<StyledButtonType>`
      background-color: blue;
      color: white;
      padding: 10px;
      border: none;
      border-radius: 5px;
      cursor: pointer;
    
      ${media.phone`
        background-color: red;
      `}
    
      ${media.tablet`
        background-color: green;
      `}
    
      ${media.desktop`
        background-color: purple;
      `}
    `;
    
    const StyledButton = ({ size = 'medium', children, theme = 'primary' }: StyledButtonType) => {
      return (
        <Button size={size} className={s.button}>
          {children}
        </Button>
      );
    };

    실제 사용 코드입니다.

    ![[Pasted image 20240618005430.png]]
    media및 상속된 함수들도 가져올 수 있습니다.

    팁 2. scss 함수

    root class 유무 check

    원본글 : https://pantaley.com/blog/Add-class-to-the-most-outer-selector-using-Sass-mixin/

    <!-- example 1 -->
    <div class="parent">
        <div class="child"><span>Hello World<span></div>
    </div>
    
    <!-- example 2 -->
    <div class="parent bold">
        <div class="child"><span>Hello World<span></div>
    </div>
    
            <!-- example 2 -->
    <div class="parent test">
        <div class="child"><span>Hello World<span></div>
    </div>

    위와 같은 구조가 있다고 가정합니다.
    parent 클래스가 상태에 따른 클래스(bold, test)를 가졌을때 그 클래스 이하의 특정 클래스의 스타일을 변경할때
    대부분 아래처럼 기준이 되는 클래스의 바로 아래에서 depth를 추적하여 스타일을 추가했었습니다.

    .parent {
        &.test {
            .child {
                span {
                    font-weight:bold;
                }
            }
        }
        .child {
            span {
                color: gray;
            }
        }
    }

    하지만 이 함수를 이용하면 수정이 필요한 곳에서 바로 선언하여 수정할 수 있습니다.

    @mixin most-outer-selector($new-class) {
        $current-selector: &;
        $new-selector: [];
    
        @each $item in $current-selector {
            $first-item: nth($item, 1);
    
            $appended-item: $first-item + $new-class;
    
            $new-item: set-nth($item, 1, $appended-item)1;
            $new-selector: append($new-item, $new-selector);
        }
    
        @at-root #{$new-selector} {
            @content;
        }
    }

    아래처럼 사용가능합니다.

     .parent {
        .child {
            span {
                @include most-outer-selector(".bold") {
                    font-weight: bold;
                }
            }
        }
    }

    prev, next

    jquery의 prev(), next() 처럼 기준이 되는 클래스의 이전, 이후 에게 속성을 주는 함수입니다. css로 렌더링 되기 때문에 nth-child로 렌더링 됩니다.

    https://codepen.io/rudwnok/pen/abwQyPW
    리스트 선언을 함수 내부에서 동적으로 생성되게 개선하기

    @content의 다른 형식

    기존의 mixin과 include가 바뀐 방식입니다.
    [예제 1-1]

    <div class="alpha">alpha</div>
    <div class="beta">beta</div>
    
    
    <div class="alpha1">alpha1</div>
    <div class="beta1">beta1</div>
    @mixin example {
      .alpha {
        @content (alpha);
      }
      .beta {
        @content (beta);
      }
      .koma {
        @content (koma);
      }
    }
    
    @include example using ($slot) {
      @if $slot == alpha {
        color :red;
      } @else if $slot == beta {
        color :blue;
      } @else if $slot == koma {
        color :blue;
      }
    }

    팁 3. tailwind

    말줄임 표시는 truncate

    자르다의 뜻을 가진 truncate로 말줄임을 사용할 수 있고, 이 클래스 안에는

    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; 

    가 들어있습니다.

    n번째 줄부터 말줄임은 line-clamp-n

    2번째 줄부터 말줄임은line-clamp-2로 사용할 수 있습니다.

    container-queries

    vieport 기준이 아닌 특정 요소의 크기에 따라 반응적으로 적용할 수 있는 속성인 css 의 container query를 제공하고 있습니다.

    아직 빨간불이 많은 기능입니다.

    ![[Pasted image 20231126014105.png]]

    tailwind.config.js 에 추가해줍니다.

    module.exports = {
      content: ["./src/**/*.{js,jsx,ts,tsx}"],
      plugins: [require("@tailwindcss/container-queries")], // 추가
    };

    실제 적용 코드는 이렇습니다.

    <div className="@container">
    <div className="@[60em]:max-w-[--card-width]"> </div> // @container의 60em 사이즈가 됬을때 max-width를 --card-width만큼 설정
    </div>

    grid

    기존 css 속성인

    grid-template-columns: repeat(auto-fill,minmax(280px,1fr))

    을 tailwind에서 사용하려면

    <div className="grid-cols-[repeat(auto-fill,minmax(294px,1fr))]"></div>

    의 형식으로 사용할 수 있습니다.

    확장프로그램

    tailwind-merge

    tailwind는 받은 props를 classname으로 인식하지 않습니다. classnames 처럼 받은 classname를 classname으로 인식하게 해줍니다.

    tailwind-fold

    길어진 클래스명을 ... 으로 보여주지만, twMerge 를 통한 prop로 전달받는 classname의 형식일 경우 접히지 않습니다.

    ![[Pasted image 20240617160652.png]]
    오히려 숨겨진 class명으로 div태그가 아니라면 다행이지만 div 태그라면 이 dom이 어떤 용도로 사용되어있는지 파악이 어렵습니다.

    headwind

    tailwind classname들의 순서를 정렬해주며 중복된 classname을 제거해줍니다.

    Tailwind CSS IntelliSense

    tailwind의 classname의 힌트를 보여줍니다.

    Tailwind Documentation

    vscode 앱 내에서 Tailwind의 문법을 검색할수 있는 창을 열어줍니다.

    tailwind 를 사용하려면 위 확장프로그램이 강제됩니다.

    댓글