티스토리 뷰
React.js로 컴포넌트를 만들어 배포한적이 있었다
https://ljy1011.tistory.com/182
React 컴포넌트 Npm에 배포하기 (with TS)
react 로 만든 컴포넌트를 npm 사이트에 배포해 보자! (typescript 적용) https://www.npmjs.com/package/react-divided-progress-bar react-divided-progress-bar A progress-bar which has divided section based on React.js. Latest version: 0.1.7, l
ljy1011.tistory.com
당시 야심차게 만들었던 progress bar 컴포넌트 였다. 프로젝트 진행중 progress bar가 divide 된 ui가 필요한적이 있어 직접 만들었었다.
그리고 기왕 만드는김에 나처럼 해당 ui가 필요한 사용자들을 위해 직접 Npm에 배포해보았다.
https://www.npmjs.com/package/react-divided-progress-bar
react-divided-progress-bar
A progress-bar which has divided section based on React.js. Latest version: 0.1.9, last published: 6 months ago. Start using react-divided-progress-bar in your project by running `npm i react-divided-progress-bar`. There are no other projects in the npm re
www.npmjs.com
(마지막 업데이트가 6개월전... 사실상 방치 되다 싶이 했다.)
그 당시에는 제법 열심히 만들었다고 생각 했지만, 지금 다시 바라보니
헛점투성이에 개선 할것이 수두룩 하였다! (총체적 난국...)
예전에 만들었다고 방치하는것 보다, 꾸준히 잘못된 점을 개선하는 자세를 가져 보기로 했다. 그것이 개발자의 성장에 좋은 덕목이 될 것이라 생각한다!
자 이제 천천히 문제점을 찾아 보아 개선해 보자!
차례는 다음과 같다.
1. Storybook 도입
기존: 컴포넌트를 test 할때 직접 react app 을 실행 시켜 test 함.
변경: Storybook 도입
기존에는 react app에서 컴포넌트를 직접 import 해 test 하였다. storybook 을 도입하게 됨으로써의 장점은 다음과 같다.
1. 격리된 환경
캡슐화: Storybook을 사용하면 나머지 애플리케이션과 별도로 구성 요소를 개발하고 테스트할 수 있다. 이는 애플리케이션 종속성이나 상태 관리에 대해 걱정하지 않고 단일 구성 요소에 집중할 수 있음을 의미한다.
2. 향상된 문서
컴포넌트 라이브러리: 스토리북은 생활 스타일 가이드이자 종합 컴포넌트 라이브러리 역할을 합니다. 각 스토리는 구성 요소의 사용법, 소품 및 가능한 상태를 문서화 한다.
시각적 문서: 개발자는 구성 요소가 다양한 소품과 상태로 렌더링되는 방식을 확인하여 서면 문서를 보완하는 시각적 참조를 제공할 수 있다.
3. 테스트 용이성
Edge Cases: Storybook을 사용하면 다양한 엣지 케이스에 대한 스토리를 쉽게 생성하여 가능한 모든 시나리오에서 구성 요소를 테스트할 수 있다.
시각적 회귀 테스트: Storybook을 Chromatic 또는 Loki와 같은 도구와 통합하면 시각적 회귀 테스트가 가능해 UI 변경으로 인해 예상치 못한 시각적 버그가 발생하지 않는지 확인할 수 있다.
4. 협업 및 피드백
비개발자 공동 작업: 디자이너와 기타 비개발자는 Storybook의 구성 요소와 상호 작용하여 애플리케이션을 실행하지 않고도 피드백을 제공할 수 있다.
쇼케이스 및 공유: Storybook의 UI는 이해관계자와 공유할 수 있으므로 개발 프로세스 초기에 구성 요소를 더 쉽게 선보이고 피드백을 수집할 수 있다.
5. 추가 기능 및 생태계
확장성: Storybook은 동적 소품 편집을 위한 손잡이, 이벤트 처리를 위한 작업, 접근성 테스트 도구 등 테스트 및 개발을 향상시키는 풍부한 추가 기능 에코시스템을 지원한다.
6. 일관된 UI/UX
균일성: 개발자는 개별적으로 개발함으로써 구성 요소가 애플리케이션의 여러 부분에서 실제로 재사용 가능하고 일관성이 있는지 확인합니다.
실무에서 storybook 을 사용해본 경험이 있기 때문에, 바로 도입해보기로 했다.
다음 명령어를 실행해 storybook을 설치해 주자.
nxpx sb init
storybook 모듈 설정
처음에는 컴포넌트가 위치한 root directory에 storybook 을 설치하려 했다.
하지만 곧 다음과 같은 단점을 발견하였다.
1. 관리해야 할 종속성이 증가함 (package.json과 node_modules 폴더가 추가된다.)
2. 통합 구성이 불편해짐. root의 설정과 환경을 storybook module이 참조하기 힘들다.
그리하여 storybook 컴포넌트는 root으로 빼기로 하였다.
우선 props부터 분리하도록 하자. 각 ui상 변경점인 각 color별로 분리해 주었다.
docs에서 각 props의 default 값도 설정해 주었다. 이는 argType의 Table property 에서 설정이 가능하다.
https://storybook.js.org/docs/api/arg-types
Storybook: Frontend workshop for UI development
Storybook is a frontend workshop for building UI components and pages in isolation. Thousands of teams use it for UI development, testing, and documentation. It's open source and free.
storybook.js.org
import type { Meta, StoryObj } from "@storybook/react";
import ProgressBar, { ProgressBarStyle } from "./index";
const ProgressBarColor = Object.keys(ProgressBarStyle.color).join("|");
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = {
title: "Example/ProgressBar",
component: ProgressBar,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: "full",
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ["autodocs"],
// More on argTypes: https://storybook.js.org/docs/api/argtypes
argTypes: {
value: {
control: "number",
table: {
defaultValue: { summary: "0" },
},
},
increaseDuration: {
control: "number",
table: {
defaultValue: { summary: "1000" },
},
},
color: {
control: "multi-select",
table: {
type: {
summary: ProgressBarColor,
},
defaultValue: { summary: "default" },
},
options: [
"primary",
"secondary",
"info",
"success",
"warning",
"danger",
"black",
],
},
colorChange: {
control: "boolean",
table: {
defaultValue: { summary: "false" },
},
},
divide: {
control: "boolean",
table: {
defaultValue: { summary: "true" },
},
},
maxValue: {
control: "number",
table: {
defaultValue: { summary: "100" },
},
},
animated: {
control: "boolean",
table: {
defaultValue: { summary: "false" },
},
},
stripped: {
control: "boolean",
table: {
defaultValue: { summary: "false" },
},
},
},
마지막으로 storybook 컴포넌트 파일(.stories.tsx/ts/jsx/js) 은 배포시 빌드될 필요가 없으니 tsconfig를 개발용과 배포용으로 분리 하였다.
배포 스크립트
// tsc - p 배포tsconifg 파일
"scripts": {
"prepare": "rm -rf dist && mkdir dist && tsc -p tsconfig.build.json && npm run copy-styles",
},
배포용 (tsconfig.build.json)
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "commonjs",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": false,
"declaration": true,
"outDir": "./dist",
"jsx": "react-jsx"
},
"include": ["src"],
// exclude stories 추가
"exclude": ["**/*.stories.*"]
}
2. 트러블 슈팅 ( Cannot read properties of null (reading 'style') )
Storybook 을 통해 발견한 치명적 버그 이다.
그전 테스트 환경에서는 발견하지 못했지만, 각 color별 컴포넌트 변경시
Cannot read properties of null (reading 'style')
에러가 발생한 것이다.
해당 에러의 원인은 간단했다.
1. 에러는 animateProgessBar > updatePerceentage 함수 안에서 발생한것이다.
해당 함수는 js로 progress animation을 구현하는 함수 이다. (request animation 사용)
2. 에러의 본질적 원인은 DOM element (progressElement) 를 찾지 못해 발생한 것이다.
그리고 이는 앞서 말했던, 컴포넌트 이동시에만 발생했던 것이다.
이를 통해 추론해보면 progress bar DOM element가 umnount된 후에도, 해당 element에 접근해 animate를 했다는것!
unmount시 animation을 종료 시키지 않는 아주 기초적인 실수를 햇던 것이다...
이를 해결하기 위해 useEffect hook을 사용했다.
useEffect(() => {
...
// animation id 바인딩용 변수
let animation: number | null = null;
animation = requestAnimationFrame(
animateProgressBar(increaseDuration >= 0 ? increaseDuration : 0)
);
return () => {
if (animation) {
cancelAnimationFrame(animation);
}
};
},[...])
unmount 시점에 cancelAnimationFrame 함수를 호출했다. 파라미터로는 requestAnimationFrame의 return값에 해당하는 id number를 넣어주면 된다. 해당 id는 animation이라는 변수를 만들어 바인딩 해주면 된다.
https://developer.mozilla.org/en-US/docs/Web/API/Window/cancelAnimationFrame
Window: cancelAnimationFrame() method - Web APIs | MDN
The window.cancelAnimationFrame() method cancels an animation frame request previously scheduled through a call to window.requestAnimationFrame().
developer.mozilla.org
3. Type Interface 개선
현재 typescript의 이점을 제대로 살리지 못하고 있다.
각 prop의 option 들의 type 추론이 되지 않아 사용자의 개발 환경에서 확인이 불가능 한것이다.
결국 개발을 할때 color의 option을 문서에서 직접 알아내야 하는 불편함이 있는 것이다.
이를 해결하기 위해 코드들 손보기로 한다.
우선 기존의 코드를 살펴보면
export type ProgressBarProps = {
color?: string
};
type ColorClass = {
[color: string]: stirng
}
const colorClass: ColorClass = {
primary: "bg-primary",
secondary: "bg-secondary",
info: "bg-info",
success: "bg-success",
warning: "bg-warning",
danger: "bg-danger",
black: "bg-black",
}
progressElement.classList.add(colorClass[color] ?? defaultColorClass);
Props 는 color를 string type 으로 받고,
각 color를 class명과 mapping 하는 객체인 colorClasss는 index signature 를 사용하도록 되어있다..
이러니 상위 컴포넌트에서는 당연히 color의 key type을 추론 할 수 없었던 것!
과거의 나를 이해 할 수가 없군!!!
당시의 의도를 기억이 안나지만... 그냥 ts 사용법을 잘 몰랐던거 같다..
이제는 개선할수 있다!
1. 우선 쓸데 없는 type을 제거하자!
const colorClass = {
primary: "bg-primary",
secondary: "bg-secondary",
info: "bg-info",
success: "bg-success",
warning: "bg-warning",
danger: "bg-danger",
black: "bg-black",
};
index signature가 적용된 type을 제거하고, 객체 선언만 해주자!
물론 key 값을 가지고 있는 type을 따로 선언해도 돼지만, 이는 새로운 color가 추가될때 마다 type과 객체 둘다에 작업을 해야 하는 불편함이 있을수 있다.
편의상 객체 선언만 하도록 변경 하였다.
2. colorClass의 key type에 접근
이는 keyof typeof 로 구현하였다.
export type ProgressBarProps = {
color?: keyof typeof colorClass;
...
};
사실 여기서 끝을 내려고 했으나... typescript를 잘아시는 회사 동료 분에게 자문을 얻어 추가적으로 더 수정하였다.
3. 추가적인 Type Narrowing
( Record 타입 / Typeof Array[number] 패턴 / as const 적용 )
우선 color class의 mapping 기능을 하던 colorClass 객체에 대한 의구심이 들었다.
웨에서 확인 가능하듯이, 결국 key의 value들은 'bg-' prefix 만 붙인것에 불과 했기 때문이다.
const colorClass = {
primary: "bg-primary",
secondary: "bg-secondary",
info: "bg-info",
success: "bg-success",
warning: "bg-warning",
danger: "bg-danger",
black: "bg-black",
};
그럼에도 불구하고 굳이 key-value mapping을 시켰던 이유는, js 환경에서 개발시 잘못된 color prop이 전달될 경우 defaulValue로 class를 설정하고자 했기 때문이다.
// defaultColorClass 적용
progressElement.classList.add(colorClass[color] ?? defaultColorClass);
progressBarElement.classList.add(colorClass[color] ?? defaultColorClass);
하지만, 이에 대해 동료분이 문제를 제기한것이 애초에 잘못된 prop이 오면 그에 따른 style 예외 처리를 굳이 할 필요 없다는 의견을 주셨다.
그 의견이 일리가 있어, 다른 대중적인 css liibrary를 참고 하기로 하였다. 그중 하나가 bootstap 이였다.
bootstrap 역시 잘못된 variant에 대한 예외 처리가 없었다. 참고해 굳이 쓸데 없는 default 값 지정을 빼기로 했다.
// defaultColorClass 적용 해제, colorClass 객체 제거
progressElement.classList.add(`bg-${color}`);
progressBarElement.classList.add(`bg-${color}`);
이제 mapping기능이 필요 없어진 colorClass 를 배열로 만든뒤, as const 를 적용해 const assertion을 적용해 줬다.
(const로 선언한 객체는 객체 내부의 타입들이 넓은 범위의 타입으로 추론된다. 하지만 const assertion을 사용하면 그 범위를 좁혀줄수 있다.)
그리고 const assertion된 객체(배열) 내부 type에 접근하고 싶으면 Typeof Array[number] 패턴을 적용해 주면 된다.
const COLOR_CLASS = [
"primary",
"secondary",
"info",
"success",
"warning",
"danger",
"black",
] as const;
export type ProgressBarProps = {
...
color?: (typeof COLOR_CLASS)[number];
};
이렇게 props에서도 type 추론이 가능해졌다.
.
.
.
여기서 끝이 아니다!!
여기 progress 변화에 따른 color 정보를 가지고 있는 colorInfo 객체가 있다.
해당 객체는 colorClass의 key값을 key로 같고, value로 being,end property를 가진다.
과거의 나는 이를 단순하게 index signature로 구현했었다.
type ColorInfo = {
[color: string]: {
begin: number[];
end: number[];
};
};
const colorInfo: ColorInfo = {
primary: {
begin: [144, 202, 249],
end: [66, 165, 245],
},
secondary: {
begin: [227, 126, 255],
end: [214, 67, 255],
},
...
};
이 방식의 문제점은, colorClass 의 key 값 (primary,secondary ..) 이 아닌 다른 값을 넣어도 컴파일 에러가 발생하지 않는다는 것이다.
이를 개선 하고자 Record Type을 추가 하였다.
Record<Key, Value> 키가 Key타입이고 값이 Value 타입인 객체 타입을 생성함
const COLOR_CLASS = [
"primary",
"secondary",
"info",
"success",
"warning",
"danger",
"black",
] as const;
const COLOR_INFO: Record<
(typeof COLOR_CLASS)[number],
{
begin: [number, number, number];
end: [number, number, number];
}
> = {
primary: {
begin: [144, 202, 249],
end: [66, 165, 245],
},
secondary: {
begin: [227, 126, 255],
end: [214, 67, 255],
},
...
};
이로서 type narrowing 작업도 마무리 하였다!
4. 성능 개선: requestAnimationFrame 에서 css animation으로
기존에는 requestAnimationFrame 함수를 활용해 animation 처리를 하였다.
하지만 이는 성능상 문제점을 야기 하였다.
간단히 말하자면 requestAnimationFrame은 다음 repaint 직전에 callback 함수를 실행하고, 결과를 repaint 단계에 반영한다.
(콜백의 수는 보통 1초에 60회지만, 일반적으로 대부분의 웹 브라우저에서는 W3C 권장사항에 따라 디스플레이 주사율과 일치한다)
이는 곧 requestAnimationFrame은 repaint 를 발생 시킨다는 것이다.
물론 requestAnimationFrame은 css animation에 비해서 미세한 정밀하고 섬세한 에니메이션을 구현할 수 있다는 장점이 있다.
js 코드상에서 수동으로 각 frame을 조작하며, 직접 target이 되는 element에 접근해 style을 자유자재로 바꿀수 있다.
하지만 progress bar 컴포넌트는 복잡한 에니메이션이 필요 없으니, css 로 하는게 성능상 이점이 있다.
물론 여기서 한 가지더 고려해야 할 점은 css animation을 사용하지만, transform이나 transition을 사용하지 않는 이상 어차피 repaint 단계는 일어난다는 것이다.
그럼에도 불구하고 css가 성능상 이점인 이유는 다음과 같다!
1. 브라우저의 최적화
GPU 가속
- CSS 애니메이션: 브라우저는 CSS 애니메이션을 GPU에서 실행할 수 있도록 최적화되어 있습니다. 특히 변환(transform)과 불투명도(opacity) 같은 속성들은 GPU에서 직접 처리되므로, 렌더링 성능이 크게 향상됩니다.
- JavaScript 애니메이션: 일반적으로 CPU에서 실행되며, 직접 GPU 가속을 제어하기 위해서는 추가적인 코드가 필요합니다. 이는 브라우저가 자동으로 최적화할 수 있는 범위를 벗어나기 때문에 성능 저하가 발생할 수 있습니다.
컴포지팅 단계
- CSS 애니메이션: 레이아웃이나 페인트 단계 없이 컴포지팅 단계에서 바로 처리할 수 있는 애니메이션을 적용할 수 있습니다. 이는 브라우저가 요소의 위치나 모양을 다시 계산할 필요 없이 레이어만 이동시키므로 매우 빠릅니다.
- JavaScript 애니메이션: JavaScript 애니메이션은 종종 레이아웃과 페인트 단계를 포함하게 되므로, 애니메이션 성능이 저하될 수 있습니다. 특히 DOM 요소의 스타일을 변경할 때 브라우저가 레이아웃을 다시 계산해야 하는 경우가 많습니다.
2. 브라우저 리소스 관리
효율적인 리소스 사용
- CSS 애니메이션: 브라우저는 CSS 애니메이션을 관리하고 최적화할 수 있는 다양한 기법을 사용합니다. 예를 들어, 애니메이션이 보이지 않거나 비활성화된 탭에서 실행 중일 때 브라우저는 애니메이션을 일시 중지하거나 자원을 절약할 수 있습니다.
- JavaScript 애니메이션: requestAnimationFrame은 브라우저의 리프레시 주기와 동기화되어 성능을 최적화하지만, 비활성화된 탭에서 실행되는 애니메이션은 자동으로 최적화되지 않습니다. 이를 관리하려면 개발자가 직접 코드를 작성해야 합니다.
3. 손쉬운 사용성과 유지보수성
선언적 접근
- CSS 애니메이션: 선언적 접근 방식으로 애니메이션을 정의하므로, 코드가 간결하고 가독성이 높습니다. 이는 유지보수성과 협업에 유리합니다. 애니메이션의 상태와 변경 사항을 쉽게 파악할 수 있습니다.
- JavaScript 애니메이션: 절차적 접근 방식으로 각 프레임을 수동으로 제어해야 하므로 코드가 복잡해질 수 있습니다. 애니메이션 로직이 복잡한 경우, 디버깅과 유지보수가 어려울 수 있습니다.
4. 일관된 성능
일관성 있는 동작
- CSS 애니메이션: 브라우저가 애니메이션을 처리하는 방식이 표준화되어 있어 다양한 브라우저와 장치에서 일관된 성능을 제공합니다. 브라우저의 최적화 기능을 통해 성능이 보장됩니다.
- JavaScript 애니메이션: 다양한 브라우저와 장치에서의 성능이 다를 수 있습니다. 특히 복잡한 로직이 포함된 애니메이션은 브라우저나 장치에 따라 성능 차이가 발생할 수 있습니다.
자 이제 본격적으로 바꾸어 보도록 하자!
우선 requsetAnimation 을 사용하는 기존 코드이다.
useEffect(() => {
// cancelAnimation에 사용될 animation id를 저장하는 변수
let animation: number | null = null;
const animateProgressBar = (duration: number) => {
// 현재 진행 퍼센티지를 저장하고 있는 참조값에서 시작 퍼센티지를 가져오기
const startPercentage = curPercentage.current;
// 애니메이션 시작 시간을 기록
const startTime = performance.now();
// 애니메이션 업데이트 함수
return function updatePercentage(timestamp: number) {
// progressRef로 참조된 HTMLDivElement를 가져오기
const progressElement = progressRef.current as HTMLDivElement;
// 현재 시간과 애니메이션 시작 시간의 차이를 계산하여 경과 시간을 얻기
const elapsed = timestamp - startTime;
// 경과 시간의 비율을 계산하여 0에서 1 사이의 값을 얻기
const progress = Math.min(elapsed / duration, 1);
// 현재 퍼센티지를 계산
// 시작 퍼센티지에서 목표 퍼센티지까지 progress 비율에 따라 증가
const currentPercentage =
startPercentage +
(targetPercentage.current - startPercentage) * progress ?? 0;
// progressElement의 width 스타일을 현재 퍼센티지로 설정
progressElement.style.width = currentPercentage + "%";
// colorChange가 true인 경우 배경색을 변경
if (colorChange) {
progressElement.style.backgroundColor = getCurColor(currentPercentage);
}
// 현재 퍼센티지를 저장하여 다음 프레임에서 사용할 수 있도록 하기
curPercentage.current = currentPercentage;
// progress가 1보다 작고 애니메이션이 진행 중이면, 다음 프레임을 요청
if (progress < 1 && isAnimating.current) {
animation = requestAnimationFrame(updatePercentage);
} else {
// 애니메이션이 완료되었음을 표시
isAnimating.current = false;
}
};
};
...
animation = requestAnimationFrame(
animateProgressBar(increaseDuration >= 0 ? increaseDuration : 0)
);
}
// unmount시 animation cancel
return () => {
isAnimating.current = false;
if (animation) {
cancelAnimationFrame(animation);
}
};
},[value, increaseDuration, maxValue])
다음은 css animation을 사용하는 변경된 코드이다.
const animateProgressBar = (duration: number) => {
// progressBarRef로 참조된 HTMLDivElement를 가져오기
const progressBarElement = progressBarRef.current as HTMLDivElement;
// progressRef로 참조된 HTMLDivElement를 가져오기
const progressElement = progressRef.current as HTMLDivElement;
// progressElement의 현재 너비를 progressBarElement의 너비로 나눠 시작 퍼센티지를 계산
const startPercentage =
(parseInt(getComputedStyle(progressElement).getPropertyValue("width")) /
parseInt(getComputedStyle(progressBarElement).getPropertyValue("width"))) *
100;
// progressElement에 애니메이션을 적용
progressElement.animate(
[
// 애니메이션의 시작 상태: 현재 너비와 (옵션) 시작 색상을 설정
{
width: startPercentage + "%",
backgroundColor: colorChange
? getCurColor(startPercentage) // colorChange가 true이면 시작 색상을 설정
: undefined, // colorChange가 false이면 배경색을 변경하지 않음
},
// 애니메이션의 끝 상태: 목표 너비와 (옵션) 끝 색상을 설정
width: targetPercentage.current + "%",
backgroundColor: colorChange
? getCurColor(targetPercentage.current) // colorChange가 true이면 시작 색상을 설정
: undefined,// colorChange가 false이면 배경색을 변경하지 않음
},
],
{
...progressAnimation, // 추가 애니메이션 설정을 복사
duration, // 애니메이션의 지속 시간을 설정
}
);
};
css animation 와 js animation 성능 비교 (둘다 똑같은 환경에서 test 하였다.)
requestAnimationFrame: (대략 500ms 시점: 한 프레임에 0.35 ms 소요) & (전체 20.03ms 소요)
30fps에 맞춰 에니메이션이 정확히 30 번 reflow 된것을 볼 수 있다!
css animation: (대략 500ms 시점: 한 프레임에 0.22 ms 소요) & (전체 16.99ms 소요)
총 24번 reflow가 발생 하였다. (requestAnimationFramer과 다르게 30fps에 맞추지 않았다.)
비교에서 알 수 있듯이, css animation이 성능 상 우위인것을 확인할수 있다.
이유는 다음과 같다.
1. 전체 reflow 횟수가 더 적다.
2. 화면을 그리는 각 task에도 js callback 함수 실행부분이 존재하지 않아, css animation이 더 빠른것을 확인할 수 있다.
5. 코드 리팩토링
ForwardRef 적용 + className props에 추가
https://react.dev/reference/react/forwardRef
forwardRef – React
The library for web and native user interfaces
react.dev
컴포넌트를 사용시 특수한 용도로 사용되는 props들이 있다.
List rendering을 위한 key prop 이나, HTML 엘리먼트 접근을 위해 ref prop 같은 prop들 말이다.
이 중 ref prop을 적용 시키기 위해서는 forwardRef를 사용해야 한다.
ts에서 forwardRef 사용시, 제네릭에 첫번째 인자에는 ref의 대상이 되는 element 를 (여기서는 div 가 되겠다),
두번째는 prop의 type을 설정해 주면 된다. 나는 className만 추가 type으로 받을거라, className만 따로 props에 추가해주었다.
만약 div element의 type을 그대로 갖고 갈거면 React.HTMLAttributes<HTMLDivElement> 을 추가해주면 된다.
Clsx 적용
기존에는 className을 useEffect hook 에서 바인딩 했다.
바인딩시 ref를 사용해 element의 className을 직접 변경하는 식으로 진행했었다.
const progressRef = useRef<HTMLDivElement>(null);
const progressBarRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const progressElement = progressRef.current as HTMLDivElement;
const progressBarElement = progressBarRef.current as HTMLDivElement;
progressElement.className = "progress";
progressBarElement.className = "progress-bar";
progressElement.classList.add(`bg-${color}`);
progressBarElement.classList.add(`bg-${color}`);
if (stripped) {
progressElement.classList.add("progress-bar-striped");
}
if (animated) {
progressElement.classList.add("progress-bar-animated");
}
}, [color, stripped, animated]);
이는 className 변경시 가독성을 떨어뜨리고, className 초기화 코드와 추가 코드가 분리된다는 단점이 있었다.
이 단점을 보완하기 위해
수정 후)
1. clsx 라이브러리 사용으로 훨씬 간결하고 가독성있게 class를 추가함
2. useMemo(memozation)을 사용 함으로 렌더링 시 불필요한 재계산을 방지하여 성능을 최적화 함.
const getProgressBarClass = useMemo((): string => {
return clsx("progress-bar", `bg-${color}`);
}, [color]);
const getProgressClass = useMemo((): string => {
return clsx(
"progress",
`bg-${color}`,
stripped && "progress-bar-striped",
animated && "progress-bar-animated"
);
return (
<div ref={progressBarRef} className={getProgressBarClass}>
<div ref={progressRef} className={getProgressClass}>
</div>
</div>
)
'프로젝트' 카테고리의 다른 글
Rollup + React.js + Typescript 으로 NPM package 배포 하기 (0) | 2024.10.23 |
---|
- Total
- Today
- Yesterday
- typescript gsls
- rollup typescript react
- rollup react.js npm
- 394. decode string js
- react fiber 3d
- eslint
- ts glsl
- react glsl
- next.js import glsl
- vue
- react 3d
- react 3d 에니메이션
- react three fiber
- rollup ts react npm
- [leetcode] 394. decode string
- [leetcode] 394. decode string js
- 394. decode string javascript
- leva
- react three fiber leva
- vue3
- next.js glsl
- attempted import error: bvh_struct_definitions' is not exported from './gpu/bvhshaderglsl.js' (imported as 'bvhshaderglsl').
- react 3d text
- Vue.js
- react 3d animation
- rollup typescript
- three.js leva
- webpack glsl
- 394 decode string
- react leva
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |