Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Doc: [Tailwind] pxr 단위 생성 시 예외처리 및 배열 초기화 성능 10% 향상 & DX 향상 #59

Closed
jaem1n207 opened this issue Nov 9, 2023 · 0 comments
Assignees
Labels
documentation Improvements or additions to documentation

Comments

@jaem1n207
Copy link
Owner

jaem1n207 commented Nov 9, 2023

개발할 때 편의를 위해 px 단위로 스타일링을 진행하고 웹 접근성을 위해 최종적으로 rem 단위로 표현하도록 합니다. 이를 위해 임의로 구성 파일에서 pxr 단위를 생성해 사용합니다. 이러한 과정에서 발생한 문제와 해결하는 과정에서 배열을 초기화하는 다양한 방법과 Tailwind를 어떻게 사용하면 더 좋을지 등 깨달은 점을 문서로 남겨봅니다.

발생한 문제

Uncaught RangeError: Invalid array length 예외가 발생하거나 빈 객체가 반환되어 pxr 단위 생성이 무효화 됩니다.

재현 방법

  1. step을 end-start 값보다 큰 값으로 전달합니다.
  2. step을 0 또는 음수값으로 전달합니다.
  3. start를 end보다 크게 설정합니다.

목표

  1. 각 예외 사항에 대해 적절한 메시지를 제공합니다.
  2. step에 대한 규칙을 강제합니다.
    start: 2, end: 400, step: 8로 전달한다고 가정해보겠습니다. 2부터 8의 배수씩 증가하되 400으로 끝나야 한다는 의도일 것입니다. 하지만 이는 약속한 디자인 규칙을 깹니다. 8의 배수씩 증가해야 한다면 400으로 끝나는 게 아닌, 402로 끝나야 합니다. 이는 의도와 별개로 규칙을 위반한 것입니다. 따라서 해당 행위를 막도록 해야 합니다.

솔루션

각 예외 사항에 대해 적절한 메시지를 제공

각 예외 사항들에 대해 적절한 메시지를 제공해 올바른 값을 받을 수 있도록 합니다.

const is = (type, val) => ![, null].includes(val) && val.constructor === type;

if (!is(Number, start) || !is(Number, end) || !is(Number, step)) {
  throw new TypeError(
    `start, end, step 은 모두 숫자여야 해요. start: ${start}, end: ${end}, step: ${step}`,
  );
} else if (step <= 0) {
  throw new RangeError(`step은 0보다 커야 해요. step: ${step}`);
} else if (step > end - start) {
  throw new RangeError(`step은 end - start 보다 작아야 해요. step: ${step}`);
} else if (start > end) {
  throw new RangeError(`start는 end보다 작아야 해요. start: ${start}, end: ${end}`);
}

step에 대한 규칙을 강제

steppxr 단위를 제공할 때 배수 단위로 간격을 조정하기 위한 값입니다.

step이 8이라면 8의 배수로 클래스가 생성됩니다. 기기별로 화소 밀도가 다른데 5px로 디자인된 그리드, 4px로 디자인된 그리드가 있을 때 1.5배 해상도를 가진 기기에서 보면 픽셀이 쪼개져 끝이 흐려보이는 현상이 나타납니다. 이게 배수를 지정해서 관리하는 이유기도 합니다. 이런 이유로 step값을 이용해 필요한 클래스를 생성합니다.

하지만 start: 2, end: 400, step: 8 같은 상황일 때 문제가 발생합니다. 사용하는 측에선 8의 배수로 생성하도록 했지만 400으로 끝나야 한다고 합니다. 하지만 생각해보면 8의 배수면 400이 아닌 394 또는 402로 끝나야 정상입니다. 이는 정한 디자인 규칙을 위반합니다. 따라서 의도가 어떻든 이런 경우는 start, end 값에 따라 step의 값에 대해 유효한 값인지 검사하는 과정이 필요합니다.

if ((end - start) % step !== 0) {
  throw new RangeError(
    `start, end, step은 서로 배수여야 해요. start: ${start}, end: ${end}, step: ${step}`,
  );
}

pxr 단위 생성 속도 향상

딱히 중요하지 않지만 속도를 향상시킬 지점이 있습니다.

기존 pxr을 생성하는 로직 일부로 Array.from() 함수에서 length 객체를 전달해 원하는 길이의 배열을 생성하는 트릭을 사용하고 있었습니다. 두 번째 인수로 mapping 함수를 전달할 수 있으므로 값으로 배열을 초기화할 때도 유용합니다.

결론부터 말하자면, 이 부분에서 Array.from({ length: N })Array(N).prototype.map()로 변경함으로써 속도 이점을 얻을 수 있습니다. JS에서 객체와 배열을 동일한 방식으로 사용할 수 있습니다. 배열에서 인덱스로 요소에 접근하는 것처럼, 객체에서 key로 속성 값에 접근하는 게 가능합니다. 이 부분에 약간의 속도 차이가 있는데요. V8 엔진은 배열을 일반 객체와 구별하여 보다 배열처럼 동작하도록 최적화되어 구현했기 때문입니다.

아래는 기존 코드입니다:

// 기존 코드
const range = (start, end) => {
  const length = end - start + 1;
  return Array.from({ length }, (_, i) => start + i);
};

Array.from 함수는 유사 배열 객체로부터 길이(length)를 알아옵니다. 그리고 0부터 N-1 까지 k 변수를 인덱스로 사용한다고 가정하면, 유사 배열 객체로부터 키가 k인 속성 값을 가져옵니다. mapFn을 인자로 받았다면, 해당 함수로부터 얻은 값을 할당합니다. 그리고 이를 반환하게 됩니다.

while (k < length) {
  // 유사 배열 객체로부터 키가 k인 속성 값을 가져옵니다.
  const kValue = GetProperty(arrayLike, k);
}

객체의 키를 이용하여 값을 읽어들이기 때문에 iterator를 사용하는 Array(N) 방법보다 느린 것입니다.

유사 배열 객체란 마치 배열처럼 인덱스로 속성 값에 접근할 수 있고 length 속성을 갖는 객체를 의미합니다.

Array.prototype.map()을 사용해보면 빈 값은 건너뜁니다:

const arr = Array(3).map(() => 1); // [ , , ] - map() 함수는 빈 값을 건너뜁니다.

따라서 배열을 초기화할 때 Array.prototype.fill()과 함께 사용하면 좋을 것 같습니다. 여기에 동적 값을 생성하기 위해 Array.prototype.map() 호출을 체인으로 연결하면 됩니다. 따라서 다음과 같이 배열을 초기화하는 함수를 만들어 두고 사용할 수 있습니다.

const initializeMappedArray = (n, mapFn = (_, i) => i) => Array(n).fill(null).map(mapFn);

initializeMappedArray(4, (_, i) => i * 8); // [0, 8, 16, 24]

이는 벤치마크 결과 기존 로직에 비해 평균 약 10% 향상된 성능을 보입니다.

benchmark-pxr

유사 배열 객체를 사용하던 기존 방법이 iterable한 객체를 사용한 방법보다 느릴 뿐이지, 충분히 빠릅니다. 하지만 왜 이런 속도 차이가 발생하는지 궁금하기도 하고 간단하게 성능을 높일 수 있는 지점이므로 작업을 해보았습니다.

속도 차이가 발생하는 이유 참고 글

Tailwind

pxr 단위를 Tailwind의 기본 구성에 확장되는 형태로 변경합니다. -> Tailwind는 단순하게 유지할 때 더욱 빛을 발합니다.

기존은 pxr 단위를 Tailwind의 기본 구성을 제거하고 생성했습니다. 하지만 이는 TailwindCSS의 장점을 잃게 했습니다. 예로 Button 컴포넌트를 구현해야 하는 상황이 있습니다. 하나하나 스타일 속성을 입력하기엔 귀찮고 다른 기능 작업에 더 집중하고 싶죠. 그럼 Tailwind를 사용하는 프로젝트의 className을 그대로 복붙하면 됩니다. 하지만 커스텀한 pxr 단위를 사용하면 복붙해서 일일이 수정 작업을 해야 합니다. 따라서 기본 구성에 확장해 pxr 단위를 사용할 수 있도록 했습니다.

그 전에 생각해봐야 할 부분은 더 커지는 번들 크기였는데요.
Tailwind는 프로덕션 빌드 시에 사용하는 클래스에 대해서만 스타일시트에 추가합니다. 그럼에도 pxr 단위를 사용하게 되면 다음과 같은 상황에 중복이 발생합니다. mx-1mx-4pxr은 같은 역할을 합니다. 하지만 복붙한 mx-1 클래스명과 mx-4pxr 직접 사용한 경우 모두 번들 결과 스타일시트에 포함됩니다. 그렇다고 pxr 단위를 아예 지우는 건 이미 작업이 어느정도 진행된 상황에선 더 번거로운 작업입니다.

이는 간단하게 생각해보면 됩니다. '약간 커지는 번들 크기' 그리고 'UI 개발 편리함' 중에 선택하는 것이죠. 저는 'UI 개발 편리함'을 선택했는데요. 번들 크기가 커진다해도 아주 조금 커지는 것 뿐이고, 번들 크기가 너무 큰 상황이 온다면 차라리 UI 스타일 적용에 아낀 시간으로 다른 부분을 최적화하는 게 더 의미 있을 것이고요.

이러한 과정에서 Tailwind는 간단하게 유지할 때 더욱 빛을 발한다는 것을 깨달았습니다. 그래서 @apply 속성을 사용해 특정 스타일을 추상화하는 것도 되도록 사용하지 않을 것 같습니다. @apply 속성을 사용한다는 것은 CSS파일과 작업 파일의 사이를 이동하며 다니지 않아도 되는 Tailwind의 큰 장점을 잃어버리게 할 뿐 아니라 항상 클래스 이름을 작명해줘야 하고 CSS 번들 크기가 커지게 되죠.
보통 아래와 같은 상황일 때 @apply 속성을 사용하고 싶은 생각이 듭니다.

  1. @apply를 사용하면 전체 앱에 걸쳐 button 요소에 대한 일관된 스타일을 지정할 수 있게 되고 빠른 수정이 가능하지 않을까요?
  2. 스크롤바를 스타일하려 한다면 클래스명을 주입할 수가 없는데 @apply 를 사용할 수 밖에 없지 않을까요?
  3. @apply 없이 복잡한 셀렉터들은 어떻게 다뤄야 할까요? 예를 들어 하위 요소의 상태 기반으로 상위 요소의 스타일을 수정하는 상황 말이죠.

위 상황들은 모두 @apply 속성을 사용하지 않고 아래 방법처럼 처리할 수 있는 더 좋은 방법이 있다고 생각합니다:

  1. button 요소 컴포넌트를 만들고 여기서 스타일을 지정하면 됩니다. 커스텀 가능한 스타일을 props로 전달하려면 tailwind-merge와 같은 패키지를 사용하면 되겠죠. 이런 요소들은 애초에 확실히 컴포넌트로 분리되어야 하는 게 옳다고 봅니다.
  2. 스크롤바에 대한 스타일을 global.css 파일에 넣을 수도 있습니다. 또는 이를 제어하기 위해 클래스를 추가하는 tailwind-scrollbar 플러그인을 사용할 수도 있겠죠. 사실 이 부분은 웬만하면 브라우저가 자동으로 처리하는 게 더 합리적인 경우가 많습니다.
  3. 이 경우에 대해 Tailwind 3.2 버전부터 group 클래스를 지원합니다. 따라서 더 이상 문제가 되지 않습니다.

따라서 앞으로 Tailwind를 사용할 때 특별한 일이 없다면 구성을 단순하게 유지하려고 합니다.

TailwindCSS �개발 경험 향상해보기

보통 TailwindCSS를 사용하면 린팅, 자동 완성 기능 등을 제공하는 TailwindCSS 인텔리센스를 사용할텐데요. 이 기능은 개발자가 CSS 클래스 이름을 빠르게 입력하고, 사용 가능한 클래스 이름을 쉽게 찾을 수 있도록 도와줍니다. 하지만, 기본적으로 이 자동 완성 기능은 ** className** 속성에 대해서만 작동합니다.

className 속성 외에도 다른 속성에 대해 TailwindCSS 클래스 자동 완성 기능을 활용하는 방법과 잘 활용하는 방법과 특정 속성에 강력하게 결합되는 정규식을 사용하지 않고, 새로운 속성이 추가될 때마다 tailwindCSS.experimental.classRegex 설정을 수정하는 번거로움을 해결하는 방법에 대해 설명하려 합니다.

자동 완성 기능이 지원 되지 않는 몇 가지 예를 들어보겠습니다.
공용으로 사용할 Button 컴포넌트에서 다른 UI를 정의하고 싶을 땐 아래와 같은 방법을 많이 사용할텐데요. default, destructive 속성에선 클래스 이름을 자동 완성하지 않습니다.

import { cva } from 'class-variance-authority';

const buttonVariants = cva(
  'font-medium',
  {
    variants: {
      variant: {
        default: 'bg-primary',
        destructive: 'bg-destructive'
      },
    },
  },
);

또는 @headlessui/react 패키지에서 제공하는 Transition 컴포넌트에서 leave, leaveFrom 속성들에 대해서도 자동완성이 되지 않습니다:

import { Transition } from '@headlessui/react';

<Transition
  show={displayList}
  as={Transition.Child}
  leave="transform duration-200 transition ease-linear"
  leaveFrom="opacity-100 scale-100"
  leaveTo="opacity-0 scale-95"
>

이 경우 그 많은 클래스 이름을 모두 외우고 있지 않기 때문에 공식 문서를 오가며 클래스 이름을 하드 코딩할 수 밖에 없습니다. tailwindCSS.experimental.classRegex 설정을 사용하면, 정규식을 설정하여 className 속성 외에도 다른 속성에 대해 자동 완성 기능을 활용할 수 있습니다.
위 예시에 대해서, 다음과 같이 설정할 수 있습니다.:

"tailwindCSS.experimental.classRegex": [
  "leave\\w*\\s*=\\s*['\"`]([^'\"`]*)['\"`]",
  "default\\s*:.*?['\"`]([^'\"`]*)['\"`]",
  "destructive\\s*:.*?['\"`]([^'\"`]*)['\"`]",
]

위에선 간단한 예시라 속성이 별로 없습니다. 여기에 Button의 크기도 조절할 수 있어야 한다는 상황을 조금 더 추가해보겠습니다. variants 객체에 size 속성과 함께 sm, lg 속성이 추가되겠죠. 그럼 자연스럽게 tailwindCSS.experimental.classRegex 속성도 수정해줘야 합니다. 이는 조금 번거롭습니다. 🥲

그럼 아래처럼 ", ', ` 사이에 들어오는 문자열에 대해 자동 완성을 사용하도록 하면 되지 않을까? 라는 생각이 들 수도 있습니다.

"tailwindCSS.experimental.classRegex": [
  "[\"'`]([^\"'`]*).*?[\"'`]" 
]

이제 속성이 추가될 때마다 추가 설정을 할 필요가 없습니다. 하지만 스타일 지정과 전혀 상관 없는 href, onClick 속성에 대해서도 자동 완성이 지원되겠죠. 이 방법 역시 좋지 못한 방법이라는 것을 금방 깨달을 수 있습니다.

그럼 어떻게 새로운 속성이 추가될 때마다 tailwindCSS.experimental.classRegex 설정을 수정할 필요 없이 개발 경험을 향상할 수 있을까요?
먼저, 특정 속성에 강력하게 결합되는 정규식을 사용하지 않아야 합니다. 그럼 어떻게 원하는 곳에서 자동 완성 기능을 사용할 수 있을까요?
변수를 할당하는 것처럼 자동 완성을 사용하고 싶은 곳 앞에 특정 키워드(twa)를 선언하고 키워드 이후부터 세미콜론(;)이 오기 전까지의 모든 문자열에 대해 자동 완성을 지원하면 되지 않을까요?

백문이 불여일견, 코드로 살펴봅시다:

"tailwindCSS.experimental.classRegex": [
  ["/\\*twa\\*/ ([^;]*);", "'([^']*)'"]
]
  • "/\\*twa\\*/ ([^;]*);": /*twa*/ 주석 다음에 오는 문자열을 찾습니다. [^;]* 부분이 세미콜론이 나오기 전까지의 모든 문자를 의미합니다. 따라서 이 정규식은 /twa/ 주석 다음에 오는 세미콜론 전까지의 문자열을 찾습니다. (제 프로젝트는 블록이 세미콜론으로 끝나는 프로젝트라 이런 설정이 가능합니다)
  • "'([^']*)'": 따옴표(') 사이의 모든 문자를 찾습니다. 저희가 클래스 이름 자동 완성을 지정할 곳이죠. [^']* 부분은 닫는 따옴표가 나오기 전까지 모든 문자를 의미합니다. 따라서 이 정규식은 따옴표 사이의 문자열을 찾습니다.

이제 /twa/ 주석 다음에 오는 문자열에 대해 TailwindCSS 클래스 자동 완성 기능을 사용할 수 있게 됩니다.
twa인 이유는 상수를 선언하는 const 키워드처럼 TailwindCSS(tw)의 자동 완성(Auto Complete)를 사용하겠다는 키워드처럼 사용하기 위함입니다. 이를 간단히 줄여 twa로 칭했습니다.

이제 위에서 말했던 번거로움을 피할 수 있게 되었습니다. 아래처럼 말이죠:

const buttonVariants = /*twa*/ cva(
  'font-medium',
  {
    variants: {
      variant: {
        default: 'bg-primary',
        destructive: 'bg-destructive'
      },
      size: {
        sm: 'h-9 rounded-md px-3',
        lg: 'h-11 rounded-md px-8',
      },
    },
  },
);

또는 다음과 같이 사용할 수도 있습니다:

const DEFAULT = /*twa*/ 'text-primary';

특정 모듈을 가져와서 사용하는 것도 아니고 속성을 하나하나 지정하는 것도 아닌, 키워드를 선언하는 것처럼 짧은 주석을 이용하면 되므로 편한 개발 경험을 제공하죠. 👍

특정 속성에 강력하게 결합되는 정규식을 사용하지 않으므로, 새로운 속성이 추가될 때마다 tailwindCSS.experimental.classRegex 설정을 수정할 필요가 없게 되었습니다.

한 가지 알아두어야 할 점은 아직 이처럼 임의로 지정한 경우 자동 완성은 지원되지만 린팅은 지원하지 않습니다.

@jaem1n207 jaem1n207 added bug Something isn't working help wanted Extra attention is needed labels Nov 9, 2023
@jaem1n207 jaem1n207 self-assigned this Nov 9, 2023
@jaem1n207 jaem1n207 changed the title Bug: Tailwind 구성 파일에서 pxr 단위를 생성 시 예외처리 및 Bug: Tailwind 구성 파일에서 pxr 단위를 생성 시 예외처리 및 배열 초기화 성능 10% 향상 Nov 9, 2023
@jaem1n207 jaem1n207 added documentation Improvements or additions to documentation and removed bug Something isn't working help wanted Extra attention is needed labels Nov 9, 2023
@jaem1n207 jaem1n207 changed the title Bug: Tailwind 구성 파일에서 pxr 단위를 생성 시 예외처리 및 배열 초기화 성능 10% 향상 Doc: Tailwind 구성 파일에서 pxr 단위를 생성 시 예외처리 및 배열 초기화 성능 10% 향상 Nov 9, 2023
@jaem1n207 jaem1n207 changed the title Doc: Tailwind 구성 파일에서 pxr 단위를 생성 시 예외처리 및 배열 초기화 성능 10% 향상 Doc: [Tailwind] pxr 단위 생성 시 예외처리 및 배열 초기화 성능 10% 향상 & DX 향상 Feb 21, 2024
@jaem1n207 jaem1n207 pinned this issue May 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

1 participant