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

[#26] 워크스페이스 페이지의 불필요한 리렌더링 개선 #35

Merged
merged 14 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
b4409ef
✨ feat: css 속성 물음표 아이콘 좌표를 전역 상태로 관리하는 것이 아닌 local 상태 변수를 통해 관리하도록 변경
lee0jae330 Jan 21, 2025
524a000
Revert "✨ feat: css 속성 물음표 아이콘 좌표를 전역 상태로 관리하는 것이 아닌 local 상태 변수를 통해 …
lee0jae330 Jan 21, 2025
be4ba9c
🔨 refactor: css속성 물음표 아이콘 좌표를 전역상태가 아닌 local 상태로 관리
lee0jae330 Jan 21, 2025
3f38806
🔨 refactor: 기존에 사용하던 매직넘버를 삭제하고 useLayoutEffect를 통해 툴팁의 높이를 직접 측정하고 툴…
lee0jae330 Jan 21, 2025
9630017
🔨 refactor: css item 물음표 아이콘 위치를 전역 상태가 아닌 로컬 상태변수로 관리하도록 변경, 불필요한 cs…
lee0jae330 Jan 22, 2025
c8599ec
🔨 refactor: useCssTooltip 훅을 통해 좌표 계산을 하도록 변경
lee0jae330 Jan 22, 2025
ee6098d
Merge branch 'refactor/26' of github.com:boostcampwm-2024/refactor-we…
lee0jae330 Jan 22, 2025
1466990
🙀 chore: 타입 단언 대신 타입 선언으로 변경경
lee0jae330 Jan 22, 2025
3d31790
🙀 chore: 불필요한 변수 및 반환값 삭제
lee0jae330 Jan 23, 2025
f7d9a4e
🙀 chore: 커스텀 select에서 사용하는 type 및 enum type 폴더로 분리
lee0jae330 Jan 23, 2025
30341a5
🔨 refactor: css 클래스 리스트를 만드는 로직을 커스텀 훅으로 분리
lee0jae330 Jan 23, 2025
07a64ef
🔨 refactor: useCssPropsStore에서 각 상태를 구조분해할당으로 사용하여 모든 상태를 구독하여 불필요한 리…
lee0jae330 Jan 23, 2025
df789f4
🎨 style: 불필요한 스타일 코드 삭제
lee0jae330 Jan 23, 2025
210d915
🙀 chore: 변경된 props 반영
lee0jae330 Jan 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { TCssCategory } from '@/shared/types';
import { useCssPropsStore } from '@/shared/store';

type CssCategoryButtonProps = {
cssCategory: TCssCategory;
};
Expand All @@ -11,7 +10,8 @@ type CssCategoryButtonProps = {
* CSS 카테고리를 선택할 수 있는 버튼 컴포넌트
*/
export const CssCategoryButton = ({ cssCategory }: CssCategoryButtonProps) => {
const { selectedCssCategory, setSelectedCssCategory } = useCssPropsStore();
const selectedCssCategory = useCssPropsStore((state) => state.selectedCssCategory);
const setSelectedCssCategory = useCssPropsStore((state) => state.setSelectedCssCategory);

return (
<button
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Select, SelectSize } from '@/shared/ui';
import { useCssOptionItem, useCssOptions, useCssTooltip } from '@/shared/hooks';
import { SelectSize, TCssCategoryItem } from '@/shared/types';
import { useCssOptionItem, useCssOptions } from '@/shared/hooks';

import { CssTooltip } from '@/entities';
import Question from '@/shared/assets/question.svg?react';
import { TCssCategoryItem } from '@/shared/types';
import { Select } from '@/shared/ui';
import { useCssPropsStore } from '@/shared/store';

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

Expand All @@ -31,10 +32,10 @@ export const CssOptionItem = ({ cssItem, index }: CssOptionItemProps) => {
handleEnterKey,
handleMouseLeave,
handleChangeInputValue,
offsetX,
offsetY,
} = useCssOptionItem(cssItem);

const { leftX, topY } = useCssTooltip();

return (
<div
className={`flex h-[66px] w-full flex-shrink-0 items-center justify-between rounded-lg px-4 ${
Expand All @@ -61,12 +62,9 @@ export const CssOptionItem = ({ cssItem, index }: CssOptionItemProps) => {
onMouseEnter={(e) => handleMouseEnter(e, index)}
onMouseLeave={handleMouseLeave}
/>
<CssTooltip
description={cssItem.description}
isOpen={isHover && indexOfHover === index}
leftX={leftX}
topY={topY}
/>
{isHover && indexOfHover === index && (
<CssTooltip description={cssItem.description} leftX={offsetX} topY={offsetY} />
)}
</div>
</div>
{cssItem.type === 'select' && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,23 @@ type Story = StoryObj<typeof CssTooltip>;
export const Default: Story = {
args: {
description: 'css 툴팁입니다.',
isOpen: false,
leftX: 0,
topY: 0,
},
render: (args) => {
const [isOpen, setIsOpen] = useState(args.isOpen);
const [leftX, setLeftX] = useState(0);
const [topY, setTopY] = useState(0);

const handleMouseEnter = (e: React.MouseEvent) => {
setIsOpen(true);
setLeftX(e.currentTarget.getBoundingClientRect().x + 8);
setTopY(e.currentTarget.getBoundingClientRect().y + 8);
};

const handleMouseLeave = () => {
setIsOpen(false);
};
const handleMouseLeave = () => {};
return (
<div>
<QuestionIcon onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} />
<CssTooltip description={args.description} leftX={leftX} topY={topY} isOpen={isOpen} />
<CssTooltip description={args.description} leftX={leftX} topY={topY} />
</div>
);
},
Expand All @@ -55,28 +50,23 @@ export const Default: Story = {
export const ScreenOverflow: Story = {
args: {
description: 'css 툴팁입니다.',
isOpen: false,
leftX: 0,
topY: 0,
},
render: (args) => {
const [isOpen, setIsOpen] = useState(args.isOpen);
const [leftX, setLeftX] = useState(0);
const [topY, setTopY] = useState(0);

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

const handleMouseLeave = () => {
setIsOpen(false);
};
const handleMouseLeave = () => {};
return (
<div>
<QuestionIcon onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} />
<CssTooltip description={args.description} leftX={leftX} topY={topY} isOpen={isOpen} />
<CssTooltip description={args.description} leftX={leftX} topY={topY} />
</div>
);
},
Expand Down
16 changes: 9 additions & 7 deletions apps/client/src/entities/workspace/CssTooltip/CssTooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import { createPortal } from 'react-dom';
import { useCssTooltip } from '@/shared/hooks';

type CssTooltipProps = {
description: string;
isOpen: boolean;
leftX: number;
topY: number;
};

export const CssTooltip = ({ description, isOpen, leftX, topY }: CssTooltipProps) => {
if (!isOpen) {
return null;
}
export const CssTooltip = ({ description, leftX, topY }: CssTooltipProps) => {
const { tooltipX, tooltipY, tooltipRef } = useCssTooltip(leftX, topY);
return createPortal(
<div
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`}
style={{ left: `${leftX + 18}px`, top: topY >= 0 ? `${topY + 8}px` : `${-topY}px` }}
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`}
style={{
left: `${tooltipX + 18}px`,
top: tooltipY >= 0 ? `${tooltipY + 8}px` : `${-tooltipY}px`,
}}
ref={tooltipRef}
>
<p>{description}</p>
</div>,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as Blockly from 'blockly/core';

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

import { Spinner } from '@/shared/ui';
import { capturePreview, trackEvent } from '@/shared/utils';
import { cssStyleToolboxConfig } from '@/shared/blockly';
import toast from 'react-hot-toast';
import { useParams } from 'react-router-dom';
Expand All @@ -19,7 +19,7 @@ import { useState } from 'react';
export const SaveButton = () => {
const workspaceId = useParams().workspaceId as string;
const { mutate: saveWorkspace, isPending } = useSaveWorkspace(workspaceId);
const { totalCssPropertyObj } = useCssPropsStore();
const totalCssPropertyObj = useCssPropsStore((state) => state.totalCssPropertyObj);
const { workspace } = useWorkspaceStore();
const { isResetCssChecked } = useResetCssStore();
const [isCapture, setIsCapture] = useState<boolean>(false);
Expand Down
4 changes: 2 additions & 2 deletions apps/client/src/pages/Workspacepage/WorkspacePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { useParams } from 'react-router-dom';
* 워크스페이스 페이지 컴포넌트
*/
export const WorkspacePage = () => {
const { workspaceId } = useParams();
useGetWorkspace(workspaceId as string);
const { workspaceId } = useParams<{ workspaceId: string }>();
useGetWorkspace(workspaceId!);
usePreventLeaveWorkspacePage();
const { currentStep, isCoachMarkOpen, openCoachMark } = useCoachMarkStore();
const toolboxDiv = document.querySelector('.blocklyToolboxDiv');
Expand Down
23 changes: 23 additions & 0 deletions apps/client/src/shared/hooks/css/useCssClassList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useEffect, useState } from 'react';

import { TOption } from '@/shared/types';
import { useClassBlockStore } from '@/shared/store';

export const useCssClassList = () => {
const { classBlockList } = useClassBlockStore();
const [cssClassList, setCssClassList] = useState<string[]>([]);

useEffect(() => {
setCssClassList(classBlockList);
}, [classBlockList]);

const selectOptions: TOption[] = [
{ value: '', label: '클래스를 선택해주세요' },
...cssClassList.map((cssClass) => ({
value: cssClass,
label: cssClass,
})),
];

return { selectOptions };
};
12 changes: 9 additions & 3 deletions apps/client/src/shared/hooks/css/useCssOptionItem.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { useCssPropsStore, useCssTooltipStore } from '@/shared/store';
import { useEffect, useState } from 'react';

import { TCssCategoryItem } from '@/shared/types';
import { useCssOptions } from '@/shared/hooks';
import { useCssPropsStore } from '@/shared/store';

export const useCssOptionItem = (cssItem: TCssCategoryItem) => {
const { handleCssOptionChange } = useCssOptions();
const { setOffsetX, setOffsetY } = useCssTooltipStore();
const { currentCssClassName, totalCssPropertyObj, selectedCssCategory } = useCssPropsStore();
const currentCssClassName = useCssPropsStore((state) => state.currentCssClassName);
const totalCssPropertyObj = useCssPropsStore((state) => state.totalCssPropertyObj);
const selectedCssCategory = useCssPropsStore((state) => state.selectedCssCategory);

Comment on lines +9 to +11
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

구조분해할당을 지양하는 것이 베스트지만, 여러 상태를 불러와야할 경우 useShallow도 추천드립니당

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

const [offsetX, setOffsetX] = useState<number>(0);
const [offsetY, setOffsetY] = useState<number>(0);

useEffect(() => {
if (totalCssPropertyObj[currentCssClassName]) {
setCssOptionValue(totalCssPropertyObj[currentCssClassName].cssOptionObj[cssItem.label] || '');
Expand Down Expand Up @@ -84,5 +88,7 @@ export const useCssOptionItem = (cssItem: TCssCategoryItem) => {
handleMouseEnter,
handleMouseLeave,
handleChangeInputValue,
offsetX,
offsetY,
};
};
5 changes: 4 additions & 1 deletion apps/client/src/shared/hooks/css/useCssOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { debounce } from '@/shared/utils';
import { useCallback } from 'react';

export const useCssOptions = () => {
const { setCheckedCssPropertyObj, setCssOptionObj, currentCssClassName } = useCssPropsStore();
const setCheckedCssPropertyObj = useCssPropsStore((state) => state.setCheckedCssPropertyObj);
const setCssOptionObj = useCssPropsStore((state) => state.setCssOptionObj);
const currentCssClassName = useCssPropsStore((state) => state.currentCssClassName);

const { setIsCssChanged } = useWorkspaceChangeStatusStore();
const handleCssPropertyCheckboxChange = (
property: string,
Expand Down
36 changes: 21 additions & 15 deletions apps/client/src/shared/hooks/css/useCssTooltip.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import { useCssTooltipStore } from '@/shared/store';
import { useEffect } from 'react';
import { useWindowSize } from '@/shared/hooks';
import { useLayoutEffect, useRef, useState } from 'react';

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

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

useEffect(() => {
const tooltipHeight = 40;
setLeftX(offsetX);
if (offsetY + tooltipHeight > screenHeight) {
setTopY(-offsetY + tooltipHeight); // 높이를 벗어나는 것임
} else {
setTopY(offsetY);
useLayoutEffect(() => {
if (tooltipRef.current) {
setTooltipHeight(tooltipRef.current.getBoundingClientRect().height);
}
}, [offsetX, offsetY, screenWidth, screenHeight]);
return () => setTooltipHeight(0);
}, [tooltipRef.current]);

let tooltipX = leftX;
let tooltipY = 0;

if (topY + tooltipHeight > screenHeight) {
tooltipY = -topY + tooltipHeight;
} else {
tooltipY = topY;
}

return { leftX, topY };
return { tooltipX, tooltipY, tooltipRef };
};
1 change: 1 addition & 0 deletions apps/client/src/shared/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export { useDeleteImage } from './queries/useDeleteImage';
export { useCssTooltip } from './css/useCssTooltip';
export { useCssOptions } from './css/useCssOptions';
export { useCssOptionItem } from './css/useCssOptionItem';
export { useCssClassList } from './css/useCssClassList';

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

Expand Down
5 changes: 2 additions & 3 deletions apps/client/src/shared/hooks/queries/useGetWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ import { workspaceKeys } from '@/shared/hooks';
export const useGetWorkspace = (workspaceId: string) => {
const workspaceApi = WorkspaceApi();
const userId = getUserId() || createUserId();
const { initCssPropertyObj } = useCssPropsStore();
const initCssPropertyObj = useCssPropsStore((state) => state.initCssPropertyObj);
const { initClassBlockList } = useClassBlockStore();
const { setCanvasInfo, setName } = useWorkspaceStore();
const { resetChangedStatusState } = useWorkspaceChangeStatusStore();
const { setIsResetCssChecked } = useResetCssStore();
const { setInitialImageMap, setInitialImageList } = useImageModalStore();
const { data, isPending, isError } = useSuspenseQuery({
const { data, isError } = useSuspenseQuery({
queryKey: workspaceKeys.detail(workspaceId),
queryFn: () => {
resetChangedStatusState();
Expand Down Expand Up @@ -55,5 +55,4 @@ export const useGetWorkspace = (workspaceId: string) => {
setInitialImageMap(data.workspaceDto.imageMap);
setInitialImageList(data.workspaceDto.imageList);
}, [isError, data]);
return { data, isPending, isError };
};
1 change: 0 additions & 1 deletion apps/client/src/shared/store/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
export { useLoadingStore } from './useLoadingStore';
export { useModalStore } from './useModalStore';
export { useCssPropsStore } from './useCssPropsStore';
export { useCssTooltipStore } from './useCssTooptipStore';
export { useClassBlockStore } from './useClassBlockStore';
export { useWorkspaceChangeStatusStore } from './useWorkspaceChangeStatusStore';
export { useBlocklyWorkspaceStore } from './useBlocklyWorkspaceStore';
Expand Down
3 changes: 2 additions & 1 deletion apps/client/src/shared/store/useCssPropsStore.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { removeCssClassNamePrefix, trackEvent } from '../utils';

import { TTotalCssPropertyObj } from '@/shared/types';
import { create } from 'zustand';
import { removeCssClassNamePrefix, trackEvent } from '../utils';

type TcssProps = {
currentCssClassName: string;
Expand Down
24 changes: 0 additions & 24 deletions apps/client/src/shared/store/useCssTooptipStore.ts

This file was deleted.

10 changes: 10 additions & 0 deletions apps/client/src/shared/types/customSelectType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export enum SelectSize {
SMALL = 'SMALL',

MEDIUM = 'MEDIUM',
}

export type TOption = {
value: string;
label: string;
};
2 changes: 2 additions & 0 deletions apps/client/src/shared/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ export type { TTabConfig, TTabsConfig, TTabToolboxConfig } from './tabType';
export type { TBlock, TToolboxConfig } from './styleToolboxType';
export type { TButtonContent } from './modalButtonType';
export type { TImage } from './imageTagType';
export type { TOption } from './customSelectType';
export { SelectSize } from './customSelectType';
Loading
Loading