10/24: from tailwind to vanilla-extract, 마이그레이션 후기

그른손·2023년 10월 24일
0
post-thumbnail

내 포트폴리오에 기존에 쓰고 있던 스타일링 라이브러리 테일윈드다.

테일윈드를 채택한 이유

빠르게 뚝딱뚝딱 만들기에 최적이고, 넓은 커버리지로 제공되는 유틸리티 클래스로 디자인 시스템이 있는 것처럼 일관성있는 디자인 가능하고, 새 스타일링 라이브러리 학습 필요성 느껴서

기존에 쓰면서 불편했던 것

  • 테일윈드 유틸리티 클래스를 또 배워야 한다. 그리고 유틸리티 클래스 키워드가 css랑 다르다.(rounded 등…) => 약간의 러닝 커브가 있다. (큰 문제는 아니지만)
  • DOM과 스타일링 코드가 분리되지 않는다. 만들 때는 편하지만, 코드 가독성에 악영향을 준다.
  • 적용해야되는 스타일이 많으면 스타일 코드가 엄청 길어짐. 가독성이 안좋아지고,
  • 유지보수가 불편하다. 어느 스타일 코드가 어디 부분에 있는지 찾아보는 것도 일이다.
  • 반응형 설정이 불편하다. md:font-size-lg md:font-blue-500 이런식으로 md 화면크기에 적용하고 싶은 스타일마다 md:프리픽스를 붙여줘야 한다.
    총평 : 내 취향이 아니야!

마이그레이션을 결심한 이유

한 번 뚝딱 만들고 마는 게 아니라 스타일이든 기능이든 계속 개선을 하면서 만들고 있는데, 유지보수에서 불편함을 많이 느꼈다.
또한 나만 보는 코드이긴 하지만, 가독성 좋은 코드를 작성하려고 하는 입장에서 DOM과 분리가 되지 않는 테일윈드 스타일링이 내 취향과 잘 안맞았다. (=컴포넌트에는 컴포넌트 관련 내용만 있었으면 좋겠음. 스타일 분리하고 싶음!)

바닐라 익스트랙트를 선정한 이유

원래 바닐라 익스트랙트는 블로그 앱(Vitriol)에 적용하려고 했는데, 그러려면 먼저 블로그 앱에 ts 마이그레이션을 해야 했다. 그 과정에서 d3라이브러리로 구현한 그래프에 타입을 적용하는 데 어려움을 겪어서 결국 블로그 앱에는 emotion을 사용하기로 했다.
=> 바닐라 익스트랙트의 장점(스타일링의 타입 추론, 빌드 타임 css 등)을 알고 꼭 한번 사용하고 싶었는데, 마침 포트폴리오는 타입스크립트로 구현했으니 적용하기 좋을 것이라고 생각했다.
=> 필요성보다 호기심으로 기술 스택을 선정하는 건 내 나쁜 버릇이긴 함…
원래 마이그레이션은 css-in-js로 하려고 했다. 하지만 styled-components를 쓰면서 스타일 컴포넌트에 하나하나 이름 붙이는 게 어렵기도 하고, css-in-js의 단점(런타임에서 스타일을 생성하기 때문에 상대적으로 성능이 떨어짐)을 생각하면 테일윈드에서 css-in-js로 옮겨가는 건 내가 편하게 스타일링하기 위해 성능 저하를 유발하는 꼴이 되지 않나 싶었다. 바닐라 익스트랙트는 모든 스타일 코드를 빌드타임에서 추출하여 적용하기 때문에 런타임 성능에 영향을 안 미치니까 좋을 거라고 생각했다.

어떻게 적용했는지?

원래는 테일윈드가 설치되어있는 상태에서 바닐라 익스트랙트를 점진적으로 적용하려고 했는데, 두 라이브러리를 동시에 설치해서 사용하려 하니 오류가 발생했다. 바닐라 익스트랙트에서는 스타일을 css.ts에만 정의해야 하는데, 테일윈드 스타일을 불러오면서 이와 충돌이 있었던 것으로 추정된다. 자세히는 모름…
그래서 테일윈드를 완전히 삭제하고, 바닐라 익스트랙트를 설치하고 한 컴포넌트, 한 페이지씩 스타일을 바닐라 익스트랙트로 옮겨나갔음

스타일 적용법

위에 말했듯이 css.ts에 스타일을 적용해서 export하고, 컴포넌트에서 import해서 스타일링을 적용할 태그의 className에 넣어주면 된다.
페이지의 경우 pages 디렉토리에 pages.css.ts 파일을 만들어 여러 페이지의 스타일을 한 번에 관리하고, 컴포넌트의 경우는 이미 Components 디렉토리 안에 사용처별로 서브디렉토리(Header, About, About/Skills, About/Contacts 등..)를 만들어 두었으므로, 각 서브디렉토리에 HeaderComponents.css.ts와 같은 이름으로 스타일 파일을 만들었다.
스타일파일 내에서는 다음과 같이 스타일 객체를 만들고 export한다.

export const headerStyle = {
	container: style({ position: ‘fixed’,}),
	logoSection: style({ display: ‘flex,}),
	.
	.
	.
	}

그리고 스타일 객체 변수명이 컴포넌트 명칭에 따라 엄청 길어지는 경우(projectImageSectionStyle이라든가…)가 있기 때문에, 불러와서 사용할 때는 import { Style as style } 로 축약해서 불러와서 사용한다.

//nonono
<div className={projectImageSectionStyle.container>

//yesyesyes
<div className={style.container}>

컴포넌트 안에 ui 로직이 너무 많은 경우, 부분별로 객체를 세분화했다.

export const projectInfoContainerStyle = {
  info: {
    title: style({}),
    type: style({}),
    description: style({...
    }),
    stackList: style({}),
    stackListItem: style({}),
  },
  docs: {
    container: style({}),
    title: style({}),
    list: style({}),
    listItem: style({}),
  },
  links: {
    container: style({}),
    title: style({}),
    list: style({}),
    listItem: style({}),
    nextProject: {
      container: style({}),
      title: style({}),
      link: style({}),
    },
  },
};

이런 식으로 보기 좋게 작성하려고 애썼다!

또한 빌드타임에서 css를 추출하는 것이기 때문에 각 빌드툴 별로 별도의 설정으로 바닐라 익스트랙트를 연결해줘야 한다. 나는 이번에 vite를 사용하고 있기 때문에, 바닐라 익스트랙트와 vite를 연결해주는 플러그인을 설치하고, vite-config.ts 파일에 아래와 같이 추가해줬다.

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), vanillaExtractPlugin()],
  assetsInclude: ["**/*.ttf"],
});

마이그레이션 하면서 어려웠던 점

테일윈드 스타일을 css로 옮기기

처음에 제일 불편했던 것. pt-16이 정확히 몇 rem의 padding-top을 주는 건지, rounded-sm은 border-radius로는 몇인지 하나하나 테일윈드 docs를 찾아봐야 했다. 그러다 이렇게 노가다하는거 너무 아니꼬운 사람이 변환 툴같은거 만들어놓지 않았을까? 하고 찾아보니까 tailwind-to-css라는 앱이 있어서 엄청 유용하게 썼다! css로도 바꿔주고 js 스타일 객체로도 바꿔주는데, 바닐라 익스트랙트에서는 js 스타일 객체와 같은 형식으로 스타일링하기 때문에 그냥 복붙만 하면 되서 편했다.
다만 테일윈드에서 기존 유틸리티 클래스 대신 수동 스타일링 (pt-[16px]처럼 직접 값을 작성하는 경우)에는 잘 감지하지 못하는 경우가 있어서 정신 똑띠 차리고 써야한다!
그리고 반응형의 경우 바꿔주긴 하는데, 바닐라 익스트랙트에서는 미디어쿼리 사용 방식이 해당 앱에서 제공하는 거랑 조금 달라서 수동으로 옮겨야 했다.

{
    "@media": {
      [mediaQueries.SCREEN_SM]: {
        width: "60%",
      },
      [mediaQueries.SCREEN_MD]: {
        width: "40%",
      },
    },
  }

이런 식으로 옮겼다!

그리고 반응형의 경우는 mediaQueries.ts 파일을 만들어서 상수 변수로 관리하기로 했다.

export const mediaQueries = {
  SCREEN_SM: "screen and (min-width: 640px)",
  SCREEN_MD: "screen and (min-width: 768px)",
  SCREEN_LG: "screen and (min-width: 1024px)",
  SCREEN_XL: "screen and (min-width: 1280px)",
  SCREEN_2XL: "screen and (min-width: 1530px)",
  SCREEN_3XL: "screen and (min-width: 1536px)",
  SCREEN_4XL: "screen and (min-width: 1850px)",
  SCREEN_5XL: "screen and (min-width: 2000px)",
  SCREEN_6XL: "screen and (min-width: 2300px)",
};
// 2xl과 3xl이 미묘하게 맞닿아있는 이유는 3xl부터는 내가 직접 정의한 거라서…
// 일단 마이그레이션 후에 고칠 예정

이런 식으로!
런타임의 코드와 상호작용 불가
바닐라 익스트랙트는 빌드타임에 별도의 css파일을 css.ts에서 추출하는 것이기 때문에, 런타임에서 돌아가고있는 코드와 상호작용할 수 없다. css-in-js처럼 프롭을 넘겨준다거나, 그걸로 상태 업데이트 때에 다른 스타일을 적용하려면 다른 로직을 써야 한다.

          <li
            key={skill.title}
            onClick={() => {
              onItemClick(skill);
            }}
            style={{ background: skill.bgColorCode }}
            className={`${style.skillItem} ${
              selectedSkill &&
              selectedSkill.title === skill.title &&
              style.skillItemSelected
            }
            `}
          >

그래서 런타임에서 정의된 색상 코드 변수를 직접 넣어줘야 하는 경우에는 어쩔 수 없이 style 프롭으로 인라인 스타일링을 해야 한다. 그리고 조건부 스타일링의 경우에는 위에 나온 것처럼 조건 연산자를 활용해서 특정 조건일 때 selectedStyle같은 이름으로 정의된 ‘특정 조건의 스타일’을 적용해주는 식으로 해야 했다.
이는 createTheme 등의 기능을 이용해서 테마를 생성하면 더 자연스럽게 연결이 가능한 부분인데, 여의치 않은 이유가 있었다.
1. 색상 코드 변수로 스타일링하는 경우는 두 가지다. colorScheme의 색상을 적용할 때, 그리고 firestore db에 저장되어있는 프로젝트 문서 내의 bgColorCode를 불러와서 적용할 때.
2. 두 경우 모두, 바닐라 익스트랙트와는 완전 별개로 존재하는 소스이기 때문에, 바닐라 익스트랙트 테마로 관리하려면 지금까지 만든 colorScheme 전역 상태 로직이나 db 내의 데이터를 모두 뜯어고쳐야 한다. 리소스가 엄청나게 많이 드는 작업은 아니지만, 구현 시간을 고려할 때는 확실히 손해가 더 클 것 같았다.
3. 그리고 colorScheme의 경우, 스타일 사용처가 명확히 정의되지 않은 부분이 있다. 백그라운드에 BG 색상을 넣고 color에 TEXT색상을 넣는 로직은 어디서든 재사용이 가능하니 theme으로 정의할 수 있지만, SECONDARY나 ACCENT같은 보조 색상을 뽑아와서 내 맘대로 적용하는 경우가 더러 있어서, 그런 부분까지 theme으로 세부적으로 적용하면 스타일 별로 들여야 하는 공수가 상당하겠다고 생각했다.
결국 colorScheme을 적용할 때나 프로젝트 문서의 bgColorCode를 사용할 때에만 인라인 스타일링을 하기로 했다.
또한 colorScheme이 적용되어야 하는 부분에서 인라인 스타일링이 너무 자주 등장하면 보기 안좋을 것 같아서, 백그라운드에 BG색상을 넣고 color에 TEXT 색상을 넣는 로직만 뚝 떼서 별도의 컴포넌트로 만들어줬다.

import { ReactNode } from "react";
import { useDarkMode } from "../../utils/hooks/useDarkMode";
import { useColorScheme } from "../../utils/hooks/useColorScheme";

interface BackgroundProps {
  children: ReactNode;
}

export default function Background({ children }: BackgroundProps) {
  const darkMode = useDarkMode();
  const colorScheme = useColorScheme();
  return (
    <div
      style={{
        color: darkMode ? colorScheme.DARK.TEXT : colorScheme.LIGHT.TEXT,
        background: darkMode ? colorScheme.DARK.BG : colorScheme.LIGHT.BG,
        width: "100%",
        height: "100%",
      }}
    >
      {children}
    </div>
  );
}

중복되는 스타일링 : css-in-ts 방식의 장점을 살리지 못한다?

컴포넌트 내의 특정 부분에 반복해서 등장하는 스타일이 있다. ContactsContainer나 ProfileBox, SkillsContainer의 title에 해당하는 부분이 그러한데, 기존에는 단순히 스타일만 갖고 있는 서브 컴포넌트를 만들기엔 좀 어색한 것 같아서 일일히 같은 스타일을 적용했다. (styled-components였으면 <AboutPageTitleSection>같은 컴포넌트로 만들면 됐을텐데!) 그런데 이렇게 작성해둔 코드를 단순히 1:1로 바닐라 익스트랙트로 옮기다보니, 분명 동일한 스타일인데 여러 군데에 반복해서 등장하는 경우가 좀 눈에 띄게 되었다. 이 부분은 일단 마이그레이션이 끝난 후에 보이는대로 분리해서 적용할 예정이다. (그대로 두면 하나를 수정하려 할 때 세 군데 네 군데를 똑같이 바꿔야 하는 불편이 생기니까!)

후기

마이그레이션은 문명의 충돌이다 (스승 왈)

  • 어떤 환경에서 다른 환경으로 이동하는 것은 쉬운 일이 아니다. 테일윈드에서 바닐라 익스트랙트로의 전환은 단순히 코드를 다시 작성하는 것 이상의 고민을 요구했다. 두 가지 다른 사고방식과 규칙, 원칙이 충돌하면서 생기는 문제들이 생각보다 머리 아팠고, 실제로 아직 테일윈드 시절의 로직이 그대로 적용되어있어 어색한 부분도 몇몇 남아있다.(마이그레이션이 아직 안 끝났다는 것! 정권 바뀌었으니 다 갈아엎어야겠지?)
  • 그리고 포트폴리오 앱 정도의 작은 규모에서도 이 정도라면 실제로 서비스되는 앱에서 마이그레이션을 하려면 엄청난 리소스가 들어가겠다는 생각이 들었다! 그런 만큼 새로운 게 나왔다고 휙휙 바꿀 수도 없겠지! 마이그레이션에 들어가는 리소스는 결국 돈과 시간이니까! 사용하는 기술이 outdate되었다고 해도 기존의 코드가 지금까지처럼 문제 없이 비즈니스 가치를 창출할 수 있다면, 상당히 중대한 이유가 아니고서는 커다란 서비스가 무거운 엉덩이를 들고 움직일 이유가 되진 못할 것이다.
  • 그러니까 '새로운 게 나왔으니까 이건 레거시야'라고들 말하는 스택들로 여전히 서비스되고 있는 앱들도, 당연히 개발자가 게을러서 그런 게 아닌 것! 뭐가 새로 나왔다고 해서 그게 믿을만 한지, 잘 유지될지, 방향성이 바뀌어서 곤란해지지는 않을지도 모르는 상황에서 초가삼간 다 엎어가면서까지 갈아탈 필요성이 없을 뿐! (여기는 개인적인 감상 내지는 상상)
profile
프론트엔드 개발자

0개의 댓글