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

MyTemplatePage 초기 랜더링 Layoutshift 개선 #706

Merged
merged 10 commits into from
Sep 27, 2024
Merged
5 changes: 4 additions & 1 deletion frontend/src/api/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ export const getTemplateList = async ({
memberId,
}: TemplateListRequest) => {
const queryParams = new URLSearchParams({
keyword,
sort,
page: page.toString(),
size: size.toString(),
Expand All @@ -59,6 +58,10 @@ export const getTemplateList = async ({
queryParams.append('tagIds', tagIds.toString());
}

if (keyword) {
queryParams.append('keyword', keyword);
}

Copy link
Contributor

Choose a reason for hiding this comment

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

놓친 부분이 있었네요~!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

놓친부분이라기보다는 백엔드에서 요구사항이 변경에 따른 변화입니다..!ㅎㅎ
이전에는 키워드가 없어도 반드시 쿼리에 추가해서 보내달라고했는데,
이번에 리팩토링하면서 키워드가 없을때 주지 않도록 해달라고 하여 변경하게 되었습니다!ㅎㅎ

const url = `${TEMPLATE_API_URL}${memberId ? '/login' : ''}?${queryParams.toString()}`;

const response = await customFetch<TemplateListResponse>({
Expand Down
43 changes: 26 additions & 17 deletions frontend/src/components/Footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
import { Text } from '@/components';
import { useAuth } from '@/hooks/authentication';

import * as S from './Footer.style';

const Footer = () => (
<S.FooterContainer>
<Text.Small color='inherit'>
Copyright{' '}
<Text.Small color='inherit' weight='bold'>
Codezap
</Text.Small>{' '}
© All rights reserved.
</Text.Small>
<S.ContactEmail href='mailto:[email protected]'>
<Text.Small color='inherit' weight='bold'>
문의 :
</Text.Small>{' '}
<Text.Small color='inherit'>[email protected]</Text.Small>{' '}
</S.ContactEmail>
</S.FooterContainer>
);
const Footer = () => {
const { isChecking } = useAuth();

if (isChecking) {
return null;
}
Comment on lines +7 to +11
Copy link
Contributor

Choose a reason for hiding this comment

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

해당 suspense 처리만 해주어도 footer 관련 Layout Shift가 해결이 되나요??

Copy link
Contributor Author

Choose a reason for hiding this comment

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

이 부분은 Header와 동일한 로직으로 변경한 것인데요, 로그인한 유저인지 아닌지 검사하는동안 footer를 null 로 처리했다가 나중에 return 해주었습니다.
(footer는 언제나 맨 하단에 위치하기 때문에 나중에 떠도 Layout Shift가 발생하지 않을 것이라 생각하였습니다.)

이렇게 하면 Layout Shift을 완벽하게 해결할 순 없지만 처음에 너무 빨리 렌더링 되는 것을 막아 깜빡이는 현상을 없앨 수 있었습니다.

Copy link
Contributor

Choose a reason for hiding this comment

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

괜찮은 방법이라고 생각되네요! 더 좋은 방법이 떠오르지 않습니당


return (
<S.FooterContainer>
<Text.Small color='inherit'>
Copyright{' '}
<Text.Small color='inherit' weight='bold'>
Codezap
</Text.Small>{' '}
© All rights reserved.
</Text.Small>
<S.ContactEmail href='mailto:[email protected]'>
<Text.Small color='inherit' weight='bold'>
문의 :
</Text.Small>{' '}
<Text.Small color='inherit'>[email protected]</Text.Small>{' '}
</S.ContactEmail>
</S.FooterContainer>
);
};

export default Footer;
103 changes: 57 additions & 46 deletions frontend/src/pages/MyTemplatesPage/MyTemplatePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ import { useNavigate } from 'react-router-dom';

import { DEFAULT_SORTING_OPTION, SORTING_OPTIONS } from '@/api';
import { ArrowUpIcon, PlusIcon, SearchIcon, ZapzapCuriousLogo } from '@/assets/images';
import { Flex, Heading, Input, PagingButtons, Dropdown, Button, Modal, Text, LoadingBall } from '@/components';
import { Flex, Heading, Input, PagingButtons, Dropdown, Button, Modal, Text } from '@/components';
import { useWindowWidth, useDebounce, useToggle, useDropdown, useInput } from '@/hooks';
import { useAuth } from '@/hooks/authentication';
import { useCategoryListQuery } from '@/queries/categories';
import { useTagListQuery } from '@/queries/tags';
import { useTemplateDeleteMutation, useTemplateListQuery } from '@/queries/templates';
import { useTemplateDeleteMutation, useTemplateCategoryTagQueries } from '@/queries/templates';
import { END_POINTS } from '@/routes';
import { theme } from '@/style/theme';
import { scroll } from '@/utils';
Expand Down Expand Up @@ -36,18 +34,17 @@ const MyTemplatePage = () => {

const [page, setPage] = useState<number>(1);

const { data: templateData, isPending } = useTemplateListQuery({
const [{ data: templateData }, { data: categoryData }, { data: tagData }] = useTemplateCategoryTagQueries({
keyword: debouncedKeyword,
categoryId: selectedCategoryId,
tagIds: selectedTagIds,
sort: sortingOption.key,
page,
});
const { data: categoryData } = useCategoryListQuery();
const { data: tagData } = useTagListQuery();
const templates = templateData?.templates || [];
const categories = categoryData?.categories || [];
const tags = tagData?.tags || [];

const templateList = templateData?.templates || [];
const categoryList = categoryData?.categories || [];
const tagList = tagData?.tags || [];
const totalPages = templateData?.totalPages || 0;

const { mutateAsync: deleteTemplates } = useTemplateDeleteMutation(selectedList);
Expand All @@ -73,13 +70,13 @@ const MyTemplatePage = () => {
};

const handleAllSelected = () => {
if (selectedList.length === templates.length) {
if (selectedList.length === templateList.length) {
setSelectedList([]);

return;
}

setSelectedList(templates.map((template) => template.id));
setSelectedList(templateList.map((template) => template.id));
};

const handleDelete = () => {
Expand All @@ -89,11 +86,7 @@ const MyTemplatePage = () => {
};

const renderTemplateContent = () => {
if (isPending) {
return <LoadingBall />;
}

if (templates.length === 0) {
if (templateList.length === 0) {
if (debouncedKeyword !== '') {
return (
<Flex justify='center' align='center' padding='2rem'>
Expand All @@ -108,7 +101,7 @@ const MyTemplatePage = () => {

return (
<TemplateGrid
templates={templates}
templateList={templateList}
cols={getGridCols(windowWidth)}
isEditMode={isEditMode}
selectedList={selectedList}
Expand All @@ -122,7 +115,7 @@ const MyTemplatePage = () => {
<TopBanner name={name ?? '나'} />
<S.MainContainer>
<Flex direction='column' gap='2.5rem' style={{ marginTop: '4.5rem' }}>
<CategoryFilterMenu categories={categories} onSelectCategory={handleCategoryMenuClick} />
<CategoryFilterMenu categoryList={categoryList} onSelectCategory={handleCategoryMenuClick} />
</Flex>

<Flex direction='column' width='100%' gap='1rem'>
Expand All @@ -133,7 +126,7 @@ const MyTemplatePage = () => {
돌아가기
</Button>
<Button variant='outlined' size='small' onClick={handleAllSelected}>
{selectedList.length === templates.length ? '전체 해제' : '전체 선택'}
{selectedList.length === templateList.length ? '전체 해제' : '전체 선택'}
</Button>
<Button
variant={selectedList.length ? 'contained' : 'text'}
Expand Down Expand Up @@ -168,45 +161,28 @@ const MyTemplatePage = () => {
getOptionLabel={(option) => option.value}
/>
</Flex>
{tags.length !== 0 && (
<TagFilterMenu tags={tags} selectedTagIds={selectedTagIds} onSelectTags={handleTagMenuClick} />
{tagList.length !== 0 && (
<TagFilterMenu tagList={tagList} selectedTagIds={selectedTagIds} onSelectTags={handleTagMenuClick} />
)}
{renderTemplateContent()}

{templates.length !== 0 && (
{templateList.length !== 0 && (
<Flex justify='center' gap='0.5rem' margin='1rem 0'>
<PagingButtons currentPage={page} totalPages={totalPages} onPageChange={handlePageChange} />
</Flex>
)}
</Flex>

{isDeleteModalOpen && (
<Modal isOpen={isDeleteModalOpen} toggleModal={toggleDeleteModal} size='xsmall'>
<Flex direction='column' justify='space-between' align='center' margin='1rem 0 0 0' gap='2rem'>
<Flex direction='column' justify='center' align='center' gap='0.75rem'>
<Text.Large color='black' weight='bold'>
정말 삭제하시겠습니까?
</Text.Large>
<Text.Medium color='black'>삭제된 템플릿은 복구할 수 없습니다.</Text.Medium>
</Flex>
<Flex justify='center' align='center' gap='0.5rem'>
<Button variant='outlined' onClick={toggleDeleteModal}>
취소
</Button>
<Button onClick={handleDelete}>삭제</Button>
</Flex>
</Flex>
</Modal>
<ConfirmDeleteModal
isDeleteModalOpen={isDeleteModalOpen}
toggleDeleteModal={toggleDeleteModal}
handleDelete={handleDelete}
/>
)}
</S.MainContainer>

<S.ScrollTopButton
onClick={() => {
scroll.top('smooth');
}}
>
<ArrowUpIcon aria-label='맨 위로' />
</S.ScrollTopButton>
<ScrollTopButton />
</S.MyTemplatePageContainer>
);
};
Expand Down Expand Up @@ -240,3 +216,38 @@ const NewTemplateButton = () => {
};

export default MyTemplatePage;

interface ConfirmDeleteModalProps {
isDeleteModalOpen: boolean;
toggleDeleteModal: () => void;
handleDelete: () => void;
}

const ConfirmDeleteModal = ({ isDeleteModalOpen, toggleDeleteModal, handleDelete }: ConfirmDeleteModalProps) => (
Copy link
Contributor

Choose a reason for hiding this comment

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

요 모달은 따로 서브 컴포넌트로 파일 분리 안해도 괜찮을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

앗, 서브컨포넌트 분리하던거 다 reset 한줄 알았는데 아래 남아있었군요!
이 부분은 추후 MyTemplatesPage 리펙토링 할 때 함께 하겠습니다 👍

<Modal isOpen={isDeleteModalOpen} toggleModal={toggleDeleteModal} size='xsmall'>
<Flex direction='column' justify='space-between' align='center' margin='1rem 0 0 0' gap='2rem'>
<Flex direction='column' justify='center' align='center' gap='0.75rem'>
<Text.Large color='black' weight='bold'>
정말 삭제하시겠습니까?
</Text.Large>
<Text.Medium color='black'>삭제된 템플릿은 복구할 수 없습니다.</Text.Medium>
</Flex>
<Flex justify='center' align='center' gap='0.5rem'>
<Button variant='outlined' onClick={toggleDeleteModal}>
취소
</Button>
<Button onClick={handleDelete}>삭제</Button>
</Flex>
</Flex>
</Modal>
);

const ScrollTopButton = () => (
<S.ScrollTopButton
onClick={() => {
scroll.top('smooth');
}}
>
<ArrowUpIcon aria-label='맨 위로' />
</S.ScrollTopButton>
);
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import CategoryFilterMenu from './CategoryFilterMenu';
const meta: Meta<typeof CategoryFilterMenu> = {
title: 'CategoryFilterMenu',
component: CategoryFilterMenu,
args: { categories },
args: { categoryList: categories },
};

export default meta;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import { CategoryEditModal } from '../';
import * as S from './CategoryFilterMenu.style';

interface CategoryMenuProps {
categories: Category[];
categoryList: Category[];
onSelectCategory: (selectedCategoryId: number) => void;
}

const CategoryFilterMenu = ({ categories, onSelectCategory }: CategoryMenuProps) => {
const CategoryFilterMenu = ({ categoryList, onSelectCategory }: CategoryMenuProps) => {
const [selectedId, setSelectedId] = useState<number>(0);
const [isEditModalOpen, toggleEditModal] = useToggle();
const [isMenuOpen, toggleMenu] = useToggle(false);
Expand All @@ -35,17 +35,17 @@ const CategoryFilterMenu = ({ categories, onSelectCategory }: CategoryMenuProps)
}
};

const [defaultCategory, ...userCategories] = categories.length ? categories : [{ id: 0, name: '' }];
const [defaultCategory, ...userCategories] = categoryList.length ? categoryList : [{ id: 0, name: '' }];

const indexById: Record<number, number> = useMemo(() => {
const map: Record<number, number> = { 0: 0, [defaultCategory.id]: categories.length };
const map: Record<number, number> = { 0: 0, [defaultCategory.id]: categoryList.length };

userCategories.forEach(({ id }, index) => {
map[id] = index + 1;
});

return map;
}, [categories.length, defaultCategory.id, userCategories]);
}, [categoryList.length, defaultCategory.id, userCategories]);

return (
<>
Expand Down Expand Up @@ -81,7 +81,7 @@ const CategoryFilterMenu = ({ categories, onSelectCategory }: CategoryMenuProps)
<S.HighlightBox
data-testid='category-highlighter-box'
selectedIndex={indexById[selectedId]}
categoryCount={categories.length}
categoryCount={categoryList.length}
isMenuOpen={isMenuOpen}
/>
</S.CategoryListContainer>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { Meta, StoryObj } from '@storybook/react';

import { tags } from '@/mocks/tagList.json';

import TagFilterMenu from './TagFilterMenu';

const meta: Meta<typeof TagFilterMenu> = {
title: 'TagFilterMenu',
component: TagFilterMenu,
args: { tags },
args: { tagList: tags },
};

export default meta;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,40 +11,40 @@ import * as S from './TagFilterMenu.style';
const LINE_HEIGHT_REM = 1.875;

interface Props {
tags: Tag[];
tagList: Tag[];
selectedTagIds: number[];
onSelectTags: (selectedTagIds: number[]) => void;
}

const TagFilterMenu = ({ tags, selectedTagIds, onSelectTags }: Props) => {
const TagFilterMenu = ({ tagList, selectedTagIds, onSelectTags }: Props) => {
const [deselectedTags, setDeselectedTags] = useState<Tag[]>([]);
const [isTagBoxOpen, toggleTagBox] = useToggle(false);
const [height, setHeight] = useState('auto');
const [height, setHeight] = useState(`${LINE_HEIGHT_REM}rem`);
const containerRef = useRef<HTMLDivElement>(null);
const [showMoreButton, setShowMoreButton] = useState(false);
const windowWidth = useWindowWidth();

const updateTagContainerState = () => {
if (containerRef.current) {
const containerHeight = containerRef.current.scrollHeight;
useEffect(() => {
const updateTagContainerState = () => {
if (containerRef.current) {
const containerHeight = containerRef.current.scrollHeight;

setHeight(isTagBoxOpen ? `${containerHeight}px` : `${LINE_HEIGHT_REM}rem`);
setHeight(isTagBoxOpen ? `${containerHeight}px` : `${LINE_HEIGHT_REM}rem`);

if (containerHeight > remToPx(LINE_HEIGHT_REM)) {
setShowMoreButton(true);
} else {
setShowMoreButton(false);
if (containerHeight > remToPx(LINE_HEIGHT_REM)) {
setShowMoreButton(true);
} else {
setShowMoreButton(false);
}
}
}
};
};

useEffect(() => {
updateTagContainerState();
}, [tags, selectedTagIds, isTagBoxOpen, windowWidth]);
}, [tagList, selectedTagIds, isTagBoxOpen, windowWidth]);

const handleButtonClick = (tagId: number) => {
if (selectedTagIds.includes(tagId)) {
const deselectedTag = tags.find((tag) => tag.id === tagId);
const deselectedTag = tagList.find((tag) => tag.id === tagId);

if (deselectedTag) {
setDeselectedTags((prev) => [deselectedTag, ...prev.filter((tag) => tag.id !== tagId)]);
Expand All @@ -57,10 +57,10 @@ const TagFilterMenu = ({ tags, selectedTagIds, onSelectTags }: Props) => {
}
};

const selectedTags = selectedTagIds.map((id) => tags.find((tag) => tag.id === id)!).filter(Boolean);
const selectedTags = selectedTagIds.map((id) => tagList.find((tag) => tag.id === id)!).filter(Boolean);

const unselectedTags = deselectedTags.concat(
tags.filter(
tagList.filter(
(tag) => !selectedTagIds.includes(tag.id) && !deselectedTags.some((deselectedTag) => deselectedTag.id === tag.id),
),
);
Expand Down
Loading