Skip to content

Commit 8e6a057

Browse files
authored
[#26] 워크스페이스 페이지의 불필요한 리렌더링 개선 (#35)
* ✨ feat: css 속성 물음표 아이콘 좌표를 전역 상태로 관리하는 것이 아닌 local 상태 변수를 통해 관리하도록 변경 * Revert "✨ feat: css 속성 물음표 아이콘 좌표를 전역 상태로 관리하는 것이 아닌 local 상태 변수를 통해 관리하도록 변경" This reverts commit b4409ef. * 🔨 refactor: css속성 물음표 아이콘 좌표를 전역상태가 아닌 local 상태로 관리 * 🔨 refactor: 기존에 사용하던 매직넘버를 삭제하고 useLayoutEffect를 통해 툴팁의 높이를 직접 측정하고 툴팁의 좌표를 계산하는 방식으로 변경 * 🔨 refactor: css item 물음표 아이콘 위치를 전역 상태가 아닌 로컬 상태변수로 관리하도록 변경, 불필요한 cssTooltiptore 삭제 * 🔨 refactor: useCssTooltip 훅을 통해 좌표 계산을 하도록 변경 * 🙀 chore: 타입 단언 대신 타입 선언으로 변경경 * 🙀 chore: 불필요한 변수 및 반환값 삭제 * 🙀 chore: 커스텀 select에서 사용하는 type 및 enum type 폴더로 분리 * 🔨 refactor: css 클래스 리스트를 만드는 로직을 커스텀 훅으로 분리 * 🔨 refactor: useCssPropsStore에서 각 상태를 구조분해할당으로 사용하여 모든 상태를 구독하여 불필요한 리렌더링이 발생하는 문제를 해결함 * 🎨 style: 불필요한 스타일 코드 삭제 * 🙀 chore: 변경된 props 반영
1 parent 28cbcf8 commit 8e6a057

File tree

22 files changed

+119
-126
lines changed

22 files changed

+119
-126
lines changed

apps/client/src/entities/workspace/CssCategoryButton/CssCategoryButton.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { TCssCategory } from '@/shared/types';
22
import { useCssPropsStore } from '@/shared/store';
3-
43
type CssCategoryButtonProps = {
54
cssCategory: TCssCategory;
65
};
@@ -11,7 +10,8 @@ type CssCategoryButtonProps = {
1110
* CSS 카테고리를 선택할 수 있는 버튼 컴포넌트
1211
*/
1312
export const CssCategoryButton = ({ cssCategory }: CssCategoryButtonProps) => {
14-
const { selectedCssCategory, setSelectedCssCategory } = useCssPropsStore();
13+
const selectedCssCategory = useCssPropsStore((state) => state.selectedCssCategory);
14+
const setSelectedCssCategory = useCssPropsStore((state) => state.setSelectedCssCategory);
1515

1616
return (
1717
<button

apps/client/src/entities/workspace/CssOptionItem/CssOptionItem.tsx

+10-12
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { Select, SelectSize } from '@/shared/ui';
2-
import { useCssOptionItem, useCssOptions, useCssTooltip } from '@/shared/hooks';
1+
import { SelectSize, TCssCategoryItem } from '@/shared/types';
2+
import { useCssOptionItem, useCssOptions } from '@/shared/hooks';
33

44
import { CssTooltip } from '@/entities';
55
import Question from '@/shared/assets/question.svg?react';
6-
import { TCssCategoryItem } from '@/shared/types';
6+
import { Select } from '@/shared/ui';
77
import { useCssPropsStore } from '@/shared/store';
88

99
type CssOptionItemProps = {
@@ -17,7 +17,8 @@ type CssOptionItemProps = {
1717
* CSS 속성을 선택할 수 있는 컴포넌트
1818
*/
1919
export const CssOptionItem = ({ cssItem, index }: CssOptionItemProps) => {
20-
const { totalCssPropertyObj, currentCssClassName } = useCssPropsStore();
20+
const totalCssPropertyObj = useCssPropsStore((state) => state.totalCssPropertyObj);
21+
const currentCssClassName = useCssPropsStore((state) => state.currentCssClassName);
2122
const { handleCssPropertyCheckboxChange, handleCssOptionChange, handleColorChange } =
2223
useCssOptions();
2324

@@ -31,10 +32,10 @@ export const CssOptionItem = ({ cssItem, index }: CssOptionItemProps) => {
3132
handleEnterKey,
3233
handleMouseLeave,
3334
handleChangeInputValue,
35+
offsetX,
36+
offsetY,
3437
} = useCssOptionItem(cssItem);
3538

36-
const { leftX, topY } = useCssTooltip();
37-
3839
return (
3940
<div
4041
className={`flex h-[66px] w-full flex-shrink-0 items-center justify-between rounded-lg px-4 ${
@@ -61,12 +62,9 @@ export const CssOptionItem = ({ cssItem, index }: CssOptionItemProps) => {
6162
onMouseEnter={(e) => handleMouseEnter(e, index)}
6263
onMouseLeave={handleMouseLeave}
6364
/>
64-
<CssTooltip
65-
description={cssItem.description}
66-
isOpen={isHover && indexOfHover === index}
67-
leftX={leftX}
68-
topY={topY}
69-
/>
65+
{isHover && indexOfHover === index && (
66+
<CssTooltip description={cssItem.description} leftX={offsetX} topY={offsetY} />
67+
)}
7068
</div>
7169
</div>
7270
{cssItem.type === 'select' && (

apps/client/src/entities/workspace/CssTooltip/CssTooltip.stories.tsx

+4-14
Original file line numberDiff line numberDiff line change
@@ -25,28 +25,23 @@ type Story = StoryObj<typeof CssTooltip>;
2525
export const Default: Story = {
2626
args: {
2727
description: 'css 툴팁입니다.',
28-
isOpen: false,
2928
leftX: 0,
3029
topY: 0,
3130
},
3231
render: (args) => {
33-
const [isOpen, setIsOpen] = useState(args.isOpen);
3432
const [leftX, setLeftX] = useState(0);
3533
const [topY, setTopY] = useState(0);
3634

3735
const handleMouseEnter = (e: React.MouseEvent) => {
38-
setIsOpen(true);
3936
setLeftX(e.currentTarget.getBoundingClientRect().x + 8);
4037
setTopY(e.currentTarget.getBoundingClientRect().y + 8);
4138
};
4239

43-
const handleMouseLeave = () => {
44-
setIsOpen(false);
45-
};
40+
const handleMouseLeave = () => {};
4641
return (
4742
<div>
4843
<QuestionIcon onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} />
49-
<CssTooltip description={args.description} leftX={leftX} topY={topY} isOpen={isOpen} />
44+
<CssTooltip description={args.description} leftX={leftX} topY={topY} />
5045
</div>
5146
);
5247
},
@@ -55,28 +50,23 @@ export const Default: Story = {
5550
export const ScreenOverflow: Story = {
5651
args: {
5752
description: 'css 툴팁입니다.',
58-
isOpen: false,
5953
leftX: 0,
6054
topY: 0,
6155
},
6256
render: (args) => {
63-
const [isOpen, setIsOpen] = useState(args.isOpen);
6457
const [leftX, setLeftX] = useState(0);
6558
const [topY, setTopY] = useState(0);
6659

6760
const handleMouseEnter = (e: React.MouseEvent) => {
68-
setIsOpen(true);
6961
setLeftX(e.currentTarget.getBoundingClientRect().x + 8);
7062
setTopY(-e.currentTarget.getBoundingClientRect().y + 40);
7163
};
7264

73-
const handleMouseLeave = () => {
74-
setIsOpen(false);
75-
};
65+
const handleMouseLeave = () => {};
7666
return (
7767
<div>
7868
<QuestionIcon onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} />
79-
<CssTooltip description={args.description} leftX={leftX} topY={topY} isOpen={isOpen} />
69+
<CssTooltip description={args.description} leftX={leftX} topY={topY} />
8070
</div>
8171
);
8272
},

apps/client/src/entities/workspace/CssTooltip/CssTooltip.tsx

+9-7
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
import { createPortal } from 'react-dom';
2+
import { useCssTooltip } from '@/shared/hooks';
23

34
type CssTooltipProps = {
45
description: string;
5-
isOpen: boolean;
66
leftX: number;
77
topY: number;
88
};
99

10-
export const CssTooltip = ({ description, isOpen, leftX, topY }: CssTooltipProps) => {
11-
if (!isOpen) {
12-
return null;
13-
}
10+
export const CssTooltip = ({ description, leftX, topY }: CssTooltipProps) => {
11+
const { tooltipX, tooltipY, tooltipRef } = useCssTooltip(leftX, topY);
1412
return createPortal(
1513
<div
16-
className={`text-gray-white text-tooltip-sm fixed left-0 top-0 z-[9999] rounded-3xl ${topY >= 0 ? 'rounded-tl-none' : 'rounded-bl-none'} bg-green-500 px-3 py-2`}
17-
style={{ left: `${leftX + 18}px`, top: topY >= 0 ? `${topY + 8}px` : `${-topY}px` }}
14+
className={`text-gray-white text-tooltip-sm fixed z-[9999] rounded-3xl ${tooltipY >= 0 ? 'rounded-tl-none' : 'rounded-bl-none'} bg-green-500 px-3 py-2`}
15+
style={{
16+
left: `${tooltipX + 18}px`,
17+
top: tooltipY >= 0 ? `${tooltipY + 8}px` : `${-tooltipY}px`,
18+
}}
19+
ref={tooltipRef}
1820
>
1921
<p>{description}</p>
2022
</div>,

apps/client/src/entities/workspace/SaveButton/SaveButton.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import * as Blockly from 'blockly/core';
22

3+
import { capturePreview, trackEvent } from '@/shared/utils';
34
import { useCssPropsStore, useResetCssStore, useWorkspaceStore } from '@/shared/store';
45

56
import { Spinner } from '@/shared/ui';
6-
import { capturePreview, trackEvent } from '@/shared/utils';
77
import { cssStyleToolboxConfig } from '@/shared/blockly';
88
import toast from 'react-hot-toast';
99
import { useParams } from 'react-router-dom';
@@ -19,7 +19,7 @@ import { useState } from 'react';
1919
export const SaveButton = () => {
2020
const workspaceId = useParams().workspaceId as string;
2121
const { mutate: saveWorkspace, isPending } = useSaveWorkspace(workspaceId);
22-
const { totalCssPropertyObj } = useCssPropsStore();
22+
const totalCssPropertyObj = useCssPropsStore((state) => state.totalCssPropertyObj);
2323
const { workspace } = useWorkspaceStore();
2424
const { isResetCssChecked } = useResetCssStore();
2525
const [isCapture, setIsCapture] = useState<boolean>(false);

apps/client/src/pages/Workspacepage/WorkspacePage.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import { useParams } from 'react-router-dom';
1111
* 워크스페이스 페이지 컴포넌트
1212
*/
1313
export const WorkspacePage = () => {
14-
const { workspaceId } = useParams();
15-
useGetWorkspace(workspaceId as string);
14+
const { workspaceId } = useParams<{ workspaceId: string }>();
15+
useGetWorkspace(workspaceId!);
1616
usePreventLeaveWorkspacePage();
1717
const { currentStep, isCoachMarkOpen, openCoachMark } = useCoachMarkStore();
1818
const toolboxDiv = document.querySelector('.blocklyToolboxDiv');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { useEffect, useState } from 'react';
2+
3+
import { TOption } from '@/shared/types';
4+
import { useClassBlockStore } from '@/shared/store';
5+
6+
export const useCssClassList = () => {
7+
const { classBlockList } = useClassBlockStore();
8+
const [cssClassList, setCssClassList] = useState<string[]>([]);
9+
10+
useEffect(() => {
11+
setCssClassList(classBlockList);
12+
}, [classBlockList]);
13+
14+
const selectOptions: TOption[] = [
15+
{ value: '', label: '클래스를 선택해주세요' },
16+
...cssClassList.map((cssClass) => ({
17+
value: cssClass,
18+
label: cssClass,
19+
})),
20+
];
21+
22+
return { selectOptions };
23+
};

apps/client/src/shared/hooks/css/useCssOptionItem.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
import { useCssPropsStore, useCssTooltipStore } from '@/shared/store';
21
import { useEffect, useState } from 'react';
32

43
import { TCssCategoryItem } from '@/shared/types';
54
import { useCssOptions } from '@/shared/hooks';
5+
import { useCssPropsStore } from '@/shared/store';
66

77
export const useCssOptionItem = (cssItem: TCssCategoryItem) => {
88
const { handleCssOptionChange } = useCssOptions();
9-
const { setOffsetX, setOffsetY } = useCssTooltipStore();
10-
const { currentCssClassName, totalCssPropertyObj, selectedCssCategory } = useCssPropsStore();
9+
const currentCssClassName = useCssPropsStore((state) => state.currentCssClassName);
10+
const totalCssPropertyObj = useCssPropsStore((state) => state.totalCssPropertyObj);
11+
const selectedCssCategory = useCssPropsStore((state) => state.selectedCssCategory);
1112

1213
const [cssOptionValue, setCssOptionValue] = useState<string>('');
1314
const [isHover, setIsHover] = useState<boolean>(false);
@@ -18,6 +19,9 @@ export const useCssOptionItem = (cssItem: TCssCategoryItem) => {
1819
cssItem.type === 'select' && cssItem.option!.length > 0 ? cssItem.option![0] : ''
1920
);
2021

22+
const [offsetX, setOffsetX] = useState<number>(0);
23+
const [offsetY, setOffsetY] = useState<number>(0);
24+
2125
useEffect(() => {
2226
if (totalCssPropertyObj[currentCssClassName]) {
2327
setCssOptionValue(totalCssPropertyObj[currentCssClassName].cssOptionObj[cssItem.label] || '');
@@ -84,5 +88,7 @@ export const useCssOptionItem = (cssItem: TCssCategoryItem) => {
8488
handleMouseEnter,
8589
handleMouseLeave,
8690
handleChangeInputValue,
91+
offsetX,
92+
offsetY,
8793
};
8894
};

apps/client/src/shared/hooks/css/useCssOptions.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { debounce } from '@/shared/utils';
44
import { useCallback } from 'react';
55

66
export const useCssOptions = () => {
7-
const { setCheckedCssPropertyObj, setCssOptionObj, currentCssClassName } = useCssPropsStore();
7+
const setCheckedCssPropertyObj = useCssPropsStore((state) => state.setCheckedCssPropertyObj);
8+
const setCssOptionObj = useCssPropsStore((state) => state.setCssOptionObj);
9+
const currentCssClassName = useCssPropsStore((state) => state.currentCssClassName);
10+
811
const { setIsCssChanged } = useWorkspaceChangeStatusStore();
912
const handleCssPropertyCheckboxChange = (
1013
property: string,
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
1-
import { useCssTooltipStore } from '@/shared/store';
2-
import { useEffect } from 'react';
3-
import { useWindowSize } from '@/shared/hooks';
1+
import { useLayoutEffect, useRef, useState } from 'react';
42

5-
export const useCssTooltip = () => {
6-
const { leftX, topY, offsetX, offsetY, setLeftX, setTopY } = useCssTooltipStore();
3+
import { useWindowSize } from '@/shared/hooks';
74

8-
const { screenWidth, screenHeight } = useWindowSize();
5+
export const useCssTooltip = (leftX: number, topY: number) => {
6+
const tooltipRef = useRef<HTMLDivElement | null>(null);
7+
const [tooltipHeight, setTooltipHeight] = useState<number>(0);
8+
const { screenHeight } = useWindowSize();
99

10-
useEffect(() => {
11-
const tooltipHeight = 40;
12-
setLeftX(offsetX);
13-
if (offsetY + tooltipHeight > screenHeight) {
14-
setTopY(-offsetY + tooltipHeight); // 높이를 벗어나는 것임
15-
} else {
16-
setTopY(offsetY);
10+
useLayoutEffect(() => {
11+
if (tooltipRef.current) {
12+
setTooltipHeight(tooltipRef.current.getBoundingClientRect().height);
1713
}
18-
}, [offsetX, offsetY, screenWidth, screenHeight]);
14+
return () => setTooltipHeight(0);
15+
}, [tooltipRef.current]);
16+
17+
let tooltipX = leftX;
18+
let tooltipY = 0;
19+
20+
if (topY + tooltipHeight > screenHeight) {
21+
tooltipY = -topY + tooltipHeight;
22+
} else {
23+
tooltipY = topY;
24+
}
1925

20-
return { leftX, topY };
26+
return { tooltipX, tooltipY, tooltipRef };
2127
};

apps/client/src/shared/hooks/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export { useDeleteImage } from './queries/useDeleteImage';
1010
export { useCssTooltip } from './css/useCssTooltip';
1111
export { useCssOptions } from './css/useCssOptions';
1212
export { useCssOptionItem } from './css/useCssOptionItem';
13+
export { useCssClassList } from './css/useCssClassList';
1314

1415
export { workspaceKeys } from './query-key/workspaceKeys';
1516

apps/client/src/shared/hooks/queries/useGetWorkspace.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ import { workspaceKeys } from '@/shared/hooks';
1717
export const useGetWorkspace = (workspaceId: string) => {
1818
const workspaceApi = WorkspaceApi();
1919
const userId = getUserId() || createUserId();
20-
const { initCssPropertyObj } = useCssPropsStore();
20+
const initCssPropertyObj = useCssPropsStore((state) => state.initCssPropertyObj);
2121
const { initClassBlockList } = useClassBlockStore();
2222
const { setCanvasInfo, setName } = useWorkspaceStore();
2323
const { resetChangedStatusState } = useWorkspaceChangeStatusStore();
2424
const { setIsResetCssChecked } = useResetCssStore();
2525
const { setInitialImageMap, setInitialImageList } = useImageModalStore();
26-
const { data, isPending, isError } = useSuspenseQuery({
26+
const { data, isError } = useSuspenseQuery({
2727
queryKey: workspaceKeys.detail(workspaceId),
2828
queryFn: () => {
2929
resetChangedStatusState();
@@ -55,5 +55,4 @@ export const useGetWorkspace = (workspaceId: string) => {
5555
setInitialImageMap(data.workspaceDto.imageMap);
5656
setInitialImageList(data.workspaceDto.imageList);
5757
}, [isError, data]);
58-
return { data, isPending, isError };
5958
};

apps/client/src/shared/store/index.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
export { useLoadingStore } from './useLoadingStore';
22
export { useModalStore } from './useModalStore';
33
export { useCssPropsStore } from './useCssPropsStore';
4-
export { useCssTooltipStore } from './useCssTooptipStore';
54
export { useClassBlockStore } from './useClassBlockStore';
65
export { useWorkspaceChangeStatusStore } from './useWorkspaceChangeStatusStore';
76
export { useBlocklyWorkspaceStore } from './useBlocklyWorkspaceStore';

apps/client/src/shared/store/useCssPropsStore.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import { removeCssClassNamePrefix, trackEvent } from '../utils';
2+
13
import { TTotalCssPropertyObj } from '@/shared/types';
24
import { create } from 'zustand';
3-
import { removeCssClassNamePrefix, trackEvent } from '../utils';
45

56
type TcssProps = {
67
currentCssClassName: string;

apps/client/src/shared/store/useCssTooptipStore.ts

-24
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export enum SelectSize {
2+
SMALL = 'SMALL',
3+
4+
MEDIUM = 'MEDIUM',
5+
}
6+
7+
export type TOption = {
8+
value: string;
9+
label: string;
10+
};

apps/client/src/shared/types/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,5 @@ export type { TTabConfig, TTabsConfig, TTabToolboxConfig } from './tabType';
1818
export type { TBlock, TToolboxConfig } from './styleToolboxType';
1919
export type { TButtonContent } from './modalButtonType';
2020
export type { TImage } from './imageTagType';
21+
export type { TOption } from './customSelectType';
22+
export { SelectSize } from './customSelectType';

0 commit comments

Comments
 (0)