- 프로젝트: loplat UI
- 키워드: Toast, class, setTimeout
- 상황
- 기존 loplat UI의 Toast 컴포넌트는 react-hot-toast 라는 라이브러리를 fork 한 후, 사내 디자인에 맞게 변형하여 사용하고 있었다.
- Toast 컴포넌트의 변경사항이나 버그가 있을 때 코드의 흐름을 이해하기 어려웠고, 필요한 기능에 비해 용량이 매우 컸다.(20Kb)
- 직접 Toast를 구현하는 것이 유지보수, 용량, 성능 모든 면에서 더 좋을 것이다.
- 해결 방법
- 아래의 예시 코드와 같이 Toast 를 App에서 렌더링하고, toast 인스턴스를 통해 함수를 트리거하는 것으로 설계했다.
'Toast' 컴포넌트와 'toast' 인스턴스를 연결하는 것이 구현의 핵심이다.
<Toast /> ... <Button onClick={() => toast.success('성공')}> Success </Button>
- 최종 결과물은 총 5개의 파일이며, 각 파일은 다음과 같은 역할을 한다.
- Toast.tsx: ToastBar를 담고 있는 Wrapper
- ToastBar.tsx: 하나의 메시지를 담고 있는 Bar UI
- toaster.ts: toast 함수(success, danger, warning, info)를 담고 있는 class
- types: typescript 관련 타입 선언
- utils: 기타 함수들(ex> 적절한 colorset을 함수)
- 먼저 toaster.ts 파일의 class를 작성한다.
리액트의 setState 함수를 멤버 변수로 저장한 후, addToastItem과 removeToastItem 메서드 내부에서 setState 함수를 이용하는 방식이다.type SetToastItems = React.Dispatch<SetStateAction<ToastItem[]>>; class Toaster { setToastItems: SetToastItems = () => { return; }; constructor(setState: SetToastItems | null) { if (setState) this.setToastItems = setState; } addToastItem({ type, message }: Omit<ToastItem, 'id'>): void { this.setToastItems((state: ToastItem[]) => [{ id: generateUniqueId(), type, message }, ...state]); } removeToastItem(toastId: ToastItem['id']): void { this.setToastItems((state: ToastItem[]) => { const indexToRemove = state.findIndex((toastItem) => toastItem.id === toastId); if (indexToRemove > -1) { return [...state.slice(0, indexToRemove), ...state.slice(indexToRemove + 1)]; } return state; }); } success(message: ToastItem['message']): void { this.addToastItem({ type: 'success', message }); } info(message: ToastItem['message']): void { this.addToastItem({ type: 'info', message }); } danger(message: ToastItem['message']): void { this.addToastItem({ type: 'danger', message }); } warning(message: ToastItem['message']): void { this.addToastItem({ type: 'warning', message }); } }
Toast.tsx 파일에서 해당 class를 이용하여 toast instance를 만들고, export 한다.
그럼 setToastItems를 통해 Toast 컴포넌트와 toast instance가 연결된다!
export let toast: Toaster = new Toaster(null); export const Toast = ({ ... }: Props): React.ReactElement => { const [toastItems, setToastItems] = useState<ToastItem[]>([]); useEffect(() => { /** Initialize */ toast = new Toaster(setToastItems); }, []); ... }
이제 toast.success() 와 같이 함수를 호출하면 addToastItem 호출 -> setToastItems 호출로 인해 toastItem이 추가된다. - 추가된 ToastItems 배열을 렌더링한다.
return ( <ToastWrapper {...}> {toastItems.map((toastItem) => ( <ToastBar toastItem={toastItem} ... key={toastItem.id} /> ))} </ToastWrapper> );
- ToastBar가 mount, unmount될 때 opacity transition로 애니메이션 효과를 준다.
// ToastBar.tsx let toastDuration = 3000; const ANIMATION_DURATION = 350; ... // lifecycle(add/remove) 관련 로직 useEffect(() => { setOpacity(1); const timeoutForRemove = setTimeout(() => { onRemoveToastItem(toastItem.id); }, toastDuration); const timeoutForOpacity = setTimeout(() => { setOpacity(0); }, toastDuration - ANIMATION_DURATION); return () => { clearTimeout(timeoutForRemove); clearTimeout(timeoutForOpacity); }; }, [toastItem, onRemoveToastItem, toastDuration]);
setTimeout을 이용하여
- toastDuration이 끝나기 직전(i.e. toastDuration - ANIMATION_DURATION) opacity를 서서히 줄이는 애니메이션을 트리거하고,
- toastDuration이 지났을 때 toastItem을 제거해야함을 부모에게 알릴 수 있다.(onRemoveToastItem) - 버튼을 눌러 toast 함수를 실행하면, ToastBar가 렌더링 되는 것을 확인할 수 있다! 스토리북으로 확인하기
- 아래의 예시 코드와 같이 Toast 를 App에서 렌더링하고, toast 인스턴스를 통해 함수를 트리거하는 것으로 설계했다.
- 의의
- 이해하기 어려운 레거시 코드와 로직을 5개의 파일로 압축하였고, class 방식을 도입해 핵심기능을 그대로 구현했다.
레거시 코드를 충분히 이해하고 개발을 진행하니 0.5일 만에 컴포넌트를 만들어낼 수 있었다. - 번들링 용량을 20KB → 8KB로 60% 줄였다.
- ToastBar를 연달아 생성하거나 지워도 frame이 60fps로 유지된다.
- 이해하기 어려운 레거시 코드와 로직을 5개의 파일로 압축하였고, class 방식을 도입해 핵심기능을 그대로 구현했다.
- known issues
- react portal을 이용하여 layer를 분리하면 좋을 것 같다.
- translate를 이용한 ToastBar의 위치 조정, 메시지 길이에 따른 ToasterBar height 계산 등 추가 코드가 있지만 핵심 로직이 아니므로 포스팅에선 생략했다.
'프론트엔드 > React' 카테고리의 다른 글
노래맞히기 게임 구현하기(feat. holwer.js, classList) (0) | 2022.03.06 |
---|---|
선언형 Portal, Modal 컴포넌트 구현하기 (0) | 2022.02.15 |
리액트에서 달팽이 모양으로 움직이는 애니메이션 만들기 (0) | 2022.01.19 |
SVGR id collision issue 해결하기 (0) | 2021.11.30 |
Firebase auth 정보를 redux로 관리하기 (0) | 2021.11.18 |